BepInEx 6 IL2CPP 运行时 C# 调试 mod,通过 HTTP 执行 C# 代码,便于 人工或 Agent 进行热调试。
BepInEx/config/Il2cppConsoleMod.cfg:
[Server]
ListenAddress = 127.0.0.1
Port = 18765
[Security]
Enabled = false
AcknowledgeRisk = false
Enabled:总开关。AcknowledgeRisk:确认安全风险。- 两个都必须为
true才能启动。 - 默认监听 127.0.0.1,改 0.0.0.0 可局域网访问。
启动后日志打印随机 token,请求需带 X-Debug-Token:
curl -X POST http://127.0.0.1:18765/exec -H "Content-Type: text/plain" -H "X-Debug-Token: <token>" --data-binary @payload.cs
payload.cs:
public static class Payload { public static object Execute() => 1 + 1; }
GET /help 返回接口简述与内嵌的完整 README(markdown,编译时随 DLL 打包;同样需要 token)。
返回完整 LogOutput.log 原文(text/plain),同样需要 token:
curl -s http://127.0.0.1:18765/logcat -H "X-Debug-Token: <token>"
像 C# Interactive 控制台一样逐句执行,变量跨请求持久,无需 class Payload 包装:
curl -X POST http://127.0.0.1:18765/script -H "X-Debug-Token: <token>" --data-binary 'var x = 1;'
curl -X POST http://127.0.0.1:18765/script -H "X-Debug-Token: <token>" --data-binary 'x + 1' # Result: 2
curl -X POST http://127.0.0.1:18765/script -H "X-Debug-Token: <token>" --data-binary 'var y = x + 1;'
curl -X POST http://127.0.0.1:18765/script/reset -H "X-Debug-Token: <token>" # 清空会话状态
- 裸语句即可;预置 using:
System/System.Linq/System.Collections.Generic/System.Text/System.Threading.Tasks/UnityEngine。 - 提交以不带分号的表达式结尾时自动返回其值(响应中
HasResult区分"返回了 null"与"无返回值")。 - 脚本内可声明类型,但不能使用
namespace(需要完整程序时请用/exec)。 - 会话状态随提交次数线性增长,长时间调试建议定期
/script/reset。 - 程序集引用在首次提交时快照;游戏运行中后加载的程序集不会出现在脚本里(这种情况用
/exec)。 - 脚本内避免
async/await(Unity 主线程没有同步上下文)。
scripts/console.py 提供类 shell 的交互客户端(纯标准库,零依赖):
python3 scripts/console.py <ip> <token> [--port 18765]
进入后逐句输入 C# 代码回车执行;!reset 清空会话,!help 查看帮助,exit 或 Ctrl-D 退出。
在 /script 会话里可以对游戏方法做热补丁。必须用 /script 而不要用 /exec:/exec 的载荷编译进可回收 ALC,请求一结束就被卸载,补丁随之失效;/script 的程序集不可回收,补丁常驻。下面几点为强烈建议但非强制。
- 不要使用反射。 interop 程序集里游戏成员全部为 public,直接引用即可;il2cpp dumper 产物 DummyDll/stub 里的
private/protected只是转储产物,在 BepInEx/Il2cppInterop 中被全部公有化。目标方法一律用属性 +nameof绑定。 - 日志请走 BepInEx 日志源(写入
LogOutput.log);UnityEngine.Debug.Log在部分宿主上不可见。Logger与UnityEngine.Logger撞名时用全限定BepInEx.Logging.Logger.CreateLogSource。 - 以下代码中的
Example.*/TargetMethod均为占位符,按实际目标替换。
1. 挂载(属性式声明,零反射):
using System;
using BepInEx.Logging;
using HarmonyLib;
using Example.Gameplay;
[HarmonyPatch(typeof(SomeLogic), nameof(SomeLogic.TargetMethod))]
public static class DemoHook
{
public static int HitCount;
private static readonly ManualLogSource Log = BepInEx.Logging.Logger.CreateLogSource("DemoHook");
[HarmonyPrefix]
public static bool Prefix(SomeLogic __instance) // __instance = 本次调用实例
{
var n = System.Threading.Interlocked.Increment(ref HitCount);
Log.LogWarning($"[DemoHook] hit #{n} on {__instance?.GetType().Name}");
return false; // false = 跳过原方法(原方法为 void 时同样生效,可用于“屏蔽”副作用)
// return true; // true = 放行,继续执行原方法
}
}
var hook = new Harmony("Demo.Hook");
hook.PatchAll(typeof(DemoHook).Assembly);2. 验证(不用每次进行,仅在行为异常时可进行调试;补丁已注册 / 强制 GC 后仍存活):
System.GC.Collect();
System.GC.WaitForPendingFinalizers();
var n = 0;
foreach (var m in hook.GetPatchedMethods()) n++;
"patched methods: " + n // 期望 >= 13. 撤销:
hook.UnpatchSelf(); // 撤销该实例的全部补丁
// 或 HarmonyLib.Harmony.UnpatchID("Demo.Hook");4. 语义要点:
- Prefix 返回
bool:false= 跳过原方法体(拦截/屏蔽),true= 照常执行。对返回void的原方法同样有效。 Harmony.GetPatchInfo(...)在本版 HarmonyX 中是静态方法;UnpatchSelf()已取代过时的实例版UnpatchAll()。- 计数/日志只在进程内存与
LogOutput.log中,重启游戏即全部消失,不会污染存档或产生残留。
dotnet build -c Release
输出到 BepInEx/plugins,只需部署 Il2cppConsoleMod.dll。