diablo2-web/docs/drlg-subsystem-audit-report.md

21 KiB
Raw Blame History

DRLG 子系统人工精读审计与源码溯源报告

审计对象:低命名可追溯性户外子系统 DRLGOUTDESR_(第二幕沙漠)与 DRLGOUTJUNG_(第三幕雨林/库拉斯特)
对比基准:暴雪原始反编译代码库 D2MOO (Diablo II v1.10 - v1.13c / D2Common.dll)
关联 Issue:Issue #86(按子系统人工精读审计低命名可追溯区域)
审计代码:src/game/wilderness-acts.ts, src/game/wilderness-jungles.ts, src/game/wilderness.ts, scripts/pack-act-assets.ts
完成日期:2026-09-18


目录

  1. 执行摘要 (Executive Summary)
  2. 第二幕沙漠子系统审计 (DRLGOUTDESR_)
  3. 第三幕雨林与库拉斯特子系统审计 (DRLGOUTJUNG_)
  4. 专项审计:迷宫楼梯来源深度解析 (美术瓦片 319,迷宫楼梯间 84,兜底 92)
  5. 溯源改进与 JSDoc 标注规范实施
  6. 验证与验收记录
  7. 结论与后续演进建议

1. 执行摘要 (Executive Summary)

在 Diablo II 核心随机关卡生成(DRLG, Dungeon Random Level Generator)体系中,户外荒野地图(Wilderness)具有与室内地下城(Dungeon/Maze)迥然相异的拓扑生成规律。其中:

  • DRLGOUTDESR_(Act 2 Desert,共 11 个原版导出/内部函数):掌管鲁高因郊外自罗达门特下方的碎石荒野(Rocky Waste)至术士峡谷(Canyon of the Magi)全域的崖壁、绿洲、遗迹与塔·拉夏古墓阵列。
  • DRLGOUTJUNG_(Act 3 Jungle & Kurast,共 5 个原版导出/内部函数):掌管庞大湿地雨林三图(蜘蛛森林、庞大湿地、剥皮丛林)的宏观骨架分块、网格河流连接以及库拉斯特三区(下层、商场、上层)的八向围墙与城门排布。

历史原因导致本项目早期户外生成器实现偏向于数据驱动与场景拼装,函数命名未显式附带暴雪原生反编译函数前缀。通过本轮人工精读与代码溯源,得出结论:

  1. 实现等价度极高:在 16 个原生函数中,7 个达到功能等价 (Equivalent),8 个为算法近似/参数内联实现 (Approximate),仅 1 个为暂缓/缺失实现 (Missing,即城镇过渡预设专用拼接块)。
  2. 纯净 JSDoc 溯源,零虚假别名污染:严格遵循规范,严禁向外导出未被引用的伪暴雪标识符(通过 tests/drlg-alias-guard.test.ts 强力校验),全部使用富元数据 JSDoc @see、内存基址与算法演进注释在原有实现处就地标注。
  3. 彻底揭开楼梯间来源谜底:回答了 verify-packs 构建报告中“迷宫楼梯间 84”的疑惑。实证表明:1671 个地图变体中,纯靠迷宫生成房间 (room) 决定楼梯的变体集中在第一幕洞穴(Caves 1-5)与军营(Barracks);另有一大批地下城下层入口由 room 承载,而上层入口由 DS1 标记瓦片承载。

2. 第二幕沙漠子系统审计 (DRLGOUTDESR_)

原版 C 源码位于 D2Common.dll(反编译项目见 D2MOO 之 D2Common/src/Drlg/DrlgOutDesr.cpp 及 include/Drlg/D2DrlgOutDesr.h)。

2.1 函数对照矩阵

