Skip to content

Commit 70d4a7b

Browse files
committed
feat(patching): enhance run-history and character selection patches
- Added `RunHistoryMissingModelDbGetByIdPatch` to provide fallbacks for missing character and act models in run-history UI. - Introduced `CharacterVanillaSelectionPolicyAllCharactersPatch` to filter character selection based on the current selection scope. - Updated `CharacterVanillaSelectionPolicyPatches` to maintain selection policy scope during character selection processes. - Registered new patches in `PatcherSetup` to improve handling of missing models and character visibility in vanilla selection.
1 parent fa474eb commit 70d4a7b

5 files changed

Lines changed: 189 additions & 91 deletions

File tree

Const.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,15 @@ public static class Const
2929
/// On-disk settings file name.
3030
/// </summary>
3131
public const string SettingsFileName = "settings.json";
32+
33+
/// <summary>
34+
/// BaseLib main Harmony instance id.
35+
/// </summary>
36+
public const string BaseLibHarmonyId = "BaseLib";
37+
38+
/// <summary>
39+
/// Harmony id used by RitsuLib content-registry patcher.
40+
/// </summary>
41+
public const string FrameworkContentRegistryHarmonyId = ModId + ".framework-content-registry";
3242
}
3343
}
Lines changed: 69 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
using System.Reflection;
2-
using System.Reflection.Emit;
3-
using HarmonyLib;
41
using MegaCrit.Sts2.Core.Models;
52
using MegaCrit.Sts2.Core.Nodes.Screens.RunHistoryScreen;
63
using MegaCrit.Sts2.Core.Rooms;
@@ -10,26 +7,35 @@
107

118
namespace STS2RitsuLib.Lifecycle.Patches
129
{
10+
internal static class RunHistoryMissingModelScope
11+
{
12+
[ThreadStatic] private static int _depth;
13+
14+
internal static bool IsActive => _depth > 0;
15+
16+
internal static void Enter()
17+
{
18+
_depth++;
19+
}
20+
21+
internal static void Exit()
22+
{
23+
if (_depth > 0)
24+
_depth--;
25+
}
26+
}
27+
1328
/// <summary>
14-
/// Replaces <c>ModelDb.GetById&lt;CharacterModel&gt;</c> and <c>GetById&lt;ActModel&gt;</c> in run-history UI with
15-
/// <see cref="RunHistoryMissingModelSupport" /> so missing mod content does not throw.
29+
/// Creates an execution scope for run-history UI methods that may read missing mod models.
1630
/// </summary>
1731
public class RunHistoryMissingModelDbGetByIdTranspilerPatch : IPatchMethod
1832
{
19-
private static readonly MethodInfo CharacterFallback =
20-
AccessTools.DeclaredMethod(typeof(RunHistoryMissingModelSupport),
21-
nameof(RunHistoryMissingModelSupport.CharacterForRunHistory));
22-
23-
private static readonly MethodInfo ActFallback =
24-
AccessTools.DeclaredMethod(typeof(RunHistoryMissingModelSupport),
25-
nameof(RunHistoryMissingModelSupport.ActForRunHistory));
26-
2733
/// <inheritdoc />
2834
public static string PatchId => "run_history_missing_model_db_getbyid_transpile";
2935

3036
/// <inheritdoc />
3137
public static string Description =>
32-
"Transpile run-history methods to use Character/Act fallbacks when ModelDb has no entry";
38+
"Create run-history scope for missing-model fallbacks";
3339

3440
/// <inheritdoc />
3541
public static bool IsCritical => false;
@@ -52,41 +58,64 @@ public static ModPatchTarget[] GetTargets()
5258
}
5359

5460
/// <summary>
55-
/// Harmony transpiler: redirect ModelDb lookups to RitsuLib fallbacks.
61+
/// Enters run-history missing-model support scope.
5662
/// </summary>
57-
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
63+
public static void Prefix()
5864
{
59-
foreach (var code in instructions)
60-
{
61-
if (code.operand is MethodInfo called)
62-
{
63-
if (IsModelDbGetByIdFor(called, typeof(CharacterModel)))
64-
{
65-
code.opcode = OpCodes.Call;
66-
code.operand = CharacterFallback;
67-
}
68-
else if (IsModelDbGetByIdFor(called, typeof(ActModel)))
69-
{
70-
code.opcode = OpCodes.Call;
71-
code.operand = ActFallback;
72-
}
73-
}
74-
75-
yield return code;
76-
}
65+
RunHistoryMissingModelScope.Enter();
7766
}
7867

79-
private static bool IsModelDbGetByIdFor(MethodInfo mi, Type typeArg)
68+
/// <summary>
69+
/// Exits run-history scope even if the target method throws.
70+
/// </summary>
71+
public static void Finalizer()
8072
{
81-
if (!mi.IsGenericMethod || mi.DeclaringType != typeof(ModelDb))
73+
RunHistoryMissingModelScope.Exit();
74+
}
75+
}
76+
77+
/// <summary>
78+
/// Uses run-history-specific fallbacks for missing character/act lookups.
79+
/// </summary>
80+
public class RunHistoryMissingModelDbGetByIdPatch : IPatchMethod
81+
{
82+
/// <inheritdoc />
83+
public static string PatchId => "run_history_missing_model_db_getbyid";
84+
85+
/// <inheritdoc />
86+
public static string Description =>
87+
"Use Character/Act fallbacks in run-history scope when ModelDb.GetById has no entry";
88+
89+
/// <inheritdoc />
90+
public static bool IsCritical => false;
91+
92+
/// <inheritdoc />
93+
public static ModPatchTarget[] GetTargets()
94+
{
95+
return [new(typeof(ModelDb), nameof(ModelDb.GetById), [typeof(ModelId)])];
96+
}
97+
98+
/// <summary>
99+
/// Replaces vanilla GetById throws with run-history fallback models.
100+
/// </summary>
101+
public static bool Prefix<T>(ModelId id, ref T __result) where T : AbstractModel
102+
{
103+
if (!RunHistoryMissingModelScope.IsActive)
104+
return true;
105+
106+
if (typeof(T) == typeof(CharacterModel))
107+
{
108+
__result = (T)(AbstractModel)RunHistoryMissingModelSupport.CharacterForRunHistory(id);
82109
return false;
110+
}
83111

84-
var def = mi.GetGenericMethodDefinition();
85-
if (def.Name != nameof(ModelDb.GetById))
112+
if (typeof(T) == typeof(ActModel))
113+
{
114+
__result = (T)(AbstractModel)RunHistoryMissingModelSupport.ActForRunHistory(id);
86115
return false;
116+
}
87117

88-
var args = mi.GetGenericArguments();
89-
return args.Length == 1 && args[0] == typeArg;
118+
return true;
90119
}
91120
}
92121
}

