mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 07:29:00 -07:00
462 lines
16 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|