Skip to content

Commit 06a6fca

Browse files
ManaetherManaether
andauthored
ISEKAI RPG LEVELING MPcompat (#542)
* fix/update_stats_windows_synchronisation * fix(logs): cleanup some logs in a debug state and refactor stat management with list * style(IsekaiRpgLeveling): format whole file * fix(raid): fix human raider having desync on rank and stats * Revert "fix(raid): fix human raider having desync on rank and stats" This reverts commit 0460bbb. * fix/revert_last_commit_and_update_rng_spawn_fix * fix/put_log_behind_flag * fix/sync_spawn_tree_rng * fix/refactor_and_formating_and_small_fix_to_rng_fixes * fix/fix_crash_from_random_override * fix/fix_bulk_manacore_usage * fix/raid_fix * style/cleanup * fix: updated nameof(_currentPawnRng) to nameof(Random) and get iTabSelPawnGetter at init to reduce load * fix: patch Random using PatchingUtilities.RandRedirector * fix: ITabFillTabPrefix get selected pawn from Selector --------- Co-authored-by: Manaether <manaether@ManaStone.localdomain>
1 parent cd92605 commit 06a6fca

1 file changed

Lines changed: 370 additions & 0 deletions

File tree

Source/Mods/IsekaiRPG.cs

Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Reflection;
4+
using System.Reflection.Emit;
5+
using HarmonyLib;
6+
using Multiplayer.API;
7+
using RimWorld;
8+
using Verse;
9+
10+
namespace Multiplayer.Compat
11+
{
12+
/// <summary>Isekai RPG Leveling by Jelly Creative</summary>
13+
/// <see href="https://steamcommunity.com/sharedfiles/filedetails/?id=3657580708"/>
14+
[MpCompatFor("JellyCreative.IsekaiLeveling")]
15+
public class IsekaiRPGCompat
16+
{
17+
private static readonly string[] StatAllocationFieldNames = ["strength", "vitality", "dexterity", "intelligence", "wisdom", "charisma"];
18+
private static readonly string[] PendingFieldNames = ["pendingSTR", "pendingVIT", "pendingDEX", "pendingINT", "pendingWIS", "pendingCHA"];
19+
20+
// ── Types ──────────────────────────────────────────────────────────
21+
private static Type isekaiComponentType;
22+
private static Type isekaiStatAllocationType;
23+
private static Type passiveTreeTrackerType;
24+
private static Type windowStatsType;
25+
private static Type iTabType;
26+
private static Type raidRankSystemType;
27+
private static Type pawnStatGeneratorType;
28+
private static Type treeAutoAssignerType;
29+
private static Type manaCoreCompType;
30+
31+
// ── Field accessors ────────────────────────────────────────────────
32+
private static FieldInfo[] statAllocationFields;
33+
private static FieldInfo statAllocationAvailablePoints;
34+
private static FieldInfo isekaiCompStatsField;
35+
private static FieldInfo isekaiCompPassiveTreeField;
36+
private static FieldInfo raidRankSystemRandomField;
37+
private static FieldInfo pawnStatGeneratorRandomField;
38+
private static FieldInfo treeAutoAssignerRngField;
39+
40+
private static AccessTools.FieldRef<object, Pawn> statsWindowPawn;
41+
private static AccessTools.FieldRef<object, int>[] statsWindowPending;
42+
private static AccessTools.FieldRef<object, int> statsWindowPointsSpent;
43+
44+
// ── MP sync fields ─────────────────────────────────────────────────
45+
private static ISyncField[] statSyncFields; // [0..5] stats, [6] availableStatPoints
46+
47+
// ── Constructor ────────────────────────────────────────────────────
48+
public IsekaiRPGCompat(ModContentPack mod)
49+
{
50+
isekaiComponentType = Resolve("IsekaiLeveling.IsekaiComponent");
51+
isekaiStatAllocationType = Resolve("IsekaiLeveling.IsekaiStatAllocation");
52+
iTabType = Resolve("IsekaiLeveling.UI.ITab_IsekaiStats");
53+
windowStatsType = Resolve("IsekaiLeveling.UI.Window_StatsAttribution");
54+
passiveTreeTrackerType = Resolve("IsekaiLeveling.SkillTree.PassiveTreeTracker");
55+
raidRankSystemType = Resolve("IsekaiLeveling.MobRanking.RaidRankSystem");
56+
pawnStatGeneratorType = Resolve("IsekaiLeveling.PawnStatGenerator");
57+
treeAutoAssignerType = Resolve("IsekaiLeveling.SkillTree.TreeAutoAssigner");
58+
manaCoreCompType = Resolve("IsekaiLeveling.CompUseEffect_ManaCore");
59+
60+
if (isekaiComponentType == null || isekaiStatAllocationType == null
61+
|| passiveTreeTrackerType == null || windowStatsType == null
62+
|| iTabType == null || raidRankSystemType == null
63+
|| pawnStatGeneratorType == null || treeAutoAssignerType == null
64+
|| manaCoreCompType == null)
65+
{
66+
Log.Error("[IsekaiMP] One or more required types could not be resolved — patches will NOT be applied.");
67+
return;
68+
}
69+
70+
raidRankSystemRandomField = AccessTools.Field(raidRankSystemType, "random");
71+
pawnStatGeneratorRandomField = AccessTools.Field(pawnStatGeneratorType, "random");
72+
treeAutoAssignerRngField = AccessTools.Field(treeAutoAssignerType, "rng");
73+
74+
raidRankSystemRandomField.SetValue(null, PatchingUtilities.RandRedirector.Instance);
75+
pawnStatGeneratorRandomField.SetValue(null, PatchingUtilities.RandRedirector.Instance);
76+
treeAutoAssignerRngField.SetValue(null, PatchingUtilities.RandRedirector.Instance);
77+
78+
if (raidRankSystemRandomField == null || pawnStatGeneratorRandomField == null || treeAutoAssignerRngField == null)
79+
{
80+
Log.Error("[IsekaiMP] One or more required fields could not be resolved — patches will NOT be applied.");
81+
return;
82+
}
83+
84+
isekaiCompStatsField = AccessTools.Field(isekaiComponentType, "stats");
85+
isekaiCompPassiveTreeField = AccessTools.Field(isekaiComponentType, "passiveTree");
86+
87+
statAllocationFields = new FieldInfo[StatAllocationFieldNames.Length];
88+
for (int i = 0; i < StatAllocationFieldNames.Length; i++)
89+
statAllocationFields[i] = AccessTools.Field(isekaiStatAllocationType, StatAllocationFieldNames[i]);
90+
statAllocationAvailablePoints = AccessTools.Field(isekaiStatAllocationType, "availableStatPoints");
91+
92+
statSyncFields = new ISyncField[StatAllocationFieldNames.Length + 1];
93+
for (int i = 0; i < StatAllocationFieldNames.Length; i++)
94+
statSyncFields[i] = MP.RegisterSyncField(isekaiStatAllocationType, StatAllocationFieldNames[i]);
95+
statSyncFields[StatAllocationFieldNames.Length] = MP.RegisterSyncField(isekaiStatAllocationType, "availableStatPoints");
96+
97+
statsWindowPawn = AccessTools.FieldRefAccess<Pawn>(windowStatsType, "pawn");
98+
statsWindowPending = new AccessTools.FieldRef<object, int>[PendingFieldNames.Length];
99+
for (int i = 0; i < PendingFieldNames.Length; i++)
100+
statsWindowPending[i] = AccessTools.FieldRefAccess<int>(windowStatsType, PendingFieldNames[i]);
101+
statsWindowPointsSpent = AccessTools.FieldRefAccess<int>(windowStatsType, "pointsSpent");
102+
103+
PatchAndLog(isekaiComponentType, "DevAddLevel", prefix: nameof(DevAddLevelPrefix));
104+
PatchAndLog(iTabType, "FillTab", prefix: nameof(ITabFillTabPrefix), postfix: nameof(ITabFillTabPostfix));
105+
PatchAndLog(windowStatsType, "ApplyChanges", prefix: nameof(ApplyChangesPrefix));
106+
PatchAndLog(passiveTreeTrackerType, "Unlock", prefix: nameof(UnlockNodePrefix));
107+
PatchAndLog(passiveTreeTrackerType, "Respec", prefix: nameof(RespecPrefix));
108+
109+
MP.RegisterSyncWorker<object>(SyncIsekaiStatAllocation, isekaiStatAllocationType);
110+
MP.RegisterSyncMethod(typeof(IsekaiRPGCompat), nameof(SyncedDevAddLevel));
111+
MP.RegisterSyncMethod(typeof(IsekaiRPGCompat), nameof(SyncedApplyStats));
112+
MP.RegisterSyncMethod(typeof(IsekaiRPGCompat), nameof(SyncedUnlockNode));
113+
MP.RegisterSyncMethod(typeof(IsekaiRPGCompat), nameof(SyncedRespec));
114+
MP.RegisterSyncDelegateLambda(manaCoreCompType, "GetBulkAbsorbOptions", 0);
115+
}
116+
117+
private static Type Resolve(string typeName)
118+
{
119+
var type = AccessTools.TypeByName(typeName);
120+
if (type == null)
121+
Log.Error($"[IsekaiMP] Could not resolve type '{typeName}' — mod version may have changed.");
122+
return type;
123+
}
124+
125+
private static void PatchAndLog(Type targetType, string methodName, string prefix = null, string postfix = null, string transpiler = null)
126+
{
127+
var method = AccessTools.DeclaredMethod(targetType, methodName);
128+
if (method == null)
129+
{
130+
Log.Error($"[IsekaiMP] Could not find method '{targetType.Name}.{methodName}' — patch skipped.");
131+
return;
132+
}
133+
MpCompat.harmony.Patch(method,
134+
prefix: prefix != null ? new HarmonyMethod(typeof(IsekaiRPGCompat), prefix) : null,
135+
postfix: postfix != null ? new HarmonyMethod(typeof(IsekaiRPGCompat), postfix) : null,
136+
transpiler: transpiler != null ? new HarmonyMethod(typeof(IsekaiRPGCompat), transpiler) : null);
137+
}
138+
139+
private static ThingComp GetCompByType(Pawn pawn, Type compType)
140+
{
141+
foreach (var comp in pawn.AllComps)
142+
if (compType.IsInstanceOfType(comp))
143+
return comp;
144+
return null;
145+
}
146+
147+
private static Pawn FindPawnByComp(Func<ThingComp, bool> match)
148+
{
149+
foreach (var map in Find.Maps)
150+
foreach (var pawn in map.mapPawns.AllPawnsSpawned)
151+
{
152+
var comp = GetCompByType(pawn, isekaiComponentType);
153+
if (comp != null && match(comp))
154+
return pawn;
155+
}
156+
157+
if (Find.WorldPawns != null)
158+
foreach (var pawn in Find.WorldPawns.AllPawnsAlive)
159+
{
160+
var comp = GetCompByType(pawn, isekaiComponentType);
161+
if (comp != null && match(comp))
162+
return pawn;
163+
}
164+
165+
return null;
166+
}
167+
168+
// ═══════════════════════════════════════════════════════════════════
169+
// STAT ALLOCATION
170+
// ═══════════════════════════════════════════════════════════════════
171+
172+
private static bool ApplyChangesPrefix(object __instance)
173+
{
174+
if (!MP.IsInMultiplayer) return true;
175+
176+
Pawn pawn = statsWindowPawn(__instance);
177+
if (pawn == null) return true;
178+
179+
var pending = new int[StatAllocationFieldNames.Length];
180+
for (int i = 0; i < pending.Length; i++)
181+
pending[i] = statsWindowPending[i](__instance);
182+
183+
int pointsSpent = 0;
184+
bool godMode = Prefs.DevMode && DebugSettings.godMode;
185+
if (!godMode)
186+
{
187+
var comp = GetCompByType(pawn, isekaiComponentType);
188+
if (comp != null)
189+
{
190+
object statsObj = isekaiCompStatsField.GetValue(comp);
191+
for (int i = 0; i < pending.Length; i++)
192+
pointsSpent += pending[i] - (int)statAllocationFields[i].GetValue(statsObj);
193+
}
194+
}
195+
196+
SyncedApplyStats(pawn, pending, pointsSpent, godMode);
197+
return false;
198+
}
199+
200+
private static void SyncedApplyStats(Pawn pawn, int[] statValues, int pointsSpent, bool godMode)
201+
{
202+
var comp = GetCompByType(pawn, isekaiComponentType);
203+
if (comp == null) return;
204+
205+
object statsObj = isekaiCompStatsField.GetValue(comp);
206+
207+
for (int i = 0; i < statAllocationFields.Length; i++)
208+
statAllocationFields[i].SetValue(statsObj, statValues[i]);
209+
210+
if (!godMode && pointsSpent > 0)
211+
{
212+
int remaining = (int)statAllocationAvailablePoints.GetValue(statsObj);
213+
statAllocationAvailablePoints.SetValue(statsObj, remaining - pointsSpent);
214+
}
215+
216+
RefreshStatsWindows(pawn, statsObj);
217+
}
218+
219+
private static void RefreshStatsWindows(Pawn pawn, object statsObj)
220+
{
221+
foreach (var window in Find.WindowStack.Windows)
222+
{
223+
if (!windowStatsType.IsInstanceOfType(window)) continue;
224+
if (!ReferenceEquals(statsWindowPawn(window), pawn)) continue;
225+
226+
for (int i = 0; i < statAllocationFields.Length; i++)
227+
statsWindowPending[i](window) = (int)statAllocationFields[i].GetValue(statsObj);
228+
229+
statsWindowPointsSpent(window) = 0;
230+
}
231+
}
232+
233+
// ═══════════════════════════════════════════════════════════════════
234+
// ITAB — stat field watch
235+
// ═══════════════════════════════════════════════════════════════════
236+
237+
private static void ITabFillTabPrefix(object __instance, ref bool __state)
238+
{
239+
if (!MP.IsInMultiplayer) return;
240+
241+
if (Find.Selector.SingleSelectedThing is not Pawn selectedPawn) return;
242+
243+
var comp = GetCompByType(selectedPawn, isekaiComponentType);
244+
var stats = comp != null ? isekaiCompStatsField.GetValue(comp) : null;
245+
if (stats == null) return;
246+
247+
__state = true;
248+
MP.WatchBegin();
249+
foreach (var field in statSyncFields)
250+
field.Watch(stats);
251+
}
252+
253+
private static void ITabFillTabPostfix(bool __state)
254+
{
255+
if (__state) MP.WatchEnd();
256+
}
257+
258+
// ═══════════════════════════════════════════════════════════════════
259+
// SKILL TREE
260+
// ═══════════════════════════════════════════════════════════════════
261+
262+
private static bool _suppressUnlockPrefix = false;
263+
264+
private static bool UnlockNodePrefix(string nodeId, Pawn pawn, ref bool __result)
265+
{
266+
if (!MP.IsInMultiplayer || _suppressUnlockPrefix || pawn == null) return true;
267+
SyncedUnlockNode(pawn, nodeId);
268+
__result = false;
269+
return false;
270+
}
271+
272+
private static void SyncedUnlockNode(Pawn pawn, string nodeId)
273+
{
274+
var comp = GetCompByType(pawn, isekaiComponentType);
275+
if (comp == null) { Log.Warning($"[IsekaiMP] SyncedUnlockNode: no IsekaiComponent on {pawn.LabelShort}"); return; }
276+
277+
var passiveTree = isekaiCompPassiveTreeField.GetValue(comp);
278+
if (passiveTree == null) { Log.Warning($"[IsekaiMP] SyncedUnlockNode: null passiveTree on {pawn.LabelShort}"); return; }
279+
280+
_suppressUnlockPrefix = true;
281+
try { AccessTools.DeclaredMethod(passiveTreeTrackerType, "Unlock").Invoke(passiveTree, [nodeId, pawn]); }
282+
finally { _suppressUnlockPrefix = false; }
283+
}
284+
285+
private static bool _suppressRespecPrefix = false;
286+
287+
private static bool RespecPrefix(object __instance)
288+
{
289+
if (!MP.IsInMultiplayer || _suppressRespecPrefix) return true;
290+
291+
Pawn owner = FindPawnByComp(c => ReferenceEquals(isekaiCompPassiveTreeField.GetValue(c), __instance));
292+
if (owner == null)
293+
{
294+
Log.Warning("[IsekaiMP] RespecPrefix: could not identify owning pawn — running locally (may desync!)");
295+
return true;
296+
}
297+
298+
SyncedRespec(owner);
299+
return false;
300+
}
301+
302+
private static void SyncedRespec(Pawn pawn)
303+
{
304+
var comp = GetCompByType(pawn, isekaiComponentType);
305+
if (comp == null) { Log.Warning($"[IsekaiMP] SyncedRespec: no IsekaiComponent on {pawn.LabelShort}"); return; }
306+
307+
var passiveTree = isekaiCompPassiveTreeField.GetValue(comp);
308+
if (passiveTree == null) { Log.Warning($"[IsekaiMP] SyncedRespec: null passiveTree on {pawn.LabelShort}"); return; }
309+
310+
_suppressRespecPrefix = true;
311+
try { AccessTools.DeclaredMethod(passiveTreeTrackerType, "Respec").Invoke(passiveTree, null); }
312+
finally { _suppressRespecPrefix = false; }
313+
}
314+
315+
// ═══════════════════════════════════════════════════════════════════
316+
// DEV TOOLS
317+
// ═══════════════════════════════════════════════════════════════════
318+
319+
private static bool _suppressDevAddLevelPrefix = false;
320+
321+
private static bool DevAddLevelPrefix(object __instance, int levels)
322+
{
323+
if (!MP.IsInMultiplayer || _suppressDevAddLevelPrefix) return true;
324+
325+
Pawn pawn = FindPawnByComp(c => ReferenceEquals(c, __instance));
326+
if (pawn == null)
327+
{
328+
Log.Warning("[IsekaiMP] DevAddLevelPrefix: could not identify owning pawn — running locally (may desync!)");
329+
return true;
330+
}
331+
332+
SyncedDevAddLevel(pawn, levels);
333+
return false;
334+
}
335+
336+
private static void SyncedDevAddLevel(Pawn pawn, int levels)
337+
{
338+
var comp = GetCompByType(pawn, isekaiComponentType);
339+
if (comp == null) { Log.Warning($"[IsekaiMP] SyncedDevAddLevel: no IsekaiComponent on {pawn.LabelShort}"); return; }
340+
341+
_suppressDevAddLevelPrefix = true;
342+
try { AccessTools.DeclaredMethod(isekaiComponentType, "DevAddLevel").Invoke(comp, [levels]); }
343+
finally { _suppressDevAddLevelPrefix = false; }
344+
}
345+
346+
// ═══════════════════════════════════════════════════════════════════
347+
// SYNC WORKER — IsekaiStatAllocation
348+
// ═══════════════════════════════════════════════════════════════════
349+
350+
private static void SyncIsekaiStatAllocation(SyncWorker sync, ref object statsAllocation)
351+
{
352+
if (sync.isWriting)
353+
{
354+
var statsAllocationRef = statsAllocation;
355+
sync.Write(FindPawnByComp(c => ReferenceEquals(isekaiCompStatsField.GetValue(c), statsAllocationRef)));
356+
}
357+
else
358+
{
359+
var pawn = sync.Read<Pawn>();
360+
if (pawn != null)
361+
{
362+
var comp = GetCompByType(pawn, isekaiComponentType);
363+
if (comp != null)
364+
statsAllocation = isekaiCompStatsField.GetValue(comp);
365+
}
366+
}
367+
}
368+
369+
}
370+
}

0 commit comments

Comments
 (0)