RitsuLibFramework.PatcherSetup.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ private static void RegisterLifecyclePatches()
8282
patcher.RegisterPatch<NContinueRunInfoShowInfoModelNotFoundPatch>();
8383
patcher.RegisterPatch<NRunHistoryRefreshAndSelectRunSuppressRethrowPatch>();
8484
patcher.RegisterPatch<RunHistoryMissingModelDbGetByIdTranspilerPatch>();
85+
patcher.RegisterPatch<RunHistoryMissingModelDbGetByIdPatch>();
8586
patcher.RegisterPatch<NMultiplayerLoadGameScreenBeginRunMissingCharacterPatch>();
8687
patcher.RegisterPatch<NMultiplayerTestCharacterPaginatorAllCharactersPatch>();
8788
patcher.RegisterPatch<NCustomRunLoadScreenBeginRunMissingCharacterPatch>();
@@ -284,6 +285,7 @@ private static void RegisterCharacterAssetPatches()
284285
patcher.RegisterPatch<CharacterCombatSpineOverridePatch>();
285286
patcher.RegisterPatch<CharacterGameOverScreenCompatibilityPatch>();
286287
patcher.RegisterPatch<CharacterVanillaSelectionPolicyPatches>();
288+
patcher.RegisterPatch<CharacterVanillaSelectionPolicyAllCharactersPatch>();
287289
patcher.RegisterPatch<ModCreatureCombatAnimationPlaybackPatch>();
288290
patcher.RegisterPatch<NCreatureCombatAnimationInitialBootstrapPatch>();
289291
patcher.RegisterPatch<NCreatureNonSpineDeathAnimationTriggerPatch>();
Lines changed: 106 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.Reflection;
2-
using System.Reflection.Emit;
32
using HarmonyLib;
43
using MegaCrit.Sts2.Core.Models;
54
using MegaCrit.Sts2.Core.Multiplayer.Game.Lobby;
@@ -8,22 +7,80 @@
87