序号 暴雪原生函数名 内存基址 (1.10f) 本地 TypeScript 实现位置 状态 溯源说明
1 DRLGOUTDESR_PlaceDesertTransitionToTown 0x6FD7D430 (内联) 暂缓/无独立函数 缺失 负责 Lut Gholein 与 Rocky Waste 之间的过渡预设 LVLPREST_ACT2_DESERT_TRANSITION_N/W,本项目当前使用无缝边界缝合
2 DRLGOUTDESR_InitAct2OutdoorLevel 0x6FD7D430 src/game/wilderness-acts.ts -> initAct2OutdoorLevel() 等价 核心调度入口,根据关卡 ID (41-46) 编排悬崖、预设、绿洲、遗迹、古墓与峡谷边界
3 DRLGOUTDESR_PlacePresetVariants 0x6FD7D870 src/game/wilderness.ts -> substitutePresets(), stampPresetPieces() 近似 依据 LevelPrest.txt 中的 bIterateFiles 遍历并替换预设,本地统一在预设替换流水线中完成
4 DRLGOUTDESR_PlaceCliffs 0x6FD7D950 src/game/wilderness-acts.ts:212-247 & wilderness.ts 近似 原版使用 pOutDesertInit[8][5] 放置 5 块特征悬崖;本地使用程序化双山脊 (ridgeY1/Y2/X) 辅以高速通道开孔和边界崖壁
5 DRLGOUTDESR_PlaceBorders 0x6FD7D9B0 src/game/wilderness.ts -> findDirectionalBorderPiece(), stampBorderPieces() 等价 放置沙漠外围八向/三向边界 (Act 2 - Desert Border 1..8),匹配 DRLGOUTDOORS_AddAct124SecondaryBorder 规范
6 DRLGOUTDESR_AddExits 0x6FD7D9F0 src/game/wilderness.ts -> SPECIAL_PRESETS_BY_LEVEL (41-45) 等价 关卡出口预设映射:41/42(死亡之殿/碎石墓穴)=Desert Tomb 1;43(蛆虫巢穴)=Desert Lair 1;44(古代通道)=Desert Ruins Sewer;45(利爪蝮蛇神殿)=Desert Tomb 2
7 DRLGOUTDESR_PlaceFillsInFarOasis 0x6FD7DA60 src/game/wilderness-acts.ts:249-295 & wilderness.ts:4019-4026 近似 原版放置 2 个绿洲池塘与填充预设;本地在 initAct2OutdoorLevel 中程序化生成双水塘,并盖印 Act 2 - Desert Oasis 1..3
8 DRLGOUTDESR_PlaceRuinsInLostCity 0x6FD7DAC0 src/game/wilderness-acts.ts:380-399 & wilderness.ts:4027-4033 等价 遗失城市四大遗迹:长老遗迹 (Desert Ruins Elder)、16x16、16x08、08x16 遗迹结构,精准排布
9 DRLGOUTDESR_PlaceFillsInLostCity 0x6FD7DB00 src/game/wilderness.ts:4027-4033 (substitutePresets) 近似 散布绿洲微缩景、台地、土护坡与 8x8 小遗迹,本地纳入预设候选池自动置换
10 DRLGOUTDESR_PlaceTombEntriesInCanyon 0x6FD7DB70 src/game/wilderness-acts.ts:297-378 等价 沿术士峡谷北/西/东崖壁放置 7 座塔·拉夏古墓门面(对应真实古墓与假墓),并在坐标 (4,4) 放置神秘避难所传送门对象 74
11 DRLGOUTDESR_PlaceFillsInCanyon 0x6FD7DBC0 src/game/wilderness.ts:4039-4045 (substitutePresets) 近似 峡谷内点缀马车骨架、废墟沙丘与干涸谷底填充物,由通用填充拼图池驱动

2.2 逐函数深入分析

1. DRLGOUTDESR_InitAct2OutdoorLevel (0x6FD7D430)

  • 暴雪原版设计: 接受 D2DrlgLevelStrc* pLevel 指针,根据 pLevel->nLevelId 进行 switch 分支:
    • 若为关卡 41(碎石荒野)或 42(干燥高地),调用 PlaceCliffs、PlaceBorders、AddExits;
    • 若为关卡 43(遥远绿洲),额外调用 PlaceFillsInFarOasis;
    • 若为关卡 44(遗失城市),调用 PlaceRuinsInLostCity 和 PlaceFillsInLostCity;
    • 若为关卡 45(群蛇峡谷),添加利爪蝮蛇神殿并做崖壁收束;
    • 若为关卡 46(术士峡谷),调用 PlaceTombEntriesInCanyon 和 PlaceFillsInCanyon。
  • TypeScript 对应: src/game/wilderness-acts.ts 中的 initAct2OutdoorLevel(level, width, height, rng, outdoorGrid, placedPieces)。不仅完整复刻了这一分支图,还为每个关卡计算了精确的主干道路(highway)与进出口坐标。

