@@ -17,7 +17,8 @@ public class IsekaiRPGCompat
1717 private static readonly string [ ] StatAllocationFieldNames = [ "strength" , "vitality" , "dexterity" , "intelligence" , "wisdom" , "charisma" ] ;
1818 private static readonly string [ ] PendingFieldNames = [ "pendingSTR" , "pendingVIT" , "pendingDEX" , "pendingINT" , "pendingWIS" , "pendingCHA" ] ;
1919
20- // ── Types ──────────────────────────────────────────────────────────
20+ #region Fields
21+
2122 private static Type isekaiComponentType ;
2223 private static Type isekaiStatAllocationType ;
2324 private static Type passiveTreeTrackerType ;
@@ -27,24 +28,32 @@ public class IsekaiRPGCompat
2728 private static Type pawnStatGeneratorType ;
2829 private static Type treeAutoAssignerType ;
2930 private static Type manaCoreCompType ;
31+ private static Type windowCreatureStatsType ;
32+ private static Type iTabCreatureStatsType ;
33+ private static Type mobRankComponentType ;
3034
31- // ── Field accessors ────────────────────────────────────────────────
3235 private static FieldInfo [ ] statAllocationFields ;
3336 private static FieldInfo statAllocationAvailablePoints ;
3437 private static FieldInfo isekaiCompStatsField ;
3538 private static FieldInfo isekaiCompPassiveTreeField ;
3639 private static FieldInfo raidRankSystemRandomField ;
3740 private static FieldInfo pawnStatGeneratorRandomField ;
3841 private static FieldInfo treeAutoAssignerRngField ;
42+ private static FieldInfo mobRankStatsField ;
3943
4044 private static AccessTools . FieldRef < object , Pawn > statsWindowPawn ;
4145 private static AccessTools . FieldRef < object , int > [ ] statsWindowPending ;
4246 private static AccessTools . FieldRef < object , int > statsWindowPointsSpent ;
47+ private static AccessTools . FieldRef < object , Pawn > creatureWindowPawn ;
48+ private static AccessTools . FieldRef < object , int > [ ] creatureWindowPending ;
49+ private static AccessTools . FieldRef < object , int > creatureWindowPointsSpent ;
50+
51+ private static ISyncField [ ] statSyncFields ;
52+
53+ #endregion
4354
44- // ── MP sync fields ─────────────────────────────────────────────────
45- private static ISyncField [ ] statSyncFields ; // [0..5] stats, [6] availableStatPoints
55+ #region Constructor
4656
47- // ── Constructor ────────────────────────────────────────────────────
4857 public IsekaiRPGCompat ( ModContentPack mod )
4958 {
5059 isekaiComponentType = Resolve ( "IsekaiLeveling.IsekaiComponent" ) ;
@@ -77,43 +86,74 @@ public IsekaiRPGCompat(ModContentPack mod)
7786
7887 if ( raidRankSystemRandomField == null || pawnStatGeneratorRandomField == null || treeAutoAssignerRngField == null )
7988 {
80- Log . Error ( "[IsekaiMP] One or more required fields could not be resolved — patches will NOT be applied." ) ;
89+ Log . Warning ( "[IsekaiMP] One or more required fields could not be resolved — patches will NOT be applied." ) ;
8190 return ;
8291 }
92+ else
93+ {
8394
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 ) ;
95+ isekaiCompStatsField = AccessTools . Field ( isekaiComponentType , "stats" ) ;
96+ isekaiCompPassiveTreeField = AccessTools . Field ( isekaiComponentType , "passiveTree" ) ;
97+
98+ statAllocationFields = new FieldInfo [ StatAllocationFieldNames . Length ] ;
99+ for ( int i = 0 ; i < StatAllocationFieldNames . Length ; i ++ )
100+ statAllocationFields [ i ] = AccessTools . Field ( isekaiStatAllocationType , StatAllocationFieldNames [ i ] ) ;
101+ statAllocationAvailablePoints = AccessTools . Field ( isekaiStatAllocationType , "availableStatPoints" ) ;
102+
103+ statSyncFields = new ISyncField [ StatAllocationFieldNames . Length + 1 ] ;
104+ for ( int i = 0 ; i < StatAllocationFieldNames . Length ; i ++ )
105+ statSyncFields [ i ] = MP . RegisterSyncField ( isekaiStatAllocationType , StatAllocationFieldNames [ i ] ) ;
106+ statSyncFields [ StatAllocationFieldNames . Length ] = MP . RegisterSyncField ( isekaiStatAllocationType , "availableStatPoints" ) ;
107+
108+ statsWindowPawn = AccessTools . FieldRefAccess < Pawn > ( windowStatsType , "pawn" ) ;
109+ statsWindowPending = new AccessTools . FieldRef < object , int > [ PendingFieldNames . Length ] ;
110+ for ( int i = 0 ; i < PendingFieldNames . Length ; i ++ )
111+ statsWindowPending [ i ] = AccessTools . FieldRefAccess < int > ( windowStatsType , PendingFieldNames [ i ] ) ;
112+ statsWindowPointsSpent = AccessTools . FieldRefAccess < int > ( windowStatsType , "pointsSpent" ) ;
113+
114+ PatchAndLog ( isekaiComponentType , "DevAddLevel" , prefix : nameof ( DevAddLevelPrefix ) ) ;
115+ PatchAndLog ( iTabType , "FillTab" , prefix : nameof ( ITabFillTabPrefix ) , postfix : nameof ( ITabFillTabPostfix ) ) ;
116+ PatchAndLog ( windowStatsType , "ApplyChanges" , prefix : nameof ( ApplyChangesPrefix ) ) ;
117+ PatchAndLog ( passiveTreeTrackerType , "Unlock" , prefix : nameof ( UnlockNodePrefix ) ) ;
118+ PatchAndLog ( passiveTreeTrackerType , "Respec" , prefix : nameof ( RespecPrefix ) ) ;
119+
120+ MP . RegisterSyncWorker < object > ( SyncIsekaiStatAllocation , isekaiStatAllocationType ) ;
121+ MP . RegisterSyncMethod ( typeof ( IsekaiRPGCompat ) , nameof ( SyncedDevAddLevel ) ) ;
122+ MP . RegisterSyncMethod ( typeof ( IsekaiRPGCompat ) , nameof ( SyncedApplyStats ) ) ;
123+ MP . RegisterSyncMethod ( typeof ( IsekaiRPGCompat ) , nameof ( SyncedUnlockNode ) ) ;
124+ MP . RegisterSyncMethod ( typeof ( IsekaiRPGCompat ) , nameof ( SyncedRespec ) ) ;
125+ MP . RegisterSyncDelegateLambda ( manaCoreCompType , "GetBulkAbsorbOptions" , 0 ) ;
126+ }
127+
128+ windowCreatureStatsType = AccessTools . TypeByName ( "IsekaiLeveling.UI.Window_CreatureStats" ) ;
129+ iTabCreatureStatsType = AccessTools . TypeByName ( "IsekaiLeveling.UI.ITab_CreatureStats" ) ;
130+ mobRankComponentType = AccessTools . TypeByName ( "IsekaiLeveling.MobRanking.MobRankComponent" ) ;
131+
132+ if ( windowCreatureStatsType == null || iTabCreatureStatsType == null || mobRankComponentType == null )
133+ {
134+ Log . Warning ( "[IsekaiMP] Animal stats types not found — animal stat sync will not be applied." ) ;
135+ }
136+ else
137+ {
138+ mobRankStatsField = AccessTools . Field ( mobRankComponentType , "stats" ) ;
139+
140+ creatureWindowPawn = AccessTools . FieldRefAccess < Pawn > ( windowCreatureStatsType , "pawn" ) ;
141+ creatureWindowPending = new AccessTools . FieldRef < object , int > [ PendingFieldNames . Length ] ;
142+ for ( int i = 0 ; i < PendingFieldNames . Length ; i ++ )
143+ creatureWindowPending [ i ] = AccessTools . FieldRefAccess < int > ( windowCreatureStatsType , PendingFieldNames [ i ] ) ;
144+ creatureWindowPointsSpent = AccessTools . FieldRefAccess < int > ( windowCreatureStatsType , "pointsSpent" ) ;
145+
146+ PatchAndLog ( windowCreatureStatsType , "ApplyChanges" , prefix : nameof ( CreatureApplyChangesPrefix ) ) ;
147+ PatchAndLog ( iTabCreatureStatsType , "FillTab" , prefix : nameof ( ITabCreatureFillTabPrefix ) , postfix : nameof ( ITabFillTabPostfix ) ) ;
148+
149+ MP . RegisterSyncMethod ( typeof ( IsekaiRPGCompat ) , nameof ( SyncedApplyCreatureStats ) ) ;
150+ }
115151 }
116152
153+ #endregion
154+
155+ #region Utility
156+
117157 private static Type Resolve ( string typeName )
118158 {
119159 var type = AccessTools . TypeByName ( typeName ) ;
@@ -165,9 +205,9 @@ private static Pawn FindPawnByComp(Func<ThingComp, bool> match)
165205 return null ;
166206 }
167207
168- // ═══════════════════════════════════════════════════════════════════
169- // STAT ALLOCATION
170- // ═══════════════════════════════════════════════════════════════════
208+ #endregion
209+
210+ #region Stat Allocation
171211
172212 private static bool ApplyChangesPrefix ( object __instance )
173213 {
@@ -230,9 +270,9 @@ private static void RefreshStatsWindows(Pawn pawn, object statsObj)
230270 }
231271 }
232272
233- // ═══════════════════════════════════════════════════════════════════
234- // ITAB — stat field watch
235- // ═══════════════════════════════════════════════════════════════════
273+ #endregion
274+
275+ #region ITab
236276
237277 private static void ITabFillTabPrefix ( object __instance , ref bool __state )
238278 {
@@ -255,9 +295,9 @@ private static void ITabFillTabPostfix(bool __state)
255295 if ( __state ) MP . WatchEnd ( ) ;
256296 }
257297
258- // ═══════════════════════════════════════════════════════════════════
259- // SKILL TREE
260- // ═══════════════════════════════════════════════════════════════════
298+ #endregion
299+
300+ #region Skill Tree
261301
262302 private static bool _suppressUnlockPrefix = false ;
263303
@@ -312,9 +352,9 @@ private static void SyncedRespec(Pawn pawn)
312352 finally { _suppressRespecPrefix = false ; }
313353 }
314354
315- // ═══════════════════════════════════════════════════════════════════
316- // DEV TOOLS
317- // ═══════════════════════════════════════════════════════════════════
355+ #endregion
356+
357+ #region Dev Tools
318358
319359 private static bool _suppressDevAddLevelPrefix = false ;
320360
@@ -343,9 +383,9 @@ private static void SyncedDevAddLevel(Pawn pawn, int levels)
343383 finally { _suppressDevAddLevelPrefix = false ; }
344384 }
345385
346- // ═══════════════════════════════════════════════════════════════════
347- // SYNC WORKER — IsekaiStatAllocation
348- // ═══════════════════════════════════════════════════════════════════
386+ #endregion
387+
388+ #region Sync Worker
349389
350390 private static void SyncIsekaiStatAllocation ( SyncWorker sync , ref object statsAllocation )
351391 {
@@ -366,5 +406,88 @@ private static void SyncIsekaiStatAllocation(SyncWorker sync, ref object statsAl
366406 }
367407 }
368408
409+ #endregion
410+
411+ #region Animal Stats
412+
413+ private static bool CreatureApplyChangesPrefix ( object __instance )
414+ {
415+ if ( ! MP . IsInMultiplayer ) return true ;
416+
417+ Pawn pawn = creatureWindowPawn ( __instance ) ;
418+ if ( pawn == null ) return true ;
419+
420+ var pending = new int [ StatAllocationFieldNames . Length ] ;
421+ for ( int i = 0 ; i < pending . Length ; i ++ )
422+ pending [ i ] = creatureWindowPending [ i ] ( __instance ) ;
423+
424+ int pointsSpent = 0 ;
425+ bool godMode = Prefs . DevMode && DebugSettings . godMode ;
426+ if ( ! godMode )
427+ {
428+ var comp = GetCompByType ( pawn , mobRankComponentType ) ;
429+ if ( comp != null )
430+ {
431+ object statsObj = mobRankStatsField . GetValue ( comp ) ;
432+ for ( int i = 0 ; i < pending . Length ; i ++ )
433+ pointsSpent += pending [ i ] - ( int ) statAllocationFields [ i ] . GetValue ( statsObj ) ;
434+ }
435+ }
436+
437+ SyncedApplyCreatureStats ( pawn , pending , pointsSpent , godMode ) ;
438+ return false ;
439+ }
440+
441+ private static void SyncedApplyCreatureStats ( Pawn pawn , int [ ] statValues , int pointsSpent , bool godMode )
442+ {
443+ var comp = GetCompByType ( pawn , mobRankComponentType ) ;
444+ if ( comp == null ) return ;
445+
446+ object statsObj = mobRankStatsField . GetValue ( comp ) ;
447+ if ( statsObj == null ) return ;
448+
449+ for ( int i = 0 ; i < statAllocationFields . Length ; i ++ )
450+ statAllocationFields [ i ] . SetValue ( statsObj , statValues [ i ] ) ;
451+
452+ if ( ! godMode && pointsSpent > 0 )
453+ {
454+ int remaining = ( int ) statAllocationAvailablePoints . GetValue ( statsObj ) ;
455+ statAllocationAvailablePoints . SetValue ( statsObj , remaining - pointsSpent ) ;
456+ }
457+
458+ RefreshCreatureStatsWindows ( pawn , statsObj ) ;
459+ }
460+
461+ private static void RefreshCreatureStatsWindows ( Pawn pawn , object statsObj )
462+ {
463+ foreach ( var window in Find . WindowStack . Windows )
464+ {
465+ if ( ! windowCreatureStatsType . IsInstanceOfType ( window ) ) continue ;
466+ if ( ! ReferenceEquals ( creatureWindowPawn ( window ) , pawn ) ) continue ;
467+
468+ for ( int i = 0 ; i < statAllocationFields . Length ; i ++ )
469+ creatureWindowPending [ i ] ( window ) = ( int ) statAllocationFields [ i ] . GetValue ( statsObj ) ;
470+
471+ creatureWindowPointsSpent ( window ) = 0 ;
472+ }
473+ }
474+
475+ private static void ITabCreatureFillTabPrefix ( object __instance , ref bool __state )
476+ {
477+ if ( ! MP . IsInMultiplayer ) return ;
478+
479+ if ( Find . Selector . SingleSelectedThing is not Pawn selectedPawn ) return ;
480+
481+ var comp = GetCompByType ( selectedPawn , mobRankComponentType ) ;
482+ var stats = comp != null ? mobRankStatsField . GetValue ( comp ) : null ;
483+ if ( stats == null ) return ;
484+
485+ __state = true ;
486+ MP . WatchBegin ( ) ;
487+ foreach ( var field in statSyncFields )
488+ field . Watch ( stats ) ;
489+ }
490+
491+ #endregion
369492 }
370493}
0 commit comments