98
namespace STS2RitsuLib.Scaffolding.Characters.Patches
109
{
11-
/// <summary>
12-
/// Applies <see cref="IModCharacterVanillaSelectionPolicy" /> when vanilla character-select builds visible and
13-
/// random-eligible character lists.
14-
/// </summary>
15-
public class CharacterVanillaSelectionPolicyPatches : IPatchMethod
10+
internal static class CharacterVanillaSelectionPolicyScope
1611
{
17-
private static readonly MethodInfo? ModelDbAllCharactersGetter =
18-
AccessTools.PropertyGetter(typeof(ModelDb), nameof(ModelDb.AllCharacters));
12+
[ThreadStatic] private static SelectionScope _currentScope;
13+
[ThreadStatic] private static int _scopeDepth;
14+
15+
public static void Enter(MethodBase originalMethod)
16+
{
17+
var scope = ResolveScope(originalMethod);
18+
if (scope == SelectionScope.None)
19+
return;
20+
21+
if (_scopeDepth++ == 0)
22+
_currentScope = scope;
23+
}
24+
25+
public static void Exit(MethodBase originalMethod)
26+
{
27+
var scope = ResolveScope(originalMethod);
28+
if (scope == SelectionScope.None || _scopeDepth <= 0)
29+
return;
30+
31+
_scopeDepth--;
32+
if (_scopeDepth == 0)
33+
_currentScope = SelectionScope.None;
34+
}
35+
36+
public static IEnumerable<CharacterModel> Apply(IEnumerable<CharacterModel> source)
37+
{
38+
return _currentScope switch
39+
{
40+
SelectionScope.Visible => source.Where(character => character is not IModCharacterVanillaSelectionPolicy
41+
{
42+
HideFromVanillaCharacterSelect: true,
43+
}),
44+
SelectionScope.RandomEligible => source.Where(character =>
45+
character is not IModCharacterVanillaSelectionPolicy
46+
{
47+
AllowInVanillaRandomCharacterSelect: false,
48+
}),
49+
_ => source,
50+
};
51+
}
1952

20-
private static readonly MethodInfo? VisibleCharactersMethod =
21-
AccessTools.DeclaredMethod(typeof(CharacterVanillaSelectionPolicyPatches), nameof(GetVisibleCharacters));
53+
private static SelectionScope ResolveScope(MethodBase originalMethod)
54+
{
55+
if (originalMethod.DeclaringType == typeof(NCharacterSelectScreen) &&
56+
originalMethod.Name == nameof(NCharacterSelectScreen.InitCharacterButtons))
57+
return SelectionScope.Visible;
2258

23-
private static readonly MethodInfo? RandomEligibleCharactersMethod =
24-
AccessTools.DeclaredMethod(typeof(CharacterVanillaSelectionPolicyPatches),
25-
nameof(GetRandomEligibleCharacters));
59+
if ((originalMethod.DeclaringType == typeof(NCharacterSelectScreen) &&
60+
(originalMethod.Name == nameof(NCharacterSelectScreen.UpdateRandomCharacterVisibility) ||
61+
originalMethod.Name == "RollRandomCharacter")) ||
62+
(originalMethod.DeclaringType == typeof(NCharacterSelectButton) &&
63+
originalMethod.Name == nameof(NCharacterSelectButton.Init)) ||
64+
(originalMethod.DeclaringType == typeof(StartRunLobby) &&
65+
originalMethod.Name == "BeginRunLocally"))
66+
return SelectionScope.RandomEligible;
2667

68+
return SelectionScope.None;
69+
}
70+
71+
private enum SelectionScope
72+
{
73+
None,
74+
Visible,
75+
RandomEligible,
76+
}
77+
}
78+
79+
/// <summary>
80+
/// Maintains selection-policy scope for vanilla character-select flows.
81+
/// </summary>
82+
public class CharacterVanillaSelectionPolicyPatches : IPatchMethod
83+
{
2784
/// <inheritdoc />
2885
public static string PatchId => "character_vanilla_selection_policy";
2986

@@ -47,56 +104,54 @@ public static ModPatchTarget[] GetTargets()
47104
];
48105
}
49106

