Skip to content

Commit 24fc85d

Browse files
authored
Merge pull request #11 from SixteenthBit/master
Add lifecycle hooks for mod initialization
2 parents 592ff43 + 8c62251 commit 24fc85d

3 files changed

Lines changed: 215 additions & 2 deletions

File tree

LifecycleHooks.cs

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
using Core;
2+
using HarmonyLib;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Reflection;
6+
7+
namespace TerrariaInjector
8+
{
9+
/// <summary>
10+
/// Registers Harmony patches on Terraria's XNA lifecycle methods and dispatches
11+
/// callbacks to mod assemblies that define matching static methods.
12+
///
13+
/// Mods can opt in by defining any of these public static void methods:
14+
/// OnGameReady() — Main.Initialize() postfix (graphics, window, Main.instance ready)
15+
/// OnContentLoaded() — Main.LoadContent() postfix (SpriteBatch, content pipeline ready)
16+
/// OnFirstUpdate() — First Main.Update() postfix (game loop active)
17+
/// OnShutdown() — Main_Exiting prefix (before Terraria disposes systems)
18+
/// </summary>
19+
public static class LifecycleHooks
20+
{
21+
private static readonly Logger Logger = new Logger("Lifecycle");
22+
23+
private static readonly List<MethodInfo> _onGameReady = new List<MethodInfo>();
24+
private static readonly List<MethodInfo> _onContentLoaded = new List<MethodInfo>();
25+
private static readonly List<MethodInfo> _onFirstUpdate = new List<MethodInfo>();
26+
private static readonly List<MethodInfo> _onShutdown = new List<MethodInfo>();
27+
28+
private static bool _gameReadyFired;
29+
private static bool _contentLoadedFired;
30+
private static bool _firstUpdateFired;
31+
private static bool _shutdownFired;
32+
33+
/// <summary>
34+
/// Discover lifecycle methods in mod assemblies and register Harmony patches
35+
/// on Terraria's Main class to dispatch them at the right time.
36+
/// </summary>
37+
public static void Register(Assembly game, Harmony harmony, List<Assembly> mods)
38+
{
39+
DiscoverMethods(mods);
40+
41+
int total = _onGameReady.Count + _onContentLoaded.Count +
42+
_onFirstUpdate.Count + _onShutdown.Count;
43+
Logger.Info($"Registering hooks ({total} method(s) found across {mods.Count} mod(s))");
44+
45+
var mainType = game.GetType("Terraria.Main");
46+
if (mainType == null)
47+
{
48+
Logger.Error("Terraria.Main not found — lifecycle hooks not registered");
49+
return;
50+
}
51+
52+
// OnGameReady — Main.Initialize() postfix
53+
PatchMethod(harmony, mainType, "Initialize",
54+
BindingFlags.NonPublic | BindingFlags.Instance,
55+
nameof(OnGameReady_Postfix), patchType: "postfix");
56+
57+
// OnContentLoaded — Main.LoadContent() postfix
58+
PatchMethod(harmony, mainType, "LoadContent",
59+
BindingFlags.NonPublic | BindingFlags.Instance,
60+
nameof(OnContentLoaded_Postfix), patchType: "postfix");
61+
62+
// OnFirstUpdate — Main.Update(GameTime) postfix
63+
PatchMethod(harmony, mainType, "Update",
64+
BindingFlags.NonPublic | BindingFlags.Instance,
65+
nameof(OnFirstUpdate_Postfix), patchType: "postfix");
66+
67+
// OnShutdown — Main.Main_Exiting(object, EventArgs) prefix
68+
PatchMethod(harmony, mainType, "Main_Exiting",
69+
BindingFlags.NonPublic | BindingFlags.Instance,
70+
nameof(OnShutdown_Prefix), patchType: "prefix");
71+
}
72+
73+
private static void PatchMethod(Harmony harmony, Type targetType, string methodName,
74+
BindingFlags flags, string callbackName, string patchType)
75+
{
76+
var target = targetType.GetMethod(methodName, flags);
77+
if (target == null)
78+
{
79+
Logger.Warning($"Method not found: Main.{methodName} — hook skipped");
80+
return;
81+
}
82+
83+
var callback = typeof(LifecycleHooks).GetMethod(callbackName,
84+
BindingFlags.Public | BindingFlags.Static);
85+
86+
switch (patchType)
87+
{
88+
case "prefix":
89+
harmony.Patch(target, prefix: new HarmonyMethod(callback));
90+
break;
91+
case "postfix":
92+
harmony.Patch(target, postfix: new HarmonyMethod(callback));
93+
break;
94+
default:
95+
Logger.Warning($"Unknown patch type '{patchType}' for Main.{methodName} — hook skipped");
96+
return;
97+
}
98+
99+
Logger.Info($"Hooked Main.{methodName} ({patchType})");
100+
}
101+
102+
private static void DiscoverMethods(List<Assembly> mods)
103+
{
104+
var hookMap = new Dictionary<string, List<MethodInfo>>
105+
{
106+
{ "OnGameReady", _onGameReady },
107+
{ "OnContentLoaded", _onContentLoaded },
108+
{ "OnFirstUpdate", _onFirstUpdate },
109+
{ "OnShutdown", _onShutdown },
110+
};
111+
112+
foreach (var mod in mods)
113+
{
114+
Type[] types;
115+
try
116+
{
117+
types = mod.GetTypes();
118+
}
119+
catch (ReflectionTypeLoadException rtle)
120+
{
121+
types = rtle.Types;
122+
}
123+
124+
foreach (var type in types)
125+
{
126+
if (type == null)
127+
{
128+
continue;
129+
}
130+
131+
foreach (var kvp in hookMap)
132+
{
133+
var method = type.GetMethod(kvp.Key,
134+
BindingFlags.Public | BindingFlags.Static,
135+
null, Type.EmptyTypes, null);
136+
137+
if (method != null)
138+
{
139+
kvp.Value.Add(method);
140+
Logger.Info($"Found {kvp.Key}() in {type.FullName}");
141+
}
142+
}
143+
}
144+
}
145+
}
146+
147+
// --- Harmony callbacks ---
148+
149+
public static void OnGameReady_Postfix()
150+
{
151+
if (_gameReadyFired)
152+
{
153+
return;
154+
}
155+
_gameReadyFired = true;
156+
Logger.Info("OnGameReady fired");
157+
Dispatch(_onGameReady, "OnGameReady");
158+
}
159+
160+
public static void OnContentLoaded_Postfix()
161+
{
162+
if (_contentLoadedFired)
163+
{
164+
return;
165+
}
166+
_contentLoadedFired = true;
167+
Logger.Info("OnContentLoaded fired");
168+
Dispatch(_onContentLoaded, "OnContentLoaded");
169+
}
170+
171+
public static void OnFirstUpdate_Postfix()
172+
{
173+
if (_firstUpdateFired)
174+
{
175+
return;
176+
}
177+
_firstUpdateFired = true;
178+
Logger.Info("OnFirstUpdate fired");
179+
Dispatch(_onFirstUpdate, "OnFirstUpdate");
180+
}
181+
182+
public static void OnShutdown_Prefix()
183+
{
184+
if (_shutdownFired)
185+
{
186+
return;
187+
}
188+
_shutdownFired = true;
189+
Logger.Info("OnShutdown fired");
190+
Dispatch(_onShutdown, "OnShutdown");
191+
}
192+
193+
private static void Dispatch(List<MethodInfo> methods, string hookName)
194+
{
195+
foreach (var method in methods)
196+
{
197+
try
198+
{
199+
method.Invoke(null, null);
200+
}
201+
catch (Exception ex)
202+
{
203+
var inner = ex.InnerException ?? ex;
204+
Logger.Error($"{hookName} failed in {method.DeclaringType?.FullName}: {inner.Message}", inner);
205+
}
206+
}
207+
}
208+
}
209+
}

Program.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,8 +362,11 @@ public static void Inject(string[] args)
362362
}
363363
}
364364

365-
//Logger.Info("Writing patched game to file ...");
366-
//File.WriteAllBytes(targetPath + ".dump.exe", DumpAssembly(Game));
365+
// Register lifecycle hooks (Terraria client only)
366+
if (isTerrariaTarget && !isServer)
367+
{
368+
LifecycleHooks.Register(game, harmony, modsAssemblies);
369+
}
367370

368371
Logger.Info("Invoke game entry point ...");
369372
Thread.Sleep(1000);

TerrariaInjector.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
<None Include="Extra\ModCompile\compileconfig.rsp" />
5252
<None Include="Extra\ModHelpers.cs" />
5353
<None Include="Extra\template.cs" />
54+
<Compile Include="LifecycleHooks.cs" />
5455
<Compile Include="Logger.cs" />
5556
<Compile Include="ModCountLabel.cs" />
5657
<Compile Include="Program.cs" />

0 commit comments

Comments
 (0)