|
| 1 | +using System.Collections; |
1 | 2 | using System.Reflection; |
| 3 | +using MegaCrit.Sts2.Core.Entities.Cards; |
2 | 4 | using MegaCrit.Sts2.Core.Helpers; |
3 | 5 | using MegaCrit.Sts2.Core.Models; |
| 6 | +using MegaCrit.Sts2.Core.Rewards; |
4 | 7 | using MegaCrit.Sts2.Core.Timeline; |
| 8 | +using STS2RitsuLib.Utils; |
5 | 9 |
|
6 | 10 | namespace STS2RitsuLib.Diagnostics |
7 | 11 | { |
8 | 12 | internal static class RegistrationConflictDetector |
9 | 13 | { |
10 | 14 | private static readonly Lock IndexGate = new(); |
11 | 15 | private static Dictionary<ModelId, List<Type>>? _gameModelIdIndex; |
| 16 | + private static bool _baseLibDynamicEnumCollisionCheckCompleted; |
12 | 17 |
|
13 | 18 | private static Dictionary<ModelId, List<Type>> GetGameModelIdIndex() |
14 | 19 | { |
@@ -79,6 +84,48 @@ internal static void ValidateAndLogModelIdCollisions() |
79 | 84 | InvalidateModelIdIndex(); |
80 | 85 | } |
81 | 86 |
|
| 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 | + |
82 | 129 | internal static void ThrowIfEpochIdConflicts(string epochId, Type candidateType, |
83 | 130 | IEnumerable<Type> knownEpochTypes) |
84 | 131 | { |
@@ -132,5 +179,138 @@ private static string GetStoryId(Type type) |
132 | 179 | throw new InvalidOperationException( |
133 | 180 | $"Story type '{type.FullName}' does not expose an Id property.")); |
134 | 181 | } |
| 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); |
135 | 315 | } |
136 | 316 | } |
0 commit comments