Skip to content

Commit a404264

Browse files
committed
feat(compat): implement external framework presence checks and refactor interop registration
- Introduced ExternalFrameworkRegistry to manage and check the presence of external frameworks, enhancing compatibility with BaseLib. - Refactored existing code in health bars and hand size management to utilize the new framework presence checks, improving reliability and reducing redundant logic. - Updated RitsuLibFramework to ensure proper registration of interop components during initialization, streamlining the setup process. - Removed obsolete methods related to framework checks, simplifying the codebase and enhancing maintainability.
1 parent e6deaad commit a404264

7 files changed

Lines changed: 179 additions & 70 deletions

File tree

Combat/HandSize/BaseLibMaxHandSizeBridge.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using MegaCrit.Sts2.Core.DevConsole.ConsoleCommands;
66
using MegaCrit.Sts2.Core.Entities.Players;
77
using MegaCrit.Sts2.Core.GameActions.Multiplayer;
8+
using STS2RitsuLib.Compat;
89
using STS2RitsuLib.Patching.Core;
910
using STS2RitsuLib.Patching.Models;
1011

@@ -22,7 +23,6 @@ internal static class BaseLibMaxHandSizeBridge
2223
private static readonly Harmony Harmony = new($"{Const.ModId}.interop.max_hand_size");
2324

2425
private static MethodInfo? _baseLibGetMaxHandSizeMethod;
25-
private static bool _resolveAttempted;
2626
private static bool _postfixPatched;
2727
private static bool _loggedResolveFailure;
2828

@@ -116,13 +116,12 @@ private static bool TryResolveBaseLibGetMaxHandSize(out MethodInfo method)
116116
return true;
117117
}
118118

119-
if (_resolveAttempted)
119+
if (!ExternalFrameworkRegistry.IsFrameworkPresent(ExternalFrameworkIds.BaseLib))
120120
{
121121
method = null!;
122122
return false;
123123
}
124124

