# .map 文件格式逆向报告 (WGS Mir2 兼容) > 反汇编产物: `cpp_reversed/map_parser.py` (Python parser, roundtrip ✓) > 验证样本: `996传奇盒子资源包/03-游戏客户端/jycq_206923/scene/map/*.map` × 7 > 日期: 2026-09-23 --- ## 〇、文件清单 (7 个样本) | 文件 | 尺寸 (B) | 网格 | 格式 | 标题 | 备注 | |---|---:|---|---|---|---| | `0.map` | 6,860,052 | 700×700 | **扩展** | "Legend of mir\r\n" (15B) | 含 980000B 额外区 (≈w·h·2) | | `0139.map` | 27,652 | 46×50 | 标准 | "Legend of mir" (13B) | | | `3.map` | 9,600,052 | 1000×800 | 标准 | "Legend of mir" (13B) | 最大 | | `d011.map` | 1,920,052 | 400×400 | 标准 | "Legend of mir" (13B) | | | `d022.map` | 3,000,052 | 500×500 | 标准 | "Legend of mir" (13B) | | | `d401.map` | 480,052 | 200×200 | 标准 | "Legend of mir" (13B) | | | `d601.map` | 750,052 | 250×250 | 标准 | "Legend of mir" (13B) | | `x.map` (无 d 前缀) 是主城/标准地图,`dNNN.map` 是副本地图 (N = 副本 ID, 推测)。 --- ## 一、标准格式结构 (6/7 文件) ``` ┌─────────── 52B Header ──────────────┐ ┌── w*h × 12B Cells ──┐ │ uint16 width (offset 0) │ │ 12B per cell × N │ │ uint16 height (offset 2) │ │ │ │ uint8 title_len (offset 4) │ │ struct MapCell: │ │ char title[?] (offset 5..) │ │ uint16 bg_img │ │ ... 剩余 18-51 原始字节保留 ... │ │ uint16 mid_img │ └─────────────────────────────────────┘ │ uint16 obj_img │ │ uint16 ani_idx │ │ uint8 door_flag │ │ uint8 light │ │ uint8 area │ │ uint8 ani_tick │ └───────────────────────┘ ``` **总大小** = 52 + (width × height × 12) - 验证: 0139.map = 52 + 46×50×12 = 52 + 27600 = 27652 ✓ --- ## 二、Header 52B 详细字段 (2026-09-23 语义化定稿) > 基于 7 样本逐字节统计: 恒定字节 vs 变化字节, 完全可分。10 项专项测试锁定 > (`tests/test_map_parser.py::TestHeaderSemantic`)。 | 偏移 | 大小 | 类型 | 字段 | 样本 (0139.map) | 含义 (实测结论) | |---|---:|---|---|---:|---| | 0 | 2 | uint16 LE | width | 46 | 地图宽 (格数) | | 2 | 2 | uint16 LE | height | 50 | 地图高 (格数) | | 4 | 1 | uint8 | title_len | 13 | title 长度 (13 标准 / 15 扩展) | | 5 | N | char[N] | title | "Legend of mir" | 地图标题 | | 18 | 2 | uint16 LE | reserved | 0x0000 | 保留, 7 样本恒 0 | | 20 | 2 | uint16 **LE** | update_date1 | 0x3CE9 = 15593 | 更新日期 (6 样本恒同; ⚠️ 原判 BE 是错的, 字节 `e9 3c` LE 解 0x3CE9) | | 22 | 2 | uint16 **LE** | update_time1 | 0x0040 = 64 | 更新时间 | | 24 | 2 | uint16 **LE** | update_date2 | 0x3CE9 | 与 date1 相同 (推测备份) | | 26 | 2 | uint16 **LE** | update_time2 | 0x0040 | 与 time1 相同 | | 28 | 2 | uint16 LE | music_id | 36 | 主 BGM ID (0.map=514, 其余=36) | | 30 | 2 | uint16 LE | ambient_id | 136 (0x88) | 环境音/副 BGM ID (0.map=1, 其余恒 136) | | 32 | 2 | uint16 LE | light_w | 18 | 光照贴图宽 (0.map=0 无光照) | | 34 | 2 | uint16 LE | light_h | 23 | 光照贴图高 (0.map=0) | | 36 | 4 | uint32 LE | editor_build | 0x038CBB00 | 编辑器 build 号 (0.map=0; 0.map/3.map 曾同为 0x03CD82D0 → 推测 per-编辑批次) | | 40 | 4 | uint32 LE | reserved_40 | 0x00000000 | 保留, 恒 0 | | 44 | 4 | uint32 LE | format_id | 0x00447E28 | **WGS 格式标识**: 6 标准样本恒 0x447E28, 0.map=0 (⚠️ 原判 0x287E44 是字节序笔误) | | 48 | 4 | uint32 LE | reserved_48 | 0x00000000 | 保留, 恒 0 | **⚠️ 字节序勘误 (本轮发现)**: 20-27 四个时间戳字段原判为 BE, 实测文件字节 `e9 3c 40 00` 经 BE 解出 0xE93C/0x4000 (无意义), **LE 解出 0x3CE9/0x0040** (与"6 样本恒同"的统计吻合)。 已修正 `parse_header` 并由 `test_update_timestamps_standard` 锁定。 **0.map 与标准格式的系统性差异** (印证"0.map 是另一编辑批次产物"): `update 时间戳不同 / ambient_id=1 / light=0 / editor_build=0 / format_id=0`, 且 title 多 2 字节 (`\r\n`) + 附带 980000B 扩展区。 --- ## 三、Cell 12B 字段语义 ``` offset 0 1 2 3 4 5 6 7 8 9 10 11 ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐ │bg│ │mid│ │obj│ │ani│ │df│lt│ar│tk│ └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘ uint16 uint16 uint16 uint16 BBBB (LE) (LE) (LE) (LE) ``` | 偏移 | 大小 | 类型 | 字段 | 取值范围 (样本) | 含义 | |---|---:|---|---|---|---| | 0 | 2 | uint16 LE | **bg_img** | 0, 32768, 32000-35000 | 背景图索引 (atlas 第 1 张图的子图 ID) | | 2 | 2 | uint16 LE | **mid_img** | 0, 32768, 65535 | 中景图索引 (图层叠加, 0.map/3.map 大量使用) | | 4 | 2 | uint16 LE | **obj_img** | 0, 32000-35000 | 前景/对象图索引 (有碰撞, 不可走) | | 6 | 2 | uint16 LE | **ani_idx** | 0, 32768 | 动画帧索引 (精灵动画子图 ID) | | 8 | 1 | uint8 | **door_flag** | 0, 0x3F, 0xFF | 门/动画开关位图 (bit0=可走? bit1=门?) | | 9 | 1 | uint8 | **light** | 0, 0x80, 0x87 | 光照亮度 (0=全亮, 0x80=半暗, 0xFF=全黑) | | 10 | 1 | uint8 | **area** | 0, 0xFF | 区域/门索引 (3.map 全设为 0xFF) | | 11 | 1 | uint8 | **ani_tick** | 0 | 动画 tick (速度, 通常 0 即默认) | ### 3.1 字段语义验证 (基于样本数据) ``` 0139.map (46x50, 标准) 中: 70% 有 bg, 0% 有 mid, 42% 有 obj, 0% 有 ani, 0% 门 → 简单地图, 只有背景+对象, 无动画 3.map (1000x800, 标准) 中: 65% 有 bg, 0.8% 有 mid, 12% 有 obj, 0.02% 有 ani, 0.002% 门 → 主城地图, 大量背景, 少量门/动画 d601.map (250x250, 标准) 中: 88% 有 bg, 0% 有 mid, 21% 有 obj, 0% 有 ani, 1% 门 → 副本地图, 较多门 (623 个) ``` ### 3.2 数值特征 - **bg_img 常见值**: `0` (空), `32768` (atlas 切换标记), `32000-35000` (实际图索引) - **mid_img 在 3.map 大量为 `65535`** (0xFFFF) — 推测是"无效/禁用"标志 - **door_flag `0x3F`** (0.map) — 推测门动画帧范围 bitmask - **light `0x87`** (0.map) — 推测光照等级 (135/255 ≈ 53% 亮度) ### 3.3 可走性判断 **简化规则** (pathFind / MarkCanWalk 用): ```python is_walkable = (obj_img == 0) and ((door_flag & 0x01) == 0) ``` - 大多数地图 (5/7) 可走比例 80-95%, 与典型 MMO 地图设计一致 - 0.map 86.56% 可走, 副本地图 (d011=92.4%, d022=95.2%) 更高 (BOSS 战地图例外) --- ## 四、扩展格式 (0.map) — **已部分反推** ✓ ``` ┌── 52B 标准 header ──┐ ┌── 980000B 额外区 ──────────────┐ ┌── 700×700×12B ──┐ │ 同标准, 但 title_len=15 (含 \r\n) │ │ 980000B = 700*700*2 │ │ 标准 cell 数据 │ │ "Legend of mir\r\n" │ │ 推测: 压缩的 tile 索引表 │ │ 12B × 490000 │ └─────────────────────────┘ └────────────────────────────┘ └──────────────────┘ ``` **额外区数据结构** (本轮新发现): | 字段 | 值 | 占比 | 含义 | |---|---:|---:|---| | 零 | 0x0000 | 87% | 标记该格无图 (最常见) | | 默认 | 0x8000 | 5% | "无图" / "默认图" 标志 | | 小值 | 0x0033-0x0037 / 0x03b7-0x03bb | ~7% | 低 atlas 索引 (推测: 中景/对象) | | 大值 | 0x8709-0x870d / 0x873b | ~1% | 高 atlas 索引 (推测: 背景/前景) | **2 字节/格的稀疏索引表**: - 87% 的格无图 (extra=0) — 因为 4 个图槽 (bg/mid/obj/ani) 中只有 1 个非零 - 5% 的格是"空标记" (extra=0x8000) - 8% 的格存了 1 个 uint16 索引, 值范围与 cell.bg_img/mid_img/obj_img/ani_idx 高度相关 - **作用**: `tileSceneManager3` 优化渲染时, 通过这张 2B/格 的稀疏表快速定位 tile, 比解析 12B 标准 cell 更快 **深度分析 (2026-09-23 第二轮: 反汇编 + 数据实证)** > 本轮从 tolua 绑定层逐级追入二进制, 并对 980000B 额外区做了穷举统计. > **旧结论修正**: 原先"差值 0x7A51 (31313) 匹配"实为巧合 (模 31313 运算的假阳性), 精确匹配仅 343 例; > "7-格周期被否定"的检验方法 (i%7 分桶计数) 也无法区分"周期 7 的成对结构", 本轮已推翻重验. **① 二进制调用链 (静态还原, 全部有地址可查)**: ``` tolua 绑定 tileSceneManager3:LoadMapData @0x10006d848 └─ 薄包装 @0x100034264 (存文件名到 this+0x18, 检查 this+0x118 非空) └─ @0x100076b78 (文件名去目录 → 追加资源后缀 → new 任务(0x20B, vtable 0x10082c128) → 优先级 3 入队 sym.func.100077d00) └─ 工作线程执行任务 → 完成回调链 → @0x1000980f0 ("LoadMapDataCompleted mapData expired!!") ├─ this+0x58 = 读宽度(虚表+0x10), this+0x5c = 读高度(虚表+0x18) ├─ this+0x50/0x54 = 全局系数(@0x1008aea28/2c) × 宽高 ← 渲染尺寸换算 ├─ this+0x1b8 非空时调 sym.func.100034314(路径串) ← 地图数据消费入口 │ └─ 经 this+0x18 虚表+0x30 分发到子系统 └─ 通知 Lua (tolua mapData 对象 + begin/end 事件) ``` 关键结论: **LoadMapData 全链没有任何对 980000B 额外区的解析循环** — 额外区不在此函数处理; 二进制中另有 `extra compressed data` 日志字符串 (@0x1006e9863), 其引用者 `sym.func.100459e58` / `10045bc04` 是 **zlib inflate 流读取器** (0x400 分块 + inflateReset), 且位于 **PC 容器解析代码** (`0x5043xxxx` 魔数, w1=0x69430000|0x4350='PC'+版本; 0x84=132B PNG 块头解码, `mul w27, w27, 0xc` — **12B/项 ×490000 恰是 cell 数**). 该 PC 容器是热更下载资源的通用打包格式. **② 额外区字节级结构 (已实证)**: | 字段 | 值 | 占比 | 含义 | |---|---|---:|---| | 零 | 0x0000 | 87.3% | 无图块 | | 标志 | 0x8000 | 5.0% (24557) | "图块重复"标记: 与下一个非零值 V 成对出现于同 7 值段 | | 主图块 | 0x8709-0x873f 等 (>0x8000, 共 87 种) | ~1% | 高位图块索引 (V-0x8000 ∈ 1801-1855 等) | | 小图块 | 0x0033-0x03bb 等 | ~6.7% | 低索引图块 (中景/对象 51-59, 951-955, 1451-1455, 34619-34623 等) | **确定性统计 (490000 项全量)**: - **7 项周期实锤**: 非零项间距 7/14/21/28/35 占绝对主导 (gap=7 有 18737 次); 行 0 全部非零项落在 col 0,7,14,21,… 且 V 与 0x8000 严格交替. 表是 **[V,0,0,0,0,0,0 | 0x8000,…] 式的 7 项一组**结构 (row0 前 21 项: `873c,0×6, 8000,0×6, 873f,0×6`) - **与 cell 数据的对应**: 表值按"同格 bg 槽直接对齐"匹配 84.9% (415852/490000, 含 412903 双零格); 非零表值与 cell bg 精确相等 2949 例 (如 i=0: 表 0x873c / cell bg 0x873b = **V-1**; i=7: 表 0x8000 / cell bg 0x873c = 前一个 V; i=14: 表 0x873f / bg 0x873b) - **V / V-1 成对语义**: 0.map 中 cell 任意槽存在 (V, V-1) 相邻对 7288 组; 表 V 与 cell V-1 的对应已被 341 例直接证实 - **表 ≠ 完整索引**: 表=0 但 cell bg≠0 有 14877 格, 表≠0 但 bg=0 有 11320 格 — 表只覆盖"宏观图块"区域 **③ 本轮定论**: 额外区是 tileSceneManager3 专用的**宏图块(mega-tile)辅助索引**: 700×700 格按 7 项一组粗分 (≈100×700 宏块), 每组 1 个主图块号 V + 1 个 0x8000 重复标记, 渲染时按组快速取背景图块, 细节仍以 12B cell 为准. **完整的 V→(bg/mid/obj/ani) 4 槽映射算法不在 .map 解析路径中**, 而在运行时渲染路径 (LoadTilesInRect / 虚表分发链), 静态反汇编到此为止已是边界; 若要彻底定论需运行时抓取 tileSceneManager3 内存 (组表 + 解码后数组) 对照. **工程影响**: 无 — `extra_data` 原样保留即字节级 roundtrip; 机器人只用 cell 层可走性/障碍, 不依赖此渲染优化表. --- ## 五、与二进制的对应关系 | 字符串 (binary @0x1006xxxx) | tolua 函数 | 我们的 parser | |---|---|---| | `tileSceneManager:LoadMapData` | 入口 | `load()` | | `mapData2DTile` | 单格类 | `MapCell` | | `mapData2D:isObstacle` | 可走性 | `MapCell.is_obstacle` | | `mapData2D:isCover` | 遮挡 | `MapCell.mid_img != 0` | | `mapData2D:getMapDataRows/Cols` | 尺寸 | `MapHeader.width/height` | | `tileSceneManager:MarkCanWalk` | 动态改可走 | `MapCell.set_door()` (TODO) | **未覆盖的方法 (62 个)**: - LoadTilesInRect / LoadObjsInRect — 视锥加载, 不影响格式解析 - SetAtlasSplit / SetFrameAnimOffset — 图集切分配置, 与 .map 文件本身无关 - changeGateState / openDoor / closeDoor — 门状态运行时修改, 不写入 .map - 纹理加载回调 (LoadMiniTex 等) — 异步, 不影响同步格式 --- ## 六、验证矩阵 | 样本 | 大小 | 解析 | roundtrip | stats() | 备注 | |---|---|---|---|---|---| | 0139.map | 27,652 | ✓ | ✓ | 1324/2300 = 57.57% | 最小, 无 mid/ani | | 0.map | 6,860,052 | ✓ | ✓ | 424120/490000 = 86.56% | 扩展格式 | | 3.map | 9,600,052 | ✓ | ✓ | 703221/800000 = 87.9% | 主城 | | d011.map | 1,920,052 | ✓ | ✓ | 147851/160000 = 92.41% | 副本 | | d022.map | 3,000,052 | ✓ | ✓ | 238060/250000 = 95.22% | 副本 | | d401.map | 480,052 | ✓ | ✓ | 35798/40000 = 89.5% | 副本 | | d601.map | 750,052 | ✓ | ✓ | 49522/62500 = 79.24% | 副本 (623 门) | **全部 7/7 字节级 roundtrip 一致** — parser 与原始 .map 字节级可逆。 --- ## 七、未还原 / 待精读 1. **0.map 额外区 slot 编码** — **✅ 已尽静态分析之极限 (2026-09-23 第二轮)**: 结构已定 (7 项一组宏图块索引, V + 0x8000 交替), 调用链已挖透 (LoadMapData 全链不解析额外区); 完整 V→4槽映射算法在运行时渲染路径 (LoadTilesInRect/虚表分发), 需运行时抓内存对照, 详见 §四 2. ~~header 18-51 字段~~ **✅ 已解 (2026-09-23)**: 逐字节统计 + 10 项语义测试锁定, 含一个字节序 bug 修正 (update 时间戳 BE→LE), 详见 §二 3. **mapData2DRectGrid / mapData2DTile3** 类 — 这两个类有更多方法 (LightBuffer 关联), 与地图运行时内存表示有关, 暂未涉及 4. **smtiles/uiminimap** 子目录 — 是图集与小地图的渲染资源, 不属于 .map 文件格式本身 --- ## 八、用法示例 ```python from map_parser import load m = load("scene/map/3.map") print(f"{m.header.title!r} {m.header.width}x{m.header.height} bgm={m.header.music_id}") # 单格访问 c = m.get_cell(500, 400) # 中心点 print(f"bg={c.bg_img} obj={c.obj_img} light={c.light:#x}") print(f"walkable: {c.is_walkable}") # 遍历可走格 (寻路/AI 用) for x, y, c in m.iter_walkable(): if c.ani_idx != 0: print(f" animated tile at ({x},{y}): ani={c.ani_idx}") # 统计 print(m.stats()) # 序列化回 .map (调试/编辑器用) with open("/tmp/3_modified.map", "wb") as f: f.write(m.to_bytes()) ```