-
Notifications
You must be signed in to change notification settings - Fork 6
/
GrassyKnight.cs
343 lines (298 loc) · 14.3 KB
/
GrassyKnight.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
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
namespace GrassyKnight
{
public class GrassyKnight : Modding.Mod {
// In a previous version we accessed ModSettings.BoolValues directly,
// but it looks like the latest code in the Modding.API repo no longer
// has BoolValues as a member at all. This way of using ModSettings is
// more in line with other mod authors do so we should be somewhat
// future-proof now.
private class MySaveData : Modding.ModSettings {
public string serializedGrassDB;
}
public override Modding.ModSettings SaveSettings
{
get {
return new MySaveData {
serializedGrassDB = GrassStates.Serialize(),
};
}
set {
GrassStates.Clear();
GrassStates.AddSerializedData(
((MySaveData)value).serializedGrassDB);
}
}
private class MyGlobalSettings : Modding.ModSettings {
public bool UseHeuristicGrassKnower = false;
public bool AutomaticallyCutGrass = false;
public string ToggleCompassHotkey = "Space";
public bool DisableCompass = true;
}
private MyGlobalSettings Settings = new MyGlobalSettings();
public override Modding.ModSettings GlobalSettings {
get => Settings;
set => Settings = (MyGlobalSettings)value;
}
// Will be set to the exactly one ModMain in existance... Trusting
// Modding.Mod to ensure that ModMain is only ever instantiated once...
public static GrassyKnight Instance = null;
// Stores which grass is cut and allows queries (like "where's the
// nearest uncut grass?")
GrassDB GrassStates = new GrassDB();
// Knows if an object is grass. Very wise. Uwu. Which knower we use
// depends on configuration
GrassKnower SetOfAllGrass = null;
// Usually Unity code is contained in MonoBehaviour classes, so Unity
// has lots of very useful functionality in them (ex: access to the
// coroutine scheduler). This is forever-living MonoBehaviour object we
// use to give us that funcionality despite our non-MonoBheaviour
// status.
Behaviour UtilityBehaviour = null;
public override string GetVersion() => "1.3.0";
public GrassyKnight() : base("Grassy Knight") {
GrassyKnight.Instance = this;
}
public override void Initialize() {
base.Initialize();
// I tried creating this in the field initializer but it failed...
// I think construction is too early to make game objects, though I
// don't now why.
UtilityBehaviour = Behaviour.CreateBehaviour();
if (Settings.UseHeuristicGrassKnower) {
SetOfAllGrass = new HeuristicGrassKnower();
Log("Using HeuristicGrassKnower");
// Because the heuristic grass knower doesn't know about grass
// until it sees it for the first time, we need to constantly
// look for grass each time we enter a scene.
UnityEngine.SceneManagement.SceneManager.sceneLoaded +=
(_, _1) => UtilityBehaviour.StartCoroutine(
WaitThenFindGrass());
} else {
CuratedGrassKnower curatedKnower = new CuratedGrassKnower();
SetOfAllGrass = curatedKnower;
Log($"Using CuratedGrassKnower");
Modding.ModHooks.Instance.SavegameLoadHook +=
_ => HandleFileEntered();
Modding.ModHooks.Instance.NewGameHook +=
() => HandleFileEntered();
foreach ((GrassKey, GrassKey) alias in curatedKnower.GetAliases()) {
GrassStates.AddAlias(alias.Item1, alias.Item2);
}
}
// Triggered when real grass is being cut for real
On.GrassCut.ShouldCut += HandleShouldCut;
// Lots of various callbacks all doing the same thing: making sure
// our grassy box is full when HandleShouldCut is called.
On.GrassBehaviour.OnTriggerEnter2D += HandleGrassCollisionEnter;
On.GrassCut.OnTriggerEnter2D += HandleGrassCollisionEnter;
On.TownGrass.OnTriggerEnter2D += HandleGrassCollisionEnter;
On.GrassSpriteBehaviour.OnTriggerEnter2D += HandleGrassCollisionEnter;
// Backup we use to make sure we notice uncuttable grass getting
// swung at. This is the detector of shameful grass.
Modding.ModHooks.Instance.SlashHitHook += HandleSlashHit;
// Update the grass count whenever we change scenes or if they
// change.
GrassStates.OnStatsChanged += (_, _1) => UpdateGrassCount();
UnityEngine.SceneManagement.SceneManager.sceneLoaded +=
(_, _1) => UtilityBehaviour.StartCoroutine(
WaitThenUpdateGrassCount());
// Makes sure our grassy counter is always in-place
UtilityBehaviour.OnUpdate += HandleAttachGrassCount;
// Make sure the hero always has the grassy compass component
// attached. We could probably hook the hero object's creation to
// be more efficient, but it's a cheap operation so imma not worry
// about it.
if (!Settings.DisableCompass) {
Modding.ModHooks.Instance.HeroUpdateHook +=
HandleCheckGrassyCompass;
}
// It's dangerous out there, make sure to bring your lawnmower!
// This'll make sure the hero has their lawnmower handy at all
// times.
if (Settings.AutomaticallyCutGrass) {
Modding.ModHooks.Instance.HeroUpdateHook +=
HandleCheckAutoMower;
}
}
// Triggered anytime the user loads a save file or starts a new game
private void HandleFileEntered() {
try {
CuratedGrassKnower knower = (CuratedGrassKnower)SetOfAllGrass;
foreach (GrassKey k in knower.GetAllGrassKeys()) {
GrassStates.TrySet(k, GrassState.Uncut);
}
} catch (System.Exception e) {
LogException("Error in HandleFileEntered", e);
}
}
// We'll hook this into a bunch of Grass components' OnTriggerEnter2D
// methods. It's only responsibility is to store the game object that
// the component is attached to for a moment in case ShouldCut is
// called in the original function.
private void HandleGrassCollisionEnter<OrigFunc, Component>(
OrigFunc orig,
Component self,
Collider2D collision)
where Component : MonoBehaviour
where OrigFunc : MulticastDelegate
{
var context = new GrassyBox(self.gameObject);
try {
orig.DynamicInvoke(new object[] { self, collision });
} finally {
context.Dispose();
}
}
private void HandleCheckGrassyCompass() {
try {
// Ensure the hero has their grassy compass friend
GameObject hero = GameManager.instance?.hero_ctrl?.gameObject;
if (hero != null &&
hero.GetComponent<GrassyCompass>() == null) {
GrassyCompass compassComponent =
hero.AddComponent<GrassyCompass>();
compassComponent.AllGrass = GrassStates;
if (Settings.ToggleCompassHotkey != null) {
try {
KeyCode hotkey = (KeyCode)Enum.Parse(
typeof(KeyCode),
Settings.ToggleCompassHotkey);
compassComponent.ToggleHotkey = hotkey;
Log($"Hotkey for toggling the Grassy Compass " +
$"set to {hotkey}");
} catch (ArgumentException) {
LogError(
$"Unrecognized key name for " +
$"ToggleCompassHotkey " +
$"{Settings.ToggleCompassHotkey}. See the " +
$"README.md file for a list of all valid " +
$"key names.");
}
}
}
} catch (System.Exception e) {
LogException("Error in HandleCheckGrassyCompass", e);
}
}
private void HandleCheckAutoMower() {
try {
// Ensure the hero has their lawnmower
GameObject hero = GameManager.instance?.hero_ctrl?.gameObject;
if (hero != null && hero.GetComponent<AutoMower>() == null) {
AutoMower autoMower = hero.AddComponent<AutoMower>();
autoMower.SetOfAllGrass = SetOfAllGrass;
autoMower.GrassStates = GrassStates;
LogDebug("Attached autoMower to hero");
}
} catch (System.Exception e) {
LogException("Error in HandleCheckAutoMower", e);
}
}
private void HandleAttachGrassCount(object _, EventArgs _1) {
try {
GameObject geoCounter =
GameManager.instance?.hero_ctrl?.geoCounter?.gameObject;
if (geoCounter != null &&
geoCounter.GetComponent<GrassCount>() == null) {
geoCounter.AddComponent<GrassCount>();
LogDebug("Attached Grass Count to Geo Counter");
UpdateGrassCount();
}
} catch (System.Exception e) {
LogException("Error in HandleCheckAutoMower", e);
}
}
// Sets state of maybeGrass if it is grass
private void MaybeSetGrassState(GameObject maybeGrass, GrassState state) {
GrassKey k = GrassKey.FromGameObject(maybeGrass);
if (GrassStates.Contains(k) || SetOfAllGrass.IsGrass(maybeGrass)) {
GrassStates.TrySet(k, state);
}
}
// Meant to be called when a new scene is entered
private IEnumerator WaitThenFindGrass() {
// The docs suggest waiting a frame after scene loads before we
// consider the scene fully instantiated. We've got time, so wait
// even longer.
yield return new WaitForSeconds(0.5f);
try {
foreach (GameObject maybeGrass in
UnityEngine.Object.FindObjectsOfType<GameObject>()) {
MaybeSetGrassState(maybeGrass, GrassState.Uncut);
}
} catch (System.Exception e) {
LogException("Error in WaitThenFindGrass", e);
}
}
// Meant to be called when a new scene is entered
private IEnumerator WaitThenUpdateGrassCount() {
// The docs suggest wait a moment to make sure everything's set
yield return new WaitForSeconds(0.1f);
UpdateGrassCount();
}
private void UpdateGrassCount() {
try {
string sceneName = GameManager.instance?.sceneName;
if (sceneName != null) {
GrassCount grassCount =
GameManager.instance
?.hero_ctrl
?.geoCounter
?.gameObject
?.GetComponent<GrassCount>();
grassCount?.UpdateStats(
GrassStates.GetStatsForScene(sceneName),
GrassStates.GetGlobalStats());
}
} catch (System.Exception e) {
LogException("Error in UpdateGrassCount", e);
}
}
private static string IndentString(string str, string indent = "... ") {
return indent + str.Replace("\n", "\n" + indent);
}
public void LogException(string heading, System.Exception error) {
LogError($"{heading}\n{IndentString(error.ToString())}");
}
private bool HandleShouldCut(On.GrassCut.orig_ShouldCut orig, Collider2D collision) {
// Find out whether the original game code thinks this should be
// cut. We'll pass this value through no matter what.
bool shouldCut = orig(collision);
try {
if (shouldCut) {
// ShouldCut is a static function so we've hooked every
// function that calls ShouldCut. Our hooks will store the
// GameObject whose component's method is calling ShouldCut
// in this box so that we can grab it out. This could also
// be done by walking the stack upwards IF C# let us
// examine the argument values of stack frames, but C# does
// not give us a good way to do that so here we are.
GameObject grass = GrassyBox.GetValue();
MaybeSetGrassState(grass, GrassState.Cut);
}
} catch (System.Exception e) {
LogException("Error in HandleShouldCut", e);
// Exception stack traces seem to terminate once we're out
// of this assembly... It doesn't show who called ShouldCut
// anyways. And that's exactly the information we want if we're
// looking for more functions to hook HandleGrassCollisionEnter
// into.
LogDebug("More complete stack trace:");
LogDebug(IndentString(System.Environment.StackTrace));
}
return shouldCut;
}
private void HandleSlashHit(Collider2D otherCollider, GameObject _) {
try {
MaybeSetGrassState(otherCollider.gameObject,
GrassState.ShouldBeCut);
} catch(System.Exception e) {
LogException("Error in HandleSlashHit", e);
}
}
}
}