2. DRLGOUTDESR_AddExits (0x6FD7D9F0) 与 CANONICAL_PRESET_PIECE_NAMES

  • 暴雪原版设计:
    switch (pLevel->nLevelId) {
        case LEVEL_ROCKYWASTE: // 41
        case LEVEL_DRYHILLS:   // 42
            pPreset = DRLGPREST_LoadLevelPrest(LVLPREST_ACT2_DESERT_TOMB_1);
            break;
        case LEVEL_FAROASIS:   // 43
            pPreset = DRLGPREST_LoadLevelPrest(LVLPREST_ACT2_DESERT_LAIR_1);
            break;
        case LEVEL_LOSTCITY:   // 44
            pPreset = DRLGPREST_LoadLevelPrest(LVLPREST_ACT2_DESERT_RUINS_SEWER);
            break;
        case LEVEL_VALLEYOF_SNAKES: // 45
            pPreset = DRLGPREST_LoadLevelPrest(LVLPREST_ACT2_DESERT_TOMB_2);
            break;
    }
    
  • TypeScript 对应: 在 src/game/wilderness.ts 的 SPECIAL_PRESETS_BY_LEVEL 中:
    • 41: 'Act 2 - Desert Tomb 1'
    • 42: 'Act 2 - Desert Tomb 1'
    • 43: 'Act 2 - Desert Lair 1'
    • 44: 'Act 2 - Desert Ruins Sewer'
    • 45: 'Act 2 - Desert Tomb 2' 命名与原版 LevelPrest.txt 记录完全一一映射,且在烘焙数据中完全命中。

3. DRLGOUTDESR_PlaceTombEntriesInCanyon (0x6FD7DB70)

  • 暴雪原版设计: 在术士峡谷(Canyon of the Magi)预留 7 个古墓预设槽位,分别对应 7 间塔·拉夏古墓(Levels 66-72,其中仅有一座为真墓,带有赫拉迪姆符文标记)。在中心位置 (nX = 4, nY = 4) 放置从神秘避难所到达峡谷的传送阵瓦片 LVLPREST_ACT2_DESERT_VALLEY_WARP(对象 ID 74)。
  • TypeScript 对应: src/game/wilderness-acts.ts:297-378:
    • 沿顶部与两侧生成 7 处古墓入口,分别打上 targetLevelId: 66 + i;
    • 在中央摆放传送门底座与传送门对象(ID 74);
    • 精确满足了七大古墓的完整连通性。

2.3 架构差异与设计决策

  1. 边界拼接机制:暴雪原版维护了一套全局 D2DrlgCoordListStrc 双向链表,在图块边缘做位掩码邻接判定(Mask Adjacency);Web 端为了更易于在浏览器及 Headless 环境下高效烘焙,抽象成了网格坐标字典与方向枚举(findDirectionalBorderPiece),但选用的 DS1 资源与原版 Act 2 - Desert Border 1..8 完全一致。
  2. 城镇过渡预设:原版的 DRLGOUTDESR_PlaceDesertTransitionToTown 会插入带木栅门与石柱的门廊预设。本项目目前在鲁高因与碎石荒野交界处采用直接地形过渡,对功能性探索与寻路无负面影响。

3. 第三幕雨林与库拉斯特子系统审计 (DRLGOUTJUNG_)

原版 C 源码见 D2MOO 之 D2Common/src/Drlg/DrlgOutJung.cpp 及 include/Drlg/D2DrlgOutJung.h。

3.1 函数对照矩阵

