碎碎视界(Event Horizon)修改器 · SSMH 文档
目标包:
碎碎视界3.12.26.4916.2.apk包名:com.suisui.EventHorizon引擎:Unity IL2CPP(metadata v29,arm64-v8a) 工具:D:\suisui_mod\SSMH(GUI启动SSMH.bat/ CLIssmh_cli.py) 最后更新:2026-11-05(新增「资源反加」,原「掉落率增加」改名并升级为「资源掉落倍率」)
0. 一句话结论
SSMH = 解包 → 解析 il2cpp 元数据 → 核对 hook 目标 → 注入 Frida gadget + 悬浮窗 dex → 改包名共存 → 回编译签名 的桌面工具。
和目标像素地牢那套(改 smali 逻辑)不同:这是 Unity IL2CPP 包,游戏逻辑全在 libil2cpp.so 里,
smali 里只有壳,所以改数值只能在运行期做——用 Frida gadget 在进程内按名字找到 C# 方法并挂钩,
Java 侧只负责悬浮窗 UI 和开关存储。
1. 包体结构
碎碎视界3.12.26.4916.2.apk
├─ classes.dex 3.1 MB 只有 Unity 壳 + AndroidX + GPGS,没有游戏逻辑
├─ lib/arm64-v8a/
│ ├─ libil2cpp.so 85.5 MB 全部游戏逻辑(C# 编译产物)
│ ├─ libunity.so 21.4 MB Unity 运行时
│ ├─ libmain.so / lib_burst_generated.so
├─ assets/bin/Data/
│ ├─ Managed/Metadata/global-metadata.dat 12.6 MB IL2CPP 元数据(v29,15 147 个类型)
│ ├─ data.unity3d 67.9 MB 资源
│ └─ resources.resource 53.7 MB
└─ AndroidManifest.xml package=com.suisui.EventHorizon,单个 UnityPlayerActivity
要点:
- 只有 arm64-v8a,没有 armeabi-v7a / x86 → 修改包也只能装 64 位设备(现在基本都是)。
android:extractNativeLibs="true"→System.loadLibrary("gadget")能从lib/arm64-v8a正常加载。- manifest 里没有
<provider>、没有自定义私有<permission>,所以共存版改名只动package=一处就够。
2. 工程结构
D:\suisui_mod\SSMH\
├─ SSMH.py tkinter GUI(选包 → 解析 → 生成)
├─ ssmh_cli.py 命令行:python ssmh_cli.py <apk> [--probe] [--no-coexist]
├─ 启动SSMH.bat
├─ core/
│ ├─ paths.py 工具链探测(java/apktool/d8/zipalign/apksigner/7z/keystore/gadget)
│ ├─ meta.py global-metadata.dat v29 解析 + 8 个 hook 目标自检
│ └─ build.py 解包 → 编译 dex → 打补丁 → 投 gadget → 改包名 → 回编译签名
├─ modsrc/com/ssmh/mod/
│ ├─ Config.java 开关与倍率(静态字段,agent 每 250ms 读一次)
│ ├─ Loader.java onCreate 里解 agent 脚本 + loadLibrary("gadget");onResume 挂悬浮窗
│ ├─ ModMenu.java 悬浮窗入口
│ └─ FloatingPanel.java 悬浮球 + 面板(纯代码 UI,贴边自动隐藏)
├─ agent/agent.js 运行期引擎:il2cpp 符号解析 / 数值同步 / 掉落倍率 / 资源反加
├─ tools/
│ ├─ libgadget.so Frida gadget 16.7.19 android-arm64(必须 16.x,17 起无 Java 桥)
│ ├─ android-stub.jar 编译 dex 用的 android.jar
│ └─ modforge.keystore 自签名证书(口令 modforge,别名 modforge)
├─ work/ 解包缓存与中间产物
└─ output/ 成品 APK
外部工具(core/paths.py 里改,支持同名环境变量覆盖):
| 工具 | 路径 |
|---|---|
| JDK 17 | D:\apkwork\jdk\jdk-17.0.20.1+1 |
| apktool 2.11.1 | D:\apkwork\apktool.jar |
| build-tools 34 | D:\apkwork\buildtools\android-14(d8 / zipalign / apksigner) |
| 7-Zip | D:\7-Zip\7z.exe |
3. 数据模型(为什么这样挂钩)
global-metadata.dat 反出来(Il2CppDumper v6.7.46,配置 Change il2cpp version to 29.1)之后,钱/油/星币的位置是这样:
Session.Content.ResourcesData // 存档镜像(ISerializableData)
_money : ObscuredLong @0x18
_fuel : ObscuredLong @0x68
_stars : ObscuredLong @0xB8
_tokens : ObscuredLong @0x108
_basicCryptocurrency : BasicCryptocurrency @0x1A8 // ← 玩法真正读写的那份
BasicCryptocurrency
_money @0x10 _stars @0x18 _tokens @0x20 _fuel @0x28 // 全部是 Cryptocurrency 对象
Cryptocurrency(引用类型,混淆很轻)
_value @0x20(隐藏值) _mask @0x28
GetCurrteValue() = _value ^ _mask
SetCurrteValue(v) 写回 _value = v ^ _mask,并触发 ValueChanged 信号(UI 刷新走这里)
ObscuredLong(ACTk 七槽轮转混淆,0x50 字节)
_value1.._value7 @0x00..0x30, _index @0x38, _values[] @0x40, _newindex @0x48
→ 不要手写内存,直接调 GetValue(this) / SetValue(this, v)
GameServices.Player.PlayerResources
TryConsumeFuel(int) // 判断油量够不够 + 扣掉,返回 bool
RemoveResource(ItemId,long) // 扣任务/普通资源
AddResource(ItemId,long) // 加任务/普通资源(掉落、采集、任务奖励的唯一入账口)
get_BasicCryptocurrency() // 拿到上面那四个货币对象
GameServices.Player.PlayerSkills
_experience : Experience(含 ObscuredLong) @0x38 // 玩家等级经验,上限 337 500 000
GameServices.Research.Research
AddResearchPoints(Faction, long) // 科技点唯一入口(5 处调用)
Domain.Quests.Loot
_random : System.Random @0x18 // 战利品判定专用的随机数发生器
GameDatabase.DataModel.ResourceItem
get_Chance() // 星球资源掉率
三条设计决定:
- 不手改
ObscuredLong内存:它是七槽轮转 +_values[]托管数组,写错槽位等于写坏存档; 统一走GetValue/SetValue。 - 货币不碰
ResourcesData._money:那是序列化镜像,玩法只认BasicCryptocurrency里的Cryptocurrency; 改镜像不生效、改对象才生效。 - 倍率用「差额补足」不用「直接乘」:读当前值 → 和 300ms 前的值比 → 增量 ×(倍率−1) 补进去。 好处是不会把商店扣款、修理费这些「减少」也算成倍率,也不会每帧写值把存档写花。
- 反加是「补得比扣得多」:同一个差额检测,方向反过来——值变少时把少的补回去,
再额外补 (N−1) 倍;N=1 就是「消耗不减」,N=2 就是「消耗多少反赚多少」。
油量不参与这条(它有自己的
TryConsumeFuel开关)。
4. 八个功能怎么实现
| # | 面板文字 | 挂点 | 做法 |
|---|---|---|---|
| 1 | 油量不减 | PlayerResources.TryConsumeFuel(int) |
onEnter 把参数 amount 改成 0:原逻辑照样返回 true,一滴油都不掉 |
| 2 | 信用点获取倍率 | Cryptocurrency(money) |
每 300ms 读一次,钱变多就把差额 ×(倍率−1) 补进去 |
| 3 | 星币获取倍率 | Cryptocurrency(stars) |
同上 |
| 4 | 经验值获取倍率 | PlayerSkills._experience@0x38 |
ObscuredLong.GetValue/SetValue 读改写,补差额,封顶 337 500 000 |
| 5 | 科技点获取倍率 | Research.AddResearchPoints(Faction,long) |
onEnter 直接把 amount × 倍率(奖励入口唯一,不会重复放大) |
| 6 | 资源使用时不减少 | PlayerResources.RemoveResource(ItemId,long) |
onEnter 把 amount 改成 0(反加的 N=1 特例,保留旧开关) |
| 7 | 资源掉落倍率 | Loot._random + System.Random.Next(int) + ResourceItem.get_Chance() + PlayerResources.AddResource(ItemId,long) |
前三条让判定更容易过(Next(max) 返回值除以倍率、星球掉率乘倍率);AddResource 是资源入账唯一口,onEnter 把 amount × 倍率,到手数量直接翻倍 |
| 8 | 资源反加(油量以外的资源) | Cryptocurrency(money/stars/tokens) + PlayerResources.RemoveResource |
货币:差额检测方向反过来,值变少 M 时补回 M ×(N−1)(N 由「反加返还倍率」控制);任务材料:amount 取反成负数,原逻辑「减去」变成「加上」,净返还 M ×(N−1)。油量不走这条 |
补充说明:
- 7/8 里凡是「乘倍率/取反」的入参都是 64 位(
long)。正数用ptr(v)写, 负数(反加用)先用int64(-n),运行时没有int64()就手写补码指针(i64()helper)。 - 7 依赖「调用点没被内联」。
ResourceItem.get_Chance在 dump 里查不到直接调用(说明被内联了), 所以它只是兜底;真正起作用的是Loot._random和AddResource两条路。AddResource是资源入账唯一口,倍率同时覆盖掉落、采集、任务奖励。 - 8 的两种路径互补:货币(信用点/星币/加密币)由主循环 300ms 差额补加,不依赖调用点;
任务材料/物品在
RemoveResource上直接改参数,即时生效、不用等下一拍。 TryConsumeFuel/RemoveResource/AddResource用「改参数」而不是Interceptor.replace, 因为 replace 之后没法再调原函数,功能一关就废了;改参数可以随时开关。- 所有 hook 回调里读的是 JS 侧缓存的开关值(
S.cfgJs),不直接 JNI 读 Java 字段—— 少一次跨层调用,也避免在游戏线程上碰 JVM。
5. 悬浮窗
- 纯代码 UI(不加资源、不申请
SYSTEM_ALERT_WINDOW),挂到Activity.getWindow().getDecorView(): 它是 Unity 的 FrameLayout 根,球不占满屏,不拦游戏触摸。 - 位置真源只有
FrameLayout.LayoutParams.leftMargin/topMargin(不用 translation), 拖动范围 clamp 到[-hide, 父宽-球宽]。 - 贴边自动隐藏:松手时看球心在左半还是右半,自动吸到那一侧,
leftMargin设成-hide(球一半出屏),透明度降到 0.35;再拖出来就恢复。 阈值hide = max(10dp, 球宽/2 - 6dp)。 - 坐标存
SharedPreferences,越界自动回默认位置;面板里有「重置悬浮球位置」。 - 面板内容:6 个倍率滑杆(1 = 关闭;「反加返还倍率」的 1 是有效值,表示等额返还)
- 3 个开关(油量不减 / 资源使用时不减少 / 资源反加)+ 实时状态行(钩子数、当前生效值)。
6. 注入链(第二轮重写版)
UnityPlayerActivity.onCreate() ← smali 注入(.locals 之后,不动原有寄存器)
└ Lcom/ssmh/mod/Loader;->onCreate(Activity)
├ Config.init() 读 SharedPreferences
├ 把 assets/ssmh_agent.js 复制到 /data/user/0/<pkg>/files/ssmh_agent.js
└ System.loadLibrary("gadget") ← 此时 config 里的脚本路径已经存在
└ libgadget.so 读 libgadget.config.so,起 QuickJS 跑 agent
UnityPlayerActivity.onResume() ← smali 注入
└ Lcom/ssmh/mod/Loader;->onResume(Activity)
└ ModMenu.attach() → 悬浮球
gadget 配置(打包时写死新包名):
{"interaction":{"type":"script","path":"/data/user/0/<新包名>/files/ssmh_agent.js","on_change":"reload"},
"log":{"level":"info","console":true}}
6.1 必须用 Frida 16.x 的 gadget
从 Frida 17.0.0 起,Java 桥(frida-java-bridge)不再内置在运行时里,
脚本里 Java.perform() 直接 ReferenceError: Java is not defined(被 try 吞掉就是完全静默)。
本轮第一版用的 17.25.2,症状就是:悬浮球出来了(Java 层正常),引擎一行日志都没有。
现在 tools/libgadget.so 是 16.7.19 android-arm64(25.8 MB)。
要升 17+ 就得用 frida-compile 打包脚本并 import Java from 'frida-java-bridge',本轮没走这条路。
6.2 脚本里不要用 JS 定时器去调 Java
setInterval/setTimeout 的回调跑在 gadget 自己的线程上,在 16.7.19 里:
定时器能正常触发(文件日志能看到),但回调里 Java.perform(...) 会静默卡死,
连 android.util.Log 都写不出来。所以配置/状态改成文件桥:
files/ssmh_cfg.txt ← Java 写(开关/倍率),agent 每 300ms 读
files/ssmh_status.txt ← agent 写(引擎状态/当前倍率),面板每秒读来显示
面板左上角那四行「引擎: 已注入 (14 钩子) / 信用点x.. 星币x.. … 反加x2 / 油量.. / hooks=14」 就是 agent 写回来的。
6.3 il2cpp 初始化之前不能碰任何 il2cpp API
脚本在 loadLibrary 那刻就跑,此时 il2cpp_init 还没执行。这时调 il2cpp_domain_get()
会直接 SIGSEGV;更麻烦的是这个异常会把整个脚本运行时搞死(定时器从此不再触发)。
所以 agent 里有硬延迟:Date.now() - t0 < 5000 之前一律不探测。
6.4 镜像名带 .dll
il2cpp_image_get_name() 返回的是 Assembly-CSharp.dll,不是 Assembly-CSharp。
第一版写死不带后缀 → 72 个 assembly 全读到了却永远匹配不上,boot 一直空转。
6.5 数值怎么落地
- 货币(信用点/星币/油):直接改
Cryptocurrency的两个字段 ——[0x20]存的是值 ^ mask,[0x28]是 mask。写hidden = 新值 ^ mask即可, 不触发 ValueChanged 事件,所以从 JS 线程写也不会牵连 Unity UI。 - 经验:
PlayerSkills._experience(0x38) 是ObscuredLong七槽结构,手写会坏档, 只能调ObscuredLong.GetValue/SetValue;调用前先il2cpp_thread_attach(domain)挂到运行时。 - 数值同步:每 300ms 读一次,和上一拍比较,增量 ×(倍率−1) 补回去。 花钱/扣油是「减少」,默认不会被补,所以商店扣款照常扣——这是设计,不是 bug。
- 资源反加:同一个比较,方向反过来。
cur < last且开关打开时,cur = last + (last−cur) × (N−1)后写回(N = 反加返还倍率): N=1 回到扣款前的值(消耗不减),N=2 扣 M 变净赚 M,以此类推。 写回同样走_value ^ _mask直写字段 +ObscuredLong.SetValue写存档镜像, 所以退出重进数值还在。任务材料在RemoveResource里把amount取反, 净返还M ×(N−1),与货币口径一致。 - 资源掉落倍率:
AddResource(id, amount)入参amount × 倍率—— 掉落、采集、任务奖励都会走这里,所以是「到手数量」的倍率; 再叠加Loot._random的判定缩放,掉落判定和掉落量同时放大。 - 存档:实测不需要自己写
ResourcesData镜像,游戏在存档时会把Cryptocurrency的值同步回ResourcesData(见 §11 的持久化验证)。
7. 共存与签名
只改「应用标识」,绝不动类名(这条是像素地牢那边用命换来的经验,这里同样适用):
<manifest package="com.suisui.EventHorizon">→com.suisui.EventHorizon.ssmhandroid:authorities/ 私有<permission>/android:permission|readPermission|writePermission跟着改名apktool.yml的versionName追加-ssmhandroid:name(activity/service/receiver/provider)一律不动
再注入自己的 dex 并用独立证书签名 → 系统视为两个应用,数据目录隔离,可以和原版同装。
--no-coexist 时跳过第 1 步,用于覆盖安装原版(会破坏原版数据兼容性,慎用)。
8. 流水线
apktool d → smali + res + assets + lib
meta.Metadata(...) → 8 个 hook 目标自检(类/方法/字段)
javac --release 8 (android-stub.jar) → d8 --min-api 21 → classes.dex
build.patch_activity → UnityPlayerActivity onCreate/onResume 插入 2 条 invoke-static
build.install_gadget → libgadget.so + libgadget.config.so + assets/ssmh_agent.js
build.rename_manifest → 包名/权限/authorities(不动类名)
apktool b → base.apk
7z a → 注入 classes2.dex(apktool 只产 classes.dex)
zipalign -p 4 → 4 字节对齐
apksigner sign / verify → v1+v2+v3
耗时参考(这台机器,i5-12400):首次全流程 30.9 s(解包 + 元数据 + dex + 回编译 + 签名),
复用 work/ 缓存时 12.2 s;成品 165.4 MB(原包 151.3 MB + gadget 37.6 MB)。
实测产物(2026-11-05,新增反加 + 掉落倍率):
output/SSMH-3.12.26.4916.2.apk 161.0 MB
├─ classes.dex 3 125 756 B 原包 smali 回编译,UnityPlayerActivity 两条注入都在
├─ classes2.dex 21 260 B 自建悬浮窗 dex(com.ssmh.mod.*,6 滑杆 + 3 开关)
├─ lib/arm64-v8a/libgadget.so 25 822 568 B Frida gadget 16.7.19
├─ lib/arm64-v8a/libgadget.config.so 174 B 指向 /data/user/0/com.suisui.EventHorizon.ssmh/files/ssmh_agent.js
├─ assets/ssmh_agent.js 29 783 B
├─ AndroidManifest.xml package=com.suisui.EventHorizon.ssmh
zipalign -c -p 4 通过;apksigner verify 通过(CN=ModForge)
9. 验证方法
装包
adb install -r "D:\suisui_mod\SSMH\output\SSMH-3.12.26.4916.2.apk"
看引擎起没起(最关键的一步)
:: 引擎日志(最可靠,任何线程都能写)
adb shell cat /sdcard/Android/data/com.suisui.EventHorizon.ssmh/files/ssmh_agent.log
:: 加载器日志(gadget 有没有起来)
adb logcat -d -s SSMH:V
正常会看到(实测输出,节选):
agent script loaded
主循环已启动 (300ms)
镜像: Assembly-CSharp.dll | mscorlib.dll | ...
类解析: PlayerResources=true PlayerSkills=true Research=true Crypto=true Obscured=true Loot=true Random=true
已 attach 当前线程到 il2cpp
就绪: 钩子 14
状态: money=42662098 star=6140 token=0 fuel=100 exp=121431 cfg={"creditMul":99,"starMul":99,"expMul":99,"techMul":99,"dropMul":20,"resAdd":true,"resAddMul":2,"fuelKeep":true,"resKeep":true}
钩子命中: fuel=1 res=0 resAdd=0 addRes=0 tech=0 loot=0 rand=22204
看不到就按顺序查:
| 现象 | 原因 | 处理 |
|---|---|---|
| 球出来了但日志文件一直只有 1 行 | gadget 17.x 没有 Java 桥 | 换回 16.7.19(tools/libgadget.so) |
| 完全没有日志文件 | gadget 没加载 | adb logcat -d \| findstr nativeloader,确认 libgadget.so 真的被 dlopen |
boot#N 等 il2cpp 初始化: asm=-2 |
探测太早,il2cpp_init 还没跑 |
保持 5 秒硬延迟,别改成 0 |
boot#N ... imgs=72 但一直不就绪 |
镜像名要带 .dll |
imageByName() 已兼容两种写法 |
method 未找到: xxx |
版本换了类/方法名 | 跑 ssmh_cli.py <apk> --probe 看哪一项「注意」 |
| 面板显示「引擎: gadget 加载失败 …」 | System.loadLibrary("gadget") 失败 |
看 logcat -s SSMH 里的 Loader 堆栈 |
看功能生效:进游戏 → 点悬浮球 → 把「信用点获取倍率」拉到 5 → 打一场拿钱,
数字应该是原来的 5 倍(面板状态行会显示当前生效倍率,日志里会出现 补足 money: a -> b)。
注意:花钱仍然会扣——获取倍率只放大「获取」,不补「消费」。
看反加生效(2026-11-05 新增):
- 面板里勾上「资源反加(油量以外的资源,消耗转增加)」,把「反加返还倍率」拖到 2;
- 看日志里的 cfg 立刻变成
"resAdd":true,"resAddMul":2(300ms 内,说明文件桥是通的); - 回游戏里花一次钱(商店/修理/升级),下一拍日志出现
反加 money: 42662098 -> 42662198 (返还 x2)—— 扣 100 反而净赚 100; resAddMul=1时是「等额返还」(净 0,等于消耗不减),想只在特定场景不减就拖到 1。
看掉落倍率生效:「资源掉落倍率」拉高 → 战斗/采集结算时日志里 addRes 计数增长,
同一次入账的数量按倍率放大;rand 计数一直在涨说明 Loot 判定那条路也在拦。
无设备时的自检(只能验证结构,不能验证行为):
python ssmh_cli.py <apk> --probe :: 只解析,看 8 个目标是否 OK
python -c "import zipfile;print([n for n in zipfile.ZipFile(r'output/xxx-SSMH.apk').namelist() if 'gadget' in n or 'ssmh' in n])"
10. 踩坑记录
10.1 smali 注入不能改寄存器
UnityPlayerActivity.onCreate 原本 .locals 2 且 p1 被复用。注入点选在 .locals 之后、
用 invoke-static {p0} 只吃 p0,不动 v0/p1,避免 VerifyError。
10.2 gadget 必须在脚本落地之后再 load
Loader.onCreate 里先解 assets 到 filesDir,再 loadLibrary("gadget")。
反过来的话 gadget 启动时脚本还不存在,直接静默不加载。
10.3 replace 会吃掉原函数
Interceptor.replace 之后原函数指针就废了,功能需要「可关」的时候必须用
Interceptor.attach + 改参数(args[1] = 0),而不是 replace。
10.4 从 JS 线程调托管代码 = 随机崩
已改成 Handler.postDelayed 到主 Looper(见第 6 节)。setInterval 只负责投递,不碰 il2cpp。
10.5 ObscuredLong 字段间距是 0x50,不是 0x20
ResourcesData 里 _money@0x18、_fuel@0x68 差了 0x50;它是七槽结构
(_value1.._value7 + _index + _values[] + _newindex)。
按 ACTk 老版本 0x20 的布局去猜会写坏存档。
10.6 7z 注入 dex 会顶掉同名 entry(真实炸过)
第一版直接 7z a staged.apk build\dex\classes.dex,7z 按文件名入包,
把 apktool 刚编出来的 3.1 MB classes.dex 覆盖成自己的 17 KB → 游戏壳没了,装上也点不开。
修法(core/build.py: build_apk):先读现有 entry,挑一个没被占用的 classesN.dex 再复制改名注入。
N 必须从 2 开始:classes1.dex 系统根本不加载(第一版就是这么写成 classes1.dex,dex 白放)。
注入后还要回读 zip 确认 entry 真的进去了。
10.7 中文路径会把 aapt2 送走
work\碎碎视界3.12...-dec 这种目录,apktool 调 aapt2 时命令行编码对不上,报
failed to open directory: (2) 和一堆 \ufffd。工作目录一律 safe_stem() 成纯 ASCII。
10.8 复用缓存会二次加后缀
rename_manifest 是就地改文件,第二次跑 manifest_package() 读到的已经是 xxx.ssmh,
再拼一次就成了 xxx.ssmh.ssmh。build() 里先剥掉已知后缀再算新包名。
10.9 注入必须是幂等的
patch_activity 第一条会 raise("没匹配到"),复用缓存时调用已经在里面,
于是被误判成失败。改成:已有这两条 invoke 就记「已有」,只有连方法头都找不到才报错。
10.10 编译缓存会把旧 dex 打进包(第一次「改完没变化」就是这么来的)
build_dex() 早期只看文件存在,modsrc 改了照样复用旧 classes.dex,
现象是加载器日志一行都没有、和改之前完全一样。现在按 mtime 判定,源码新于 dex 就重编。
10.11 Frida 17 没有 Java 桥
见 §6.1。症状:悬浮球正常,引擎静默。判断方法:libgadget.so 是 17.x 就一定中招。
10.12 gadget 脚本里的日志去哪了
console.log 在 Android 上是 /dev/null(不是 logcat),
Java.perform + android.util.Log 又只在「loadLibrary 那条线程」上能用。
最终方案:new File('/sdcard/Android/data/<pkg>/files/ssmh_agent.log','a').write(...),
adb shell cat 直接看,任何线程都能写。
10.13 File 读文件:readAll() 在 16.7.19 里不是函数
new File(path,'r') 能开,但 f.readAll() 抛 TypeError: not a function。
改成 typeof f.readAll === 'function' ? f.readAll() : f.readBytes(f.size),
再 new Uint8Array(buf) 逐字节转字符串(QuickJS 里 ArrayBuffer.toString() 得到的是 [object ArrayBuffer])。
10.14 定时器线程 + Java = 静默死锁
同一份脚本,脚本顶部(loadLibrary 线程)调 Java.perform 成功;
放到 setInterval 回调里就再也没有下文(try/catch 也来不及写日志)。
所以所有 Java 交互(配置、状态、加载器错误)都被挪到了「Java 写文件 / agent 读文件」。
10.15 第一次读数时 PlayerResources 还没抓到
构造函数 hook 只在会话创建时跑一次,而 agent 要等 5 秒(等 il2cpp 初始化)才挂钩,
大多数启动都会错过。补了两个「UI 每帧都在调」的 getter hook
(get_BasicCryptocurrency、get_Experience),谁先到就用谁。
11. 当前验证状态(2026-11-05 00:35,真机 Redmi 23049RAD8C / Android 15)
| 项目 | 结果 |
|---|---|
| il2cpp 元数据解析(8 个目标类/方法/字段) | ✔ 全 OK(含新增的 get_Basic_Tokens 检查) |
| 悬浮窗 dex 编译 / classes2.dex 注入 | ✔ 21 260 B,6 滑杆 + 3 开关 |
| smali 注入(onCreate / onResume) | ✔ 成品 classes.dex 反编译核对存在 |
| gadget 加载 | ✔ nativeloader: Load ... libgadget.so ... ok |
| agent 启动 | ✔ 文件日志 agent script loaded / 主循环已启动 (300ms) |
| 符号解析 | ✔ PlayerResources / PlayerSkills / Research / Cryptocurrency / ObscuredLong / Loot / Random 全 true |
| 钩子安装 | ✔ 14 个(日志 就绪: 钩子 14,新增 AddResource) |
| 读数 | ✔ money=42662098 star=6140 token=0 fuel=100(和游戏内一致) |
| 倍率补足 | ✔ 补足 money / 经验/星币补足在面板与日志双端一致 |
| 存档持久化 | ✔ 走正常退出存档后重启,数值保持 |
| 面板 ↔ 引擎 文件桥 | ✔ 勾「资源反加」+ 拖到 x2 后 cfg 立刻变 "resAdd":true,"resAddMul":2 |
| 面板文字改名 | ✔ 「掉落率增加」→「资源掉落倍率」(上限 20 → 50) |
| 「资源反加」控件 | ✔ 复选框 + 返还倍率滑杆,x1 不再显示「关闭」(1 是有效值) |
| 贴边自动隐藏 | ✔ 球贴右边缘只露一半,点击可展开 |
TryConsumeFuel 钩子 |
✔ 本次实机计数 fuel=1(进入行星系时被调用,油量 100 未减) |
System.Random.Next 钩子 |
✔ 每 5s 计数增长(掉落路径的随机数发生器已拦到) |
AddResource 钩子 |
✔ 已挂上;libil2cpp.so 里 2 处真实 bl(0x2196A6C / 0x2196F40,都归属 private bool Resources(int id)),本轮战斗无资源产出,计数仍 0 |
RemoveResource 调用点 |
ℹ️ 全 so 只有 1 处 bl,在 ClearAll() 里 → 玩家扣材料的调用点被内联,反加主路径是货币差额 |
| 反加·消费触发 | ⚠ 待对局:需要在商店/修理处真实花一次钱看 反加 money: a -> b |
| 实机「跑图掉油」「战斗结算掉落」体感 | ⚠ 未做长时间对局验证 |
12. 换版本怎么办
python ssmh_cli.py <新apk> --probe,看 8 个目标哪个「注意」;- 「注意」的项多半是方法改名/加参数:在
core/meta.py的TARGETS里改候选名, 再到agent/agent.js的S.targets同步参数个数; - 字段偏移(0x10/0x18/0x28、0x38、0x18)如果变了,用 Il2CppDumper 重新 dump 一次
dump.cs对照(工具:D:\sssj_work\tools\il2cppdumper); - 出包后先看
logcat -s SSMH有没有「就绪」,再逐项开功能。
已知待办:
- 「资源反加」的消费触发还没做长时间对局验证:货币路径是靠 300ms 差额采样, 如果某次扣款和一次入账落在同一个采样周期里,看到的是净变化,反加会按净额补;
- 「资源使用时不减少」与「资源反加」同时打开时,反加优先(
resAdd覆盖resKeep), 面板上两条都勾着也不冲突,只是以反加为准; - 掉落倍率目前靠
Loot._random+AddResource两条路;战斗结算里如果有独立的Random实例或直接写Cryptocurrency的掉落,需要再登记一条; - 「经验倍率」写的是
PlayerSkills._experience,如果后续版本把经验挪进Session.Content.xxxData,得跟着换字段偏移; - 悬浮球贴边后点击区域只有半个球宽,手指粗的可能要拖两次;
- 面板滑杆没有输入框,想精确设成 x50 只能拖到底(掉落倍率上限已从 20 提到 50)。
13. 变更记录
13.1 2026-11-05 · 新增「资源反加」,掉落功能改名并升级
| 项 | 之前 | 现在 |
|---|---|---|
| 面板第 7 项 | 掉落率增加(上限 20) | 资源掉落倍率(上限 50,AddResource 入账数量也乘倍率) |
| 面板新增 | — | 资源反加 复选框 + 反加返还倍率 滑杆(1~9) |
| Config 字段 | dropMul |
dropMul(语义改为掉落倍率)+ resAdd + resAddMul |
| agent 钩子 | 13 个 | 14 个(新增 PlayerResources.AddResource) |
| cfg 行 | 7 行 | 9 行(多 resAdd / resAddMul) |
设计口径:
- 「资源反加」只作用于油量以外的资源——信用点、星币、加密币走 300ms 差额补加,
任务材料/物品走
RemoveResource参数取反;油量继续用「油量不减」。 - 返还倍率
N是「消耗 M 返还 M×N」:N=1 等额返还(净 0 = 消耗不减), N=2 扣多少反而多拿多少,N=9 就是 9 倍返还(净 8 倍)。 - 旧开关「资源使用时不减少」保留,等价于反加的 N=1 特例;两个都开时以反加为准。
- 「资源掉落倍率」= 判定更容易过(
Loot._random)+ 到手数量翻倍(AddResource), 两段叠加,所以体感比旧版「掉落率增加」强得多。