-
-
Notifications
You must be signed in to change notification settings - Fork 380
/
ModEntry.cs
135 lines (120 loc) · 5.76 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
#nullable disable
using System.Linq;
using StardewModdingAPI;
using StardewModdingAPI.Events;
using StardewValley;
using SFarmer = StardewValley.Farmer;
namespace Pathoschild.Stardew.TheLongNight
{
/// <summary>The mod entry point.</summary>
internal class ModEntry : Mod, IAssetEditor
{
/*********
** Accessors
*********/
/// <summary>Whether the mod just skipped a 10-minute interval.</summary>
private bool JustSkipped;
/*********
** Public methods
*********/
/// <inheritdoc />
public override void Entry(IModHelper helper)
{
helper.Events.GameLoop.UpdateTicked += this.OnUpdateTicked;
}
/// <summary>Get whether this instance can edit the given asset.</summary>
/// <param name="asset">Basic metadata about the asset being loaded.</param>
public bool CanEdit<T>(IAssetInfo asset)
{
return asset.AssetNameEquals("Data/Fish.xnb");
}
/// <summary>Edit a matched asset.</summary>
/// <param name="asset">A helper which encapsulates metadata about an asset and enables changes to it.</param>
public void Edit<T>(IAssetData asset)
{
// unlock any in-season fish after 2am
var data = asset.AsDictionary<int, string>().Data;
foreach (var entry in data.ToArray())
{
string[] fields = entry.Value.Split('/');
if (fields[1] == "trap")
continue; // ignore non-fish entries
fields[5] = $"{fields[5]} 2600 {int.MaxValue}".Trim();
data[entry.Key] = string.Join("/", fields);
}
}
/*********
** Private methods
*********/
/// <inheritdoc cref="IGameLoopEvents.UpdateTicked" />
/// <remarks>
/// All times are shown in the game's internal format, which is essentially military time with support for
/// times past midnight (e.g. 2400 is midnight, 2600 is 2am).
///
/// The game's logic for collapsing the player mostly happens in <see cref="Game1.performTenMinuteClockUpdate"/>.
/// It has three cases which affect staying up:
/// - time = 2600: dismounts the player.
/// - time ≥ 2600: sets the <see cref="Game1.farmerShouldPassOut"/> flag, which causes <see cref="Game1.UpdateOther"/>
/// to initiate a player collapse (animation #293).
/// - time = 2800: initiates a player collapse.
/// </remarks>
private void OnUpdateTicked(object sender, UpdateTickedEventArgs e)
{
if (!Context.IsWorldReady || Game1.timeOfDay < 2550)
return;
// get clock details
bool clockWillChangeNextTick = this.WillClockChangeNextTick();
// 1. Right before the clock changes, freeze the player for interval + 1. That's too fast for the player to
// notice, but long enough to bypass the farmerShouldPassOut check (which skips if the player is frozen).
Game1.farmerShouldPassOut = false;
if (clockWillChangeNextTick)
{
Game1.player.freezePause = Game1.currentGameTime.ElapsedGameTime.Milliseconds + 1;
this.Monitor.Log($"Adding freeze for {this.GetNextTime(Game1.timeOfDay)} next tick ({Game1.player.freezePause}ms).");
}
// 2. Right before the game updates the clock to 2600/2800, change it to the upcoming time. The game will then
// update to 2610/2810 instead, which has no special logic. Immediately afterwards, change the clock back to
// 2600/2800. This happens faster than the player can see.
if (clockWillChangeNextTick && (Game1.timeOfDay == 2550 || Game1.timeOfDay == 2750))
{
Game1.timeOfDay += 50;
this.Monitor.Log($"Skipping {Game1.timeOfDay} next tick.");
this.JustSkipped = true;
}
else if (this.JustSkipped)
{
Game1.timeOfDay -= 10;
this.Monitor.Log($"Skip done, reset time to {Game1.timeOfDay}.");
this.JustSkipped = false;
}
// 3. As a failsafe, if the collapse animation starts immediately remove it. This prevents the attached
// Farmer.passOutFromTired callback from being called.
FarmerSprite sprite = (FarmerSprite)Game1.player.Sprite;
var animation = sprite.CurrentAnimation;
if (animation != null && animation.Any(frame => frame.frameStartBehavior == SFarmer.passOutFromTired))
{
this.Monitor.Log("Cancelling player collapse.");
Game1.player.freezePause = 0;
Game1.player.canMove = true;
sprite.PauseForSingleAnimation = false;
sprite.StopAnimation();
}
}
/// <summary>Get whether the clock will change on the next update tick.</summary>
/// <remarks>Derived from <see cref="Game1.performTenMinuteClockUpdate"/>.</remarks>
private bool WillClockChangeNextTick()
{
return (Game1.gameTimeInterval + Game1.currentGameTime.ElapsedGameTime.Milliseconds) > 7000 + Game1.currentLocation.getExtraMillisecondsPerInGameMinuteForThisLocation();
}
/// <summary>Get the clock time that comes after the given value.</summary>
/// <param name="current">The current time.</param>
/// <remarks>Derived from <see cref="Game1.performTenMinuteClockUpdate"/>.</remarks>
private int GetNextTime(int current)
{
int next = current + 10;
if (next % 100 >= 60)
next = next - (next % 100) + 100;
return next;
}
}
}