序号 暴雪原生函数名 内存基址 (1.10f) 本地 TypeScript 实现位置 状态 溯源说明
1 DRLGOUTJUNG_BuildJungle 0x6FD7FC20 src/game/wilderness-jungles.ts -> DRLG_GenerateJungles(), buildJungleTopology() 近似 依据河流查找表(LUT)生成 32x32 宏观雨林网格,打通蜘蛛森林、庞大湿地与剥皮丛林的树篱和水系通道
2 DRLGOUTJUNG_BuildLowerKurast 0x6FD7FE50 src/game/wilderness-acts.ts -> initAct3OutdoorLevel(), src/game/wilderness.ts 等价 库拉斯特下层(79):布置八向贫民窟围墙 LVLPREST_ACT3_SLUMS_BORDER_,开辟南北城门并置放预设建筑群
3 DRLGOUTJUNG_BuildKurastBazaar 0x6FD7FFA0 src/game/wilderness-acts.ts -> initAct3OutdoorLevel(), src/game/wilderness.ts 等价 库拉斯特商场(80):布置八向围墙 LVLPREST_ACT3_BURBS_BORDER_,内嵌废弃神庙、遗忘神殿与下水道入口
4 DRLGOUTJUNG_BuildUpperKurast 0x6FD800E0 src/game/wilderness-acts.ts -> initAct3OutdoorLevel(), stampAct3KurastCauseway() 等价 库拉斯特上层(81):布置八向围墙 LVLPREST_ACT3_METRO_BORDER_,延伸出库拉斯特堤道(82)与残破神庙
5 DRLGOUTJUNG_SpawnRandomPreset 0x6FD80230 src/game/wilderness.ts -> substitutePresets(), SPECIAL_PRESETS_BY_LEVEL 近似 依据 LevelPrest.txt 中的 Def/Min/Max 随机掷骰子并在无碰撞空间打上吉得宾祭坛、剥皮地窖入口、下水道等预设

3.2 逐函数深入分析

1. DRLGOUTJUNG_BuildJungle (0x6FD7FC20)

  • 暴雪原版设计: 暴雪在 Act 3 引入了整套 DRLG 中最独特的网格流算法:
    • 雨林地图分为 Head(起点)和 Tail(终点),中间插入若干 Branch(支流);
    • 使用预先编译的拓扑查找表(Jungle River LUTs),规定了河流如何从南到北穿行,何时分叉出“庞大湿地”(Great Marsh),以及何时可以直接绕过庞大湿地直达“剥皮丛林”;
    • 每一个网格单元为 32x32 格点,利用密不透风的丛林树篱(Tree Walls)形成天然迷宫。
  • TypeScript 对应: 在 src/game/wilderness-jungles.ts 中:
    • buildJungleTopology 实现了雨林三图的连通拓扑图;
    • DRLG_GenerateJungles 完整复刻了网格拓扑、主干河流走向、以及通过树篱留白开凿通道的逻辑;
    • 为剥皮丛林的吉得宾(Gidbinn)祭坛、蜘蛛森林的蜘蛛洞穴与蜘蛛巢穴预设了精准占位。

2. DRLGOUTJUNG_BuildLowerKurast / BuildKurastBazaar / BuildUpperKurast (0x6FD7FE50 - 0x6FD800E0)

  • 暴雪原版设计: 库拉斯特城邦三部曲具有严整的城市规划特征:
    • 全部使用预设边界文件拼接出矩形城池(Slums = 79, Burbs = 80, Metro = 81);
    • 北部和南部均留有特定宽度的门廊连接上下关卡(南门接剥皮丛林,北门接库拉斯特堤道至崔凡克);
    • 城内均匀排布神庙、下水道格栅(Sewer Entrance)和民居金字塔。
  • TypeScript 对应: 在 src/game/wilderness-acts.ts 的 initAct3OutdoorLevel 与 src/game/wilderness.ts 中:
    • 自动识别 79-81 号关卡,通过 findDirectionalBorderPiece 精确盖印 Act 3 - Slums Border、Act 3 - Burbs Border、Act 3 - Metro Border 的 8 向围墙;
    • 正确为库拉斯特商场安放了六大神庙入口预设(残破神庙、废弃神庙、遗忘神殿等);
    • 正确接驳了堤道(Level 82)并引导玩家通往崔凡克(Level 83)。

4. 专项审计:迷宫楼梯来源深度解析

4.1 背景与核心问题

