feat: Upgrade server with HyNB v2.4 modular architecture, minigames, admin GM web panel, companion summons, and car radio
This commit is contained in:
1 parent
74e2963a12
commit
3e5adf1dc5
1571 files changed
+336468
-6348
No files matched your search
@@ -0,0 +1,297 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using BepInEx;
|
||||
using BepInEx.Unity.IL2CPP;
|
||||
using HarmonyLib;
|
||||
using Il2CppInterop.Runtime.Injection;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AnantaSpoonProbe;
|
||||
|
||||
// AnantaSpoonProbe v1 — read-only Spoon graph oracle for Q1/Q2.
|
||||
//
|
||||
// What it does:
|
||||
// - Harmony PREFIX/POSTFIX on L50.Spoon.SpoonRunTime.SpoonTaskMgr load/deserialize
|
||||
// methods. Every hook only LOGS + DUMPS bytes; it never blocks or alters the call.
|
||||
// - Dumps any byte[] / native-pointer graph payload to BepInEx/dumps/spoon/.
|
||||
// - F8 = safe probe (no state change): path constants + workAction count for 60004938.
|
||||
// - Ctrl+F8 = LoadSingleTask(60004938) attempt with empty md5 (safe-ignore value per R211b).
|
||||
//
|
||||
// What it does NOT do:
|
||||
// - No INativeDetour, no native icall hooks (frozen game twice before).
|
||||
// - No fake md5 (hard-crashes client, R211b). SpoonMd5 is always "".
|
||||
// - No server RPCs. All local.
|
||||
[BepInPlugin("ananta.tools.spoonprobe", "Ananta Spoon Probe", "1.0.1")]
|
||||
public class SpoonProbePlugin : BasePlugin
|
||||
{
|
||||
internal static string DumpDir = "";
|
||||
internal static string ProbeLog = "";
|
||||
internal new static BepInEx.Logging.ManualLogSource Log = null!;
|
||||
|
||||
public override void Load()
|
||||
{
|
||||
Log = base.Log;
|
||||
try
|
||||
{
|
||||
var gameRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
|
||||
DumpDir = Path.Combine(gameRoot, "BepInEx", "dumps", "spoon");
|
||||
Directory.CreateDirectory(DumpDir);
|
||||
ProbeLog = Path.Combine(DumpDir, "spoonprobe.log");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.LogWarning($"[SpoonProbe] dump dir setup failed: {ex.Message}");
|
||||
DumpDir = "";
|
||||
ProbeLog = "";
|
||||
}
|
||||
|
||||
PLog("=== AnantaSpoonProbe v1.0.1 loaded (void-postfix fix) ===");
|
||||
PLog("hooks: SpoonTaskMgr load/deserialize (log+dumponly, never block)");
|
||||
|
||||
var harmony = new Harmony("ananta.tools.spoonprobe");
|
||||
int ok = 0, miss = 0;
|
||||
string mgr = "L50.Spoon.SpoonRunTime.SpoonTaskMgr";
|
||||
string[] methods =
|
||||
{
|
||||
"GetSpoonTaskFilePath",
|
||||
"ReadRawTaskFile",
|
||||
"ReadRawTaskFileHelper",
|
||||
"ReadRawTaskFileByPointAsync",
|
||||
"GetTaskPatchHelper",
|
||||
"DeserializeTaskByteByPointer",
|
||||
"CreateTaskGraphGroup",
|
||||
"CreateResidentAndTask",
|
||||
"LoadTaskGraphConfig",
|
||||
"LoadTaskResidentInfoConfig",
|
||||
"LoadTaskEventConfig",
|
||||
"LoadTaskEventInfoHelper",
|
||||
"GetResidentPath",
|
||||
"GetSpoonEventPath",
|
||||
"LoadSingleTask",
|
||||
"LoadGraphGroup",
|
||||
"LoadTaskGraph",
|
||||
"LoadTaskGraphHelper",
|
||||
"LoadTaskWorkActionData",
|
||||
"GetTaskWorkActionByTask",
|
||||
"CheckRpcIsLoadTaskGraph",
|
||||
"SyncPlayerSingleTask",
|
||||
"SyncAcceptSingleTask",
|
||||
"ProcessSingleTaskStateChange",
|
||||
"SyncSingleTaskStateChange",
|
||||
"LoadTaskEventInfo",
|
||||
"LoadTaskResidentInfo",
|
||||
"ReloadSpoon",
|
||||
};
|
||||
foreach (var m in methods)
|
||||
{
|
||||
if (TryPatch(harmony, mgr, m)) ok++;
|
||||
else miss++;
|
||||
}
|
||||
PLog($"hooks armed: ok={ok} miss={miss} (miss = overload sig mismatch, see lines above)");
|
||||
Log.LogInfo($"[SpoonProbe] armed ok={ok} miss={miss}, dump={DumpDir} (F8=safe probe, Ctrl+F8=LoadSingleTask 60004938)");
|
||||
|
||||
ClassInjector.RegisterTypeInIl2Cpp<SpoonProbeBehaviour>();
|
||||
AddComponent<SpoonProbeBehaviour>();
|
||||
}
|
||||
|
||||
internal static void PLog(string msg)
|
||||
{
|
||||
try
|
||||
{
|
||||
var line = $"[{DateTime.Now:HH:mm:ss.fff}] {msg}\n";
|
||||
if (!string.IsNullOrEmpty(ProbeLog))
|
||||
File.AppendAllText(ProbeLog, line);
|
||||
}
|
||||
catch { }
|
||||
try { Log.LogInfo($"[SpoonProbe] {msg}"); } catch { }
|
||||
try { UnityEngine.Debug.Log($"[SpoonProbe] {msg}"); } catch { }
|
||||
}
|
||||
|
||||
internal static string Summarize(object? o)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (o is null) return "null";
|
||||
if (o is string s) return $"\"{s}\"";
|
||||
if (o is byte[] b) return $"byte[{b.Length}] md5={Md5Of(b)}";
|
||||
if (o is IntPtr p) return $"0x{p.ToInt64():X}";
|
||||
var t = o.GetType();
|
||||
if (t.FullName == "UX.Game.TaskSpoonViewInfo" || t.Name == "TaskSpoonViewInfo")
|
||||
return SummarizeSpoon(o);
|
||||
if (o is IEnumerable e && !(o is string))
|
||||
{
|
||||
int n = 0;
|
||||
var parts = new List<string>();
|
||||
foreach (var it in e)
|
||||
{
|
||||
if (n < 8) parts.Add(Summarize(it));
|
||||
n++;
|
||||
}
|
||||
return $"[{string.Join(",", parts)}{(n > 8 ? $",+{n - 8}" : "")}]";
|
||||
}
|
||||
var s2 = o.ToString();
|
||||
if (!string.IsNullOrEmpty(s2) && s2 != t.FullName) return s2!;
|
||||
return $"{t.Name}";
|
||||
}
|
||||
catch (Exception ex) { return $"<sumfail {ex.Message}>"; }
|
||||
}
|
||||
|
||||
internal static string SummarizeSpoon(object o)
|
||||
{
|
||||
try
|
||||
{
|
||||
return $"Spoon{{md5=\"{Field(o, "SpoonMd5")}\" spRaid={Field(o, "SpRaidId")} " +
|
||||
$"start={Field(o, "StartTaskId")} end={Field(o, "EndTaskId")} " +
|
||||
$"alias=\"{Field(o, "Alias")}\" event={Field(o, "EventId")} eventStart={Field(o, "EventStartTaskId")}}}";
|
||||
}
|
||||
catch (Exception ex) { return $"<spoonfail {ex.Message}>"; }
|
||||
}
|
||||
|
||||
internal static object? Field(object o, string name)
|
||||
{
|
||||
var t = o.GetType();
|
||||
var f = t.GetField(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (f is not null) return f.GetValue(o);
|
||||
var p = t.GetProperty(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (p is not null) return p.GetValue(o);
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static string Md5Of(byte[] b)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var md5 = System.Security.Cryptography.MD5.Create();
|
||||
return BitConverter.ToString(md5.ComputeHash(b)).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
catch { return "n/a"; }
|
||||
}
|
||||
|
||||
internal static void DumpBytes(byte[] b, string label)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(DumpDir) || b is null || b.Length == 0) return;
|
||||
if (b.Length > 80_000_000) { PLog($"dump SKIP {label}: {b.Length} bytes (cap)"); return; }
|
||||
var ts = DateTime.Now.ToString("yyyyMMdd-HHmmss-fff");
|
||||
var safe = "";
|
||||
foreach (var c in label) safe += (char.IsLetterOrDigit(c) || c is '_' or '-' or '.') ? c : '_';
|
||||
if (safe.Length > 60) safe = safe[^60..];
|
||||
var path = Path.Combine(DumpDir, $"{ts}_{safe}_{b.Length}b_{Md5Of(b)}.bin");
|
||||
File.WriteAllBytes(path, b);
|
||||
PLog($"dump OK {label}: {b.Length} bytes md5={Md5Of(b)} -> {Path.GetFileName(path)}");
|
||||
}
|
||||
catch (Exception ex) { PLog($"dump FAIL {label}: {ex.Message}"); }
|
||||
}
|
||||
|
||||
internal static void DumpPointer(IntPtr ptr, int length, string label)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ptr == IntPtr.Zero || length <= 0 || length > 80_000_000) return;
|
||||
var buf = new byte[length];
|
||||
Marshal.Copy(ptr, buf, 0, length);
|
||||
DumpBytes(buf, label + "-ptr");
|
||||
}
|
||||
catch (Exception ex) { PLog($"ptr dump FAIL {label}: {ex.Message}"); }
|
||||
}
|
||||
|
||||
private static bool TryPatch(Harmony harmony, string typeName, string methodName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var type = AccessTools.TypeByName(typeName);
|
||||
if (type is null) { PLog($"HOOK MISS {typeName}:{methodName} (type not found)"); return false; }
|
||||
var methods = AccessTools.GetDeclaredMethods(type);
|
||||
int found = 0, patched = 0;
|
||||
foreach (var mi in methods)
|
||||
{
|
||||
if (mi.Name != methodName) continue;
|
||||
found++;
|
||||
try
|
||||
{
|
||||
// v2 fix: a postfix declaring __result cannot compile on void
|
||||
// methods (Harmony "IL Compile Error"). All 12 v1 OK hooks
|
||||
// return values; all 16 v1 FAIL hooks return void.
|
||||
var postfix = mi.ReturnType == typeof(void)
|
||||
? new HarmonyMethod(typeof(SpoonProbePlugin), nameof(GenericPostfixVoid))
|
||||
: new HarmonyMethod(typeof(SpoonProbePlugin), nameof(GenericPostfix));
|
||||
harmony.Patch(mi,
|
||||
prefix: new HarmonyMethod(typeof(SpoonProbePlugin), nameof(GenericPrefix)),
|
||||
postfix: postfix);
|
||||
patched++;
|
||||
}
|
||||
catch (Exception ex) { PLog($"HOOK FAIL {typeName}:{methodName} ({mi}): {ex.Message}"); }
|
||||
}
|
||||
if (found == 0) { PLog($"HOOK MISS {typeName}:{methodName} (no declared method by that name)"); return false; }
|
||||
if (patched > 0) { PLog($"HOOK OK {typeName}:{methodName} x{patched}"); return true; }
|
||||
PLog($"HOOK BROKEN {typeName}:{methodName} x{found} (found but patch threw)");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex) { PLog($"HOOK FAIL {typeName}:{methodName}: {ex.Message}"); return false; }
|
||||
}
|
||||
|
||||
// Generic log-only hooks. __args/__result are supported by Harmony 2.x.
|
||||
// Never throws, never skips the original.
|
||||
public static void GenericPrefix(object __instance, object[] __args, MethodBase __originalMethod)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parts = new List<string>();
|
||||
if (__args is not null)
|
||||
for (int i = 0; i < __args.Length; i++)
|
||||
parts.Add($"a{i}={Summarize(__args[i])}");
|
||||
PLog($"ENTER {__originalMethod?.DeclaringType?.Name}.{__originalMethod?.Name}({string.Join(" ", parts)})");
|
||||
|
||||
// Byte-carrying payloads: dump every byte[] arg immediately.
|
||||
if (__args is not null)
|
||||
{
|
||||
foreach (var a in __args)
|
||||
{
|
||||
if (a is byte[] b && b.Length > 0 && b.Length <= 80_000_000)
|
||||
DumpBytes(b, $"{__originalMethod?.Name}-arg");
|
||||
}
|
||||
}
|
||||
// Native pointer + length pairs (CreateTaskGraphGroup, ReadRawTaskFileHelper).
|
||||
if (__args is not null && __args.Length >= 2
|
||||
&& __args[0] is IntPtr ptr && ptr != IntPtr.Zero)
|
||||
{
|
||||
try
|
||||
{
|
||||
int len = -1;
|
||||
if (__args[1] is int li) len = li;
|
||||
else if (__args[1] is uint lu) len = unchecked((int)lu);
|
||||
if (len > 0 && len <= 80_000_000)
|
||||
DumpPointer(ptr, len, $"{__originalMethod?.Name}");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public static void GenericPostfix(object __instance, object? __result, object[] __args, MethodBase __originalMethod)
|
||||
{
|
||||
try
|
||||
{
|
||||
PLog($"LEAVE {__originalMethod?.DeclaringType?.Name}.{__originalMethod?.Name} -> {Summarize(__result)}");
|
||||
if (__result is byte[] b && b.Length > 0)
|
||||
DumpBytes(b, $"{__originalMethod?.Name}-result");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// Void methods: no __result (declaring it fails IL compile on void targets).
|
||||
public static void GenericPostfixVoid(object __instance, object[] __args, MethodBase __originalMethod)
|
||||
{
|
||||
try
|
||||
{
|
||||
PLog($"LEAVE {__originalMethod?.DeclaringType?.Name}.{__originalMethod?.Name} -> void");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user