senyaak/homm5-editor

GitHub: senyaak/homm5-editor

基于 Electron 的《英雄无敌V:东方部落》地图编辑器,通过逆向工程实现地形渲染与编辑,不依赖游戏原生资源。

Stars: 0 | Forks: 1

# homm5-editor(实验性项目) 一个为**魔法门之英雄无敌 V:东方部落(Heroes of Might & Magic V: Tribes of the East)**制作的地图/战役编辑器原型, 基于 Electron 和 Node 构建,无原生依赖。 全面采用 TypeScript。Node 24 —— 以及 Electron 43 内部的 Node 24.18 —— 原生支持类型擦除,因此 `src/`、`electron/` 和 `tools/` 可以直接从磁盘运行 `.ts` 文件,无需构建步骤;只有 `renderer/` 会被打包(使用 esbuild),因为浏览器无法擦除类型。因此,`tsconfig.json` 仅用于类型检查。 有一个值得注意的例外:`electron/preload.cjs` 必须保持为 JavaScript。Electron 会原样读取 preload 脚本,且从不应用 Node 的类型擦除钩子,因此如果 preload 是 `.cts` 文件,它会在遇到第一个类型注解时失效——而且是悄无声息地失效,导致在窗口正常加载时 `window.editor` 却未定义。 ## 可用功能 - **地形渲染,端到端**(`npm start`):通过分层着色器合成图块纹理,并按 `` 进行合成;海面根据地面标志位平面生成;绘制河流笔刷;在不同地面类型交界处生成垂直切面;带有岩石纹理的悬崖;以及两种地面。详细说明:[docs/TERRAIN_FORMAT.md](docs/TERRAIN_FORMAT.md)。 - **`GroundTerrain.bin`**(`src/terrain.ts`):读取高度、纹理层遮罩、地面标志位和河流平面,并将它们全部写回。各个平面大小固定,因此写入操作是原地逐字节覆盖,输出结果仅在指定修改的位置有所不同。已在真实的 96×96 和 136×136 地图上完成往返测试(`npm run test-terrain`,`npm run test-terrain-write`)。 - **`map.xdb` 模型**(`src/map.ts`、`src/xml.ts`):无损 XML DOM —— 在全部 108 个示例地图上均满足 `serialize(parse(x)) === x` —— 并在其上构建了带类型的对象模型。编辑某个对象只需重写一行代码。 - **对象编辑**:提供分类且可搜索的列表;点击选择,拖动以在网格上移动;保存;并带有版本追踪功能,打包为 `.h5m` 文件。 - **外部更改监视器**(`src/watch.ts`):原版的 Nival 编辑器可以同时打开同一个地图文件夹。当其保存时,会弹出一个横幅提示你采用它的版本。基于内容哈希而非时间戳进行判断,因此我们自身的保存操作永远不会触发该提示,且字节完全相同的重写也不会被视为更改。 - **笔刷游标**:当激活笔刷时,系统箭头会被隐藏,并替换为绘制在地面上的笔刷覆盖范围——即笔划将要触及的每一个图块,且会贴合地形走势,从而在提交前就能直观看到尺寸和位置。 - **高度笔刷**:使用径向衰减进行抬高和降低,支持实时重新构建网格,并能处理格式所要求的标志位过渡——将某个顶点挖至 0 会使其被水淹没,将其从 0 抬高则会排干水恢复为地面。在干燥地图上挖出的盆地会立即被水填满。 - **地面调色板和图块笔刷**:从自带的 `.dds` 文件中预览了全部 82 个内置图块,并按类别分组。选择一个图块,激活笔刷,按住左键拖动即可绘制宽度为 1/3/5/7 个图块的区域。笔划会立即写入 GPU 上的遮罩纹理以提供实时反馈,同时也会传送至主进程,由主进程管理最终保存的字节数据。 - **河流笔刷**:Water、Bog 和 LavaFlow 并非普通图块。绘制其中之一会将其河床下挖至比两岸低 0.4 的深度,并带有 0.2 的边缘坡度,同时会写入半图块的河流平面——正是该平面让游戏将其识别为真正的河流,而不仅仅是绘制出的图像。每个顶点的下沉操作都是幂等的,且在多次笔划间保持状态,其初始状态源自地图自身的河流平面。 - **添加纹理层**(`src/terrain-layer.ts`):当地图缺失某个被选中的图块时,此操作会将新的遮罩数组及其路径拼接到容器中,并扩展所有包含该数据块的声明长度。这是唯一一种移动字节而非原地覆盖的地形编辑操作,因此测试代码会对每一个已存在的平面进行逐字节的比较(`npm run test-terrain-layer`)。 - **网格解码**(`src/geometry.ts`):包含位置、索引、UV 和纹理。详见 [docs/GEOMETRY_FORMAT.md](docs/GEOMETRY_FORMAT.md)。 ## 运行 在 Windows 上,双击 `start-editor.bat` 即可执行相同操作:它会检查 Node 环境,在首次运行时安装依赖,并在出现任何故障时保持窗口打开。 ``` npm start # build the renderer, then launch the editor npm run typecheck # tsc --noEmit across the whole project npm run test-terrain # terrain parser round-trip on sample maps npm run test-terrain-write # plane writes + the tile brush npm run test-terrain-layer # splicing a new texture layer in npm run test-map # map.xdb model + loss-less XML round-trip npm run test-watch # external-change watcher npm run test-pak # ZIP reader/writer npm run inspect # low-level dump of a .bin's structure npm run harness # the renderer in a plain browser, on a stub bridge ``` `npm run harness` 会在 :8123 端口提供 `renderer/harness.html` 服务——这是真正的 `index.html`,但在应用模块之前注入了一个桩(stubbed)版本的 `window.editor`。渲染器在模块作用域内与 Electron 进行通信,因此如果不这样做,UI 就只能通过启动整个应用来进行测试;而通过 harness,你可以在任何浏览器中点击使用笔刷和工具栏,并且它还会将每一个 IPC 调用记录在 `window.__calls` 上。 将 `HOMM5_DATA` 指向一个已解压的游戏数据文件夹,或者将数据解压到 `samples/paks/data`(已被 gitignore 忽略)—— `.pak` 归档文件本质上是普通的 ZIP 文件,可由 `tools/pak-cli.js` 处理。 ## `GroundTerrain.bin` 通过经验进行逆向工程推导,并与 WindBell 在 2009 年的分析(heroescommunity.com TID=32009)进行了交叉验证。该容器是一个由自描述数组组成的流,每个数组都由一个帧组引入: ``` 01 08 # side in VERTICES (V = tiles + 1) 02 08 # the same value again 03 # sizeB = 2 * arrayByteLength + 1 ``` **长度的最低位是一个宽度标志**,这就是为什么数组扫描过程中遇到的每个长度值看起来总是“奇数”: ``` odd -> the size is a little-endian u32 there, len = (size - 1) / 2 even -> the size IS that single byte, len = size / 2 ``` 数组通常很大,总是采用 u32 格式;路径字符串很短,总是采用单字节格式。长度为 L 的图块路径存储为 `03 <2L+4> 03 <2L> `,即一个包裹着字符串记录的外部记录——在示例地图的全部 20 个层路径中完全一致。正是因为了解了这一点,才使得*写入*一个新层成为可能,而不仅仅是读取它。 关键要点: - 数据是**按顶点**存储的,而非按图块:一个 T×T 的地图在每个平面上会产生 `(T+1)²` 个值。 - **每个数组都通过 `sizeB` 声明了自身的长度**(`len = (sizeB − 1) / 2`),因此解析器无需硬编码任何大小,且适用于任何地图。`V` 同样来自于文件本身。 - 平面顺序:纹理层(u8 遮罩 + 指向 `(AdvMapTile).xdb` 的路径),接着是**高度**(`float32`),然后是**地面标志位**(u8)、一个保留平面、位于半图块 `(2V−1)²` 网格上的**河流平面**,最后是通行性。 - ⚠️ WindBell 的规范在这里并不准确。根本不存在独立的 Plateau / Ramp / WaterDepth 平面——它们实际上是**同一个标志位平面的不同位**。这一结论是通过对所有 232 张自带地图进行测量得出的:`0` 代表水,`16` 代表地面,`32` 代表高地,第 3 位代表斜坡。 - ⚠️ 高度**并不是唯一能产生视觉效果的水平面**。标志位决定了地形在何处断裂成垂直切面,在何处保持平滑,而河流平面则承载了绘制的水面。如果缺少了这些数据,地图看起来会发生根本性的变化。 - **可通行性取决于标志位,与水体纹理毫无关系。** 标志位 `0` 标记的是可游泳的水域;在测量的 60 张自带地图中,共计 62,788 个带有该标志的顶点,100.0% 都精确地位于 `0.0` 高度。引擎会自行绘制水面——而绘制在可通行河床上的实际上是普通的地面:DarkGround 占 29%,Conquest/Dirt 占 27%,Grass 占 14%,并且 56% 的此类顶点根本没有携带高强度的纹理。`Water.xdb` 甚至都没有出现在前八名中。 相反,绘制的河流会保持标志位 `16`,无论其河床有多深,都始终是可步行的。因此,湖泊是通过 `lower` 工具挖出来的,而不是画上去的;`Water.xdb`、`Bog.xdb` 和 `LavaFlow.xdb` 只是供你走过的装饰性浅水。 - 高度 `2.0` 是**默认的地面高度**,而不是水面高度。通过 `lower` 工具挖出的河床高度始终精确为 `0.0`。 包含测量数据的完整说明:[docs/TERRAIN_FORMAT.md](docs/TERRAIN_FORMAT.md)。 ## 资产和 3D 模型 - `data.pak`(ZIP 格式,约 1.4 GB):包含 6.2 万个 `.xdb` XML 描述符、9 千个 `.dds` 纹理、位于 `bin/Geometries/` 的二进制几何体(3567 个 GUID 文件)、位于 `bin/Skeletons/` 的骨架(2242 个),以及位于 `bin/animations/` 的动画(3393 个)。 - 引用链条是纯 XML 的:地图对象 → `*.(AdvMapStaticShared).xdb`(包含 `blockedTiles`,即覆盖范围)→ `*.(Model).xdb` → 几何体二进制文件加上 `.dds`。 - 几何体二进制文件使用**与 `GroundTerrain.bin` 相同的容器格式**(具有相同的 `08 ` 和 `03 ` 标签),但其内部的网格布局(顶点/索引/子网格/材质/蒙皮)要复杂得多。 ### 网格几何体——开发中 目前没有任何公开的 HoMM5 网格解析器;本项目的解析器完全是从零开始逆向工程得出的。解码后的网格可生成可直接用于渲染的顶点和三角形,并且在重建时实现了**零破损边缘**。 - 该容器是一个由记录组成的树状结构:标量记录为 `tag 08 `,块记录为 `tag `,其中 `len = (sizeB−1)/2`。通过 `parseTree()` 对其进行遍历。 - 一个网格节点包含**位置**数据 `count₁×vec3f`、一个**重映射**表 `count₂×u16`(所有值均 < count₁)以及**索引**数据 `tris×3×u16`。引擎执行了一次顶点拆分操作:`render[i].pos = positions[remap[i]]`,而三角形则引用渲染顶点。重映射表被明确识别为那个所有值均 `< count₁` 的 u16 数组。 - 位置数据会与 `.(Geometry).xdb` 中的包围盒进行验证。文件将数据存储了两次(用于 LOD);重复的那一份会被丢弃。 - **UV** 是属性流(`tag3`)的前 4 个字节:2×int16 ÷ 2048(V ∈ [0,1],U 为平铺模式)。这一点已通过共享边缘的 UV 连续性得到了证实。 - **法线**是根据几何体计算得出的——文件中打包存储的法线数值并不精确。 - **纹理**为 `.dds` 格式(包含 DXT1/3/5 及未压缩格式),由 `src/dds.ts` 负责解码。 仍未解决:子网格级别的材质分配、骨架和动画。 详情请参阅 [docs/GEOMETRY_FORMAT.md](docs/GEOMETRY_FORMAT.md)。 ## 下一步计划 - [ ] 高度笔刷:`raise`(抬升)/`lower`(下沉)/斜坡工具。这些工具会同时编辑高度**和**标志位,否则将无法形成切面(切面表示地面类型的改变,而不是陡峭程度——详见地形说明文档),并且必须对它们触及的网格单元进行重新构建网格操作。 - [ ] 撤销/重做——由于各种笔刷功能的加入,这现在变得至关重要。 - [ ] 对象的旋转、删除、撤销/重做,以及属性面板。 - [ ] 修复剩余未解码的模型(参见 [MESH_PLAN.md](MESH_PLAN.md))。 - [ ] 战役编辑器(`*.(Campaign).xdb` 是纯 XML 格式)。
标签:3D地形渲染, Electron, MITM代理, TypeScript, 云资产清单, 地图编辑器, 安全插件, 文件解析, 游戏工具, 自动化攻击, 逆向工程