50-
// ReSharper disable once InconsistentNaming
51107
/// <summary>
52-
/// Rewrites direct reads of <see cref="ModelDb.AllCharacters" /> in target methods.
108+
/// Enters selection scope for character-list consumers.
53109
/// </summary>
54-
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions,
55-
MethodBase __originalMethod)
110+
// ReSharper disable once InconsistentNaming
111+
public static void Prefix(MethodBase __originalMethod)
56112
{
57-
if (ModelDbAllCharactersGetter == null)
58-
return instructions;
59-
60-
var useVisibleList = __originalMethod.Name == nameof(NCharacterSelectScreen.InitCharacterButtons);
61-
var replacement = useVisibleList ? VisibleCharactersMethod : RandomEligibleCharactersMethod;
62-
if (replacement == null)
63-
return instructions;
113+
CharacterVanillaSelectionPolicyScope.Enter(__originalMethod);
114+
}
64115

65-
var rewritten = false;
66-
var list = instructions.ToList();
67-
for (var index = 0; index < list.Count; index++)
68-
{
69-
var code = list[index];
70-
if (!code.Calls(ModelDbAllCharactersGetter))
71-
continue;
116+
/// <summary>
117+
/// Ensures scope cleanup even when target method throws.
118+
/// </summary>
119+
// ReSharper disable once InconsistentNaming
120+
public static void Finalizer(MethodBase __originalMethod)
121+
{
122+
CharacterVanillaSelectionPolicyScope.Exit(__originalMethod);
123+
}
124+
}
72125

73-
code.opcode = OpCodes.Call;
74-
code.operand = replacement;
75-
list[index] = code;
76-
rewritten = true;
77-
}
126+
/// <summary>
127+
/// Applies scoped selection policy to <see cref="ModelDb.AllCharacters" />.
128+
/// </summary>
129+
public class CharacterVanillaSelectionPolicyAllCharactersPatch : IPatchMethod
130+
{
131+
/// <inheritdoc />
132+
public static string PatchId => "character_vanilla_selection_policy_all_characters";
78133

79-
if (!rewritten)
80-
RitsuLibFramework.Logger.Debug(
81-
$"[CharacterSelection] No ModelDb.AllCharacters call found while patching {__originalMethod.DeclaringType?.Name}.{__originalMethod.Name}.");
134+
/// <inheritdoc />
135+
public static string Description => "Filter ModelDb.AllCharacters by current vanilla selection scope";
82136

83-
return list;
84-
}
137+
/// <inheritdoc />
138+
public static bool IsCritical => false;
85139

86-
private static IEnumerable<CharacterModel> GetVisibleCharacters()
140+
/// <inheritdoc />
141+
public static ModPatchTarget[] GetTargets()
87142
{
88-
return ModelDb.AllCharacters.Where(character => character is not IModCharacterVanillaSelectionPolicy
89-
{
90-
HideFromVanillaCharacterSelect: true,
91-
});
143+
return [new(typeof(ModelDb), nameof(ModelDb.AllCharacters), MethodType.Getter)];
92144
}
93145

94-
private static IEnumerable<CharacterModel> GetRandomEligibleCharacters()
146+
/// <summary>
147+
/// Filters getter result according to current selection scope.
148+
/// </summary>
149+
[HarmonyAfter(Const.BaseLibHarmonyId, Const.FrameworkContentRegistryHarmonyId)]
150+
[HarmonyPriority(Priority.Last)]
151+
// ReSharper disable once InconsistentNaming
152+
public static void Postfix(ref IEnumerable<CharacterModel> __result)
95153
{
96-
return ModelDb.AllCharacters.Where(character => character is not IModCharacterVanillaSelectionPolicy
97-
{
98-
AllowInVanillaRandomCharacterSelect: false,
99-
});
154+
__result = CharacterVanillaSelectionPolicyScope.Apply(__result);
100155
}
101156
}
102157
}

Scaffolding/Content/Patches/EnergyIconFormatterPatch.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ public static ModPatchTarget[] GetTargets()
5656
/// stloc (text3)
5757
/// </code>
5858
/// </summary>
59+
[HarmonyAfter(Const.BaseLibHarmonyId)]
60+
[HarmonyPriority(Priority.Last)]
5961
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
6062
{
6163
var concatMethod = AccessTools.Method(

0 commit comments

Comments
 (0)