Repository navigation
Expand file tree
/
Copy pathSubModule.cs
More file actions
338 lines (296 loc) · 13.2 KB
/
Copy pathSubModule.cs
File metadata and controls
338 lines (296 loc) · 13.2 KB
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
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using TaleWorlds.CampaignSystem;
using TaleWorlds.Core;
using TaleWorlds.InputSystem;
using TaleWorlds.Localization;
using TaleWorlds.Library;
using TaleWorlds.MountAndBlade;
using UniversalPatch.Services;
using UniversalPatch.Helpers;
using UniversalPatch.Pipeline;
using UniversalPatch.Settings;
namespace UniversalPatch
{
public class SubModule : MBSubModuleBase
{
private static string ModVersion
{
get
{
var informationalVersion = Assembly.GetExecutingAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
if (string.IsNullOrEmpty(informationalVersion))
{
return Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "unknown";
}
var plusIndex = informationalVersion!.IndexOf('+');
return plusIndex >= 0 ? informationalVersion.Substring(0, plusIndex) : informationalVersion;
}
}
private ConfigurationService? _configService;
private PatchPipeline? _pipeline;
private PatchExecutionService? _executionService;
private ItemFlagsPatch? _flagsPatch;
private StealthCalculationPatch? _stealthPatch;
protected override void OnSubModuleLoad()
{
base.OnSubModuleLoad();
try
{
// 1. Settings file location + path init
ModSettingsPathService.RestoreSettingsFileToMcmFolder();
ModSettingsPathService.EnsureRuntimeDataDirectoryInitialized();
_ = UniversalPatchSettings.Instance;
// 2. Display loaded message
var loadedMessage = new TextObject("{=UP_Msg_Loaded}[Universal Patch] v{VERSION} loaded!");
loadedMessage.SetTextVariable("VERSION", ModVersion);
LoggingService.DisplayMessage(new InformationMessage(loadedMessage.ToString(), AlkeariPalette.Success));
// 3. Config service
_configService = new ConfigurationService();
_configService.LoadConfiguration();
UniversalPatchSettings.ReapplyPatchesRequested += OnReapplyPatchesRequested;
// 4. Baselines
var xmlBaseline = new ItemXmlBaselineService();
var armorBaseline = new ArmorRuntimeBaselineService();
var priceBaseline = new PriceRuntimeBaselineService();
// 5. Create patches
_flagsPatch = new ItemFlagsPatch(xmlBaseline);
_stealthPatch = new StealthCalculationPatch(xmlBaseline);
var patches = new IItemPatch[]
{
new DuplicateDetectionPatch(xmlBaseline),
new ItemNamingPatch(_configService),
_flagsPatch,
_stealthPatch,
new ArmorStandardizationPatch(xmlBaseline, armorBaseline),
new PriceNormalizationPatch(xmlBaseline, priceBaseline),
new WeaponStandardizationPatch(xmlBaseline),
new MountStandardizationPatch(xmlBaseline),
new TierCorrectionPatch(xmlBaseline),
new WeightNormalizationPatch(xmlBaseline),
new CultureAssignmentPatch(xmlBaseline),
};
// 6. Pipeline + execution service
_pipeline = new PatchPipeline(_configService, patches);
_executionService = new PatchExecutionService(_pipeline);
// 7. Detect mods
var loadedModNames = ModDetectionHelper.GetLoadedModNames();
if (loadedModNames.Count > 0)
{
var modsMessage = new TextObject("{=UP_Msg_ModsDetected}[Universal Patch] Detected mods: {MODS}");
modsMessage.SetTextVariable("MODS", string.Join(", ", loadedModNames));
LoggingService.DisplayMessage(new InformationMessage(modsMessage.ToString(), AlkeariPalette.Secondary));
}
var isRbmLoaded = ModDetectionHelper.IsRbmLoaded();
if (isRbmLoaded)
{
LoggingService.DisplayLocalizedMessage("{=UP_Msg_RbmDetected}[Universal Patch] RBM detected", AlkeariPalette.Secondary);
}
// 8. Check conflicts
if (ModDetectionHelper.HasKnownConflictingMods())
{
LoggingService.DisplayLocalizedMessage("{=UP_Msg_ConflictWarning}[Universal Patch] Warning: Conflicting armor mods detected - adjust settings accordingly", AlkeariPalette.Attention);
}
}
catch (Exception ex)
{
var errorMessage = new TextObject("{=UP_Msg_InitError}[Universal Patch] Error during initialization: {ERROR}");
errorMessage.SetTextVariable("ERROR", ex.Message);
LoggingService.DisplayMessage(new InformationMessage(errorMessage.ToString(), AlkeariPalette.Attention));
}
}
protected override void OnSubModuleUnloaded()
{
base.OnSubModuleUnloaded();
try
{
UnsubscribeFromCampaignEvents();
UniversalPatchSettings.ReapplyPatchesRequested -= OnReapplyPatchesRequested;
_configService?.SaveConfiguration();
LoggingService.DisplayLocalizedMessage("{=UP_Msg_ConfigSaved}[Universal Patch] Configuration saved", AlkeariPalette.Success);
}
catch (Exception ex)
{
var errorMessage = new TextObject("{=UP_Msg_ConfigSaveError}[Universal Patch] Error saving configuration: {ERROR}");
errorMessage.SetTextVariable("ERROR", ex.Message);
LoggingService.DisplayMessage(new InformationMessage(errorMessage.ToString(), AlkeariPalette.Attention));
}
}
protected override void OnGameStart(Game game, IGameStarter gameStarterObject)
{
base.OnGameStart(game, gameStarterObject);
try
{
if (game.GameType is Campaign)
{
SubscribeToCampaignEvents();
// Treat each campaign load/start as a fresh session; patch once when the session launches.
if (_executionService != null)
{
_executionService.NeedsRepatch = true;
}
var settings = UniversalPatchSettings.Instance;
if (settings != null && _executionService != null)
{
_executionService.UpdateSettingsHash();
}
}
}
catch (Exception ex)
{
var errorMessage = new TextObject("{=UP_Msg_PatchError}[Universal Patch] Error during patching: {ERROR}");
errorMessage.SetTextVariable("ERROR", ex.Message);
LoggingService.DisplayMessage(new InformationMessage(errorMessage.ToString(), AlkeariPalette.Attention));
}
}
private void SubscribeToCampaignEvents()
{
try
{
CampaignEvents.OnSessionLaunchedEvent.AddNonSerializedListener(this, OnSessionLaunched);
// Both events change their argument lists across game versions, and crafting orders do
// not exist in the oldest ones, so each is bound by name to whatever shape this game has.
SubscribeToItemEvent("OnNewItemCraftedEvent");
SubscribeToItemEvent("OnCraftingOrderCompletedEvent");
}
catch (Exception ex)
{
var errorMessage = new TextObject("{=UP_Msg_EventError}[Universal Patch] Error subscribing to campaign events: {ERROR}");
errorMessage.SetTextVariable("ERROR", ex.Message);
LoggingService.DisplayMessage(new InformationMessage(errorMessage.ToString(), AlkeariPalette.Attention));
}
}
private void UnsubscribeFromCampaignEvents()
{
try
{
CampaignEvents.OnSessionLaunchedEvent.ClearListeners(this);
ClearItemEventListeners("OnNewItemCraftedEvent");
ClearItemEventListeners("OnCraftingOrderCompletedEvent");
}
catch
{
// Ignore unsubscribe errors during shutdown
}
}
private static object? GetCampaignEvent(string eventName)
{
return typeof(CampaignEvents).GetProperty(eventName, BindingFlags.Public | BindingFlags.Static)?.GetValue(null);
}
private void SubscribeToItemEvent(string eventName)
{
var campaignEvent = GetCampaignEvent(eventName);
var addListener = campaignEvent?.GetType().GetMethod("AddNonSerializedListener");
if (campaignEvent == null || addListener == null)
{
return;
}
var actionType = addListener.GetParameters()[1].ParameterType;
var parameters = actionType.GetGenericArguments().Select(t => Expression.Parameter(t)).ToArray();
var itemParameter = parameters.FirstOrDefault(p => p.Type == typeof(ItemObject));
if (itemParameter == null)
{
LoggingService.LogWarning($"{eventName} carries no item on this game version; crafted items from it are not patched");
return;
}
var handler = typeof(SubModule).GetMethod(nameof(OnItemCreated), BindingFlags.NonPublic | BindingFlags.Instance)!;
var listener = Expression.Lambda(actionType, Expression.Call(Expression.Constant(this), handler, itemParameter), parameters).Compile();
addListener.Invoke(campaignEvent, new object[] { this, listener });
}
private void ClearItemEventListeners(string eventName)
{
var campaignEvent = GetCampaignEvent(eventName);
campaignEvent?.GetType().GetMethod("ClearListeners")?.Invoke(campaignEvent, new object[] { this });
}
private void OnItemCreated(ItemObject item)
{
try
{
_flagsPatch?.ApplyFlagModifications(item);
_stealthPatch?.ApplyStealthValue(item);
}
catch (Exception ex)
{
LoggingService.LogException("OnItemCreated", ex);
}
}
private void OnSessionLaunched(CampaignGameStarter starter)
{
ApplyPatchesIfPossible("session launched");
}
protected override void OnApplicationTick(float dt)
{
base.OnApplicationTick(dt);
try
{
if (_executionService?.IsPatching == true)
{
return;
}
if (_pipeline == null)
{
return;
}
var settings = UniversalPatchSettings.Instance;
if (settings == null)
{
return;
}
if (Game.Current?.GameType is not Campaign)
{
return;
}
if (IsHotkeyPressed(settings))
{
ApplyPatchesIfPossible("hotkey");
return;
}
_executionService?.CheckSettingsChanges(dt);
}
catch (Exception ex)
{
LoggingService.LogException("OnApplicationTick", ex);
}
}
private void OnReapplyPatchesRequested()
{
if (_configService == null)
{
return;
}
try
{
_configService.LoadConfiguration();
if (_executionService == null)
{
throw new InvalidOperationException("Patch execution service not initialized.");
}
_executionService.NeedsRepatch = true;
ApplyPatchesIfPossible("apply patch button");
}
catch (Exception ex)
{
var errorMessage = new TextObject("{=UP_Msg_ReapplyError}[Universal Patch] Error reapplying patches: {ERROR}");
errorMessage.SetTextVariable("ERROR", ex.Message);
LoggingService.DisplayMessage(new InformationMessage(errorMessage.ToString(), AlkeariPalette.Attention));
}
}
private void ApplyPatchesIfPossible(string reason)
{
_executionService?.ExecutePatchingIfPossible(reason);
}
/// <summary>
/// Checks whether the hotkey combination is currently pressed.
/// Reads directly from MCM settings each frame for instant responsiveness.
/// </summary>
private static bool IsHotkeyPressed(UniversalPatchSettings settings)
{
return HotkeyHelper.IsHotkeyPressed(
settings.GetSelectedKey(),
settings.GetSelectedModifierIndex());
}
}
}