mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 23:49:02 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,461 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user