125-
_resolveAttempted = true;
126125
var type = ResolveBaseLibMaxHandSizePatchType();
127126
var resolved = type?.GetMethod(
128127
"GetMaxHandSize",
@@ -152,7 +151,7 @@ private static bool TryResolveBaseLibGetMaxHandSize(out MethodInfo method)
152151

153152
private static Type? ResolveBaseLibMaxHandSizePatchType()
154153
{
155-
var byQualifiedName = Type.GetType("BaseLib.Patches.Hooks.MaxHandSizePatch, BaseLib");
154+
var byQualifiedName = ExternalFrameworkRegistry.ResolveType("BaseLib.Patches.Hooks.MaxHandSizePatch");
156155
if (byQualifiedName != null)
157156
return byQualifiedName;
158157

Combat/HealthBars/BaseLibHealthBarForecastBridge.cs

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ internal static class BaseLibHealthBarForecastBridge
1919
private static bool _baselibSupportsForecastInterop;
2020
private static bool _loggedMissingInterop;
2121
private static bool _loggedMissingRegisterForeign;
22-
private static bool _primaryAttemptIssued;
23-
private static bool _secondaryAttemptIssued;
2422

2523
/// <summary>
2624
/// When <see langword="true" />, Ritsu's <c>NHealthBar</c> forecast postfixes should skip drawing because BaseLib
@@ -36,9 +34,8 @@ public static bool ShouldRitsuRendererStandDown()
3634
/// </summary>
3735
public static void TryRegisterPrimary()
3836
{
39-
if (_primaryAttemptIssued || _registered)
37+
if (_registered)
4038
return;
41-
_primaryAttemptIssued = true;
4239
TryRegisterCore();
4340
}
4441

@@ -47,9 +44,8 @@ public static void TryRegisterPrimary()
4744
/// </summary>
4845
public static void TryRegisterSecondary()
4946
{
50-
if (_secondaryAttemptIssued || _registered)
47+
if (_registered)
5148
return;
52-
_secondaryAttemptIssued = true;
5349
TryRegisterCore();
5450
}
5551

@@ -65,7 +61,7 @@ private static void TryRegisterCore()
6561
{
6662
if (_registered)
6763
return;
68-
if (!IsBaseLibLoaded())
64+
if (!ExternalFrameworkRegistry.IsFrameworkPresent(ExternalFrameworkIds.BaseLib))
6965
return;
7066

7167
try
@@ -132,23 +128,9 @@ private static IEnumerable<object> GetSegmentsForCreature(Creature creature)
132128
return registryType;
133129
}
134130

135-
private static bool IsBaseLibLoaded()
136-
{
137-
foreach (var mod in Sts2ModManagerCompat.EnumerateLoadedModsWithAssembly())
138-
{
139-
var assembly = mod.assembly;
140-
if (assembly == null)
141-
continue;
142-
if (assembly.GetType("BaseLib.Hooks.HealthBarForecastRegistry") != null)
143-
return true;
144-
}
145-
146-
return false;
147-
}
148-
149131
private static Type? ResolveRegistryTypeFromLoadedAssemblies()
150132
{
151-
var byQualifiedName = Type.GetType("BaseLib.Hooks.HealthBarForecastRegistry, BaseLib");
133+
var byQualifiedName = ExternalFrameworkRegistry.ResolveType("BaseLib.Hooks.HealthBarForecastRegistry");
152134
if (byQualifiedName != null)
153135
return byQualifiedName;
154136

Combat/HealthBars/BaseLibVisualGraftBridge.cs

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ internal static class BaseLibVisualGraftBridge
1515
private static bool _interopOk;
1616
private static bool _loggedMissingRegistry;
1717
private static bool _loggedMissingRegisterForeign;
18-
private static bool _primaryAttemptIssued;
19-
private static bool _secondaryAttemptIssued;
2018

2119
public static bool ShouldRitsuGraftStandDown()
2220
{
@@ -25,17 +23,15 @@ public static bool ShouldRitsuGraftStandDown()
2523

2624
public static void TryRegisterPrimary()
2725
{
28-
if (_primaryAttemptIssued || _registered)
26+
if (_registered)
2927
return;
30-
_primaryAttemptIssued = true;
3128
TryRegisterCore();
3229
}
3330

3431
public static void TryRegisterSecondary()
3532
{
36-
if (_secondaryAttemptIssued || _registered)
33+
if (_registered)
3734
return;
38-
_secondaryAttemptIssued = true;
3935
TryRegisterCore();
4036
}
4137

@@ -48,7 +44,7 @@ private static void TryRegisterCore()
4844
{
4945
if (_registered)
5046
return;
51-
if (!IsBaseLibLoaded())
47+
if (!ExternalFrameworkRegistry.IsFrameworkPresent(ExternalFrameworkIds.BaseLib))
5248
return;
5349

5450
try
@@ -94,7 +90,7 @@ static object Handler(Creature c)
9490

9591
private static Type? ResolveBaseLibRegistryType()
9692
{
97-
var byQualifiedName = Type.GetType("BaseLib.Hooks.HealthBarVisualGraftRegistry, BaseLib");
93+
var byQualifiedName = ExternalFrameworkRegistry.ResolveType("BaseLib.Hooks.HealthBarVisualGraftRegistry");
9894
if (byQualifiedName != null)
9995
{
10096
_interopOk = true;
@@ -126,19 +122,5 @@ static object Handler(Creature c)
126122
_interopOk = true;
127123
return fallback;
128124
}
129-
130-
private static bool IsBaseLibLoaded()
131-
{
132-
foreach (var mod in Sts2ModManagerCompat.EnumerateLoadedModsWithAssembly())
133-
{
134-
var assembly = mod.assembly;
135-
if (assembly == null)
136-
continue;
137-
if (assembly.GetType("BaseLib.Hooks.HealthBarVisualGraftRegistry") != null)
138-
return true;
139-
}
140-
141-
return false;
142-
}
143125
}
144126
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
namespace STS2RitsuLib.Compat
2+
{
3+
/// <summary>
4+
/// Stable framework ids used by runtime interop checks.
5+
/// </summary>
6+
internal static class ExternalFrameworkIds
7+
{
8+
public const string BaseLib = "baselib";
9+
public const string BaseLibToRitsuGenerated = "baselib-to-ritsu-generated";
10+
public const string ModConfig = "modconfig";
11+
}
12+
13+
/// <summary>
14+
/// Central registry for external framework presence checks.
15+
/// Known frameworks are probe-based and can be extended with custom detectors.
16+
/// </summary>
17+
internal static class ExternalFrameworkRegistry
18+
{
19+
private static readonly Lock Gate = new();
20+
private static readonly Dictionary<string, Func<bool>> CustomDetectors = [];
21+
22+
private static readonly Dictionary<string, ProbeSpec> BuiltInProbes = new(StringComparer.OrdinalIgnoreCase)
23+
{
24+
[ExternalFrameworkIds.BaseLib] = new(
25+
ExternalFrameworkIds.BaseLib,
26+
["BaseLib.Patches.Hooks.MaxHandSizePatch", "BaseLib.Hooks.HealthBarForecastRegistry"]),
27+
[ExternalFrameworkIds.BaseLibToRitsuGenerated] = new(
28+
ExternalFrameworkIds.BaseLibToRitsuGenerated,
29+
["BaseLibToRitsu.Generated.ModConfigRegistry"]),
30+
[ExternalFrameworkIds.ModConfig] = new(
31+
ExternalFrameworkIds.ModConfig,
32+
["ModConfig.ModConfigApi"]),
33+
};
34+
35+
private static readonly Dictionary<string, bool> PresenceCache = new(StringComparer.OrdinalIgnoreCase);
36+
37+
/// <summary>
38+
/// Registers a custom framework detector. The latest detector with the same id wins.
39+
/// </summary>
40+
public static void RegisterFrameworkDetector(string frameworkId, Func<bool> detector)
41+
{
42+
ArgumentException.ThrowIfNullOrWhiteSpace(frameworkId);
43+
ArgumentNullException.ThrowIfNull(detector);
44+
45+
lock (Gate)
46+
{
47+
CustomDetectors[frameworkId] = detector;
48+
PresenceCache.Remove(frameworkId);
49+
}
50+
}
51+
52+
/// <summary>
53+
/// Returns whether the specified framework appears to be present.
54+
/// </summary>
55+
public static bool IsFrameworkPresent(string frameworkId)
56+
{
57+
ArgumentException.ThrowIfNullOrWhiteSpace(frameworkId);
58+
59+
lock (Gate)
60+
{
61+
if (PresenceCache.TryGetValue(frameworkId, out var cached))
62+
return cached;
63+
64+
var detected = DetectFrameworkCore(frameworkId);
65+
PresenceCache[frameworkId] = detected;
66+
return detected;
67+
}
68+
}
69+
70+
/// <summary>
71+
/// Refreshes all known framework presence states.
72+
/// </summary>
73+
public static void RefreshKnownFrameworkPresence(string reason)
74+
{
75+
lock (Gate)
76+
{
77+
PresenceCache.Clear();
78+
foreach (var frameworkId in BuiltInProbes.Keys)
79+
PresenceCache[frameworkId] = DetectFrameworkCore(frameworkId);
80+
foreach (var frameworkId in CustomDetectors.Keys)
81+
PresenceCache[frameworkId] = DetectFrameworkCore(frameworkId);
82+
}
83+
84+
RitsuLibFramework.Logger.Info($"[Compat] External framework presence refreshed ({reason}).");
85+
}
86+
87+
/// <summary>
88+
/// Resolves <paramref name="fullTypeName" /> from loaded assemblies.
89+
/// </summary>
90+
public static Type? ResolveType(string fullTypeName)
91+
{
92+
var byQualifiedName = Type.GetType(fullTypeName);
93+
if (byQualifiedName != null)
94+
return byQualifiedName;
95+
96+
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
97+
{
98+
Type? type = null;
99+
try
100+
{
101+
type = assembly.GetType(fullTypeName, false);
102+
}
103+
catch
104+
{
105+
// ignored
106+
}
107+
108+
if (type != null)
109+
return type;
110+
}
111+
112+
return null;
113+
}
114+
115+
private static bool DetectFrameworkCore(string frameworkId)
116+
{
117+
if (CustomDetectors.TryGetValue(frameworkId, out var customDetector))
118+
try
119+
{
120+
return customDetector();
121+
}
122+
catch (Exception ex)
123+
{
124+
RitsuLibFramework.Logger.Warn(
125+
$"[Compat] Custom framework detector '{frameworkId}' failed: {ex.Message}");
126+
return false;
127+
}
128+
129+
if (!BuiltInProbes.TryGetValue(frameworkId, out var spec))
130+
return false;
131+
132+
return spec.TypeMarkers.Any(typeName => ResolveType(typeName) != null);
133+
}
134+
135+
private readonly record struct ProbeSpec(
136+
string FrameworkId,
137+
IReadOnlyList<string> TypeMarkers);
138+
}
139+
}

RitsuLibFramework.cs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using STS2RitsuLib.CardTags;
1111
using STS2RitsuLib.Combat.HandSize;
1212
using STS2RitsuLib.Combat.HealthBars;
13+
using STS2RitsuLib.Compat;
1314
using STS2RitsuLib.Content;
1415
using STS2RitsuLib.Data;
1516
using STS2RitsuLib.Diagnostics.CardExport;
@@ -38,6 +39,7 @@ public static partial class RitsuLibFramework
3839
{
3940
private static readonly Lock SyncRoot = new();
4041
private static readonly Dictionary<FrameworkPatcherArea, ModPatcher> FrameworkPatchersByArea = [];
42+
private static bool _frameworkInteropBootstrapRegistered;
4143

4244
private static bool _profileServicesInitialized;
4345
private static ILifecycleObserver[] _lifecycleObservers = [];
@@ -197,10 +199,7 @@ public static void Initialize()
197199

198200
IsInitialized = true;
199201
IsActive = true;
200-
BaseLibHealthBarForecastBridge.TryRegister();
201-
BaseLibVisualGraftBridge.TryRegister();
202-
BaseLibMaxHandSizeBridge.TryInitialize();
203-
MaxHandSizePatchInstaller.EnsurePatched();
202+
EnsureFrameworkInteropBootstrapRegistered();
204203
RuntimeHotkeyService.Initialize();
205204

206205
var frameworkInitializedEvent = new FrameworkInitializedEvent(
@@ -230,6 +229,24 @@ private static string BuildVersionLogText()
230229
: $"Version: {Const.Version} [compat branch: {compatBranchLabel}]";
231230
}
232231

232+
private static void EnsureFrameworkInteropBootstrapRegistered()
233+
{
234+
if (_frameworkInteropBootstrapRegistered)
235+
return;
236+
237+
_frameworkInteropBootstrapRegistered = true;
238+
SubscribeLifecycle<DeferredInitializationCompletedEvent>(_ => ConfirmExternalFrameworkInterop());
239+
}
240+
241+
private static void ConfirmExternalFrameworkInterop()
242+
{
243+
ExternalFrameworkRegistry.RefreshKnownFrameworkPresence("deferred initialization completed");
244+
BaseLibHealthBarForecastBridge.TryRegister();
245+
BaseLibVisualGraftBridge.TryRegister();
246+
BaseLibMaxHandSizeBridge.TryInitialize();
247+
MaxHandSizePatchInstaller.EnsurePatched();
248+
}
249+
233250
private static string? GetCompatBranchLabel()
234251
{
235252
#if STS2_V_0_103_2

0 commit comments

Comments
 (0)