botm
by huerji
README.md
# 圣女战旗 Mod 制作器(BotM Mod Studio)
> 《圣女战旗》(Banner of the Maid) 的 **Mod 制作器 / Mod 编辑器**:改数值与文案、
> 换立绘、改存档、造武器 / 道具 / 技能。GUI、CLI、MCP 三条通道共用同一套命令总线,
> 能力完全对等 —— 给不想敲命令的人、给脚本、也给 AI 助手。
---
## 能做什么(全部已实现)
| 能力 | 入口 |
|---|---|
| 改**角色 / 兵种 / 道具 / 装备 / 技能 / 勋章 / 掉落 / 军团 / 台词**的名称、描述 | GUI · CLI · MCP |
| 改上述各类的**数值**(HP/攻击/防御/敏捷/速度/幸运…) | GUI · CLI · MCP |
| 改**人物名称**(六语言同步) | GUI · CLI · MCP |
| 改**地形 / 天气 / 关卡信息** | GUI · CLI · MCP |
| 改**界面 UI 文案**(`commontext*`,六语言包同步) | GUI · CLI · MCP |
| 改**剧情台词与说话人**(`Dialog.bin`,六语言,按名字寻址) | GUI · CLI · MCP |
| 改**任务名/描述/奖励文案、章节名、成就、图鉴、帮助**(`ScriptControlData.bin`,11 张表) | GUI · CLI · MCP |
| 改**关卡/地图里的显示标签**(`Level/*.level` 的地形与单位名) | GUI · CLI · MCP |
| 替换**人物立绘 / 头像** | GUI · CLI · MCP |
| **换立绘图片本身**(头像 / 半身 / 全身;自动裁成游戏尺寸;同一张图的全部拷贝一次换掉) | 「立绘图片替换…」· `texture.fit` · `texture.replace_all` |
| 读 / 改**存档里角色的运行时数据**(等级、属性、兵种、技能、装备、立绘) | `save://R14/roles/2` · MCP |
| **一个角色改到底**:名称/台词(六语言)+ 等级/属性/道具,模板与存档各走各的路 | 「角色编辑…」面板 (Ctrl+E) |
| **存档备份与恢复**:整份存档的字节快照,恢复前自动留一份,恢复后逐字节校验 | 「存档备份…」面板 (Ctrl+B) · `save.snapshot` / `save.restore` |
| **造一把新武器**:以现有武器为模板,改耐久/射程/攻击/命中/防御/护甲,再叠加游戏里已有的功能(提升射程、提升士气增长、克制加成…) | 「武器工坊…」面板 (Ctrl+W) · `weapon-create` · MCP |
| **造一件新道具**:以现有道具为模板,改名称/描述/图标/品质/售价/堆叠上限,选效果(回复生命或行动力、永久加属性、加点数/勋章) | 「道具工坊…」面板 (Ctrl+I) · `item-create` · MCP |
| **造一个新技能**:以现有技能为模板,改触发时机与消耗,并可**连带造一个新 Buff**(技能的战斗数值全在 Buff 的属性通道上) | 「技能工坊…」面板 (Ctrl+K) · `skill-create` · MCP |
| **把某个角色整体换成另一个角色**(表 + 存档,一条命令) | `role-transplant` · MCP |
| **只换立绘**(等级/属性/技能一律不动) | `role-icons` · MCP |
| **立绘预检**:名字能不能显示、自动存档有没有被游戏覆盖、游戏是不是在跑 | `role-diagnose` · MCP |
| **移植残留预检**:换角色时哪些字段搬不过去、哪些留在原地(含会听到/看到的那些) | 「角色替换…」预检表 · `role-residue` · MCP |
| 立绘**预览**(头像 / 半身 / 全身,解析到真实纹理) | 「角色替换…」面板 · `role.portrait` |
| **武器工坊**:克隆一件真武器,改数值、叠已有功能 | 「武器工坊…」(Ctrl+W)· `weapon.create` |
| **道具工坊**:克隆一件真道具,改十个平铺字段与效果 | 「道具工坊…」(Ctrl+I)· `item.create` |
| **技能工坊**:克隆一个真技能,改触发与消耗,连带造新 Buff | 「技能工坊…」(Ctrl+K)· `skill.create` |
| **仓库**:把造出来的武器 / 道具变成玩家拿得到的实物 | 工坊「放入仓库」页 · `warehouse.add` |
| 跨表影响分析(同一物品出现在多张表) | `refs` |
| 批量改名 / 批量调数值 | `rename` · `scale` |
| 改动预览 → 备份 → 校验 → 回滚 | 全通道默认 |
| 变更报告导出(Markdown + JSON) | `report` |
| **实机验证:启动游戏 → 截图 → OCR 确认 → 回滚** | `live/`(MCP 11 个工具) |
| **AI 助手直接操作游戏**(看屏幕、按文字点击) | MCP |
**状态**:M0–M5 完成,**479 项测试**(478 通过 / 1 跳过)。
> 角色的**立绘与数值来自存档**,不来自 `Character.bin`(`PlayerData.ReadRecord`
> 会用 `Role.readRecord` 把整个 Role 覆盖回来)。只改表不会让已在玩的存档变样,
> 所以「换角色」是一条同时改两边的命令 —— 见
> [`docs/SAVE-FORMAT.md`](docs/SAVE-FORMAT.md)。
> **立绘换了却看不到变化**,几乎从来不是数据写错。三个真实原因,GUI 的
> 「角色替换…」面板会逐个查出来:
> ① `Rah` 是营地自动存档,游戏离开营地时用**内存里的旧角色**覆盖它 —— 只写
> 玩家槽不写它,下次进游戏就是旧立绘;② 立绘名在资源里不存在时游戏保留上一张
> 图,看起来和「没生效」一模一样;③ 只改 `Character.bin` 对已有存档无效。
> `Level/*.level` 与 `Map/*.map` 曾长期标注为「自定义二进制格式尚未逆向」——
> 现已解出,见 [`docs/DATA-SPEC.md`](docs/DATA-SPEC.md) §5.6。
---
## 快速开始
```powershell
$env:PYTHONPATH="$PWD\src"
# 桌面界面
python -m botm_editor.gui.app
# 命令行
python -m botm_editor.ai.cli tables
python -m botm_editor.ai.cli search "奶油糕点"
python -m botm_editor.ai.cli read "bin://Character.bin/items/10001"
# 角色 / 立绘替换
python -m botm_editor.ai.cli role-diagnose --roles 2,4,5 # 先查为什么没生效
python -m botm_editor.ai.cli role-icons --from 503003 --to 2 --apply # 只换立绘
python -m botm_editor.ai.cli role-transplant --from 503003 --to 2 --apply # 整体换人
# 让 AI 助手接入(MCP,stdio)
python -m botm_editor.ai.mcp_server --root "D:\...\Banner of the Maid"
```
可选依赖(按需安装,缺失只会降级对应模块):
```powershell
python -m pip install -i https://repo.huaweicloud.com/repository/pypi/simple `
UnityPy Pillow PySide6 fastmcp dnfile
```
---
## 三种用法
### 1. GUI — 给不想敲命令的人
**双击 `启动Mod制作器.bat`** 就行(等价于 `python tools/launch_gui.py`)。
已经有窗口在开的话,它会把你已有的那扇拉到前台,而不是再开一个。
起不来的话先看 `.botm_gui.log`(启动器的日志)和 `.botm_gui.out`(GUI 自己的输出)。
左侧数据表 → 中间条目 → 右侧字段。文本字段按**六种语言并排**显示,数值字段直接编辑。
点「预览并应用修改」会先弹出 diff,确认后才写入。
工具栏 **「角色替换…」(Ctrl+T)** 是立绘/角色替换的专用面板:选供体与目标、
看三张立绘预览(头像/半身/全身,解析到真实纹理)、勾选写入哪些存档,然后预检 →
应用。预检会逐个存档告诉你:立绘名能不能显示、这个槽里还是不是旧立绘、
`Rah` 自动存档有没有被游戏用内存里的旧角色覆盖、以及游戏是不是正在运行。
**游戏运行时面板拒绝写入** —— 那次写入是一场游戏必胜的竞态。
工具栏 **「立绘图片替换…」(Ctrl+P)** 换的是**图本身**:选角色 → 选槽位(头像/
半身/全身)→ 选一张你自己的画 → 选适配方式 → 看预览 → 替换。图会自动裁成游戏
要的尺寸(半身 270×300、全身 1024×2048、头像 179×159 左右),并一次换掉这张图的
**全部拷贝**。改完要重启游戏。
工具栏 **「角色编辑…」(Ctrl+E)** 改的是**一个角色的全部**,并且把「数据分在两个
文件里」这件事摆在明面上:名称 / 全名 / 描述(六语言)写 `Character.bin`,等级 /
属性 / 兵种 / 道具写存档里的运行时 Role。面板并排显示两边,数值可以选择写到
「存档(当前生效)」「模板(新游戏默认)」或两边都写。写入前自动备份、写入后回读
校验,失败自动回滚。
> 状态:面板、写入路径与 21 条测试都已完成;**真实落盘已实跑**
> (`tools/live_role_editor_apply.py` 四条分支全 PASS,存档与 `Character.bin` 逐字节复位)。
> `_apply()` 是**事务化**的 —— 任一槽失败就逆序回滚全部,与 `role.transplant` 同一套修法;
> 模板半失败会在碰存档之前中止。
### 2. CLI — 给脚本
```powershell
# 查
botm describe # 文件全貌:所有表、别名、条目数
botm search "步兵晋级勋章" # 命中道具/勋章/掉落三张表
botm read "bin://Character.bin/roles/1" # 波利娜,字段已语义化
botm refs "bin://Character.bin/items/13001" # 这个物品还被谁引用
botm schema # 导出全部字段名
# 改(默认只预览,加 --apply 才写)
botm set-text "bin://Character.bin/items/13001@name" --lang zh-CN --text "皇家步兵勋章"
botm set-text-all "bin://Character.bin/items/13001@name" --map '{\"zh-CN\":\"近卫勋章\",\"en\":\"Guard Medal\"}'
botm set-field "bin://Character.bin/roles/1@defaultMaxHP" --value 30 --apply
botm rename --match "晋级勋章" --replace "晋升勋章" --scope 13
botm scale --field defaultMaxHP --factor 1.5 --scope 1
# 立绘
botm portraits 波利娜
botm extract "ab://art/img_cha_half_1.assetbundle#Texture2D/img_cha_half_1" --out p.png
botm replace-texture "ab://art/img_cha_half_1.assetbundle#Texture2D/img_cha_half_1" --image 新图.png --apply
# 武器
botm weapon-list # 137 件武器,含解码后的战斗数值
botm weapon-effects # 游戏里已有的全部武器功能
botm weapon-create --id 22401 --template 20101 --name "天青石试作枪" \
--stat attack=20 --stat hit=120 --preset morale_up --preset long_range \
--durability -1 --tier 4 --price 500 --apply
# 仓库(玩家真正拿得到的东西;新造的武器要存进来才用得上)
botm warehouse-list --slot R14 # uid / 类型 / 名称 / 数量
botm warehouse-add --slot R14 --item 22401 --count 1 --apply
# 回滚
botm history
botm revert p-1a2b3c4d
```
### 3. MCP — 给 AI 助手
**49 个工具**,命名统一为 `botm_*`。典型流程:
```
botm_describe() → 全部数据表与别名
botm_search("步兵晋级勋章") → 定位
botm_refs("bin://Character.bin/items/13001") → 发现它还出现在表 13、14
botm_set_text(address, lang, text) → dry_run=True,返回 diff
botm_apply(patch_id) → 确认后提交
botm_revert(patch_id) → 字节级还原
botm_report() → 变更报告
```
所有写入类工具 **`dry_run` 默认 True**——AI 必须先看到 diff 再提交,和人类一样。
### 4. Live — 让 AI 直接操作游戏(实机验证)
写盘成功 ≠ 游戏读得进去。这一层启动真实游戏、截图、再回滚:
```
botm_game_launch(boot_wait=20) → 启动游戏
botm_game_shot() / botm_game_look() → 截图(look 另带焦点与存活状态)
↳ AI 用自己的视觉读那张 PNG,量出目标位置
botm_game_click(fx=0.5, fy=0.77) → 按**比例坐标**点击
botm_game_state() → 存活 / 崩溃记录
botm_verify_mod(address, mapping, → 改 → 跑两次 → 拍两张图 → 回滚;
route=[...]) 把图交给你判定(不判断、不用 OCR)
```
**MCP 侧不读屏**:实机工具一律不用 OCR——`botm_game_click_text` 与
`botm_game_read_screen` 已删除,脚本里的 `click_until`/`wait_until`/`advance`
会被明确拒绝,所以**不装 `rapidocr_onnxruntime` 也能全功能运行**。判断权归调用方:
把截图交给视觉模型。这样更准——实测 OCR 把 `VER 2.2.2.2` 读成 `VER 2.2.2`、
把 `L'étendard de la Pucelle` 读成 `C'etendara`,标题动画那一帧干脆什么都读不出,
而视觉模型一眼就能描述。
详见 [`docs/LIVE-VERIFY.md`](docs/LIVE-VERIFY.md)。两条关键实测结论:
- 本作**忽略合成键鼠输入(SendInput)**,只接受 `PostMessage` 窗口消息
—— 好处是**不需要前台焦点**,AI 可以在后台驱动游戏;
- 本作**不写任何 Player.log**,所以判据必须靠截图 + 视觉判定,不能读日志。
- **战斗场景**连 `PostMessage` 也不吃(菜单层正常)——输入中间件是 InControl,
游戏自己还有 `France.Game.GameKey` 门面。成因分析与后端选型见
[`docs/INPUT-BACKEND.md`](docs/INPUT-BACKEND.md)(方案 3 调研)。
---
## 地址格式
三段式:**表 / 记录 / 字段**。表可用编号或别名。
```
bin://Character.bin/roles/1 # 表1(角色)1号 → 波利娜
bin://Character.bin/roles/1@name[ja] # 她的日文名
bin://Character.bin/roles/1@defaultMaxHP # 初始 HP
bin://Character.bin/roles/1@defaultDEF[0] # 对物理攻击的防御
bin://Character.bin/items/13001@desc[zh-CN] # 道具描述
ab://art/img_cha_half_1.assetbundle#Texture2D/img_cha_half_1 # 立绘
text://commontext#900001 # 界面文案(容器名 = 语言包)
text://commontexten#901772 # 英文包的"游戏设置"
lv://ah01.level#4 # 关卡里的显示标签(序号寻址)
bin://Dialog.bin/1/11401@dialogsData[0].text[zh-CN] # 剧情第 1 段第 1 句台词
```
**别名**:`roles`(角色) `classes`(兵种) `items`(道具) `weapons`(武器) `equips`(装备)
`skills`(技能) `buffs`(状态) `legions`(军团) `shops`(商店) `points`(勋章) `rewards`(掉落)
`subtitles`(台词) `texts`(界面文案) 等 19 个。
**语言包**(`text://` 专用):`commontext`=zh-CN `commontexttw`=zh-TW `commontexten`=en
`commontextjp`=ja `commontextfr`=fr `commontextkr`=ko。用 `set_all_langs` 一次改六个。
**关卡标签**(`lv://` 专用):容器是文件名(`ah01.level`),`#` 后是**字符串序号**
(不是字节偏移 —— 改了长度偏移会漂移)。只读资源名、只改显示标签。
---
## 语义字段名从哪来
不是猜的——是从游戏自己的程序集里挖出来的(`tools/probe/dll_class.py`):
| 线索 | 来源类 |
|---|---|
| **Character.bin 表名** | `France.Resource.CharacterDataManager` 成员顺序 ↔ 顶层 field 号 |
| 角色字段 | `France.Resource.CharacterRoleData` |
| 兵种字段 | `France.Resource.CharacterClassData` |
| 道具字段 | `France.Resource.ItemData`(值级验证) |
| 技能字段 | `France.Resource.SkillData`(顺序完全对齐) |
| 勋章 / 掉落 / 军团 / 台词 | `PointData` / `RewardData` / `LegionData` / `VoiceSubtitleData` |
| **LevelGlobal.bin 表名** | `France.Resource.LevelGlobalManager` 成员顺序 |
| 地形 / 关卡字段 | `France.Resource.GTerrainData` / `GLevelInfoData` |
| 文本组结构 | `France.Resource.MultiLanguage {1: defaultValue, 2: multiValues}` |
| **`defaultDEF` 为何有 9 个值** | `WeaponData` 定义了 9 种攻击类型(`ATTACK_TYPES_COUNT = 9`) |
字段名附带**置信度**标注(`high` / `medium` / `low`),`botm schema` 可导出查看。
### 表标签约定(实测规律)
- **带数值的表**(角色/兵种/NG+):`1..8` 基础字段 → `101–106` 数值块(`103` 是按攻击类型索引的 9 元防御数组)→ `201+` ID 数组 → `301+` 文本/语音 → `1000` 附加
- **无数值的表**(技能/勋章/掉落/军团/台词):**顺序标签** `1..N`
- **主键**:多数表是整数,少数(关卡表)是字符串,如 `gcore-test`
---
## 为什么这个游戏好改
| 实测事实 | 意义 |
|---|---|
| Unity 5.6.5f1 + **Mono**(非 IL2CPP) | 数据与资源可静态修改 |
| `Assembly-CSharp.dll` **零混淆** | 可自由逆向,字段名可挖 |
| 核心数据是 **Protocol Buffers** | 结构自描述,可保真编辑 |
| AssetBundle 为 **LZ4HC 压缩**,但重存保真 | 立绘/文本可替换(`packer="original"`,payload 逐字节校验) |
| 官方**无** mod 支持、无创意工坊 | 只能走社区自研路线(即本项目) |
| 游戏版本 **2.2.2.2** | 标题画面实测(旧文档写的 2.0.9 已过时) |
| 运行时**不写 Player.log** | 实机验证只能走截图 + 视觉判定,见 `docs/LIVE-VERIFY.md` |
| 输入只认 **PostMessage**,忽略 SendInput | 可后台驱动,不抢用户焦点 |
---
## 核心设计
### AI 原生 = 能力对等,而非"再加个 API"
```
┌──────────────────┐
│ CommandBus │ ← 唯一能力实现
└────────▲─────────┘
┌──────────┼──────────┐
┌─────┴────┐ ┌───┴────┐ ┌───┴─────┐
│ MCP(AI) │ │ CLI │ │ GUI │
└──────────┘ └────────┘ └─────────┘
```
GUI 不拥有任何业务逻辑;CLI 与 MCP 只是把参数翻译成 `Command`。
### 低耦合高内聚
- `core/` 契约层,**零业务依赖**(CI 强制检查);
- `wire/` protobuf 引擎,不依赖契约层;
- 每个文件格式 = 一个模块,**模块之间禁止互相 import**;
- 跨模块需求(如"角色→立绘")由总线编排,不引入直接依赖。
### 保真编辑
```
serialize(parse(data)) == data # 未修改时字节级完全一致
```
这是不用官方 `protobuf` 库的原因——后者会丢弃未声明字段,直接损坏游戏数据。
同一思路也用在资源上:重存 AssetBundle 后**逐像素校验**。
### 安全链
`plan`(纯计算)→ `validate` → `diff`(预览)→ `apply`(备份 + 原子写 + 写后校验 + 失败自动回滚)。
Windows 下还会对文件锁做重试。
---
## 目录结构
```
BotMModEditor/
├── docs/
│ ├── DEVELOPMENT.md 总纲:目标 / 需求矩阵 / 架构 / 路线图
│ ├── DATA-SPEC.md 实测数据结构 / 地址规范 / 资源规范
│ ├── AI-PROTOCOL.md 命令总线 / Capability Manifest / MCP 工具
│ ├── LIVE-VERIFY.md 实机验证:AI 如何直接操作游戏检验 mod
│ └── INPUT-BACKEND.md 方案 3:输入后端调研(战斗场景怎么驱动)
├── schema/ 语义档案(表名 + 字段名 + 置信度)
├── mods/
│ └── BotMInput/ 实机输入后端:BepInEx 插件 + 文件 IPC(见 INPUT-BACKEND.md)
├── tools/
│ ├── probe/ 逆向侦察工具(只读)
│ └── live/ 实机驱动工具
│ ├── run.py 按 JSON 脚本走一条路线并截图
│ ├── verify_mod.py 完整闭环:改→跑→OCR 判定→回滚
│ ├── diagnose_input.py 输入/聚焦诊断
│ └── scripts/ 导航脚本
├── src/botm_editor/
│ ├── core/ ✅ 契约层(地址/命令/变更集/总线/工作区)
│ ├── wire/ ✅ 保真 protobuf 引擎
│ ├── modules/
│ │ ├── protobuf_bin/ ✅ Data/*.bin 读写
│ │ ├── texttable/ ✅ 界面文本表 commontext*(六语言)
│ │ ├── levelbin/ ✅ 关卡/地图 Level/*.level、Map/*.map
│ │ └── assetbundle/ ✅ 立绘提取与替换
│ ├── app/ ✅ 装配层
│ ├── ai/ ✅ CLI + MCP Server
│ ├── gui/ ✅ PySide6 界面
│ └── live/ ✅ 实机验证(截图/输入/OCR/报告)
└── tests/ ✅ 185 项:黄金/端到端/契约/MCP/GUI/文本表/关卡/嵌套/任务/实机
```
---
## 免责声明
- 仅供**个人学习与单机修改**使用,与游戏开发商 / 发行商无关,非官方作品;
- **不打包任何游戏美术、音频或完整数据文件**。唯一的例外是
`tests/fixtures/*.bin`(合计 24 KB):那是从一份已安装的游戏里裁出来的
最小测试数据,只保留几条记录,用来让解析器有东西可解析 —— 没有它,
离线测试跑不起来。删掉它们测试会失败,不是跳过。
- 修改前请务必备份存档;本工具会自动备份游戏文件;
- 请遵守游戏最终用户许可协议,并先拥有该游戏的合法副本。
## 许可证
MIT,见 [`LICENSE`](LICENSE)。
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues