map_format.md 15 KB

.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 用):

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 文件格式本身

八、用法示例

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())