Switch 原生移植与数值修改
把一个 Switch 游戏跑在 Mac / iPad 上,并永久修改游戏数值(不依赖修改器)。
什么时候用
- 要把某个 Switch 游戏跑在 Mac 或 iPad 上(不是模拟器)
- 要在游戏二进制里永久改数值(金钱、物品、经验、MP 等)
- 要把 Windows 的存档迁到 Mac/iPad,并做多端同步
- 要给某个游戏加"随时存档"这类便利功能
核心认知(先读这三条)
- 游戏逻辑 100% 在游戏的
main二进制里,移植层只翻译系统调用。所有数值修改都必须落在游戏代码上。 - 不要用运行时调试找函数。先静态解析"函数名 → 地址"表,5 分钟能定位的东西,用调试器要几小时。
- 跳板(cave)写错会直接闪退或死循环。写之前先跑检查脚本。
工作流
第一步:确认可移植性
# 从 NSP/XCI 读 title ID、版本、引擎
python3 nxnative/scripts/build_ipa_from_nsp.py games/<游戏目录> -k keys/prod.keys --platform mac
首次运行会生成 survey(<title id>_survey/survey.md),里面写着引擎、图形 API、SDK 版本。
第二步:构建 Mac 版
# 开发用(快,4-5 分钟,命中缓存)
... --platform mac
# 打包 app(含游戏数据,10-25 分钟)
... --platform mac-app --gpu none
--gpu none 在着色器对接口不匹配时必须加(否则 pso_offline 报 metal-tt 失败)。
第三步:构建 iOS 版
bash prep_build.sh
xcrun devicectl device install app --device <UDID> <DmIOS.app>
iPad 必须插线;每次重装要在设备上重新信任开发者证书。
第四步:逆向找函数
关键方法:静态解析 UE 的名字表。
import sys, struct
sys.path.insert(0, 'nxnative/dm/tools'); import nso
img = bytes(nso.cli_read('<build>/extract/<ver>/ExeFS/main').image)
TEXT = 0x5e6b890 # 从 <build>/patched_<ver>/main.fixups.tsv 的 TEXT_END 读
names = {}
for o in range(0, len(img) - 16, 8):
p, q = struct.unpack_from('<QQ', img, o)
if 0x5e6c000 <= p < 0x67d6000 and 0 < q < TEXT:
s = img[p:img.index(b'\0', p)].decode('ascii', 'ignore')
names.setdefault(s, []).append(q)
每个名字通常有两个地址:跳板(做参数整理,最后 b <实现>)和真正实现。改功能要改实现。
第五步:写补丁
补丁放在 nxnative/dm/games/<title id>/patches.py:
CUTS = {
'<build id>': [
(0x地址, '原始字节', '新字节', '说明'),
],
}
空间不够时用跳板:
原指令处: b cave
cave: 原始指令
自定义逻辑
b 原指令的下一条
第六步:快速验证
不要每次都重新构建。 宿主直接读 <build>/patched_<ver>/main.patched.bin,改字节 + 重启宿主即可,秒级。
第七步:云存档(可选)
GET /manifest.json → 槽位哈希/时间/大小
GET /files/<name> → 下载
PUT /files/<name> → 上传(自动备份旧版本)
X-Token: <token>
规则:按 mtime 取新,不能只比哈希。触发点:启动时拉取、存档提交后上传。iOS 必须走 HTTPS(ATS 限制)。
优先单指令,别为凑数字写跳板
| 倍数 | 一条指令能做到? |
|---|---|
| ×2 / ×4 / ×8 / ×16 | ✅ lsl #n |
| ×5 | ✅ add wX, wY, wY, lsl #2 |
| ×3 / ×10 / 其他 | ❌ 需要 mov + mul,即跳板 |
实战教训:金钱原本做 ×10(跳板),战斗结算 SIGILL 崩溃;改成 ×8(单指令)后稳定。
能单指令就不要跳板。
跳板四条铁律
- 回跳地址 = 补丁点下一条指令(写错会无限循环)
- 栈平衡:push/pop 对称;post-index 的
ldp会多移 sp - 寄存器冲突:跳板用到的寄存器,补丁点之后 5 条指令不能再读(
w9最容易踩) - 易失寄存器不能跨
bl:中间值存栈或放s16–s31
写完立刻检查:
python3 tools/cave_check.py <补丁点> <跳板> <长度>
数值修改的正确层次
以经验为例,有三种改法:
| 改法 | 结果 |
|---|---|
| 改倍率函数 | 只放大"加成"部分,不准 |
| 改结算中间值 | 可能只放大一项,不准 |
| 改最终获得量 | 精确 ✅ |
先找到"本次获得量"的最终值(通常是 新总 - 当前),再乘系数。
还要注意难度倍率:游戏可能在结算时再乘一层(HP/伤害/经验/MRP/金钱)。所以界面显示和实际发放可能天然不同——先确认是不是两套口径,再判断补丁对不对。
常见坑(按现象查)
| 现象 | 原因 |
|---|---|
| 黑屏 | 漏 DM_AOC_DIR / DM_NVN_SHMAP / DM_PSO_PAIRS |
| 读档报 DLC 丢失 | DLC 索引 0 不被接受;需要占位文件 + 改 aoc_load() |
| 画面冻结但音频正常 | 游戏主线程死循环;用 F12 打线程栈 |
| 闪退 | 跳板栈不平衡 / 寄存器冲突 |
| 探针 SIGBUS | 探针下在相对跳转指令上 |
| 内存扫描卡死 | 整块 vm_read,要分块 |
| 写入即崩 | 写到 key page(不可写区) |
| iPad 连不上服务器 | iOS ATS 禁止明文 HTTP |
| 旧档覆盖新档 | 只比哈希不比时间 |
参考
references/playbook.md— 完整实战手册(含命令、时间线、20 条坑)references/cave_check.py— 跳板自检脚本- 引擎:nxnative