using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; using HarmonyLib; internal static class KeepsakeToolPatch { private static readonly Type Palette = typeof(RecRoom.AGUI.ToolUIs.MakerPenPaletteMainScreen); private static readonly Type Registry = typeof(PMJEHEPKBJL); private static readonly Type ToolTypeEnum = typeof(AOCJJLANBMH); private static readonly Type ToolConfig = typeof(RuntimeSpawnableToolConfig); private static readonly Dictionary RepeatedLines = new Dictionary(); private static string Mode => (Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_TOOL") ?? (RadicadeEacBypassPlugin.KeepsakeToolEnabled?.Value == true ? "on" : "skip")).Trim(); private static bool Requested => Mode.ToLowerInvariant() switch { "on" or "true" or "1" or "yes" or "enable" or "enabled" => true, "off" or "false" or "0" => true, _ => false, }; internal static bool Forcing => Mode.ToLowerInvariant() switch { "on" or "true" or "1" or "yes" or "enable" or "enabled" => true, _ => false, }; internal static void Apply(Harmony harmony) { if (!Requested) { return; } if (Palette == null || Registry == null || ToolTypeEnum == null) { RadicadeEacBypassPlugin.LogSource.LogWarning( "[Keepsake] palette/registry/tool-type type not found (" + $"palette={Palette?.FullName ?? "null"}, registry={Registry?.FullName ?? "null"}, " + $"toolType={ToolTypeEnum?.FullName ?? "null"}) - keepsake patch skipped"); return; } Patch(harmony, Registry, "JBAINDGLMBO", new[] { ToolTypeEnum }, null, nameof(ConfigPostfix)); PatchByName(harmony, Registry, "LPBDCDIMBEN", nameof(PredicatePostfix)); PatchByNameOnFamily(harmony, Palette, "LKGMIHJOIID", nameof(PredicatePostfix)); PatchByNameOnFamily(harmony, Palette, "OLKPEGJNJGG", nameof(ConfigFilterPostfix)); PatchByNameOnFamily(harmony, Palette, "IBHJBFJMBFB", nameof(ConfigFilterPostfix)); Patch(harmony, Palette, "OnUsedPaletteItem", null, nameof(UsedPalettePrefix), null); Audit(harmony); } private static void Audit(Harmony harmony) { var service = AccessTools.TypeByName("AMAOGLDGNLL"); foreach (var name in new[] { "MJDEEABFHPJ", "NCAKOLMAAEA", "CAFCLGFGKAO", "KHPJGNADGEE", "NGJMABOMHHB", "FCOMKMPDEIO", "AEHHFFIKLG" }) PatchByName(harmony, service, name, nameof(CallPostfix), 2); if (!string.Equals(Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_AUDIT"), "1", StringComparison.Ordinal)) return; Log("[Keepsake] RADICADE_KEEPSAKE_AUDIT=1: instrumenting the room/progression models too"); foreach (var typeName in new[] { "RRUI.Data.RoomModel", "RRUI.Data.RoomListModel", "RRUI.Data.ProgressionEventsModel" }) { var model = AccessTools.TypeByName(typeName); foreach (var name in new[] { "IsKeepsakeRoom", "GetCachedAreAllKeepsakesCollected", "GetTotalKeepsakeCountPerCategoryForRoom", "GetUncollectedKeepsakeCountPerCategoryForRoom", "GetTotalKeepsakeCountForRoom", "GetUncollectedKeepsakeCountForRoom", "IsKeepsakeRoomListUpsellItemOwned", "AHACLFABDEO", "CJPFGOBHGBB" }) PatchByName(harmony, model, name, nameof(ModelPostfix), 4); } } internal static object KeepsakeMember() { try { return ToolTypeEnum == null ? null : Enum.Parse(ToolTypeEnum, "KEEPSAKE"); } catch (Exception e) { RadicadeEacBypassPlugin.LogSource.LogWarning("[Keepsake] KEEPSAKE enum member missing: " + e.Message); return null; } } internal static bool IsKeepsake(object toolType) => toolType != null && KeepsakeMember()?.Equals(toolType) == true; internal static void Log(string message) => RadicadeEacBypassPlugin.LogSource.LogInfo(message); private static void LogOnce(string message, int limit = 3) { var seen = RepeatedLines.TryGetValue(message, out var count) ? count + 1 : 1; RepeatedLines[message] = seen; if (seen <= limit) Log(message); else if (seen == limit + 1) Log($"[Keepsake] (suppressing further copies of: {message})"); } private static void ConfigPostfix(object[] __args, RuntimeSpawnableToolConfig __result) { var toolType = __args.Length > 0 ? __args[0] : null; var keepsake = IsKeepsake(toolType); if (!keepsake && !IsRestricted(__result)) return; if (!keepsake || !Forcing) return; var allowed = Field(__result, "spawnableToolTypesAllowedInRooms2"); if (Contains(allowed, KeepsakeMember())) return; AddToCollection(allowed, KeepsakeMember()); } private static void PredicatePostfix(MethodBase __originalMethod, object[] __args, ref bool __result) { var toolType = FirstToolType(__args); var keepsake = IsKeepsake(toolType); if (keepsake && Forcing && !__result) { __result = true; } } private static void CallPostfix(MethodBase __originalMethod, object[] __args, object __result) { } private static void ModelPostfix(MethodBase __originalMethod, object[] __args, object __result) { } private static string DescribeResult(object result) { if (result == null) return "(null)"; var type = result.GetType(); try { if (type == typeof(bool) || type == typeof(int) || type == typeof(long)) return result.ToString(); var status = AccessTools.Property(type, "IsCompleted")?.GetValue(result) as bool?; if (status == true) { var value = AccessTools.Property(type, "Result")?.GetValue(result); return $"{type.Name}(completed) = {value}"; } if (status == false) return $"{type.Name}(pending)"; } catch { } return type.Name; } private static void ConfigFilterPostfix(MethodBase __originalMethod, object[] __args, ref bool __result) { var config = __args.Length > 0 ? __args[0] : null; var name = Field(config, "UniqueName")?.ToString() ?? "(unnamed tool)"; var keepsake = string.Equals(name, "KEEPSAKE", StringComparison.OrdinalIgnoreCase); if (!keepsake) return; if (!Forcing || __result) return; __result = true; } private static void UsedPalettePrefix(object[] __args) { } private static object FirstToolType(object[] args) { if (args == null) return null; foreach (var arg in args) { if (arg == null) continue; if (ToolTypeEnum.IsInstanceOfType(arg) || arg.GetType() == ToolTypeEnum) return arg; } return null; } private static bool IsRestricted(object config) => Field(config, "AvailableInMakerPen") is bool makerPen && !makerPen; private static string Frames(int take = 10) { try { var frames = new StackTrace(true).GetFrames(); var sb = new StringBuilder(); var taken = 0; foreach (var frame in frames) { var method = frame.GetMethod(); var owner = method?.DeclaringType?.Name ?? "?"; if (owner.Contains("KeepsakeToolPatch") || owner.Contains("Harmony")) continue; sb.Append(owner).Append('.').Append(method?.Name ?? "?").Append(" <- "); if (++taken >= take) break; } return sb.Length == 0 ? "(no frames)" : sb.ToString().TrimEnd(' ', '<', '-'); } catch (Exception e) { return "(stack failed: " + e.GetType().Name + ")"; } } private static string DescribeList(object collection) { if (collection == null) return "(null)"; var type = collection.GetType(); var count = 0; var keepsake = false; var names = new StringBuilder(); try { var countValue = AccessTools.Property(type, "Count")?.GetValue(collection); count = countValue is int n ? n : 0; if (count == 0) return "0 item(s) []"; var indexer = type.GetProperty("Item"); for (var i = 0; i < count && i < 12; i++) { var item = indexer?.GetValue(collection, new object[] { i }); if (IsKeepsake(item)) keepsake = true; names.Append(item).Append(','); } } catch (Exception e) { return $"(list read failed: {e.GetType().Name}: {e.Message})"; } return $"{count} item(s) [{names.ToString().TrimEnd(',')}]" + (keepsake ? " KEEPSAKE present" : ""); } private static bool Contains(object collection, object item) { if (collection == null || item == null) return false; try { var contains = AccessTools.Method(collection.GetType(), "Contains"); return contains?.Invoke(collection, new[] { item }) is true; } catch { return false; } } private static bool AddToCollection(object collection, object item) { if (collection == null || item == null) return false; try { var type = collection.GetType(); MethodInfo add = null; foreach (var candidate in type.GetMethods(BindingFlags.Public | BindingFlags.Instance)) { if (candidate.Name != "Add" || candidate.GetParameters().Length != 1) continue; add = candidate; break; } if (add == null) return false; add.Invoke(collection, new[] { item }); return true; } catch (Exception e) { RadicadeEacBypassPlugin.LogSource.LogWarning( $"[Keepsake] could not add to {collection.GetType().Name}: {e.GetType().Name}: {e.Message}"); return false; } } private static string Describe(object config) { if (config == null) return "(null config)"; var sb = new StringBuilder(config.GetType().Name).Append('{'); foreach (var name in new[] { "UniqueName", "AvailableInMakerPen", "AvailableToDevsOnly", "IsBetaItem", "AvailableInSpawner", "CanSaveIntoInventions", "LimitInventionSaveForJuniors" }) { var value = Field(config, name); if (value != null) sb.Append(name).Append('=').Append(value).Append(' '); } var allowed = Field(config, "spawnableToolTypesAllowedInRooms2"); if (allowed != null) sb.Append("allowedInRooms=").Append(DescribeList(allowed)).Append(' '); return sb.Append('}').ToString(); } private static object Field(object instance, string name) { if (instance == null) return null; try { var type = instance.GetType(); var property = AccessTools.Property(type, name); if (property != null) return property.GetValue(instance); return AccessTools.Field(type, name)?.GetValue(instance); } catch { return null; } } private static void PatchByNameOnFamily(Harmony harmony, Type root, string method, string postfix) { var patched = 0; foreach (var type in Family(root)) { foreach (var found in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance)) { if (found.Name != method) continue; if (Bind(harmony, found, null, postfix)) patched++; } } if (patched == 0) RadicadeEacBypassPlugin.LogSource.LogWarning( $"[Keepsake] no method named {method} on {root.Name} or its nested types - not instrumented"); } private static void PatchByName(Harmony harmony, Type target, string method, string postfix, int maxArguments = 1) { if (target == null) { RadicadeEacBypassPlugin.LogSource.LogWarning( $"[Keepsake] type for {method} not found - not instrumented"); return; } var patched = 0; foreach (var found in target.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance)) { if (found.Name != method) continue; var parameters = found.GetParameters(); if (parameters.Length > maxArguments) continue; if (parameters.Any(p => p.ParameterType.IsByRef || p.IsOut || p.ParameterType.IsPointer)) continue; if (maxArguments == 1 && (parameters.Length != 1 || (!ToolTypeEnum.IsAssignableFrom(parameters[0].ParameterType) && parameters[0].ParameterType != ToolTypeEnum))) continue; if (Bind(harmony, found, null, postfix)) patched++; } } private static IEnumerable Family(Type root) { yield return root; Type[] nested; try { nested = root.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic); } catch { yield break; } foreach (var type in nested) foreach (var deeper in Family(type)) yield return deeper; } private static void Patch(Harmony harmony, Type target, string method, Type[] args, string prefix, string postfix) { if (target == null) { RadicadeEacBypassPlugin.LogSource.LogWarning($"[Keepsake] type for {method} not found - not instrumented"); return; } try { var found = args == null ? AccessTools.Method(target, method) : AccessTools.Method(target, method, args); if (found == null) { RadicadeEacBypassPlugin.LogSource.LogWarning( $"[Keepsake] {target.Name}.{method} not found - not instrumented"); return; } Bind(harmony, found, prefix, postfix); } catch (Exception e) { RadicadeEacBypassPlugin.LogSource.LogWarning( $"[Keepsake] FAILED to instrument {target.Name}.{method}: {e.GetType().Name}: {e.Message}"); } } private static bool Bind(Harmony harmony, MethodBase found, string prefix, string postfix) { try { var self = typeof(KeepsakeToolPatch); const BindingFlags flags = BindingFlags.Static | BindingFlags.NonPublic; harmony.Patch(found, prefix: prefix == null ? null : new HarmonyMethod(self.GetMethod(prefix, flags)), postfix: postfix == null ? null : new HarmonyMethod(self.GetMethod(postfix, flags))); return true; } catch (Exception e) { RadicadeEacBypassPlugin.LogSource.LogWarning( $"[Keepsake] FAILED to instrument {found.DeclaringType?.Name}.{found.Name}: " + $"{e.GetType().Name}: {e.Message}"); return false; } } }