Skip to content

Commit 0db23f3

Browse files
committed
feat(DynamicEnumValidation): implement dynamic enum value collision detection
- Added a new method to validate and log collisions between BaseLib and RitsuLib dynamic enum values, enhancing error detection for mod developers. - Integrated collision detection into the model database initialization process to ensure early detection of potential conflicts. - Introduced utility methods for retrieving minted dynamic enum values, improving the overall management of dynamic enums within the system.
1 parent 0eb6139 commit 0db23f3

4 files changed

Lines changed: 199 additions & 0 deletions

File tree

Combat/CardTargeting/Patches/ModelDbInitCustomTargetTypeRegistrationPatch.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using MegaCrit.Sts2.Core.Models;
2+
using STS2RitsuLib.Diagnostics;
23
using STS2RitsuLib.Patching.Models;
34

45
namespace STS2RitsuLib.Combat.CardTargeting.Patches
@@ -31,6 +32,8 @@ public static ModPatchTarget[] GetTargets()
3132
public static void Postfix()
3233
{
3334
CustomTargetTypeRegistry.RegisterBuiltIns();
35+
RitsuLibStartupAudit.Measure("modelDb.validateBaseLibDynamicEnums",
36+
RegistrationConflictDetector.ValidateAndLogBaseLibDynamicEnumValueCollisions);
3437
}
3538
}
3639
}

Diagnostics/RegistrationConflictDetector.cs

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1+
using System.Collections;
12
using System.Reflection;
3+
using MegaCrit.Sts2.Core.Entities.Cards;
24
using MegaCrit.Sts2.Core.Helpers;
35
using MegaCrit.Sts2.Core.Models;
6+
using MegaCrit.Sts2.Core.Rewards;
47
using MegaCrit.Sts2.Core.Timeline;
8+
using STS2RitsuLib.Utils;
59

