外观
Terrain Record 地形记录
状态:Authoritative Reference v2 / Layered Semantics / Tail Partial
1. 先说结论:BTL 地形不是一层
Terrain 的正确阅读方式不是“一个地形 ID 对应一种地形”,而是至少分成六层:
text
地图来源层
BTL embedded Terrain / external world.bin
|
v
磁盘记录层
16-byte Terrain record
[主地形 4B][LowerDoodad 4B][UpperDoodad 4B][Raw tail 4B]
|
v
MapTerrain 定义层
raw id -> def_mapterrain row -> type / variant image / layer metadata
|
v
TerrainType 规则层
def_mapterrain.type -> def_terraintype
-> movementcost / penalty_infantry / penalty_armor / penalty_artillery
|
v
Runtime Tile 层
current raw record -> Tile state / derived movement-validity cost
|
v
表现层
ordinary land/sea/coast + Terrain/Lower/Upper overlays
或 external chunk base + Tile-owned overlays这些层的 identity 不能混用:
- raw terrain id 不是 atlas frame index;
def_mapterrain.type不是 texture number;def_terraintype.movementcost是规则数据,不是渲染字段;- Runtime Tile layout 也不是 BTL 磁盘 layout。
2. 地图来源层:embedded 与 external
2.1 MapSource == 0
BTL 在 Country section 后直接内嵌:
text
MapWidth * MapHeight条 Terrain record,每条固定:
text
0x10 = 16 bytesAndroid 1.30.0 当前 corpus:
text
MapSource 0 = 1073 files
embedded Terrain records = 6926992.2 MapSource == 1
BTL 不携带 Terrain section。逻辑地形来自外部地图资源。
原版 world.bin 是:
text
YSAE v4 header
-> Width * Height * 0x10 Terrain records
-> Width * Height * 0x02 Province Anchor slots原版加载 world.bin Terrain 时仍调用与 embedded BTL 相同的 FUN_004746A4(Tile, terrainRecord) 绑定函数。因此:
embedded BTL 与
world.bin的 Terrain 来源不同,但进入 Runtime Tile 的 16-byte raw record 合同相同。
当前 Android 1.30.0 corpus:
text
MapSource 1 = 310 files注意:external map 的基础视觉地图可以来自预生成 chunk 图像;这不表示 Tile raw Terrain 消失。原版仍会基于 materialized Tile 提交部分 Terrain/Doodad/Area overlay。
3. 16-byte 记录:当前已闭合到 byte 级的部分
早期 clean-room API 为了不抢跑语义,只公开:
text
+0x00 uint32 Raw00
+0x04 uint32 Raw04
+0x08 uint32 Raw08
+0x0C uint32 Raw0C后续 native rendering consumer 已把前 12 字节进一步闭合为三个 4-byte slice:
| Offset | Size | 当前名称 | Byte layout | 状态 |
|---|---|---|---|---|
+0x00 | 4 | Primary Terrain slice | id:u8, variant:u8, offsetX:i8, offsetY:i8 | Confirmed |
+0x04 | 4 | LowerDoodad visual slice | id:u8, variant:u8, offsetX:i8, offsetY:i8 | Confirmed(visual consumer) |
+0x08 | 4 | UpperDoodad visual slice | id:u8, variant:u8, offsetX:i8, offsetY:i8 | Confirmed(visual consumer) |
+0x0C | 4 | Raw tail | 未命名 | Unknown |
展开后:
text
+0x00 uint8 PrimaryTerrainId
+0x01 uint8 PrimaryVariant
+0x02 int8 PrimaryOffsetX
+0x03 int8 PrimaryOffsetY
+0x04 uint8 LowerDoodadId
+0x05 uint8 LowerDoodadVariant
+0x06 int8 LowerDoodadOffsetX
+0x07 int8 LowerDoodadOffsetY
+0x08 uint8 UpperDoodadId
+0x09 uint8 UpperDoodadVariant
+0x0A int8 UpperDoodadOffsetX
+0x0B int8 UpperDoodadOffsetY
+0x0C..+0x0F raw / semantic Unknown这里的名称有意带上 visual 边界。尤其 +0x04 / +0x08:
- 已确认它们分别驱动 Lower/Upper Doodad 静态图层;
- 不能因此推导“这四个 byte 在所有 gameplay consumer 中只具有装饰意义”;
- 其他 Runtime consumer 对额外 raw layer 的读取仍应按各自证据描述。
4. 三个可视 slice 的 sentinel 与对象创建
原版静态 overlay 对三个 slice 的创建条件不同。
Primary Terrain
text
id >= 2
and variant != 255才创建对应 Terrain image object。
因此:
- raw id
0/1仍是有效主地形 identity; - 但它们通常不依赖
Tile+0x88的独立 terrain sprite; - 普通地图中的基础陆地/海面有独立 base-map path。
Lower / Upper Doodad
text
id != 0
and id != 63
and variant != 255才创建 image object。
不满足时原版对象就是 null;Reference 状态为:
text
AbsentByRawSentinel不能自行生成 generic hex 或 fallback texture。
5. def_mapterrain:raw id 到定义 row
主地形的 gameplay 绑定链从:
text
Terrain +0x00 PrimaryTerrainId开始。
视觉 lookup 对三个可视 slice 使用同一类 exact row / variant 解析:
text
raw id
-> exact def_mapterrain row
-> raw variant selects exact <tile> child
-> child.image exact name
-> shared Resource atlas lookup
-> image object or nullAndroid 1.29.0 的 def_mapterrain.xml 有 24 个 sparse terrain identity:
text
0, 1, 2, 3, 4, 5, 6, 7,
8, 9, 10, 11, 12, 13, 14, 15,
16, 18, 20, 21, 22, 26, 30, 31当前结构:
text
24 terrain rows
193 tile children
190 children with imageterrain row 的重要属性包括:
terrain:逻辑 MapTerrain identity;type:TerrainType category;layerterrain;layerlowerdoodad;layerupperdoodad;- child
tile.idx/tile.image。
因此下面两种解释都已经被否定:
text
raw terrain id == atlas frame index
def_mapterrain.type == texture number6. def_terraintype:玩法类别层
主链:
text
PrimaryTerrainId
-> def_mapterrain[terrain]
-> def_mapterrain.type
-> def_terraintype[type]def_terraintype.xml 当前有 type 0..10 共 11 个定义。其 loader 已确认读取:
text
type
movementcost
penalty_infantry
penalty_armor
penalty_artillery当前基础定义值:
| Type | Reference 中文名 | definition movementcost | 运行时边界 |
|---|---|---|---|
| 0 | 平原 | 3 | 普通基础地形 |
| 1 | 海洋 | 3 | 普通 coast path 将 PrimaryTerrainId=1 识别为水类 |
| 2 | 沙漠 | 4 | raw id 2 的 Terrain sprite 走 early pass |
| 3 | 丘陵 | 4 | 基础成本 4 |
| 4 | 山地 | 5 | 基础成本 5 |
| 5 | 高山 | 5 | 某些战斗/模式下 Runtime 会改成 -1 |
| 6 | 树林 | 4 | 基础成本 4 |
| 7 | 森林 | 5 | 基础成本 5 |
| 8 | 农田 | 3 | 基础成本 3 |
| 9 | 河流 | 6 | 基础成本 6 |
| 10 | 装饰 | 0 | 不能把 0 直接解释成“所有普通单位免费通过”;普通 movement query 当前按不可通行处理 |
这里必须区分:
XML definition value 与最终 Runtime movement policy 不是同一层。
例如高山的 XML movementcost=5,但原版绑定时存在条件化 -1;装饰的 definition 值为 0,而普通移动模型仍有额外合法性门。
penalty_* 也是 TerrainType 定义字段,不位于 BTL Terrain record 内。字段存在已经确认,但完整的所有模式/技能叠加规则不由本页扩张。
7. Runtime Tile 绑定层
Android 1.28.0 ARM64 原版 FUN_004746A4 的核心链已经闭合:
text
16-byte terrain record
-> copy into current Tile raw terrain region
Tile current raw byte 0
-> def_mapterrain lookup
-> terrain type
-> def_terraintype.movementcost
-> Tile+0xA0 terrain movement/validity cost所以 Tile+0xA0 不是城市 ID、战略分或 texture id,而是:
text
terrain movement cost / terrain validity cost高山特殊分支
原版至少存在以下条件化行为:
- primary terrain type 为 5 时,特定 Battle 状态下
Tile+0xA0 = -1; BattleMode == 6的路径还会检查额外 raw terrain layer,高山可使 Tile 被强制判为无效。
因此不能只看 XML 的 movementcost=5 判断高山最终可通行性。
Building / 主对象条件
原版移动成本链存在在目标 Tile 有 Building / 主对象等条件下使用平原成本的分支。最终路径成本不是“永远等于 XML movementcost”。
AI consumer
默认 AI 的部分目标收集 mode 检查:
text
Tile+0xA0 >= 0当前证据将其解释为“地形对该候选路径有效”,不是给 Tile 增加战略分。
8. 当前 Terrain raw 是运行时状态,不只是开局常量
Terrain raw 记录在 Runtime 中并非只读开局输入。
已确认生命周期:
text
constructor default
-> full 16-byte record copy
-> derived cost recompute
-> current record copy-out
-> battle save serialization
-> battle load / bulk restorePrimary raw byte 0 还存在跨 ARM64/x86 对齐的独立 setter:
text
if current raw0 != newValue:
raw0 = newValue
related16 = 0但当前静态审计没有找到该 setter 的 executable caller / runtime function pointer producer;已知 xref 只有 unwind metadata。
因此当前门禁是:
text
current raw0 read/capture = Confirmed
save/restore current record = Confirmed
setter body semantics = Confirmed
original runtime mutation producer = MissingAuthority
production terrain mutation implementation = NoGo这也是为什么不能因为“知道 PrimaryTerrainId”就直接宣布动态地形编辑已经安全。
9. 普通地图表现层
普通 embedded 地图的基础地形不是把每个 def_mapterrain.type 直接贴成一张纹理。
原版存在独立 base path:
text
map_land
map_sea
coast / coastmask以及 Tile-owned overlay path:
text
LowerDoodad
Terrain sprite
UpperDoodad
Area / SelectArea
Province boundary水与岸线
ordinary coast path 已确认:
text
PrimaryTerrainId == 1
-> water-class tile for coastline logic岸线由水格提交,根据六邻格生成 6-bit coast mask。
Terrain +0x01 PrimaryVariant 还会进入 coast b variant cache key。也就是说:
同一个 byte 在 Terrain overlay 中是 variant index,在普通海岸线 consumer 中也参与岸线变体选择。
这再次说明 BTL Terrain 是多 consumer、多层语义,而不是单字段单用途。
静态 overlay 提交顺序
原版两个 pass 当前闭合为:
text
early:
LowerDoodad
Terrain only when raw primary id == 2
late:
if Building exists: return
Terrain when raw primary id != 2
UpperDoodad因此 Building 会 suppress late Terrain / UpperDoodad,但不会 suppress early LowerDoodad,也不会 suppress raw id 2 的 Terrain sprite。
10. external map 表现层
external map 的 base geography 与 ordinary map 不同:
text
map%d.bin / map%d_hd.bin
+ pre-generated map chunk images / alpha原版不会为 external world 的全部基础地形逐格重画 ordinary land/sea/coast。
但是:
text
external base chunk
-> shared Tile overlay barrier
-> Tile-owned terrain/doodad/area layers仍然存在。
因此以下两句话都不准确:
- “external map 完全由 world.bin 每格直接贴图”;
- “external map 使用 chunk 后就完全不消费 Tile Terrain”。
正确模型是基础 geography 与动态/静态 Tile overlay 分层。
11. 版本证据必须分开
| Evidence | 版本 | 当前用途 |
|---|---|---|
| BTL physical corpus | Android 1.30.0 | 1383/1383 精确解析;MapSource 1073/310 |
def_mapterrain.xml | Android 1.29.0 | 24 terrain rows;218 总 element;193 tile child |
def_terraintype.xml | Android 1.29.0 | 11 type;1.28 -> 1.29 SemanticIdentical |
| Terrain raw visual consumer / static overlay corpus | Android 1.28.0 | 前 12 byte 三 slice、sprite pass、atlas lookup |
| native movement / AI terrain cost | Android 1.28.0 | primary id -> type -> movementcost -> Runtime cost |
world.bin physical format | Android 1.28.0 + 1.25 + UWP 1.16.1 | 三份样本 byte-identical |
1.29 的 def_mapterrain 相比 1.28 有一项表现定义变化:雪地 row 的 tail tile mapping 被裁剪,但 name/terrain/type/layerterrain 身份未改变。不能把这项表现资源变化误解成 movement / terrain penalty 规则变化。
12. 当前闭合状态
| 层 | 状态 | 可以安全声明什么 |
|---|---|---|
| BTL Terrain section 定位、数量、stride | Confirmed | embedded 为 width*height*0x10 |
external world.bin Terrain stride / binding | Confirmed | 同一 16-byte binding contract |
+0x00..+0x0B static visual slice layout | Confirmed | 三组 id/variant/i8 x/i8 y |
+0x00 primary gameplay binding | Confirmed | raw id -> MapTerrain -> TerrainType |
def_mapterrain exact row / variant image lookup | Confirmed | sparse id,不是 texture index |
def_terraintype movement / penalty definition loader | Confirmed | definition 层字段已知 |
| Runtime movement / AI terrain-validity 主链 | Confirmed/Partial | 主链闭合,模式/技能长尾仍需各自证据 |
| ordinary static Terrain/coast overlay | Confirmed(已审计子路径) | base 与 overlay 分层 |
| external base + Tile overlay 分层 | Confirmed(静态子路径) | chunk 不等于 Tile overlay 消失 |
+0x0C..+0x0F | Unknown | 必须 raw-preserve |
| 原版动态 terrain mutation producer | Unknown / MissingAuthority | 不允许据 setter body 猜 producer |
| arbitrary semantic terrain writer | NoGo | 不能把读侧闭合自动升级成任意写入安全 |
13. 编辑器 / writer 边界
当前可以确认:
- 未修改 Terrain record 必须 byte-preserve;
+0x0C..+0x0F必须原样保存;PrimaryTerrainId同时影响 definition、movement/validity 与表现;- variant / offset 会影响表现,PrimaryVariant 还有岸线 consumer;
- lower/upper slice 的视觉语义已经确认,但不能外推为完整 gameplay 语义。
当前不能声称:
text
all 16 bytes gameplay semantics closed
arbitrary terrain id is safe to write
dynamic terrain mutation lifecycle closed
all invalid id / variant combinations are original-compatible
external chunk visuals automatically regenerate after terrain edits所以本页是完整的当前证据分层解析,不是“把 Unknown 隐藏掉”的伪 100% 语义表。
14. 主要证据入口
text
reconstruction/src/WC4.Runtime.BattleData/BtlMapRecords.cs
reconstruction/src/WC4.Runtime.MapPresentation/TerrainAreaOverlayCommands.cs
reconstruction/docs/route4_round7_world_bin_physical_format_report.md
reconstruction/docs/route5_round6_movement_query_report.md
reconstruction/docs/rendering/terrain_coastline_rendering_report.md
reconstruction/docs/rendering/route8_final_frame_round11_terrain_area_overlay_report.md
reconstruction/docs/data-reference/xml/def_mapterrain.md
reconstruction/docs/data-reference/xml/def_terraintype.md
reports/action_ai_tile_a0_terrain_cost.md
reports/route5_combat_dependency_recovery/terrain_raw0_setter_authority.json
reports/rendering/route8_final_frame/terrain_area_overlay_resource_matrix.csv
reports/btl_reference/remaining_fixed_records.json