forked from cantorsdust/StardewMods
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathModEntry.cs
385 lines (329 loc) · 15.3 KB
/
ModEntry.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
using System;
using Microsoft.Xna.Framework.Input;
using StardewModdingAPI;
using StardewModdingAPI.Events;
using StardewValley;
using TimeSpeed.Framework;
namespace TimeSpeed
{
/// <summary>The entry class called by SMAPI.</summary>
internal class ModEntry : Mod
{
/*********
** Properties
*********/
/// <summary>Displays messages to the user.</summary>
private readonly Notifier Notifier = new();
/// <summary>Provides helper methods for tracking time flow.</summary>
private readonly TimeHelper TimeHelper = new();
/// <summary>The mod configuration.</summary>
private ModConfig Config;
/// <summary>Whether the player has manually frozen (<c>true</c>) or resumed (<c>false</c>) time.</summary>
private bool? ManualFreeze;
/// <summary>The reason time would be frozen automatically if applicable, regardless of <see cref="ManualFreeze"/>.</summary>
private AutoFreezeReason AutoFreeze = AutoFreezeReason.None;
/// <summary>Whether time should be frozen.</summary>
private bool IsTimeFrozen =>
this.ManualFreeze == true
|| (this.AutoFreeze != AutoFreezeReason.None && this.ManualFreeze != false);
/// <summary>Whether the flow of time should be adjusted.</summary>
private bool AdjustTime;
/// <summary>Backing field for <see cref="TickInterval"/>.</summary>
private int _tickInterval;
/// <summary>The number of milliseconds per 10-game-minutes to apply.</summary>
private int TickInterval
{
get => this._tickInterval;
set => this._tickInterval = Math.Max(value, 0);
}
/*********
** Public methods
*********/
/// <inheritdoc />
public override void Entry(IModHelper helper)
{
I18n.Init(helper.Translation);
// read config
this.Config = helper.ReadConfig<ModConfig>();
// add time events
this.TimeHelper.WhenTickProgressChanged(this.OnTickProgressed);
helper.Events.GameLoop.GameLaunched += this.OnGameLaunched;
helper.Events.GameLoop.UpdateTicked += this.OnUpdateTicked;
helper.Events.GameLoop.SaveLoaded += this.OnSaveLoaded;
helper.Events.GameLoop.TimeChanged += this.OnTimeChanged;
helper.Events.GameLoop.DayStarted += this.OnDayStarted;
helper.Events.Input.ButtonsChanged += this.OnButtonsChanged;
helper.Events.Player.Warped += this.OnWarped;
// add time freeze/unfreeze notification
{
bool wasPaused = false;
helper.Events.Display.RenderingHud += (_, _) =>
{
wasPaused = Game1.paused;
if (this.IsTimeFrozen)
Game1.paused = true;
};
helper.Events.Display.RenderedHud += (_, _) =>
{
Game1.paused = wasPaused;
};
}
}
/*********
** Private methods
*********/
/****
** Event handlers
****/
/// <inheritdoc cref="IGameLoopEvents.GameLaunched"/>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void OnGameLaunched(object sender, GameLaunchedEventArgs e)
{
GenericModConfigMenuIntegration.Register(this.ModManifest, this.Helper.ModRegistry, this.Monitor,
getConfig: () => this.Config,
reset: () => this.Config = new(),
save: () =>
{
this.Helper.WriteConfig(this.Config);
if (Context.IsWorldReady && this.ShouldEnable())
this.UpdateSettingsForLocation(Game1.currentLocation);
}
);
}
/// <inheritdoc cref="IGameLoopEvents.SaveLoaded"/>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void OnSaveLoaded(object sender, SaveLoadedEventArgs e)
{
if (!Context.IsMainPlayer)
this.Monitor.Log("Disabled mod; only works for the main player in multiplayer.", LogLevel.Warn);
}
/// <inheritdoc cref="IGameLoopEvents.DayStarted"/>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void OnDayStarted(object sender, DayStartedEventArgs e)
{
if (!this.ShouldEnable())
return;
this.UpdateScaleForDay(Game1.currentSeason, Game1.dayOfMonth);
this.UpdateTimeFreeze(clearPreviousOverrides: true);
this.UpdateSettingsForLocation(Game1.currentLocation);
}
/// <inheritdoc cref="IInputEvents.ButtonsChanged"/>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void OnButtonsChanged(object sender, ButtonsChangedEventArgs e)
{
if (!this.ShouldEnable(forInput: true))
return;
if (this.Config.Keys.FreezeTime.JustPressed())
this.ToggleFreeze();
else if (this.Config.Keys.IncreaseTickInterval.JustPressed())
this.ChangeTickInterval(increase: true);
else if (this.Config.Keys.DecreaseTickInterval.JustPressed())
this.ChangeTickInterval(increase: false);
else if (this.Config.Keys.ReloadConfig.JustPressed())
this.ReloadConfig();
}
/// <inheritdoc cref="IPlayerEvents.Warped"/>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void OnWarped(object sender, WarpedEventArgs e)
{
if (!this.ShouldEnable() || !e.IsLocalPlayer)
return;
this.UpdateSettingsForLocation(e.NewLocation);
}
/// <inheritdoc cref="IGameLoopEvents.TimeChanged"/>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void OnTimeChanged(object sender, TimeChangedEventArgs e)
{
if (!this.ShouldEnable())
return;
this.UpdateFreezeForTime();
}
/// <inheritdoc cref="IGameLoopEvents.UpdateTicked"/>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void OnUpdateTicked(object sender, UpdateTickedEventArgs e)
{
if (!this.ShouldEnable())
return;
this.TimeHelper.Update();
}
/// <summary>Raised after the <see cref="Framework.TimeHelper.TickProgress"/> value changes.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void OnTickProgressed(object sender, TickProgressChangedEventArgs e)
{
if (!this.ShouldEnable())
return;
if (this.IsTimeFrozen)
this.TimeHelper.TickProgress = e.TimeChanged ? 0 : e.PreviousProgress;
else
{
if (!this.AdjustTime)
return;
if (this.TickInterval == 0)
this.TickInterval = 1000;
if (e.TimeChanged)
this.TimeHelper.TickProgress = this.ScaleTickProgress(this.TimeHelper.TickProgress, this.TickInterval);
else
this.TimeHelper.TickProgress = e.PreviousProgress + this.ScaleTickProgress(e.NewProgress - e.PreviousProgress, this.TickInterval);
}
}
/****
** Methods
****/
/// <summary>Get whether time features should be enabled.</summary>
/// <param name="forInput">Whether to check for input handling.</param>
private bool ShouldEnable(bool forInput = false)
{
// is loaded and host player (farmhands can't change time)
if (!Context.IsWorldReady || !Context.IsMainPlayer)
return false;
// only handle keys if the player is free, or currently watching an event
if (forInput && !Context.IsPlayerFree && !Game1.eventUp)
return false;
return true;
}
/// <summary>Reload <see cref="Config"/> from the config file.</summary>
private void ReloadConfig()
{
this.Config = this.Helper.ReadConfig<ModConfig>();
this.UpdateScaleForDay(Game1.currentSeason, Game1.dayOfMonth);
this.UpdateSettingsForLocation(Game1.currentLocation);
this.Notifier.ShortNotify(I18n.Message_ConfigReloaded());
}
/// <summary>Increment or decrement the tick interval, taking into account the held modifier key if applicable.</summary>
/// <param name="increase">Whether to increment the tick interval; else decrement.</param>
private void ChangeTickInterval(bool increase)
{
// get offset to apply
int change = 1000;
{
KeyboardState state = Keyboard.GetState();
if (state.IsKeyDown(Keys.LeftControl))
change *= 100;
else if (state.IsKeyDown(Keys.LeftShift))
change *= 10;
else if (state.IsKeyDown(Keys.LeftAlt))
change /= 10;
}
// update tick interval
if (!increase)
{
int minAllowed = Math.Min(this.TickInterval, change);
this.TickInterval = Math.Max(minAllowed, this.TickInterval - change);
}
else
this.TickInterval = this.TickInterval + change;
// log change
this.Notifier.QuickNotify(
I18n.Message_SpeedChanged(seconds: this.TickInterval / 1000)
);
this.Monitor.Log($"Tick length set to {this.TickInterval / 1000d: 0.##} seconds.", LogLevel.Info);
}
/// <summary>Toggle whether time is frozen.</summary>
private void ToggleFreeze()
{
if (!this.IsTimeFrozen)
{
this.UpdateTimeFreeze(manualOverride: true);
this.Notifier.QuickNotify(I18n.Message_TimeStopped());
this.Monitor.Log("Time is frozen globally.", LogLevel.Info);
}
else
{
this.UpdateTimeFreeze(manualOverride: false);
this.Notifier.QuickNotify(I18n.Message_TimeResumed());
this.Monitor.Log($"Time is resumed at \"{Game1.currentLocation.Name}\".", LogLevel.Info);
}
}
/// <summary>Update the time freeze settings for the given time of day.</summary>
private void UpdateFreezeForTime()
{
bool wasFrozen = this.IsTimeFrozen;
this.UpdateTimeFreeze();
if (!wasFrozen && this.IsTimeFrozen)
{
this.Notifier.ShortNotify(I18n.Message_OnTimeChange_TimeStopped());
this.Monitor.Log($"Time automatically set to frozen at {Game1.timeOfDay}.", LogLevel.Info);
}
}
/// <summary>Update the time settings for the given location.</summary>
/// <param name="location">The game location.</param>
private void UpdateSettingsForLocation(GameLocation location)
{
if (location == null)
return;
// update time settings
this.UpdateTimeFreeze();
this.TickInterval = this.Config.GetMillisecondsPerMinute(location) * 10;
// notify player
if (this.Config.LocationNotify)
{
switch (this.AutoFreeze)
{
case AutoFreezeReason.FrozenAtTime when this.IsTimeFrozen:
this.Notifier.ShortNotify(I18n.Message_OnLocationChange_TimeStoppedGlobally());
break;
case AutoFreezeReason.FrozenForLocation when this.IsTimeFrozen:
this.Notifier.ShortNotify(I18n.Message_OnLocationChange_TimeStoppedHere());
break;
default:
this.Notifier.ShortNotify(I18n.Message_OnLocationChange_TimeSpeedHere(seconds: this.TickInterval / 1000));
break;
}
}
}
/// <summary>Update the <see cref="AutoFreeze"/> and <see cref="ManualFreeze"/> flags based on the current context.</summary>
/// <param name="manualOverride">An explicit freeze (<c>true</c>) or unfreeze (<c>false</c>) requested by the player, if applicable.</param>
/// <param name="clearPreviousOverrides">Whether to clear any previous explicit overrides.</param>
private void UpdateTimeFreeze(bool? manualOverride = null, bool clearPreviousOverrides = false)
{
bool? wasManualFreeze = this.ManualFreeze;
AutoFreezeReason wasAutoFreeze = this.AutoFreeze;
// update auto freeze
this.AutoFreeze = this.GetAutoFreezeType();
// update manual freeze
if (manualOverride.HasValue)
this.ManualFreeze = manualOverride.Value;
else if (clearPreviousOverrides)
this.ManualFreeze = null;
// clear manual unfreeze if it's no longer needed
if (this.ManualFreeze == false && this.AutoFreeze == AutoFreezeReason.None)
this.ManualFreeze = null;
// log change
if (wasAutoFreeze != this.AutoFreeze)
this.Monitor.Log($"Auto freeze changed from {wasAutoFreeze} to {this.AutoFreeze}.");
if (wasManualFreeze != this.ManualFreeze)
this.Monitor.Log($"Manual freeze changed from {wasManualFreeze?.ToString() ?? "null"} to {this.ManualFreeze?.ToString() ?? "null"}.");
}
/// <summary>Update the time settings for the given date.</summary>
/// <param name="season">The current season.</param>
/// <param name="dayOfMonth">The current day of month.</param>
private void UpdateScaleForDay(string season, int dayOfMonth)
{
this.AdjustTime = this.Config.ShouldScale(season, dayOfMonth);
}
/// <summary>Get the adjusted progress towards the next 10-game-minute tick.</summary>
/// <param name="progress">The current progress.</param>
/// <param name="newTickInterval">The new tick interval.</param>
private double ScaleTickProgress(double progress, int newTickInterval)
{
return progress * this.TimeHelper.CurrentDefaultTickInterval / newTickInterval;
}
/// <summary>Get the freeze type which applies for the current context, ignoring overrides by the player.</summary>
private AutoFreezeReason GetAutoFreezeType()
{
if (this.Config.ShouldFreeze(Game1.currentLocation))
return AutoFreezeReason.FrozenForLocation;
if (this.Config.ShouldFreeze(Game1.timeOfDay))
return AutoFreezeReason.FrozenAtTime;
return AutoFreezeReason.None;
}
}
}