610
namespace STS2RitsuLib.Diagnostics
711
{
812
internal static class RegistrationConflictDetector
913
{
1014
private static readonly Lock IndexGate = new();
1115
private static Dictionary<ModelId, List<Type>>? _gameModelIdIndex;
16+
private static bool _baseLibDynamicEnumCollisionCheckCompleted;
1217

1318
private static Dictionary<ModelId, List<Type>> GetGameModelIdIndex()
1419
{
@@ -79,6 +84,48 @@ internal static void ValidateAndLogModelIdCollisions()
7984
InvalidateModelIdIndex();
8085
}
8186

87+
internal static void ValidateAndLogBaseLibDynamicEnumValueCollisions()
88+
{
89+
if (_baseLibDynamicEnumCollisionCheckCompleted)
90+
return;
91+
92+
_baseLibDynamicEnumCollisionCheckCompleted = true;
93+
94+
var baseLibEntries = GetBaseLibCustomEnumEntries();
95+
if (baseLibEntries.Length == 0)
96+
return;
97+
98+
var ritsuEntries = GetRitsuDynamicEnumEntries()
99+
.GroupBy(static entry => (entry.EnumType, entry.Value))
100+
.ToDictionary(
101+
static group => group.Key,
102+
static group => group
103+
.OrderBy(static entry => entry.Id, StringComparer.Ordinal)
104+
.ToArray());
105+
106+
var conflicts = baseLibEntries
107+
.SelectMany(entry => ritsuEntries.TryGetValue((entry.EnumType, entry.Value), out var ritsu)
108+
? ritsu.Select(ritsuEntry => new DynamicEnumValueCollision(entry, ritsuEntry))
109+
: [])
110+
.OrderBy(static collision => collision.BaseLibEntry.EnumType.FullName, StringComparer.Ordinal)
111+
.ThenBy(static collision => collision.BaseLibEntry.Value)
112+
.ThenBy(static collision => collision.RitsuEntry.Id, StringComparer.Ordinal)
113+
.ToArray();
114+
115+
foreach (var collision in conflicts)
116+
RitsuLibFramework.Logger.ErrorNoTrace(
117+
"[DynamicEnum] BaseLib/RitsuLib value collision detected: "
118+
+ $"{collision.BaseLibEntry.EnumType.FullName} value 0x{collision.BaseLibEntry.Value:X8} "
119+
+ $"is used by BaseLib '{collision.BaseLibEntry.DisplayName}' and RitsuLib "
120+
+ $"'{collision.RitsuEntry.DisplayName}'.");
121+
122+
if (conflicts.Length > 0)
123+
RitsuLibFramework.Logger.ErrorNoTrace(
124+
"[DynamicEnum] BaseLib/RitsuLib dynamic enum value collisions are unsafe. "
125+
+ "RitsuLib does not remap minted values automatically; change the colliding RitsuLib id or "
126+
+ "the BaseLib CustomEnum field/name source.");
127+
}
128+
82129
internal static void ThrowIfEpochIdConflicts(string epochId, Type candidateType,
83130
IEnumerable<Type> knownEpochTypes)
84131
{
@@ -132,5 +179,138 @@ private static string GetStoryId(Type type)
132179
throw new InvalidOperationException(
133180
$"Story type '{type.FullName}' does not expose an Id property."));
134181
}
182+
183+
private static BaseLibCustomEnumEntry[] GetBaseLibCustomEnumEntries()
184+
{
185+
var customEnumsType = ResolveLoadedType("BaseLib.Patches.Content.CustomEnums");
186+
var generatedEntriesField = customEnumsType?.GetField(
187+
"GeneratedCustomEnumEntries",
188+
BindingFlags.Static | BindingFlags.Public);
189+
190+
if (generatedEntriesField?.GetValue(null) is not IDictionary byEnumType)
191+
return [];
192+
193+
var result = new List<BaseLibCustomEnumEntry>();
194+
foreach (DictionaryEntry enumGroup in byEnumType)
195+
{
196+
if (enumGroup.Key is not Type enumType || enumGroup.Value is not IDictionary entriesByValue)
197+
continue;
198+
199+
foreach (DictionaryEntry entry in entriesByValue)
200+
{
201+
if (!TryConvertToInt32(entry.Key, out var value))
202+
continue;
203+
204+
var (prefix, name) = ReadBaseLibGeneratedName(entry.Value);
205+
result.Add(new(enumType, value, prefix, name));
206+
}
207+
}
208+
209+
return result.ToArray();
210+
}
211+
212+
private static IEnumerable<RitsuDynamicEnumEntry> GetRitsuDynamicEnumEntries()
213+
{
214+
foreach (var entry in GetRitsuDynamicEnumEntries<CardKeyword>())
215+
yield return entry;
216+
foreach (var entry in GetRitsuDynamicEnumEntries<PileType>())
217+
yield return entry;
218+
foreach (var entry in GetRitsuDynamicEnumEntries<CardTag>())
219+
yield return entry;
220+
foreach (var entry in GetRitsuDynamicEnumEntries<RewardType>())
221+
yield return entry;
222+
foreach (var entry in GetRitsuDynamicEnumEntries<TargetType>())
223+
yield return entry;
224+
}
225+
226+
private static IEnumerable<RitsuDynamicEnumEntry> GetRitsuDynamicEnumEntries<TEnum>()
227+
where TEnum : struct, Enum
228+
{
229+
var entriesByValue = new Dictionary<int, RitsuDynamicEnumEntry>();
230+
foreach (var definition in DynamicEnumValueRegistry<TEnum>.GetDefinitionsSnapshot())
231+
{
232+
var value = Convert.ToInt32(definition.Value);
233+
entriesByValue[value] = new(typeof(TEnum), value, definition.Id, definition.ModId, true);
234+
}
235+
236+
foreach (var (id, value) in DynamicEnumValueRegistry<TEnum>.GetMintedValuesSnapshot())
237+
{
238+
var numericValue = Convert.ToInt32(value);
239+
entriesByValue.TryAdd(numericValue, new(typeof(TEnum), numericValue, id, null, false));
240+
}
241+
242+
return entriesByValue.Values;
243+
}
244+
245+
private static Type? ResolveLoadedType(string fullTypeName)
246+
{
247+
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
248+
{
249+
Type? type = null;
250+
try
251+
{
252+
type = assembly.GetType(fullTypeName, false);
253+
}
254+
catch
255+
{
256+
// ignored
257+
}
258+
259+
if (type != null)
260+
return type;
261+
}
262+
263+
return null;
264+
}
265+
266+
private static (string Prefix, string Name) ReadBaseLibGeneratedName(object? generatedName)
267+
{
268+
if (generatedName == null)
269+
return ("", "<unknown>");
270+
271+
var type = generatedName.GetType();
272+
var prefix = type.GetField("Item1")?.GetValue(generatedName) as string ?? "";
273+
var name = type.GetField("Item2")?.GetValue(generatedName) as string ?? "<unknown>";
274+
return (prefix, name);
275+
}
276+
277+
private static bool TryConvertToInt32(object key, out int value)
278+
{
279+
try
280+
{
281+
value = Convert.ToInt32(key);
282+
return true;
283+
}
284+
catch
285+
{
286+
value = 0;
287+
return false;
288+
}
289+
}
290+
291+
private readonly record struct BaseLibCustomEnumEntry(
292+
Type EnumType,
293+
int Value,
294+
string Prefix,
295+
string Name)
296+
{
297+
public string DisplayName => $"{Prefix}{Name}";
298+
}
299+
300+
private readonly record struct RitsuDynamicEnumEntry(
301+
Type EnumType,
302+
int Value,
303+
string Id,
304+
string? ModId,
305+
bool Registered)
306+
{
307+
public string DisplayName => Registered && !string.IsNullOrWhiteSpace(ModId)
308+
? $"{Id} (mod {ModId})"
309+
: $"{Id} (minted lookup)";
310+
}
311+
312+
private readonly record struct DynamicEnumValueCollision(
313+
BaseLibCustomEnumEntry BaseLibEntry,
314+
RitsuDynamicEnumEntry RitsuEntry);
135315
}
136316
}

Utils/DynamicEnumValueMinter.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,17 @@ public bool IsDynamic(TEnum value)
185185
}
186186
}
187187

188+
internal (string Id, TEnum Value)[] GetMintedValuesSnapshot()
189+
{
190+
lock (_sync)
191+
{
192+
return _byId
193+
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
194+
.Select(static pair => (pair.Key, pair.Value))
195+
.ToArray();
196+
}
197+
}
198+
188199
private TEnum Compute(string normalizedId)
189200
{
190201
var bytes = Encoding.UTF8.GetBytes(normalizedId);

Utils/DynamicEnumValueRegistry.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,11 @@ public static bool IsMinted(TEnum value)
293293
return Minter.IsDynamic(value);
294294
}
295295

296+
internal static (string Id, TEnum Value)[] GetMintedValuesSnapshot()
297+
{
298+
return Minter.GetMintedValuesSnapshot();
299+
}
300+
296301
/// <summary>
297302
/// Snapshot of all registered dynamic enum definitions, stable-ordered by id.
298303
/// 获取所有已注册动态枚举定义的快照,并按 ID 稳定排序。

0 commit comments

Comments
 (0)