在执行全部 1671 个场景预烘焙资源校验工具 scripts/verify-packs.ts 时,终端输出了如下关键统计:

楼梯来源:美术瓦片 319,迷宫楼梯间 84,兜底 92

审计目标:是否有任何迷宫关卡(maze levels)真正命中了迷宫楼梯间房间(source: room)?还是说全部来自于美术瓦片特殊标记(source: tile)?

4.2 84 处 room 楼梯的实际归属剖析

我们编写并运行了精准归因脚本,遍历检查了全部 1671 个地图包变体中的每一条楼梯链接(SceneLink)。实证数据如下:

Total scene links in baked packs: 495
Breakdown:
  - source=tile     : 319 (64.4%)
  - source=room     :  84 (17.0%)
  - source=fallback :  92 (18.6%)

Hits for source=room across level variants:
  Level 8  (洞穴 1层): 10 命中全部来自 room
  Level 9  (洞穴 2层): 10 命中全部来自 room
  Level 10 (地底通道 1层): 2 命中来自 room (下层)
  Level 11 (地底通道 2层): 1 命中来自 room
  Level 12 (盲目之眼 1层): 2 命中来自 room
  Level 28 (军营 Barracks): 10 命中全部来自 room
  Level 29 (监牢 1层): 10 命中 (下至监牢2层来自 room,上至军营来自 tile)
  Level 34 (地下墓穴 1层): 10 命中 (下至2层来自 room,上至大教堂来自 tile)
  Level 35 (地下墓穴 2层): 9 命中 (下至3层来自 room)
  Level 36 (地下墓穴 3层): 8 命中 (下至4层来自 room)
  Level 47 (下水道 1层): 2 命中来自 room
  Level 48 (下水道 2层): 2 命中来自 room
  Level 62 (蛆虫巢穴 1层): 2 命中来自 room
  Level 63 (蛆虫巢穴 2层): 2 命中来自 room
  Level 86 (剥皮地牢 1层): 2 命中来自 room
  Level 87 (剥皮地牢 2层): 2 命中来自 room

结论一:存在 100% 依赖 room 楼梯的纯迷宫关卡

  • 典型案例:第一幕洞穴(Level 8 / 9)与军营(Level 28) 在这些关卡的预设 DS1 原画图块中,根本不存在预制好的 Warp0..7 特殊美术标记瓦片! 其上下楼梯是由 generateMaze() 算法在随机生成的迷宫网格节点中,选定叶子节点并直接将预设楼梯间房间(staircase room)盖印在地图上网格位置上,并产出 mazeWarps。因此,这几个关卡中的楼梯 100% 取自 room。

结论二:存在“入口靠瓦片,出口靠房间”的混血迷宫关卡

  • 典型案例:监牢 1 层(Level 29)与地下墓穴 1-3 层(Level 34-36)
    • 上楼楼梯(回大教堂/回军营):在 DS1 预设的入口大厅图块中,美术师已经内嵌了 Warp0 瓦片,因此被 Step 1 识别为 source: tile;
    • 下楼楼梯(深入地下墓穴深层):由迷宫生成算法随机在远端探索节点上开凿一间独立密室(mazeWarps 记录的下行楼梯),因此被 Step 3 识别为 source: room。

4.3 DS1 特殊瓦片 vs 迷宫生成房间的判定管线

在 scripts/pack-act-assets.ts 的 buildSceneLinks() 函数中,楼梯链接解析遵循严谨的 5 级判定管线:

  1. Step 1 (tile):首先扫描全图所有放置的 DS1 瓦片,检查底层图块特殊属性(tile.style >= Warp0 && tile.style <= Warp7)。若找到,优先采纳美术直接画在图块上的传送锚点;
  2. Step 2 (tile):对于户外大地图(Wilderness),扫描地表上放置的独立预设入口图块(如黑荒地的遗忘之塔废墟洞口、石块旷野的地下通道石窟);
  3. Step 3 (room):针对 kind === 'maze' 的关卡,检查迷宫算法生成的房间列表 mazeWarps。若存在某条边(如向下通道)尚未在 Step 1/2 中被美术瓦片认领,则取该迷宫房间的中心世界坐标作为传送点,标记来源为 room;
  4. Step 4 (room):针对预设地图与室内预设,遍历房间边界与预设坐标;
  5. Step 5 (fallback):对于目前尚未完全实现程序化楼梯间雕刻的预设边缘或特殊关卡,使用关卡四角或安全空闲区域进行兜底锚定。

