多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

用GTK和gtkmm打造IPS补丁工具:格式、实现与避坑

用GTK和gtkmm打造IPS补丁工具:格式、实现与避坑 简介这是一款基于GTK的IPS补丁工具源自2014年的开源项目用于将IPS补丁包应用到ROM文件解决游戏汉化或修改时手动打补丁的繁琐问题适合模拟器玩家、怀旧游戏爱好者以及C开发者学习参考。代码结构清晰将核心逻辑、日志、文件输入输出等模块拆分并同时提供命令行版和GUI版命令行版通过“源文件补丁目标”三个参数即可完成打补丁操作方便理解补丁格式的解析与写入以及GTK界面程序的工程组织方式。压缩包共15个文件主体为C源文件和头文件另含makefile、界面描述文件、许可证和说明文档总体积仅21KB轻量易读makefile支持发布与调试两种构建模式可通过make或make debug1直接生成两个二进制便于二次开发与维护。目前已有254人学习从中可获取完整的IPS补丁工具实现思路、跨平台构建配置以及命令行和GUI交互的代码范例适合想学习文件补丁算法和桌面应用开发的读者。1. 一个 GTK 写的 IPS 补丁工具双击就能给旧 ROM 打补丁拿到一个老游戏的日文 ROM想打汉化补丁给群友分享自己改过的 ROM又不想传整包只想发一个小补丁。这两种场景都落在 IPS 补丁格式上而多数人第一次用命令行工具就被一堆错误码拍在脸上。这个基于 GTK 的 ips-patcher 解决的问题很直接把原 ROM 和修改后的 ROM 对比生成补丁或者把已有补丁写进 ROM半小时内能上手。它适合三类人玩老游戏汉化的、做修改版 ROM 的、想在自己工具链里嵌入 ROM 修补逻辑的 C 开发者。下文不空谈 UI按格式、实现、界面和避坑逐层拆开。2. IPS 格式与 GTK 选型先看懂 hunk 再写界面2.1 IPS 不是黑匣子hunk 结构与 EEOFIPS 全称 International Patching System是 ROM 修补圈子里最古老的格式之一。它没有文件头没有魔数校验开头第一个字节就是第一条记录的偏移。整份补丁由一串变长记录组成每条记录叫一个 hunk结构固定字段字节数大端序含义offset3是从目标文件第 0 字节起的覆盖位置size2是后续数据长度为 0 时表示 RLE 特殊记录datasize否要覆写进目标文件的新字节文件末尾一定是45 45 4F 46也就是 ASCII 的EEOF。读到这四个字节整个补丁结束后面再有内容也不应该被标准工具处理。要把格式讲透得抓住一个关键点IPS 记录的是“新数据”不记录“旧数据”。应用补丁时直接覆盖目标文件对应偏移不需要先做条件匹配也不需要知道被替换的字节长什么样。这意味着应用补丁是破坏性操作原 ROM 一旦写坏没有补丁内部的任何信息可以反推回来。这也是后面避坑章节里我反复强调先备份的原因。IPS 还有一个容易踩的隐藏限制offset 只有 3 字节最大寻址0xFFFFFF也就是标准 IPS 只能处理 16MB 以内的文件。超出部分要么换 IPS32 扩展格式要么放弃。size 字段 2 字节单条 hunk 最多 65535 字节如果一段连续修改超过这个长度创建工具必须把数据拆成多条 hunk。2.2 为什么用 C 和 gtkmm 而不是 Qt选 GTK 而不是 Qt不是 GTK 功能更强而是这个场景恰好合适。ips-patcher 承担两个职责文件选择、执行补丁。它不需要复杂的模型视图框架不需要富文本不需要网络组件。GTK3 的FileChooserDialog、Entry、Button、Label四个控件就能搭完整个界面依赖面比 Qt 小一圈编译产物也干净。C 这边用的是 gtkmm也就是 GTK 的 C 封装而不是裸 C 的 GTK。理由很简单裸 GTK 用gpointer和函数指针传参写文件 IO 时还得手工维护g_free很容易在新旧 API 之间迷失。gtkmm 把控件包成类信号用sigc::signal日常写法和 STL 风格统一处理std::string、std::vectoruint8_t自然顺手。文件解析和补丁逻辑可以独立成纯 C 函数完全不依赖 GTK这样以后想加 CLI 接口直接复用同一份核心代码。网上的 IPS 工具大多数是命令行或者用 Electron 包了一层壳。命令行工具对普通玩家不友好Electron 打包出来动辄一两百兆。GTK 的 gtkmm 版本编译出来只有几兆配合系统自带或者随包附带的 GTK 运行时就能跑这个体积优势在分发场景里非常明显。当然GTK 在 Windows 上的运行时依赖是个隐患这件事放在避坑章展开。2.3 一个最小解析骨架先识记录再谈修补在写完整逻辑之前先做一个只读不写的解析器目的是验证文件格式避免把调试困难堆到后面。这个骨架也适合用来诊断“为什么补丁打上去没效果”。#include fstream #include cstdint #include iostream void dump_ips_hunks(const std::string patch_path) { std::ifstream patch(patch_path, std::ios::binary); if (!patch) { std::cerr 无法打开补丁文件 std::endl; return; } while (patch.good()) { char off[3]; patch.read(off, 3); if (patch.gcount() 3) break; // 大端序合成 24 位偏移 uint32_t offset (uint8_t(off[0]) 16) | (uint8_t(off[1]) 8) | uint8_t(off[2]); // 三字节是 EEO 时下一位应该是 F补丁结束 if (off[0] E off[1] E off[2] O) { std::cout EEOF补丁结束 std::endl; break; } char len_buf[2]; patch.read(len_buf, 2); uint16_t length (uint8_t(len_buf[0]) 8) | uint8_t(len_buf[1]); if (length 0) { // RLE 记录2 字节重复次数 1 字节填充值 char cnt_buf[2], val; patch.read(cnt_buf, 2); patch.read(val, 1); uint16_t count (uint8_t(cnt_buf[0]) 8) | uint8_t(cnt_buf[1]); std::cout RLE offset0x std::hex offset count std::dec count value0x std::hex int(uint8_t(val)) std::endl; } else { std::cout hunk offset0x std::hex offset length std::dec length std::endl; // 跳过数据段 patch.seekg(length, std::ios::cur); } } }这段代码只做诊断不做任何写操作。逻辑说明每次读取固定长度头部先判断是不是EEO开头再读长度长度为零走 RLE 分支否则跳过长度的数据继续循环。参数说明offset 按 3 字节大端字节序组装length 按 2 字节组装两个字段的字节序完全一致后面写代码时千万不要把其中一个写成小端这种错位非常隐蔽。3. 核心实现创建补丁与应用补丁的对称逻辑3.1 创建补丁diff 产出 hunk 的两个边界条件创建补丁的思路是逐字节比较原 ROM 和修改后的 ROM把连续不同的区域提取成 hunk。实现上最需要注意两个边界一是文件尾部相同区域不能生成 hunk二是单段差异超过0xFFFF必须拆分。#include fstream #include vector #include cstdint #include algorithm #include string bool create_ips_patch(const std::string orig_path, const std::string mod_path, const std::string patch_path) { // 二进制读入两份 ROM std::ifstream in_orig(orig_path, std::ios::binary); std::ifstream in_mod(mod_path, std::ios::binary); std::ofstream out_patch(patch_path, std::ios::binary | std::ios::trunc); if (!in_orig || !in_mod || !out_patch) return false; std::vectoruint8_t orig((std::istreambuf_iteratorchar(in_orig)), std::istreambuf_iteratorchar()); std::vectoruint8_t mod((std::istreambuf_iteratorchar(in_mod)), std::istreambuf_iteratorchar()); // 标准 IPS 偏移上限是 0xFFFFFF超出应走 IPS32 if (mod.size() 0xFFFFFF) return false; size_t total std::max(orig.size(), mod.size()); // 越界字节按 0x00 处理保证比较不越界 auto byte_at [](const std::vectoruint8_t buf, size_t i) - uint8_t { return i buf.size() ? buf[i] : 0; }; size_t i 0; while (i total) { // 跳过完全相同区域 while (i total byte_at(orig, i) byte_at(mod, i)) { i; } if (i total) break; // 记录连续差异段的起点和长度 size_t hunk_start i; while (i total byte_at(orig, i) ! byte_at(mod, i)) { i; } size_t hunk_len i - hunk_start; // 单条 hunk 不能超过 0xFFFF拆成多条 while (hunk_len 0) { uint16_t chunk static_castuint16_t( std::minsize_t(hunk_len, 0xFFFF)); // 写 3 字节大端偏移 out_patch.put(static_castchar((hunk_start 16) 0xFF)); out_patch.put(static_castchar((hunk_start 8) 0xFF)); out_patch.put(static_castchar(hunk_start 0xFF)); // 写 2 字节大端长度 out_patch.put(static_castchar((chunk 8) 0xFF)); out_patch.put(static_castchar(chunk 0xFF)); // 写新 ROM 中的数据 for (size_t k 0; k chunk; k) { out_patch.put( static_castchar(byte_at(mod, hunk_start k))); } hunk_start chunk; hunk_len - chunk; } } // 标准结束标记 out_patch.put(E); out_patch.put(E); out_patch.put(O); out_patch.put(F); return true; }逻辑说明外层指针i先跳过相同字节再统计连续不同字节。byte_at的越界处理是为了支持修改后文件比原文件长的情况——原文件第 N 个字节越界时视为0x00和新数据不一样这段差异会被正确登记。内层while按0xFFFF拆段保证每条 hunk 都符合 2 字节长度字段的约束。参数说明偏移计算是从 0 开始的绝对文件偏移不是相对上一段的偏移写入out_patch时按位运算拆成字节顺序是高位到低位这就是大端序的手工实现。直接写uint32_t到流里只会得到本机字节序x86 上就是小端必然出错。3.2 应用补丁偏移、长度与 RLE 解压应用补丁是创建补丁的逆过程但实现上多是直接用偏移覆盖不做条件判断。这里同样给一份可运行的简化版支持普通 hunk 和 RLE 记录。#include fstream #include vector #include cstdint #include string bool apply_ips_patch(const std::string rom_path, const std::string patch_path) { // 必须 in|out 模式原地覆盖写 std::fstream rom(rom_path, std::ios::in | std::ios::out | std::ios::binary); std::ifstream patch(patch_path, std::ios::binary); if (!rom || !patch) return false; for (;;) { char off_buf[3]; patch.read(off_buf, 3); if (patch.gcount() 3) return false; uint32_t offset (uint8_t(off_buf[0]) 16) | (uint8_t(off_buf[1]) 8) | uint8_t(off_buf[2]); // 标准补丁以 EEOF 结束这里先判断前三个字节 if (off_buf[0] E off_buf[1] E off_buf[2] O) { break; } char len_buf[2]; patch.read(len_buf, 2); if (patch.gcount() 2) return false; uint16_t length (uint8_t(len_buf[0]) 8) | uint8_t(len_buf[1]); if (length 0) { // RLE2 字节重复次数 1 字节填充值 char cnt_buf[2], val_buf; patch.read(cnt_buf, 2); patch.read(val_buf, 1); uint16_t count (uint8_t(cnt_buf[0]) 8) | uint8_t(cnt_buf[1]); rom.seekp(offset, std::ios::beg); for (uint16_t k 0; k count; k) { rom.put(val_buf); } } else { std::vectorchar data(length); patch.read(data.data(), length); if (patch.gcount() static_caststd::streamsize(length)) { return false; // 补丁被截断 } rom.seekp(offset, std::ios::beg); rom.write(data.data(), length); } } return true; }逻辑说明rom以可读写模式打开seekp定位到目标偏移后write覆盖数据。RLE 分支用循环写填充值不用memset因为fstream没有批量填充接口循环虽然慢一点但数据量有限可接受。代码里对patch.read的返回值做了检查数据不足直接失败退出防止补丁文件被截断时写出残缺 ROM。参数说明补丁解析循环的核心是“读 3 字节偏移 → 读 2 字节长度 → 按长度读数据”任何一步读不到完整字节都应当视为格式错误。EEOF 判断放在读偏移之后、读长度之前这样避免把FF结尾的合法数据误判成结束。实际标准要求读满 4 字节EEOF才视为完整结束教学版用前三字节判断已经够用生产环境建议把第 4 个字节也读出来验证。3.3 参数与行为差异这些数字不能拍脑袋IPS 格式里的每个字段都有硬性上界这些数字来自格式定义不是实现细节。写代码时如果忽略了任何一项补丁生成后要么应用报错要么在别人的工具里无法识别。字段字节数最大合法值说明offset30xFFFFFF超过即超出标准 IPS 能力size20xFFFF单条 hunk 数据长度上限RLE count20xFFFF重复写入次数理论上不取 0EEOF4固定45 45 4F 46大小写敏感其中最容易翻车的是 RLE count 取 0。规范上 count 是两字节可以表达 0但大多数实现读到 count0 时会直接跳过这条记录个别实现却把它当作 65536 次写入。为了避免行为不可预测生成补丁时遇到连续相同字节长度不满 4 字节就不值得用 RLE直接按普通 hunk 写。创建补丁还有一个行为差异对文件尾部完全相同的区域是否生成 hunk。有些工具会生成“尾部从某处开始相同”的空记录这实际上是一条无操作 hunk应用后无害但会白白增大补丁文件。本文的创建函数在if (i total) break处终止保证尾部相同区域不会生成任何 hunk补丁体积最小。4. GTK 界面与主流程文件选择、后台线程与进度回传4.1 窗口结构两行文件选择加一个模式切换界面结构不复杂关键是选择器和模式切换要直观。第一行放“原 ROM”第二行放“修改后的 ROM 或补丁文件”中间放一个下拉框切换“创建补丁”和“应用补丁”两种模式。下面是一份 gtkmm3 风格的窗口骨架放在PatcherWindow类里。#include gtkmm.h class PatcherWindow : public Gtk::Window { public: PatcherWindow() : m_box(Gtk::ORIENTATION_VERTICAL, 8), m_row1(Gtk::ORIENTATION_HORIZONTAL, 8), m_row2(Gtk::ORIENTATION_HORIZONTAL, 8) { set_title(IPS Patcher); set_default_size(580, 240); // 模式切换创建补丁或应用补丁 m_mode.append(创建补丁原 ROM → 修改后 ROM); m_mode.append(应用补丁ROM .ips); m_mode.set_active(0); // 第一行原文件 m_entry1.set_placeholder_text(选择原 ROM); m_button1.set_label(浏览…); m_row1.pack_start(m_entry1, Gtk::PACK_EXPAND_WIDGET, 8); m_row1.pack_start(m_button1, Gtk::PACK_SHRINK, 0); // 第二行目标文件 m_entry2.set_placeholder_text(选择修改后 ROM 或补丁文件); m_button2.set_label(浏览…); m_row2.pack_start(m_entry2, Gtk::PACK_EXPAND_WIDGET, 8); m_row2.pack_start(m_button2, Gtk::PACK_SHRINK, 0); // 底部操作区 m_run_button.set_label(执行); m_status.set_text(等待选择文件); m_box.pack_start(m_mode, Gtk::PACK_SHRINK, 0); m_box.pack_start(m_row1, Gtk::PACK_SHRINK, 0); m_box.pack_start(m_row2, Gtk::PACK_SHRINK, 0); m_box.pack_start(m_status, Gtk::PACK_SHRINK, 0); m_box.pack_start(m_run_button, Gtk::PACK_SHRINK, 0); add(m_box); show_all_children(); m_button1.signal_clicked().connect( sigc::mem_fun(*this, PatcherWindow::on_pick_file1)); m_button2.signal_clicked().connect( sigc::mem_fun(*this, PatcherWindow::on_pick_file2)); m_run_button.signal_clicked().connect( sigc::mem_fun(*this, PatcherWindow::on_run)); m_mode.signal_changed().connect( sigc::mem_fun(*this, PatcherWindow::on_mode_changed)); } private: void on_pick_file1(); void on_pick_file2(); void on_run(); void on_mode_changed(); Gtk::VBox m_box; Gtk::HBox m_row1; Gtk::HBox m_row2; Gtk::Entry m_entry1; Gtk::Entry m_entry2; Gtk::Button m_button1; Gtk::Button m_button2; Gtk::ComboBoxText m_mode; Gtk::Button m_run_button; Gtk::Label m_status; };逻辑说明pack_start控制控件在容器内的伸缩策略。文件输入框用PACK_EXPAND_WIDGET让 Entry 占满剩余宽度按钮用PACK_SHRINK保持在最右侧不动。模式下拉框放在两组文件选择上方因为用户需要先决定“我是创建还是应用”再选择对应文件顺序更自然。参数说明ComboBoxText在 gtkmm3 里用signal_changed通知选择变化在 gtkmm4 里被改成了property_active()加回调迁移时注意接口差异。Entry的set_placeholder_text在用户未输入时显示灰色提示不参与文件路径的实际值获取内容统一用get_text()。4.2 把耗时的补丁操作放进 worker 线程GUI 程序一个经典翻车点直接在按钮回调里执行文件 IO界面会瞬间卡死。如果 ROM 是几十 MB 甚至上百 MB 的 CD 镜像逐字节比较可以在界面上冻结好几秒Windows 会直接弹出“程序未响应”。正确做法是主线程只负责收集参数和启停控件耗时计算放到独立线程完成后通过信号切回主线程。#include thread void PatcherWindow::on_run() { // 主线程收集界面参数避免子线程访问控件 std::string first m_entry1.get_text(); std::string second m_entry2.get_text(); int mode m_mode.get_active_row_number(); if (first.empty() || second.empty()) { m_status.set_text(请先选择文件); return; } // 进入处理状态禁用按钮防止重复触发 m_run_button.set_sensitive(false); m_status.set_text(正在处理…); // 后台线程执行补丁逻辑 m_worker std::thread([this, first, second, mode]() { if (mode 0) { // 创建补丁原 ROM 修改后 ROM → 新 .ips m_success create_ips_patch(first, second, first .ips); } else { // 应用补丁原 ROM .ips → 原地写回 m_success apply_ips_patch(first, second); } // 通知主线程处理完毕 m_dispatcher.emit(); }); }逻辑说明first、second、mode三个变量在进入线程前用值捕获复制了一份子线程完全不触碰控件成员避免数据竞争。m_dispatcher是Glib::Dispatcher它的emit()只能在子线程调用调用后 GTK 主循环会在主线程执行预先连接好的回调这是 gtkmm 推荐的线程回传方式。参数说明get_active_row_number()返回下拉框当前选中项序号0 是创建补丁1 是应用补丁。m_success是类的成员变量主线程在回调里读取它决定弹窗内容。如果补丁文件很大建议在 worker 里周期性调用gdk_threads_add_idle或让 Dispatcher 携带进度值避免界面长时间无反馈。4.3 文件存在性与扩展名校验先查再看文件选择对话框已经限制了用户能看到哪些文件但不能保证用户选的是有效 ROM 或合法补丁。执行前至少要做三层检查路径非空、文件存在、扩展名基本匹配。检查越早用户困惑越少。#include glibmm/fileutils.h #include glib/gstdio.h bool check_input_files(const std::string rom, const std::string patch, bool is_create_mode) { // 第一层文件必须存在且是普通文件 if (!Glib::file_test(rom, Glib::FILE_TEST_IS_REGULAR)) { return false; } if (!is_create_mode !Glib::file_test(patch, Glib::FILE_TEST_IS_REGULAR)) { return false; } // 第二层扩展名提示 if (!is_create_mode patch.size() 4 patch.substr(patch.size() - 4) ! .ips) { return false; // 扩展名不对提示用户确认 } return true; }这里用Glib::file_test而不是std::ifstream因为file_test能区分“文件不存在”和“没有读取权限”返回信息更直接。扩展名.ips全小写是比较保守的约定实际很多补丁分发用.IPS大写校验时建议用g_ascii_strcasecmp做大小写不敏感比较。模式切换的回调里应同步更新第二行的提示文字。创建模式下第二行是“修改后的 ROM”应用模式下第二行是“IPS 补丁文件”输入框的 placeholder 跟随模式变化能避免一半以上的误操作。这个细节成本极低但对新手观感提升非常明显。5. 避坑记录五个实战翻车的现象、原因与解决5.1 大端序不是默认项三个字节的偏移写错现象生成补丁后应用时 ROM 完全错乱文本全变成乱码而且错位位置看起来有规律。原因x86 机器上直接out_patch.write(reinterpret_castchar*(offset), 3)写入的是小端字节序。IPS 要求 3 字节大端偏移0x123456正确写法是12 34 56小端写出来是56 34 12所有 hunk 的写入位置全部错位越到文件后面错得越离谱。解决不要依赖整数的二进制内存布局用位运算逐字节写出。创建补丁时hunk_start 16、hunk_start 8、hunk_start各取一字节按高位到低位顺序写入。应用补丁读到三个字节后用(b0 16) | (b1 8) | b2组装回整数。这两个方向都不能图省事。5.2 RLE 被误判成压缩正常数据被“解坏”现象某些 ROM 文件里自带大量重复字节比如大字库区域全是0x00生成补丁后体积剧增或者应用时画面出现大块花屏。原因很多第一次写 IPS 工具的人会把 RLE 理解成“压缩相同的字节”于是见到连续重复数据就生成 RLE 记录。问题在于 IPS 的 RLE 并不是无损压缩的可选优化它是 hunk 的一种合法表达方式应用工具必须正确解析。如果某处碰巧有 3 字节相同生成器却按 RLE 写了一条 count 很大的记录应用时会覆盖掉大片不应该被修改的数据。解决创建补丁时不要看见重复就压缩。我一般设定的阈值是连续相同至少 4 字节才考虑 RLE并且算一下 RLE 记录头部 5 字节是否比普通 hunk 的 2 字节头部更省。真正安全的做法是简化版只用普通 hunk牺牲一点补丁体积换取 100% 的应用兼容。5.3 Windows 下双击闪退缺的不是代码是运行时现象编译好的 exe 在 Linux 上运行正常拷到 Windows 双击后闪退命令行运行报找不到libgtk-3-0.dll。原因GTK 在 Windows 上没有系统级依赖需要随程序分发一组运行时 DLL。用 MSYS2 编译时链接器只保证编译通过不负责把动态库依赖装进目标机器。用户机器上没装 GTK3 Runtime程序连窗口都创建不了。解决发布目录里带上bin目录下所有libgtk-3-0.dll、libglib-2.0-0.dll、libgdk-3-0.dll、libpango-1-0-0.dll等依赖或者用 MSYS2 的打包脚本收集依赖。更省事的方案是直接给 Linux 用户提供 AppImage给 Windows 用户提供带运行时的 zip 压缩包分别在对应系统上测试一遍再发布。5.4 中文路径导致 IO 失败UTF-8 与本地代码页现象在 Windows 上选择D:\游戏汉化\patch.ips程序提示文件打开失败但文件明明存在。原因GTK3 在 Windows 上文件对话框返回的是 UTF-8 编码路径而std::ifstream底层用的是 CRT 的本地代码页中文 Windows 默认 GBK。UTF-8 的“戏”字节序列在 GBK 里可能解析成别的字符路径根本对不上。解决读文件时统一走 Glib 的编码转换Glib::filename_from_utf8(path)拿到本地编码再传给std::ifstream。或者反过来用Glib::filename_to_utf8把本地编码转成 UTF-8 显示。Linux 和 macOS 上默认全是 UTF-8没有这个困扰但代码里保留转换分支以后 Windows 用户拿来即用。5.5 16MB 边界超过 0xFFFFFF 的偏移是无效的现象给一个 32MB 的 ROM 生成补丁应用时后半段全部没变化或者工具直接报错。原因IPS 标准偏移字段是 3 字节最大 0xFFFFFF约 16MB。超过这个大小的文件中所有偏移落在 0xFFFFFF 之后的内容根本没法表达。有的新工具会静默丢弃这些 hunk有的按 0 处理行为完全取决于实现。解决生成前判断目标文件大小超过 16MB 直接提示改用 IPS32 或者 UPS 格式。IPS32 是 IPS 的扩展偏移字段扩到 6 字节通过在一开始写入PATCH标识来区分但兼容性完全依赖接收方的工具版本分发前必须和对方确认。6. 进阶批处理校验与 IPS32 的取舍6.1 用脚本验证补丁而不是肉眼打完补丁别急着打开模拟器先用脚本做一次完整性对比。理想流程是原 ROM → 应用补丁 → 逐字节对比修改后 ROM。这个验证闭环比任何 UI 提示都可靠能暴露字节序、截断、错位等所有问题。# Linux / macOS 下直接对比两个文件是否一致 cmp -l modded.sfc applied.sfc # 任何平台先做大小对比再算 CRC32 cksum modded.sfc applied.sfccmp -l会列出所有差异字节的序号和值如果输出为空说明补丁应用结果与手工修改的 ROM 完全一致。cksum输出 CRC32 和文件大小两份值完全相同才算通过。我习惯把这条命令写进 Makefile每次改代码后自动跑一遍比手动点按钮靠谱得多。6.2 当文件超过 16MB 时怎么办IPS32 扩展格式在文件开头写入PATCH五个字节之后的 hunk 使用 6 字节偏移。实现 IPS32 的解析器并不复杂在现有循环里加一个偏移读取分支即可但真正麻烦的是分发接收方用的工具如果不支持 IPS32得到的补丁就是废品。常见做法是优先输出标准 IPS超过 16MB 再弹提示让用户选择 IPS32 或引导对方换工具。这属于产品决策不是技术能力问题。如果项目定位是个人工具我建议标准 IPS 为主IPS32 作为可选开关默认关闭。最后说一个我自己的习惯。早期做这类工具时我只测了“创建补丁 → 自己应用”这一条路发给别人后收到反馈说部分模拟器打不上补丁排查半天才发现是 RLE 记录写得太激进。从那以后我每次提交补丁生成相关代码都会强制走一遍“原 ROM → 应用补丁 → 逐字节对比”的闭环验证而且至少要拿两种不同工具交叉测试一次否则不敢往外发。希望后面这些细节能帮你在做 ips-patcher 时少走几步弯路。本文还有配套的精品资源点击获取
返回列表