Files
Radicade/2023Patch/RadicadePatch/KeepsakeToolPatch.cs
T
2026-10-16 23:33:13 -07:00

462 lines
16 KiB
C#

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<string, int> RepeatedLines = new Dictionary<string, int>();
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<Type> 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;
}
}
}