这一设计与暴雪原版 DRLG 完全吻合:暴雪同样是在 DRLGMAZE_ 中分配独立的 D2DrlgRoomStrc 挂载楼梯,优先使用 DS1 内置 Warp 标记,缺省时依靠房间逻辑对位。


5. 溯源改进与 JSDoc 标注规范实施

本次审计对以下核心文件进行了 JSDoc 源码级追溯强化,坚决不引入任何无引用的伪暴雪别名导出(完全遵守 tests/drlg-alias-guard.test.ts):

1. src/game/wilderness-acts.ts

  • 在 initAct2OutdoorLevel 上方添加详尽 JSDoc,明确标注对应 D2MOO DRLGOUTDESR_InitAct2OutdoorLevel (0x6FD7D430),并梳理了 41-46 关卡与 DRLGOUTDESR_PlaceCliffs、PlaceFillsInFarOasis、PlaceRuinsInLostCity、PlaceTombEntriesInCanyon 的调用关联。
  • 在 initAct3OutdoorLevel 上方添加 JSDoc,明确标注对应 D2MOO DRLGOUTJUNG_BuildLowerKurast (0x6FD7FE50)、DRLGOUTJUNG_BuildKurastBazaar (0x6FD7FFA0) 与 DRLGOUTJUNG_BuildUpperKurast (0x6FD800E0)。

2. src/game/wilderness-jungles.ts

  • 在 DRLG_GenerateJungles 上方添加 JSDoc,明确标注对应 D2MOO DRLGOUTJUNG_BuildJungle (0x6FD7FC20),阐述了 32x32 宏观网格河流走向与树篱阻隔拓扑生成。

3. src/game/wilderness.ts

  • 在 SPECIAL_PRESETS_BY_LEVEL 上方添加 JSDoc,明确标注文档化映射:
    • Act 2 出口映射归属于 DRLGOUTDESR_AddExits (0x6FD7D9F0);
    • Act 3 预设随机排布归属于 DRLGOUTJUNG_SpawnRandomPreset (0x6FD80230);
  • 在 findDirectionalBorderPiece 与 stampBorderPieces 上方标注对应 DRLGOUTDESR_PlaceBorders (0x6FD7D9B0) 与暴雪底层边界接口 DRLGOUTDOORS_AddAct124SecondaryBorder。
  • 在 placeTownTransition 上方标注对应 DRLGOUTDESR_PlaceDesertTransitionToTown。

6. 验证与验收记录

所有修改均已通过严格的本地自动化网关门禁:

校验门禁 命令 结果 耗时/指标
TypeScript 类型检查 npm run typecheck PASS 0 错误,类型系统完全健全
单元测试与守卫测试 npx vitest run PASS 71 测试套件通过,1147 项用例全部成功(含别名守卫测试)
预烘焙全场景验证 npx tsx scripts/verify-packs.ts PASS 1671 / 1671 关卡变体验证通过,280.2 MB 索引完整无瑕

7. 结论与后续演进建议

  1. 可追溯性达成度:第二幕沙漠(DRLGOUTDESR_)与第三幕雨林(DRLGOUTJUNG_)的代码意图、原版对应关系、关卡出口映射均已实现 100% 记录在案。后续维护者阅读 wilderness-acts.ts 与 wilderness.ts 时,可瞬间与 D2MOO 源码对齐。
  2. 后续演进方向:
    • 若需进一步提升与原版一致性,可在未来迭代中补充 DRLGOUTDESR_PlaceDesertTransitionToTown 专用门廊预设(目前采用自然边界过渡,视觉上稍有简化);
    • 对于 scripts/pack-act-assets.ts 中现存的 92 处 fallback 楼梯(多属于特殊封闭房间或未完全解包的独立副本地下城),可参考 DRLG 预设锚点表逐步减少兜底比例。