反汇编产物:
cpp_reversed/map_parser.py(Python parser, roundtrip ✓) 验证样本:996传奇盒子资源包/03-游戏客户端/jycq_206923/scene/map/*.map× 7 日期: 2026-09-23
| 文件 | 尺寸 (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, 推测)。
┌─────────── 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)
基于 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 扩展区。
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 即默认) |
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 个)
0 (空), 32768 (atlas 切换标记), 32000-35000 (实际图索引)65535 (0xFFFF) — 推测是"无效/禁用"标志0x3F (0.map) — 推测门动画帧范围 bitmask0x87 (0.map) — 推测光照等级 (135/255 ≈ 53% 亮度)简化规则 (pathFind / MarkCanWalk 用):
is_walkable = (obj_img == 0) and ((door_flag & 0x01) == 0)
┌── 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 字节/格的稀疏索引表:
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 项全量):
873c,0×6, 8000,0×6, 873f,0×6)③ 本轮定论:
额外区是 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 个):
| 样本 | 大小 | 解析 | 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 字节级可逆。
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())