NOTE

逆向方法与数据支撑:本文基于 IDA 对 32 位客户端 Torchlight2.exe(ImageBase 0x400000,文内所有 sub_XXXXXX0xXXXXXX 均为绝对虚地址)执行逐指令反汇编与控制流分析,并结合对原版全量数据资产的系统性普查(涵盖 1,142 份 UNITS/MONSTERS DAT、1,659 份 SKILLS DAT、407 份 MISSILES/**/*.LAYOUT 以及 187 份 DUNGEONS DAT),关键特性通过动态进程内存插桩实测确证。本文严格收录已收敛至真值账本(Ledger C-0140 ~ C-0152)的技术结论;对于源工程中标记为“未定位 / 未查 / 推断”的边界逻辑,一律如实标注,不作过度推论。

核心工程结论:怪物 DAT 存在多项键名与引擎真实语义严重背离的设计(如 FOLLOW_RADIUSMOTION_RADIUSVIEW_ANGLE=0BRAVERYNEAR_WALK_RADIUSCHANCE 等);同时存在若干缺乏有效消费逻辑的无用键或越界值(如未注册的 AITYPE 字符串、PROPS_MAX 拼写错误、NUM_PETS_ALLOWED: -1、动画事件 HITTHREE / HITFOUR 等)。此类配置虽不会引发引擎崩溃报错,但会导致运行时怪物的行为逻辑出现隐性偏离。


0. 核心结论速览

  1. FOLLOW_RADIUS 并非单纯的“放弃追击半径”:全镜像中仅存在单一消费者 CUnit::getSightRange0x51B410),其返回值实为 $\max(\text{FOLLOW_RADIUS}, \text{视野链})$。因此放弃追击的临界判定距离实为 $\max(\text{FOLLOW}, \text{视野})$。原版 1,069 只怪中有 116 只是 SIGHT > FOLLOW,若按字面语义判定放弃追击,会导致怪物在临界距离处每帧在 SLEEP 与 CHASE 状态间来回振荡,完全无法产生有效位移。
  2. MOTION_RADIUS 并非待机漫游半径:代码段中唯一的浮点读取逻辑(0x522510)仅用于特定场景——当宝箱怪(UnitType 167)处于 MIMICIDLE 伪装状态时,将其作为视距参数。原版常规怪物在待机状态下既不主动漫游也不自主转身(实证详见 §2.7)。
  3. VIEW_ANGLE 为视野锥体全张角,0 度代表完全丧失正面视觉:消费点位于 CCharacter::getViewAngle0x51B540);配置为 360 度方代表全向感知。全语料中配置为 0 的 102 个单位全数属于木桩、陷阱、蛋与肉袋等无索敌需求的静态目标。
  4. SIGHT_RADIUS 在装载时会叠加基于游戏难度的全局增益:通过 GLOBALS.DAT 中的 *_MONSTER_ATTENTION_BONUS 配置(原版四档分别为 0 / 0 / 2 / 6),宝箱怪享有豁免。在 VERYHARD 难度下,基础视距为 7.0 的怪物实际有效视距达 13.0。
  5. DAMAGE_REACT_RADIUS 的仇恨扩散采用“边到边”测距:通过 sub_51A830 进行判定,测距扣除双方的 COLLISION_RADIUS,使用观察者自身的判定半径,且拉取索敌的目标为直接攻击者。
  6. AITYPE 八大枚举名称在底层仅收敛为 5 个 HandlerDEFENDERRESURRECTER 仅为单行 Thunk 转发至 NORMALDUMMY 未注册 Case 分支;任何未识别的字符串(如官方配置中的 GOOD)均静默回退至 NORMAL
  7. BRAVERY 数值越大越倾向于坚守不退:判定阈值为 $\frac{\text{HP}}{\text{MaxHP}} < 1 - \text{BRAVERY}$。默认值 1.0 代表绝对不退,0 代表受到任何伤害即触发逃跑判定;且该函数仅负责“逃跑状态的维持”,不负责状态的初次准入。
  8. [SKILL] 块中的权重与几率门限分工明确CAST_ODDS 为多技能候选池中的加权抽签权重CHANCE 为技能中签后的独立百分比判定门(基于闭区间 $[0, 100]$ 执行 $\le$ 判定,默认值 40 实际通过率为 $\frac{41}{101} \approx 40.6%$);CANCEL_CHANCE 的消费逻辑尚未定位。
  9. MONSTERCOOLDOWNMSCOOLDOWNMS 具备施法者类型优先级分流:当施法者为 Monster(UnitType 27)时,引擎优先读取前者(sub_6DBD70)。原版数据中有 123 份技能配置了该特化冷却。
  10. NUM_PETS_ALLOWED 达标后的行为是“禁止释放该召唤技能”:配置为 -1 的无上限逻辑因无符号零扩展(movzx)导致比较分支永不成立,属于失效代码(默认值实际以单字节 255 封顶)。
  11. 技能 DAT 引擎关键缺省值FINDTARGETANGLERANGEMINCOOLDOWNMS 内部默认均为 0.0;TARGETRANGE 缺省继承 RANGE;冷却时间内部统一换算为秒存储,任何绝对冷却时间 $< 0.01$ 秒的数值均会被强制置零。
  12. 投射物(Missile)描述符 55 项属性字段偏移确证:通过属性注册函数 sub_4C61C0 完成解析,修正了早期技术文档中的 3 处偏差(偏移 448 为 FRICTIONMAX DISTANCE 的 Getter 采用配置值与运行时增量叠加模式;水平沿地追踪开关为 TRACK GROUND 而非 VERTICAL AIMING)。引擎内部存在 AOE RAIDUS 拼写错误。
  13. 关卡刷怪的双轨制架构:包含 CLevel::Populate(基于全图可行走密度的程序化填充)与布局文件中的 Unit Spawner(静态锚点)两套完全独立的体系。Populate 的区域参数输入为栅格值严格为 0 的格子数;类别包含 10 项;全局道具数量上限键在引擎中拼写为 PPROPS_MAX;早期流传的“1 MB 碰撞列表内存预算限制”实为格式化日志字符串的误读。
  14. 动画伤害关键帧回退机制.ANIMATION 文件中未声明 NAME[KEY] 事件会被引擎默认赋值为类型 0(即 HIT),并在运行时真实结算物理伤害(实机插桩确证);HITTHREEHITFOUR 事件类型在运行时未注册任何消费分支。

1. 字段偏移与引擎缺省值总表

下表汇集了怪物核心 DAT 键值在引擎内存中的布局与硬编码缺省值。表中“硬编码缺省”一列来源于引擎底层调用链(基于 sub_677FA0 全局调用点反查确认),而非 GUTS 编辑器界面的展示默认值。

DAT 键名引擎内部字段偏移硬编码缺省值装载 / 消费虚地址详见章节
AITYPECCharacter+0x318空字符串 $\Rightarrow$ 0 (NORMAL)解析 0x55E1100x55FD10 起);分派 0x5B0140§3
SIGHT_RADIUSCCharacter+0x3307.0sub_51D460(装载时叠加难度增益)§2.1
VIEW_ANGLECCharacter+0x33890.0getViewAngle 0x51B540§2.4
MOTION_RADIUSCCharacter+0x33C4.5唯一读取点 0x522510§2.3
FOLLOW_RADIUSCCharacter+0x34018.0唯一消费者 getSightRange 0x51B410§2.2
DAMAGE_REACT_RADIUSCCharacter+0x344 (836)2.0sub_51A830§2.5
COLLISION_RADIUSCCharacter+0x190 (400)0.5装载 0x56D380§2.5
COLLISION_ADD0.25装载 0x55E110§2.5
REACH_BONUS0.75装载 0x56D380§2.5
ATTACK_RANGE0.375
NEAR_WALK_RADIUS0.0§2.6
BRAVERYCCharacter+0x7441.0消费函数 sub_523900§4
TURN_RATE10.0§2.6
WALKINGSPEED / RUNNINGSPEED2.5 / 6.25六个移动速度键同一加载函数 sub_518F10§2.6
[SKILL] CHANCECSkill+0x10C40百分比门 sub_6BE050§5
[SKILL] CANCEL_CHANCECSkill+0x1100未定位消费者§5
[SKILL] CAST_ODDSCSkill+0x1141加权随机 0x5ADF00§5
技能 COOLDOWNMS / MONSTERCOOLDOWNMSCSkillProperty+0xB0 / +0xB4(秒)0.0动态分流 sub_6DBD70§5.4
技能 NUM_PETS_ALLOWEDCSkillProperty+0x2C4 (1 字节)−1(实际截断为 255)CSkill__canUse 0x6BFCE1§5.5

NOTE

表中标记为“—”的项表示源逆向材料中未独立记录该项的专属结构体偏移,并非该字段不存在。


2. AI 行为圈层:六个半径与一个角度

2.1 SIGHT_RADIUS:装载期的难度加成与机制

单位数据装载函数 sub_51D460 在解析 SIGHT_RADIUS(存储于 CCharacter+0x330,硬编码缺省为 7.0)时,执行了基于难度的动态修正:

c
if (SIGHT_RADIUS > 0 && !isUnitType(167 /* Mimic */))
    SIGHT_RADIUS = max(0, SIGHT_RADIUS + globals[0xB0 + difficulty * 4]);
// difficulty = *(sub_41CE60() + 7048), 对应: 0 EASY / 1 NORMAL / 2 HARD / 3 VERYHARD

上述四个全局变量偏移由 sub_641160GLOBALS.DAT 中读取并写入,键名与结构体偏移严格对齐:

GLOBALS 键名写入地址 / 偏移原版配置值
EASY_MONSTER_ATTENTION_BONUSpush 键名 0x641C2B $\rightarrow$ fstp [esi+0B0h] 0x641C560
NORMAL_MONSTER_ATTENTION_BONUS+0xB40
HARD_MONSTER_ATTENTION_BONUS+0xB82
VERYHARD_MONSTER_ATTENTION_BONUS+0xBC6

分析结论:在 VERYHARD 难度下,一只基础视距配置为 7.0 的怪物,实际加载进内存的有效感知半径为 13.0,近乎翻倍。两处防护分支表明:仅当半径配置大于 0 时才施加加成(确保盲区单位保持 0),且宝箱怪(UnitType 167)显式豁免该增益。同一函数对 MOTION_RADIUS+0x33C)执行了完全相同的加成运算与宝箱怪排除逻辑。

同族的 *_MONSTER_RESIST_BONUS(对应全局偏移 +0xC0..0xCC)在原版 GLOBALS.DAT均未配置,其硬编码默认值为 fldz(0.0)。虽然引擎确实实现了将其载入全局对象的代码,但其最终下游消费者因多对象偏移混淆而尚未唯一定位,属于潜在的 Mod 扩展挂载点。

2.2 FOLLOW_RADIUS:追击视距动态下限机理

FOLLOW_RADIUS(存储于 CCharacter+0x340,缺省 18.0)在引擎全镜像中仅存在单一读取点:即 CUnit::getSightRange0x51B410,汇编指令 fld [esi+340h])。该函数的执行逻辑如下:

c
r = max(FOLLOW_RADIUS, getSightBonus(this));        // getSightBonus 基于 SIGHT_RADIUS 计算
m = clamp(getStat(144) * 0.01 - getStat(233) * 0.01, 0, 1);   // 致盲状态衰减
r *= clamp(1 - m, 0, 1);
if (getStat(202) > 0) r = min(r, 2.0);              // BLIND 状态硬性限制

该函数的调用方全部集中于追击与主动进攻状态机:AIHunt_update0x5AE6D0)、AIAttack_updateAICastSkill_updateAISkillApproach_update 以及 CMonster::onAttackedBy

判定机理FOLLOW_RADIUS 并非独立的脱战距离,而是怪物追击状态下视野感知的动态下限。真正的脱离追击距离实为 $\max(\text{FOLLOW_RADIUS}, \text{当前视野链})$。

工程影响:若误将脱战距离简单等同于 FOLLOW_RADIUS,对于原版全语料中 116 种 SIGHT > FOLLOW 的怪物(例如孢子发射器配置为 SIGHT: 50 vs FOLLOW: 18),在处于 $(18, 50]$ 区间内时,怪物会在每帧的 SLEEP 与 CHASE 状态间来回切换振荡,导致物理位移死锁。以地雷单位 mine_trap_01 为例,其 DAT 中显式配置 RUNNINGSPEED: 0FOLLOW_RADIUS: 0,正是依赖该机制将其永久锁定于原地。

2.3 MOTION_RADIUS:宝箱怪伪装态视距与待机漫游辨析

字段位于 CCharacter+0x33C,硬编码缺省为 4.5sub_677FA0 @ 0x51D460)。在引擎代码段(.text)中,仅存在一处针对该字段的浮点读取操作——即 CCharacter__findAITarget 内部的 0x522510

c
mimic = hasUnitType(167 /* Mimic */) && hasStateNamed("MIMICIDLE");
视距  = mimic ? MOTION_RADIUS : SIGHT_RADIUS;

此外,装载函数 sub_51D460 中施加难度加成的逻辑同样显式包含了 !isUnitType(167) 条件分支,进一步确证了两处逻辑对宝箱怪的特化处理。关卡环境配置中的 ENV_SIGHT_MOTION_RANGE(缺省 9.0)会在特定状态(state == 48)下将当前视距与 MOTION_RADIUS 实施统一封顶。

事实澄清:早期第三方文档曾将 MOTION_RADIUS 解释为“怪物待机状态下的随机漫游半径”,这一推测被反汇编彻底推翻。在原版引擎架构中,常规怪物在待机状态下既不执行漫游巡逻,也不进行自主随机转身(详见 §2.7)。

2.4 VIEW_ANGLE:视野全张角与静态靶标样本

视野角度计算函数 CCharacter::getViewAngle0x51B540)的实现逻辑如下:

c
// CCharacter::getViewAngle @ 0x51B540
angle = (1 + getStat(42)) * (VIEW_ANGLE_dat + getStat(14));
// 当 CAIManager::isFlagSet(0) 置位时,强制返回 360.0

该函数的唯一调用方为 CCharacter__findAITarget0x5223A0),其将计算出的角度与视距共同传递给 Scene__findSkillTargetInCone——表明 AI 索敌在底层复用了技能系统的锥形几何选取算法。DAT 属性 VIEW_ANGLE 载入 CCharacter+0x338,硬编码缺省为 90.0 度。

原版全量数据资产的取值分布如下:

  • 360 度:210 种单位;
  • 0 度:102 种单位
  • 180 度:99 种单位;
  • 120 度:61 种单位;
  • 270 度:19 种单位;
  • 60 度:18 种单位。

配置为 0 度的 102 种单位在类型上全部归属于训练假人(木桩)、地刺陷阱、场景尸体、怪物卵与肉袋——此类单位在设计上被严格禁止自主索敌。若误将 0 度按“无限制”解析,会导致场景内的所有不可动假人转入敌对追击状态。

2.5 DAMAGE_REACT_RADIUS:受击仇恨扩散的边到边判定

在受到伤害时,CBaseUnit::applyDamage0x53D5A0)会在两处关键点调用 CLevel::broadcastAggro0x5DEEB0,实参为 (level, 攻击者, 受害者, unittype过滤器 = 0))。该函数遍历当前关卡中的所有有效单位,跳过攻击者自身、已死亡单位、已拥有锁定目标的单位(偏移 +800 非空)以及未通过单位类型过滤的目标;剩余单位通过 sub_51A830(i, 受害者) 执行几何相交检验:

c
dist_xz(i, 受害者) - i.COLLISION_RADIUS - 受害者.COLLISION_RADIUS <= i.DAMAGE_REACT_RADIUS   // 忽略 Y 轴高度差

若通过该阈值判定,则立即调用 i.setTarget(攻击者)。该算法呈现三项核心特征:

  1. 以观察者(受波及的同伴)自身的 DAMAGE_REACT_RADIUS 为基准,而非受创者;
  2. 判定采用边到边(Edge-to-Edge)测距,双方的 COLLISION_RADIUS 碰撞半径均被扣除;
  3. 仇恨重定向的目标为直接发起攻击的实体,而非受创的同伴。

其中 COLLISION_RADIUS 位于 CCharacter+0x190(400,缺省 0.5),DAMAGE_REACT_RADIUS 位于 CCharacter+0x344(836,缺省 2.0),均由 0x56D380 从 DAT 文件中反序列化加载。

NOTE

关于玩家近战基础射程:UNITS/PLAYERS/BASE.DAT 中未显式声明 COLLISION_RADIUSCOLLISION_ADDREACH_BONUS,因而统一采用引擎默认值:$0.5 + 0.25 + 0.75 = 1.5$。三者采用线性相加的合成方式源于早期文档归纳,尚无逐字节反汇编确证。

2.6 NEAR_WALK_RADIUS 与引擎真实默认值校准

早期第三方复刻实现中曾存在 4 项凭经验推测的默认值,这些推测值虽然数值合理,但与引擎底层硬编码存在确定性偏差:

属性键名早期推测值引擎硬编码真实默认值受缺省影响的怪(全量 1,142)
WALKINGSPEED2.02.519 种
RUNNINGSPEED7.06.2519 种
TURN_RATE9.010.0356 种
NEAR_WALK_RADIUS3.00.0860 种

其中 NEAR_WALK_RADIUS 的偏差尤为显著:3.0 容易被误读为停止追击的停步距离。根据既有文档记载,其设计本意为“当与目标接近至该距离时,由奔跑切换为步行状态”;引擎硬编码缺省为 0.0,且全语料中 860 种怪物未配置该项。

包括 WALKINGSPEEDRUNNINGSPEED 在内的六项移动速度属性,均由同一加载例程 sub_518F10 一次性读取;动画播放速率与物理位移速度在引擎中属于两个完全独立的缩放倍率。

2.7 原版怪物待机静止与视锥判定特性

对引擎汇编指令与状态机的深入跟踪证实了以下三项关于怪物待机行为的事实:

  1. AISpawnAndWander(状态 49)属于未引用的无效状态:全代码镜像中针对 setState 的特征指令匹配显示,AIIdle(状态 2)、AIHunt(状态 4)、AIFlee(状态 10)以及 AIGetRange(状态 11)均存在密集的有效调用分支,而状态 49 的调用点计数为 0。
  2. AIFidget(状态 1)不产生空间位移:处理函数 sub_5B1090 仅检测当前模型是否具备名为 FIDGET 的闲置动作片段,并对外同步网络消息,不包含任何位移与朝向改变计算。
  3. AIIdle 处理例程完全依赖目标捕获:在未通过 findAITarget 捕获有效攻击目标前,待机处理例程直接返回 0,不执行任何自主寻路或步进。

结论:原版游戏中的常规怪物在未进入警戒前均处于绝对静止状态;具备自主漫游巡逻逻辑的仅限于城镇或野外特定 NPC(通过 WANDER RADIUS 机制驱动)。在引入 VIEW_ANGLE 视锥检测后,背对玩家的怪物将无法自主感知从背后接近的目标(实测在标准关卡测试中,约 44% 的背向怪物无法察觉玩家的接近),这属于符合原版引擎设计的真实机制。

CAUTION

上述结论基于汇编调用链未见移动指令的实证;若需彻底确证是否存在极其隐蔽的隐式转向逻辑,仍需依赖游戏客户端的逐帧内存轨迹观测。


3. AITYPE 枚举:八大名称到五类 Handler 的底层分派

怪物 AI 类型字段存储于 CCharacter+0x318,在构造函数 CCharacter_ctor0x555500)中清零;多人联机同步通过 sub_531700 发送 NetSetAITypeMsg(消息号 85)。引擎内部的字符串名称表共 8 项,单项步长为 28 字节(对应当前编译版本的 std::wstring),在三个模块中定义完全一致:EditorGetAITypes0x4DE0D0,数据指针 unk_318D038)、DAT 文本解析区(unk_32C8DC8)以及编辑器描述标签(unk_3312108)。

内部枚举值枚举标识字符串原版语料用量运行时处理函数 (Handler)允许的状态机跳转是否选取攻击技能
0NORMAL72sub_5AF830AICastSkill, AIHunt
1RANGEDDEFENDER20sub_5AEDF0AICastSkill, AIFlee, AIHunt从不选取
2DEFENDER3sub_5B0120 $\rightarrow$ 单行 Thunk 转发至 sub_5AF830等同于 NORMAL
3RANGEDCASTER120sub_5AF1F0AICastSkill, AIFlee, AIHunt
4CIRCLER95sub_5AF4A0AIHunt, AICastSkill, AIGetRange
5RESURRECTER0sub_5B0120 $\rightarrow$ 转发至 sub_5AF830等同于 NORMAL
6DUMMY76未注册 Case 分支 (无操作)
7SUPPORT1sub_5AF050AICastSkill从不选取

核心调度逻辑:分派入口位于 AIIdle_dispatchByAIType0x5B0140),全局唯一调用方为 CMonster__updateAI_tickAndDispatch0x5B1E40)状态机中的 Case 2 (AIIdle)。这表明:AITYPE 分类仅在待机闲置状态下参与决策下一步的状态流转;一旦实体迁移至 AIHunt(追击)或 AICastSkill(施法)状态后,该分类逻辑不再重复参与决策。

DAT 文本反序列化机制0x55E110,偏移 0x55FD10 起):通过 getString("AITYPE", L"") 获取配置串后转大写,若长度大于 0 则在 8 项枚举表中执行线性比对,比对命中后写入数组下标。若未声明该键,或声明了非表中字符串,字段值均维持构造函数的初始值 0(即 NORMAL),且系统不产生任何异常告警。 官方数据中存在典型案例:SHADOWBAT.DATFUNGUS_SPORE_PET.DAT 中显式配置了 AITYPE: GOOD,在底层均静默回退至 NORMAL 运作。此外,RESURRECTER 不仅在原版资产中零引用,其底层实现也仅是指向 NORMAL 的空转发别名。

五类实际处理函数的通用共性

  1. 包含 rand(0, 100) < 80 的索敌条件放宽逻辑;
  2. 调用 findAITarget(this, 10, relax) 寻找候选目标(第二个实参 10 的具体物理含义尚未逆向闭环);
  3. 若单位存在从属宿主(Owner),当其脱离宿主超过 35.0 世界单位时自动丢弃仇恨目标;
  4. 仅当 getPetStance() == 1Aggressive)时主动寻找敌人。getPetStance0x51C990)对于无从属玩家的野怪实体,直接读取 CCharacter+0x7E4,该偏移在构造阶段硬编码为 1。因此野外怪物默认具备侵略索敌能力;一旦被魅惑或收服,当姿态切换为 Passive(0)或 Defensive(2)后,上述五个处理函数将全体跳过主动索敌分支。

CIRCLER 伪绕圈机理sub_5AF4A0 在地址 0x5AF70F 处的控制流显示,当攻击目标处于近战距离之外时,首先触发 setState(AIHunt = 4);随后执行 rand_int_between_volatile(0, 100) < 20 概率分支,命中后将状态修改为 setState(AIGetRange = 11),并命令单位走向以目标为中心、边长为 $\pm 3.0$ 世界单位的正方形区域内部的随机落点($X$ 坐标与 $Z$ 坐标分别通过独立的 rand_float_between(c - 3.0, c + 3.0) 生成)。数值 3.0 与概率 20 均为代码中的硬编码字面量。若将其简化实现为“圆周平移”,将损失角落区域约 21% 的候选采样空间。由于该随机判定位于 AIIdle 内部,其实际触发频率受后续状态机的流转周期制约。

DUMMY 类型的静态验证:原版语料中配置为 AITYPE = DUMMY 的 76 个单位涵盖木桶、路障、标牌与可破坏场景构件,其“完全不参与状态分派”的底层表现与无交互装饰物的语义完全自洽。经近战攻击节律表交叉检验,除 1 处特殊样本外,其余 75 个 DUMMY 单位均无有效战斗攻击节律。


4. BRAVERY 溃逃机制:生命比例阈值的数学补数

属性字段位于 CCharacter+0x744(单精度浮点),引擎硬编码缺省值为 1.0sub_677FA0 @ 0x56D380)。该字段在构造函数(0x555B110x565744)与 DAT 加载器(0x56D570)中写入,其唯一的读取消费逻辑位于 sub_5239000x523960 / 0x52396E):

c
maxHP = getMaxHealth();
hp    = min(floor(this[1376]), maxHP);           // 当前 HP 存储于 +0x560
if (this[1706] == 0) return false;               // 全局或单位逃跑许可开关 (未闭环)
if (hp <= 1) return true;                        // 濒死保底判定 (仅剩 1 点生命值时无条件溃逃)
if (maxHP * 0.15 >= hp && state == AIFlee(10)) return true;
bravery = this[1860];
if (AIManager::isFlagSet(3)) bravery *= 0.5;    // 全局恐慌标志影响 (未闭环)
if (hp / maxHP < 1.0 - bravery && state == AIFlee(10)) return true;
return false;

不同 BRAVERY 配置下的逃跑临界阈值对照:

配置值 BRAVERY官方语料统计逃跑阈值 $1 - \text{BRAVERY}$实际逃跑触发时机
1.0 (缺省)83 种0.0绝不逃跑
0.068 种1.0受到伤害即溃逃
0.564 种0.5当前生命值降至 50% 以下时维持逃跑
10.032 种−9.0绝对不逃跑(较缺省值具备更高防溢出裕度)
0.416 种0.6当前生命值降至 60% 以下时维持逃跑
0.115 种0.9当前生命值降至 90% 以下时维持逃跑

机制结论BRAVERY 的数学本质是溃逃生命值比率阈值的数学补数($1 - \text{BRAVERY}$),而非 $[0, 1]$ 区间内的逃跑发生概率。

NOTE

状态机流转边界sub_523900 本身承担的是“维持逃跑状态”的校验,而非“初次进入逃跑状态”的触发器(其多处分支均要求当前状态满足 state == AIFlee(10))。怪物初次进入 AIFlee 的逻辑通常由特定 AI 类型触发(例如 RANGEDDEFENDER0x5AF010RANGEDCASTER 中包含“与目标距离过近时以 10% 概率切入 state 10”的专用分支)。此外,this[1706]+0x6AA)开关的具体控制源与 AIManager::isFlagSet(3) 的设置上下文仍有待进一步反汇编定位。


5. 怪物技能配置:[SKILL] 块与概率判定分流

5.1 数据面与引用解析

怪物 DAT 中的技能挂载以 [SKILL] 块形式声明:

[SKILL]
<STRING>NAME:FlamingFlaskBoss     ← 技能内部引用名 (非本地化显示名)
<INTEGER>LEVEL:1
<INTEGER>CHANCE:40
<INTEGER>CANCEL_CHANCE:20
<INTEGER>PANE:0
[/SKILL]

块内支持的合法属性包含:NAMELEVELCHANCECANCEL_CHANCECAST_ODDSLEVEL_REQUIREDENABLEDPANEROWCOLUMNCHARGES。对全量 1,142 份怪物 DAT 进行普查:

  • 597 种怪物挂载了技能,544 种未配置技能
  • 累计包含 1,553 条技能条目,涵盖 710 个互不相同的技能标识;
  • 全量 1,553 条技能引用在底层技能字典中实现了 100% 完整解析,0 处悬空落空。其中显式配置 CHANCE 的条目为 703 条,配置 CANCEL_CHANCE 的为 1,255 条,配置 CAST_ODDS 的为 473 条,其余均使用引擎默认值。

在数据结构映射上,配置中的 NAME 对应技能资产内部的引用标识符(如 AlchemistEmberBeam),而非面向玩家界面的本地化显示名称(如 Ember Lance)。

普攻与技能的底层统一性:原版怪物的基础物理普通攻击在底层同样作为技能实体实现(例如高频被引用的 SturmbeornMelee2/3TROLL ATTACK3/4BONEGOLEMMELEE1/2 等),技能 DAT 内部明确声明了 ANIMATION(施法动作片段)与 RANGE(攻击射程)。在实现战斗判定时,必须统一协调由角色属性计算的攻击范围(COLLISION_RADIUS + COLLISION_ADD + REACH_BONUS)与技能专用 RANGE 之间的判定优先级。

5.2 结构体偏移与判定分流

上述三项概率参数载入 CSkill 结构体的三个连续字段:

属性键名内部字段偏移硬编码缺省值真实算法语义
CHANCE+268 (0x10C)40候选技能中签后的独立百分比通过门
CANCEL_CHANCE+272 (0x110)0未定位明确消费逻辑
CAST_ODDS+276 (0x114)1技能候选池中的加权抽签权重

CAST_ODDS(权重判定):函数 AI_pickRandomOffensiveSkill0x5ADF00)在遍历通过基础准入校验的技能列表时,通过 WeightedRandom__addEntry(idx, *(DWORD *)(skill + 276))0x5AE05D)累加权重,最后由 WeightedRandom__pick()0x5AE07F)完成加权随机抽取。缺省值 1 表示未显式配置权重的技能均享有相同的基础抽取概率。基础准入校验包含约 7 项硬性约束:技能可用标志位(+81)、运行状态过滤(+60 != 4)、当前法力值充足性验证(双重检查)、未处于冷却期、可见与启用状态标记,以及目标对象的合法性校验(canUseSelectedSkillOn)。

CHANCE(百分比门限):唯一消费函数为 sub_6BE050

c
return CHANCE >= 100 || rand_int_between_seeded(&skill[114], 0, 100) <= CHANCE;

该函数由 CCharacter__canUseSelectedSkillOn0x524F70)直接调用。由于采用闭区间 $[0, 100]$ 且判定条件为 $\le$,默认配置值 40 对应的理论通过概率实际为 $\frac{41}{101} \approx 40.6%$,而非标称的 40.0%。该分支使用基于技能专属随机种子的随机数流(种子位于 CSkill+456),具备确定性伪随机特征。

若将二者的语义混淆,将同时破坏怪物多技能的轮换分布与整体施法节律,导致怪物在运行时表现出过度偏向某一特定技能的异常行为。

5.3 SKILL_TYPE == OFFENSIVE 分支特性

反汇编注释中曾记录 cmp [level+0x4C], 2 判定恒为真。在 CSkillProperty__ctorFromDat0x6DC3BF 处)确证了针对该偏移写入常量 2(C7 46 4C 02 00 00 00)的操作;然而鉴于偏移 0x4C 过于通用,未对全局所有写入链路完成全量枚举,因此该分支恒真性仍保留为“部分核实”状态。

5.4 MONSTERCOOLDOWNMS 与施法者类型分流

函数 sub_6DBD70 为技能冷却周期的统筹获取接口(由 CSkill__start = 0x6C39E0 等 3 处关键节点调用):

c
owner = *(this + 56);
u = *(owner + 20);
if (owner && u && !*(u + 388) && hasUnitType(u, 27 /* Monster */))
    return *(float *)(this + 180);   // 偏移 0xB4: 读取 MONSTERCOOLDOWNMS
else
    return *(float *)(this + 176);   // 偏移 0xB0: 读取 COOLDOWNMS

其中类型 27 代表 Monster。全量语料的 1,659 份技能 DAT 中,1,040 份配置了 COOLDOWNMS,另有 123 份显式配置了特化的 MONSTERCOOLDOWNMS。若忽略该分流逻辑,这 123 份技能在由怪物释放时将完全偏离设计冷却节律。

5.5 NUM_PETS_ALLOWED:上限阻断与无效边界值

属性 NUM_PETS_ALLOWED 映射至 CSkillProperty+0x2C4占用 1 个字节),缺省继承父级配置,无父级时填入 $-1$(or edi, 0FFFFFFFFh @ 0x6DE6B9)。其唯一消费者为 CSkill__canUse0x6BFCE1):

assembly
movzx eax, byte ptr [eax+2C4h]     ; 将 NUM_PETS_ALLOWED 执行无符号零扩展至 32 位 (0..255)
cmp   eax, 0FFFFFFFFh              ; 检查是否等于 -1
jz    ok                           ; 永不跳转 —— 零扩展后的数值绝不可能等于 0xFFFFFFFF
cmp   [edi+738h], eax              ; 施法者当前激活的随从数 vs 上限阈值
jnb   fail                         ; 若 >= 上限,则 canUse 直接返回 false

关键机制归纳

  1. 当召唤物数量达到上限后,引擎执行的行为是阻断施法技能的后续释放,而非顶替销毁最早生成的召唤物;
  2. 配置项中的 -1(表示无上限)因底层汇编的 movzx 零扩展指令导致比较恒不成立,属于失效逻辑。其单字节存储导致 $-1$ 实际保存为数值 255,即引擎所支持的理论随从数量硬上限为 255。

数据普查显示:全语料中包含 88 种具备召唤能力的怪物、123 门召唤技能与 41 类衍生召唤物。以关卡中的 Forest Spore Launcher 为例,其设计本质并非弹道发射单元,而是通过召唤生成 FUNGUS_SPORE 实施攻击。


6. 技能 DAT 的底层硬编码缺省值 (CSkillProperty__ctorFromDat @ 0x6DC320)

当结构体父级指针([esi+0x34])为空时,属性加载链路中的缺省初始化指令与地址分布如下:

属性键名硬编码缺省值对应指令与虚地址
FINDTARGETANGLE0.0fldz @ 0x6DDFA9
RANGEMIN0.0fldz @ 0x6DE0BC
COOLDOWNMS0.0fldz @ 0x6DE1E0
RANGE维持原值
TARGETRANGE继承 RANGEfld [esi+94h] @ 0x6DE05F

数值转换机制:冷却时间在内存中统一换算为进行存储:DAT 文件中的毫秒参数在读取时乘以常量 flt_2142178(通过 .rdata 确认为 0.001);父级继承分支再乘以 flt_21414381000.0)转回毫秒。在装载函数末尾的 0x6DE2C2 处包含强制门限:若计算后的绝对冷却时间小于 0.01 秒(flt_2142654 = 0.01),将被直接置零。该逻辑属于绝对置零而非向下取整。

NOTE

FINDTARGETANGLE 缺省为 0.0 在理论上存在“无限制全向”或“零度狭缝”两种对立解释,由于其直接消费逻辑(技能系统的锥形视锥裁剪)尚未完全唯一定位,建议在工程实现中保留原始导出,避免激进覆盖。


7. 远程攻击链路:技能 $\rightarrow$ 布局 $\rightarrow$ 投射物的级联触发

以典型远程技能 Tar Spit 为例,引擎内部的执行链路呈现三级嵌套结构:

怪物配置 DAT:  [SKILL] NAME:Tar Spit

  ├─► 技能配置: SKILLS/MONSTERS/GEL/TARSPIT.DAT
  │     参数: RANGE 12 / RANGEMIN 3 / COOLDOWNMS 3000 / ANIMATION attack1 / TARGET POSITION
  │     事件触发: [LEVEL1][EVENT_TRIGGER] FILE: 指向目标 .LAYOUT 布局文件

  ├─► 布局生成节点: 该布局内部包含 Unit Spawner
  │     参数: GROUP:Missiles / RESOURCE:TARSPIT / COUNT 1 / SPAWN ON CREATE:false
  │     时钟驱动: 由同布局的 Timeline 在 TIMEPERCENT:0 触发 Spawn Units 逻辑

  ├─► 投射物布局: MISSILES/MONSTERS/GEL/TARSPIT.LAYOUT (DESCRIPTOR: Missile)
  │     运动动力学: MAX VELOCITY 10 / MAX DISTANCE 14 / ARCH HEIGHT 0.6 / TRACK GROUND
  │     碰撞交互: TARGET POSITION / COLLIDES WITH OBJECTS:false / ACTIVE: 挂接粒子特效

  └─► 消亡衍生事件: 技能配置中的 [EVENT_MISSILEDIE][EXECUTE_SKILL] SKILL:Tar Floor 递归执行衍生技能

语料普查数据

  • 目录 MEDIA/MISSILES/**/*.LAYOUT 累计包含 407 个文件与 403 个 Missile 描述符;基于 MISSILE NAME 属性建立全局索引,映射命中率为 403/403,命名冲突数为 0,空命名数为 0;
  • 在去重后的 710 条怪物技能中,283 条包含 Unit Spawner 节点,427 条不包含
  • 针对这 283 条包含 Spawner 的技能,按 GROUP 属性进行分类解析:
    • GROUP: Missiles:共 240 条,实现 100% 完整解析命中
    • GROUP 节点:共 78 条(属于召唤仆从机制);
    • Spawn Class:11 条;
    • Particle 特效:8 条;
    • Items 道具掉落:1 条(金币生成 GOLD150)。

TIP

引用有效性校验标准:早期若直接统计全量 Unit Spawner 节点,会因未区分节点职能而误将仆从召唤节点判定为失效弹道。对 240 条属于 Missiles 分组的节点执行独立分析确认,其底层物理文件不存在任何死引用。对于其余 427 条无 Spawner 的怪物技能,多走瞬时伤害、AOE 或状态增益(Buff)链路,本轮分析暂未展开。

投射物底层物理动力学(sub_6E4C50

  • 导向插值模式:追踪寻的采用归一化向量线性插值(Normalized LERP),而非基于四元数的球面插值(Slerp);
  • 固定时间步长:内部物理计算采用硬编码常量 0x289494C = 0.0333333 秒(即固定 30 Hz 积分步长),与全局游戏模拟的 62.5 Hz Tick 频率存在解耦;
  • 瞄准高度偏移:投射物追踪的目标中心点高度为 $\text{target.y} + 2 \times \text{RADIUS}$(对应目标躯干质心),而非单位脚底坐标。

8. 投射物 Missile 描述符:55 项属性与结构体字段偏移

8.1 逆向获取方法与字符串解析陷阱

投射物描述符各属性的内存映射源于核心注册函数 sub_4C61C0CMissileDescriptor 属性注册表)。函数内部通过 <类型标识, Getter函数指针, Setter函数指针, 字段说明文本, 属性键名> 的结构模板注册了全部 55 条属性记录,通过进一步反编译提取访问器对类成员的引用,确立了每个键值在结构体内部的真实字节偏移。

字符串解码的工程陷阱

  1. 字符集编码格式判别:在反汇编二进制数据段中,宽字符(UTF-16LE)与窄字符(ASCII)必须依据偶数位的零字节特征严格区分,不可盲信逆向反编译工具推导的字符串签名。若将宽字符误按 UTF-8 读取,会导致解析结果呈现为离散的孤立字母(例如一系列仅包含“R”、“M”的假阳性数组);
  2. 终止符内存对齐:宽字符串的 \0\0 终止符必须按偶数地址对齐定位。若在原始内存中执行无对齐的双零扫描,极易误命中前一字符的高位零与终止符低位零构成的奇数地址对(例如 RADIUS 中末尾字符“S”之后紧邻的高位字节),进而导致整个缓冲区发生连带越界拼接。

8.2 字段字节偏移速查表

属性键名引擎注册表官方英文说明结构体字节偏移 (Byte Offset)
MAX VELOCITY / MIN VELOCITYMax / Min velocity of the missile440 / 444
FRICTIONFriction of the missile448
AOE RAIDUSThe radius of the explosion.452(引擎内部存在拼写错误
GROUND OFFSETVertical offset from ground if it follows it472
RADIUSRadius of the missile488
RADIUS FOR UNITSUnit Collision Radius of the missile492
NUM RICOCHETSNumber of Ricochets500
TARGET POSITION / COLLIDES WITH OBJECTS / COLLIDES WITH WORLD507 / 508 / 509
TRACK GROUNDTracks the ground.510
FOLLOW GROUNDMUST stick to the ground.511
USE OWNER TARGETUse firing owner's target512
MIN DISTANCEThe minimum required distance a missile MUST travel536
ARCH HEIGHT / ARCH DISTANCE540 / 544
MAX DISTANCEThe distance allowed to travel before killing the missile.Setter: 568;Getter: *(+572) + *(+568)
HOMING SPEEDA number between 0-1 … 0 - no follow and 1 being fast follow.684
GRAVITYDownward acceleration688
HOME AFTER X SECONDSThe amount of time before homing kicks in.692
TARGETING ANGLEThe angle the missile can target enemies.704
MAX TURN RATE / TURN ACCELERATION / TURN RANDOMIZATION / RANDOMIZATION RATE708 / 712 / 716 / 720
AOE DAMAGE SCALE / DAMAGE SCALE / DAMAGE SOAK SCALE732 / 736 / 740
MIN PERCENT DAMAGE / MAX PERCENT DAMAGEMin / Max Percent of the damage graph756 / 760
VERTICAL AIMINGCan aim up or down vertically420
PIERCING / IGNORE SMASHABLES418 / 419
FULL VELOCITYIf true the missile starts at full velocity416
DAMAGE TYPEThe type of damage used856
RETARGET / RETARGET ANGLEretargets after a hit …864 / 860
COLLIDE AFTER X / APPLY FORCE AFTER X… after X Seconds.476 / 480
RESET DISTANCE ON RICOCHET / RANDOMIZE ON LAUNCH / RANDOM TURNS BEFORE HOMING504 / 515 / 516
PLAY HITSPlay standard hit damage particles on struck unit773
FORCE (重复注册项)Force applied per second / Force of missile436 / 764
RELEASE / ACTIVE / HIT / DIE / VISUAL / MISSILE NAME特效挂接与标识引用Getter 均映射至 112,Setter 表现为 null
REPEAT RATE / COLLISION RADIUS / AOE RADIUS编辑器专属辅助配置两侧均未提供有效读写操作 (null)

完整 55 条属性定义(包含 Getter/Setter 虚地址与未实现字段分布)归档于 REGISTRY/missile_descriptor_props.json

8.3 针对早期《弹道.md》文档的三处关键技术修正

早期文档描述引擎底层真实实现逆向核实依据
+112 标识速度缩放倍率字段偏移为字节 448(双字索引 112),对应属性为 FRICTION(阻尼摩擦系数)。该动力学限制逻辑为:$\text{速率} = \max(\text{FRICTION} \times \text{当前速率}, \text{MIN_VELOCITY})$,体现为每帧摩擦衰减与最低限速。全语料中有 20 枚投射物配置了 FRICTION,23 枚配置了 MIN VELOCITY注册表属性键名反查
+140 为最大射程单一数值MAX DISTANCE 的 Setter 写入偏移 568(双字索引 142),但 Getter 返回的计算式为 *(+572) + *(+568),即基础配置值与运行时增量叠加。这是全量 55 项属性中唯一一处读写不对称的真实机制。Getter 与 Setter 汇编指令比对
if (this[510]) desired.y = 0 标记为未命名的水平追踪开关偏移 510 对应的属性键名为 TRACK GROUND("Tracks the ground."),绝非 VERTICAL AIMING(偏移 420,"Can aim up or down vertically")。此处的 this 在汇编中为字节级基址,this+424 即对应线速度向量。属性注册表偏移精准对齐

全语料中 TRACK GROUND 出现 90 次,VERTICAL AIMING 出现 129 次。二者字面含义易被混淆,但字段偏移与控制分支截然不同,配置错误会导致截然不同的弹道运动轨迹。

8.4 注册表命名冲突与拼写缺陷

  • AOE RAIDUSAOE RADIUS 的语义分歧:拼写错误的 AOE RAIDUS(偏移 452)对应实际生效的爆炸杀伤半径;而拼写正确的 AOE RADIUS 在底层属于编辑器专属可视化开关("Will turn on and off the AOE radius."),其访问器与 COLLISION RADIUS 共享(0x4C56A0 / 0x4C5680)。若在 Mod 中试图修改爆炸作用范围,必须写入带有拼写错误的 AOE RAIDUS
  • FORCE 键的重复注册:该键名在注册表中被调用了两次,分别对应偏移 436("Force applied per second")与偏移 764("Force of missile")。在 DAT 解析阶段何者具有最终覆盖权,尚属未闭环项。

9. 关卡实体填充系统 (Populate):架构分流、密度算法与参数映射

9.1 双轨制架构对比

架构特性系统 A:CLevel::Populate (程序化密度填充)系统 B:Unit Spawner 描述符 (静态锚点生成)
核心执行函数sub_5ED9B00x5ED9B0..0x5EF5D6,函数体 7,206 字节)sub_629A30(函数体 6,570 字节)
功能定位关卡初始化加载时,按空间密度与全图面积批量生成实体:常规怪、次级怪、精英、道具、NPC、小动物与金币读取 .LAYOUT 文件中预设的固定刷怪锚点与技能召唤实体(描述符 Code 5,包含 53 个字段)
触发时机关卡构建与传送过图阶段(由 sub_43B1D0performWarp 关卡生成流水线中调用)场景对象激活与触发器驱动
关键日志特征包含格式化串 "TIME TO ADD RANDOM MONSTERS""Monsters ACTUALLY Created:""MONSTERSET"
空间碰撞校验基于关卡内可行走 Room Piece 的 AABB 空间加权采样池调用 CLevel__findFreeSpotNear_r2 执行 3 轮邻近有效落点探测

NOTE

早期分析曾因系统 B 未直接调用 isWalkable 而误判其缺乏空间校验,实测确认其通过二级封装函数 findFreeSpotNear_r2 实现了严密的落点碰撞筛查。

9.2 系统 A 数量生成公式:sub_600810(this, idx, area)

IF (absMin > 0 || absMax > 0):
    n = rand_int_synced(min(absMin, absMax), max(absMin, absMax))       # 绝对数量区间优先生效
ELIF perMin <= 0 || perMax <= 0:
    n = 0
ELSE:
    n = ceil( rand_float_synced(area · K · min(perMin, perMax), area · K · max(perMin, perMax)) )

常量定义: K = flt_2179B4C = float32(1.0 / 6.5) ≈ 0.1538461595773697
  • 参数存储位置:配置项存储于 CLevelTemplateData(构造函数 sub_60BBE0,由 CDungeon::LoadStratasub_5D7C20 中自 DUNGEON DAT 加载):绝对数量字段位于 +0xB84 / +0xB88 + 8 * idx,密度系数位于 +0xB1C / +0xB20 + 8 * idx
  • 参数类型:输入参数 area 为无符号 32 位整型(汇编通过 fild 结合 test edx, edx / jge / fadd 2^32 实施无符号浮点转换);
  • 联机确定性:两条生成分支均使用同步伪随机数流(synced),确保多人联机模式下各客户端生成的关卡实体总量绝对一致;
  • 重试上限限制:常规怪物重试上限为 10 次(cmp ecx, 0Ah @ 0x5EE637),精英怪重试上限为 20 次(cmp ebp, 14h @ 0x5EE92D);达到阈值后强制插入 1 个单位并重置计数器。

9.3 area 参数的物理含义与所属对象

area 变量归属于当前关卡实例 CLevel + 0x54(早期记录曾误记为 CLevelTemplateData + 0x54)。在 Populate 调用处,mov eax, [esi+54h]0x5EDF8F)作为第三实参传入,而 sub_600810 的上下文对象 this 指向 [esi+24Ch](模板数据)。

代码段中仅存在两处针对 inc dword ptr [ebx+54h] 的递增写入(位于关卡 Chunk 瓦片网格拼装函数 sub_5F36D0 的双层遍历循环内:0x5F3E960x5F436F):

c
*(BYTE*)(*(DWORD*)(lvl[16] + 4*x) + z) = buf[idx];
if (!buf[idx]) ++lvl[21];   // 结构体偏移 0x54 (双字索引 21)

物理定义area 严格等于关卡寻路格网中数值恰好为 0(即 0x00 可行走)的栅格单元总数。每个栅格单元对应 0.4 世界单位(floor((x − originX) * 2.5))。

WARNING

集合包含关系差异area 与可通行检测并非同一谓词。通行检测函数 CLevel::isWalkablesub_5DB690)的判定条件为有符号比较 grid <= 0cmp byte ptr [...], 0 配合 setle @ 0x5DB712),而 area 仅累加 grid == 0 的单元。因此,area 是全体可行走区域的严格子集

9.4 类别索引与 DAT 键名映射表

静态初始化函数 sub_1081C20 顺序构建了密度属性名称表(每项为 0x1C 字节的 std::wstring,起始于 unk_28CF208),sub_1081E90 顺序构建了绝对数量表。数组索引严格对应 sub_600810 中的类别枚举 idx。系统共定义了 10 类实体(早前资料常遗漏后三类),Populate 核心流程驱动前 7 类:

类别枚举 (idx)实体类别标识密度控制键 (per-meter)绝对数量控制键原版 187 份地牢数据使用频度 (MIN)
0MONSTERSMONSTERS_PER_METER_MIN/MAXMONSTER_MIN/MAX (单数)密度: 160 份 / 绝对: 3 份
1MONSTERS_SECMONSTERS_SEC_PER_METER_MIN/MAXMONSTER_SEC_MIN/MAX密度: 0 份 / 绝对: 6 份
2CHAMPIONSCHAMPIONS_PER_METER_MIN/MAXCHAMPIONS_MIN/MAX绝对配置: 111 份
3PROPSPROPS_PER_METER_MIN/MAXPROPS_MIN / PPROPS_MAX绝对配置: 0 份
4NPCSNPCS_PER_METER_MIN/MAXNPCS_MIN/MAX绝对配置: 140 份
5CREEPSCREEPS_PER_METER_MIN/MAXCREEPS_MIN/MAX绝对配置: 4 份
6GOLDGOLD_PER_METER_MIN/MAXGOLD_MIN/MAX绝对配置: 47 份
7FISHFISH_PER_METER_MIN/MAXFISH_MIN/MAX0 份
8RARE_FISHRARE_FISH_PER_METER_MIN/MAXRARE_FISH_MIN/MAX0 份
9FORMATIONSFORMATIONS_PER_METER_MIN/MAX无(绝对表在 RARE_FISH 处截止)0 份

两处重要的键名陷阱

  1. PPROPS_MAX 拼写错误:绝对数量表中类别 3 的上限键在引擎中被误拼为多出一个字母 P 的 PPROPS_MAX。因此在 DAT 中若书写标准拼写的 PROPS_MAX引擎将永远无法识别该字段
  2. 单复数命名不一致:绝对数量表采用单数形式 MONSTER_MIN/MAX,而密度表采用复数形式 MONSTERS_PER_METER_*。配置为 MONSTERS_MINMONSTERS_MAX 将无法被系统解析。

原版 187 份 DUNGEON DAT 均未直接触发上述拼写错误,但对于自制 Mod 而言属于高危隐患。

9.5 “1 MB 碰撞列表内存预算限制”的伪概念辨析

在部分历史技术文档中曾流传“关卡刷怪受制于 1 MB 碰撞列表内存预算”的假说。经完整反汇编核实,数值 1048576(1 MB)在 sub_5ED9B0 内部仅作为格式化输出字符串的常量出现(包括 "String Pool Size ( 1048576 bytes = 1 mb ) :" @ 0x5EF083"Sound Memory Usage ..." @ 0x5EF201"Path Node Memory Usage ..." @ 0x5EF2C3 以及 "Collision List Memory Usage ..." @ 0x5EF393)。四处逻辑完全一致,均属于信息性日志输出,全函数中不存在任何将 1 MB 作为阈值实施边界截断(如 cmp/jae)或内存分配控制的代码。

9.6 典型关卡实测数据验证

以标准战场关卡 sundered_zone(29 张拼接瓦片,配置文件 A3-BATTLEFIELD.DAT)为例:

  • 格网统计指标:$\text{area} = \mathbf{506,886}\text{ 格}$(网格尺寸 $1328 \times 1352$);
  • 标量基数:$\text{area} \times K = 77,982.46$;
  • 系统 A 程序化生成量:MONSTERS 195 只(密度 0.0025)、CREEPS 156 只(密度 0.002)、CHAMPIONS 2..3 只(绝对数量优先)、NPCS 1 只、MONSTERS_SEC 1 只;
  • 系统 B 预设静态锚点量:同一场景中由 LAYOUT 显式布置的敌对单位累计为 332 只


10. 怪物核心数值缩放与动画伤害帧机制

10.1 MAXHP / ARMOR / MINDAMAGE / MAXDAMAGE:基于等级曲线的百分比缩放

怪物基础战斗数值的实际生效值遵循统一的百分比缩放公式:

$$\text{实际属性值} = \text{DAT配置值}(%) \times \text{BYLEVEL曲线}(\text{unitLevel}) \times 0.01$$

该逻辑分别由基础加载函数 sub_55CEE0 与精英怪物专用函数 sub_522BE0 实现。基础模板 BASE.DAT 中各属性均配置为 100(即 $1.0\times$ 基础倍率),全语料中 1,114 / 1,142 种怪物直接继承此项。其底层计算细节如下:

计算维度引擎底层真实规则与实现细节
数值取整模式混合取整策略:生命值采用 (int) 强制向下截断;护甲、元素抗性与最终伤害采用 ceil 向上取整;伤害曲线采样值自身采用 floor 向下取整。
0.01 精度系数系数采用单精度浮点常数 0.01f(从 .rdata 通过 x87 fld dword 读取,在 64 位上下文中展开为 (double)0.01f)。若外部复刻采用双精度标准 0.01,当“曲线值 $\times$ 百分比”恰好为 100 的整数倍时,浮点微差会导致 ceil 取整结果出现 1 点误差。
元素护甲计算$\lceil \text{ARMOR_[ELEM]}% \times \text{已缩放物理护甲} \times 0.01 \rceil$(对应 ARMOR_<ELEM>)。引擎提供 7 个元素通道,缺省值 −1 作为跳过写入的哨兵值,而非数值 0。
ARMOR_PHYSICAL 二次叠加元素槽位 0 在循环处理完成后,会显式覆盖物理护甲并自清零。这表明该配置项是在基础 ARMOR 缩放结果之上额外施加的二次百分比乘区
元素键名规则属性键名格式固定为 "ARMOR_" + toUpperW(元素名);标准支持枚举包含:PhysicalMagicalFireIceElectricPoisonAll
难度等级枚举难度索引取值:0 (EASY)、1 (NORMAL,无曲线后缀)、2 (HARD)、3 (VERYHARD)。若解析进入未覆盖分支,曲线指针将被置空并触发异常,无安全默认值。
单位级 *_GRAPH 覆盖若在单位 DAT 中显式配置专属曲线覆盖,引擎不再为其追加难度后缀,这等价于使该单位退出难度维度的动态数值缩放。
MINDAMAGE 保护守卫单独配置 MINDAMAGE 不会触发曲线缩放逻辑,引擎分支保护仅校验 MAXDAMAGE != 0
MINHP / MAXHP 缺省差异两处函数存在缺省值分歧:常规怪物函数 sub_55CEE0 缺省为 1.0,而精英怪函数 sub_522BE0 缺省为 0.0。
曲线首节点前外推若单位等级低于等级曲线首个配置点,引擎不执行钳制,而是沿前两点斜率向下执行线性外推(例如 HEALTH_MONSTER_BYLEVEL 前两个坐标点为 $(1, 11)$ 与 $(2, 16)$,则等级 0 对应的计算值 $\text{Eval}(0) = 6$,而非截断值 11)。

运行时内存抽样(Frida 插桩实测,覆盖 40 只活跃怪物)

  • 证实 SET_HP_PCT 缺省值为 1.0(在场 40/40 只怪物的当前生命值 +0x560 严格等于基础随机生命值 +0x564;全语料 1,142 份怪物配置均未显式声明该键);
  • 元素护甲公式经实机实测验证:7 种样本怪物的非零槽位严格吻合 $\lceil 75 \times \text{ARMOR} \times 0.01 \rceil$(如配置值为 3 对应输出 3,配置值为 8 对应输出 6,逐项吻合)。

10.2 伤害关键帧:漏写 NAME 的事件回退为 HIT 并真实结算

动画剪辑中的关键帧事件配置于 .ANIMATION 文件的 [ANIMATIONS]/[ANIMATION]/[KEY] 节点中(包含 NAME 事件类型名、FRAME 浮点时间戳与可选参数)。引擎内置了一张包含 53 项事件类型的全局字符串表(基址 0x330E820,单项步长 28 字节)。事件类型在底层直接映射为数组下标,由 sub_5AC860 提取 NAME 字符串并通过 sub_6871A0 检索下标——若字符串未命中,函数返回 0,而下标 0 正好对应核心伤害事件 HIT

全语料统计(覆盖 746 个动画文件、6,248 门动作片段、15,608 个关键帧事件):

  • 标准伤害事件分布:HIT 1,747 条、HITTWO 109 条、HITTHREE 126 条、HITFOUR 31 条;
  • 触发缺省回退的事件累计 61 条(其中 59 条完全遗漏声明 NAME 键,2 条字面填写为 unknown),分布于 50 个动画片段中;其中 41 个动画片段的唯一伤害事件全赖此回退分支生效

动态进程实证(断点位于 loc_5615CC: 在受控环境下驱动 Chakawary 攻击处于无敌状态的角色,截获 21 条来自帧号 22.5893 的伤害结算。在全量 15,608 条动画事件中,该帧号唯一精确匹配 CHAKAWARY/Attack4 中未写 NAME 的回退事件,且该片段未包含任何显式伤害关键帧。这证实了:遗漏事件名称的关键帧事件不会被引擎静默丢弃,而是被作为有效 HIT 伤害帧真实执行。

HITTHREE (38) 与 HITFOUR (39) 缺乏消费逻辑: 通过审查每帧动画事件消费链路(虚表槽 111 CCharacter::updateAnimAndLocomotion 0x56B9D0 内部调用):

  • consumeHitEventsAndStrike0x561530)仅接收并处理事件类型 {0, 30}
  • 辅助处理函数 0x5288F0 仅处理事件类型 {0, 15}
  • 0x537F50 的 50 路主分派 Switch 语句中,事件类型 {15, 30, 38, 39} 统一跳转至 Default 空分支 0x539F9E

结论:在复刻实现中,事件收集列表(Collected)与实际消费集合(Consumed)必须严格解耦,事件类型 38 与 39 在运行时不应触发任何伤害结算

时间戳换算:关键帧帧号转换为秒采用硬编码单精度浮点常量 0.033333335(基于 30 fps 基准),其在双精度表示下的展开值与字面量 1/30 存在细微浮点差异;物理伤害与动作关键帧在同一 Tick 帧内落地,不存在延迟。

10.3 怪物受击反应(Stagger / Flinch)机制现状

根据对 642 个动画目录的系统性普查:仅有 123 个目录(19%) 包含了实际的受击动画片段(如 Hit1 101 处、Hit2 57 处、Hit 27 处等)。

WARNING

资产库中字符串 HIT 累计出现 1,679 次,但绝大多数属于攻击动画内的伤害判定关键帧事件名([KEY] NAME:HIT),而非受击动画片段名。若在文本检索时不加区分,极易产生“绝大多数怪物均具备完整受击动作”的认知假象。

底层硬直函数 CUnit__applyStagger0x527860)的核心逻辑如下:当偏移 +0x278 标志为真时,将 +0x284 赋值为 10.0,向客户端广播同步网络消息并复位标志。已确认的调用场景包括 CIRCLERRANGEDDEFENDER 在目标突入近战范围时调用 applyStagger(0, 1) 并切入 AIHunt。而受击动画的具体调度时机以及硬直是否打断当前施法,属于尚未完全闭环项。


11. 给 MOD 开发者的工程技术参考

11.1 无效配置清单(死键 / 拼写错误 / 表外值)

配置文件书写内容引擎底层真实处理关联章节
AITYPE 未在 8 项合法枚举中(例如 GOOD静默作为 NORMAL 处理,不产生语法报错§3
AITYPE: DEFENDERAITYPE: RESURRECTER底层调用与 NORMAL 完全相同的处理函数(单行 Thunk)§3
AITYPE: DUMMY未注册任何状态机流转 Case 分支(等价于绝对静态无行为)§3
地牢配置 PROPS_MAX引擎内部读取的属性键名为带有拼写错误的 PPROPS_MAX,正确拼写永不被读取§9.4
地牢配置 MONSTERS_MIN / MONSTERS_MAX绝对数量表仅注册单数形式 MONSTER_MIN/MAX,复数配置无效§9.4
投射物配置 AOE RADIUS: <数值>该拼写为编辑器可视化开关;实际生效的爆炸杀伤半径为 AOE RAIDUS§8.4
技能配置 NUM_PETS_ALLOWED: -1因零扩展比较失效,不会产生无上限效果,实际以单字节上限 255 封顶§5.5
动画事件配置 HITTHREE / HITFOUR引擎主分派管线中未为其注册任何伤害结算调用,属于静默分支§10.2
单位级 *_GRAPH 覆盖曲线引擎不再为覆盖曲线追加难度后缀,单位将完全脱离难度维度的动态数值缩放§10.1
仅单独声明 MINDAMAGE曲线缩放的前置守卫仅检查 MAXDAMAGE != 0,单独声明不触发缩放§10.1

11.2 属性命名与真实算法语义偏差对照

配置属性键名直觉误区引擎真实执行语义
FOLLOW_RADIUS脱战 / 放弃追击距离追击状态下视野的动态下限;实际脱战判定距离为 $\max(\text{FOLLOW}, \text{当前视野链})$
MOTION_RADIUS待机随机漫游半径宝箱怪处于 MIMICIDLE 伪装状态下的视距;原版常规怪待机时不漫游
VIEW_ANGLE: 0全向无死角视野完全丧失正面视觉;配置为 360 度方为全向感知
BRAVERY越大越勇 / 溃逃发生率数值越大越不逃跑;数学本质为血量比率阈值的补数($1 - \text{BRAVERY}$)
NEAR_WALK_RADIUS停止追击的停步距离引擎硬编码缺省为 0.0;技术规范语义为由跑切走的缓冲过渡距离
[SKILL] CHANCE: 40严格 40.0% 施法概率基于闭区间 $[0, 100]$ 的 $\le$ 判定,实际通过概率为 $\frac{41}{101} \approx 40.6%$
[SKILL] CAST_ODDS独立触发几率多技能候选池中参与加权抽签的相对权重
技能 COOLDOWNMS全局通用的技能冷却当施法者属于怪物类型时,其执行优先级被 MONSTERCOOLDOWNMS 覆盖
投射物 VERTICAL AIMING垂直方向沿地追踪开关沿地追踪开关为 TRACK GROUND(偏移 510);该属性(偏移 420)对应仰角俯仰能力
投射物 MAX DISTANCE绝对最大射程标量Getter 返回配置值与运行时增量的动态累加和(*(+572) + *(+568)
HOMING SPEED弹道角速度转速归一化线性插值权重因子,基于固定 30 Hz 时间步长计算

11.3 引擎支持但原版未充分利用的扩展挂载点

属性键名引擎解析事实工程可用性评级
GLOBALS.DAT *_MONSTER_RESIST_BONUS引擎读取四档难度值并存入全局对象 +0xC0..0xCC,原版均未配置(缺省 0.0)字段已被载入内存,但最终消费者因偏移重叠尚未唯一闭环;可供 Mod 深度拓展
GLOBALS.DAT *_MONSTER_ATTENTION_BONUS怪物仇恨视距的全局难度加成(原版配置 0 / 0 / 2 / 6)已逐字节确证,对游戏战斗感知影响显著
技能 MONSTERCOOLDOWNMS怪物专用的技能冷却覆盖(原版 123 门技能显式配置)已逐字节确证,制作怪物特化技能的关键配置项

11.4 通用逆向与开发纪律

  1. 缺省值必须以汇编硬编码调用点为准:本文所有“硬编码缺省”均通过 sub_677FA0 及其下游初始化指令反查确立,绝不可依赖文本语料中的常见值进行推测,亦不可轻信 GUTS 编辑器界面的初始数值。
  2. 逻辑异常通常表现为无报错的隐性行为漂移:配置错误极少引发客户端崩溃或产生错误弹窗;排查异常必须依赖全语料统计(如唤醒计数异常峰值、无有效引用的 Spawner 统计等)与动态插桩,避免凭借表象主观臆断。

12. 未解与尚未完全闭环事项清单

为保持技术文档的严谨性,下表完整列出当前逆向分析中明确归类为“尚未闭环 / 仍待实证”的边界细节:

待解细节项当前分析现状与技术瓶颈
CANCEL_CHANCE 的消费逻辑尚未在控制流中唯一定位到读取该字段的有效指令(语料中 1,255 条显式配置,缺省为 0)
FINDTARGETANGLE 的几何解释消费逻辑尚未唯一定位;配置为 0.0 时究竟解释为“无限制全向”还是“零度狭缝”存在歧义
*_MONSTER_RESIST_BONUS 的实际影响槽位读取点已被反汇编定位,但下游对多类实体属性的计算存在指针偏移混淆,尚未完全追踪至最终数值
sub_6DBD70!*(u + 388) 标志位的语义施法者条件分支中用于跳过怪物冷却的特定状态位尚未查明
findAITarget(this, 10, …) 的第二个实参含义数值 10 的物理属性(是候选者检索数量上限、抑或是特定空间阈值)未查明
AIHuntAIGetRangeAIFlee 状态机执行周期具体状态处理函数的内部循环周期未逆向完毕,导致 CIRCLER 重新进入选点逻辑的实际频率未知
怪物技能候选集的完整过滤规则AI_pickRandomOffensiveSkill 与防御技能选择函数的候选集生成边界尚未全部逆向遍历
SKILL_TYPE == OFFENSIVE 分支的恒真性构造函数中写入常量 2 已确证,但尚未穷尽所有外部写入途径,属于部分核实
BRAVERY 逃跑维持中的开关标志this[1706] 开关含义与 AIManager::isFlagSet(3) 的置位来源未查明;初次触发逃跑的全部条件尚未闭环
ENV_SIGHT_MOTION_RANGE 生效的 state == 48环境视距封顶逻辑中特定状态 48 的枚举定义未查明
NEAR_WALK_RADIUS 的运行时刻语义规范定义源自早期设计文档归纳,尚未完成针对步进切换逻辑的逐指令反汇编核验
玩家近战距离“三键线性相加”模式“碰撞半径 + 增量 + 额外射程”相加的模式源于既有文档,尚未实现代码级反汇编确证
怪物施法动作期间的位移锁定从“伤害关键帧依赖动画”反推怪物施法期间不可移动,属于工程推论,未做全量实机验证
待机怪物绝对不执行转向的完整性汇编调用链未见转向代码,但未穷尽 AIFidget 的全部派生分支,保留观测反证窗口
Populate 中 area 的重叠网格计数口径关卡瓦片拼装时若两块 Chunk 重叠覆盖同一坐标,引擎会重复自增两次,而离线分析基于扁平网格,二者容差尚未量化
关卡栅格中是否存在负值网格单元area 仅统计 == 0 的单元,而通行判定接受 <= 0,最终烘焙成果中是否存在 $< 0$ 单元未系统排查
Populate 类别 7..9 的下游消费细节钓鱼点、稀有鱼点与阵型数据的底层消费链路未在本文展开
投射物描述符中 13 项 null 访问器的内部映射针对未导出标准访问器的 13 项属性,未作主观臆断,维持 null 状态记录
投射物 FORCE 重复注册的优先级裁决偏移 436 与 764 在 DAT 文件解析时的最终覆盖规则尚未确定
投射物转向随机游走的随机数流归属rand_float_between 使用的 RNG 实例归属未查明(决定联机各端轨迹是否逐帧一致)
427 门无 Unit Spawner 的怪物技能机制瞬时伤害、点对点 Buff 与贴地特效的具体逻辑尚未分类展开
受击硬直的触发源与打断特性CUnit__applyStagger 中标志位的赋值调用方与施法动作打断逻辑尚未逆向完毕
元素护甲 −1 哨兵在动态内存中的观测由于 −1 跳过写入与缺省 0 写入在初始全零内存槽中表现同形,无法通过动态内存快照直接辨析
历史账本中 CSkillProperty__ctorFromDat 虚地址分歧账本条目 C-0147 记录为 0x515F90 附近,C-0149 记录为 0x6DC320;本文按各自上下文引用,未强行合并

附录 A:核心符号与函数虚地址表 (Torchlight2.exe, ImageBase 0x400000)

功能模块 / 关键逻辑绝对虚地址 (Virtual Address)
CCharacter_ctor (初始化 AITYPE、写入 stance=1、写入 BRAVERY 默认值)0x555500(BRAVERY 写入点:0x555B11
AITYPE DAT 文本解析与比对0x55E110(字符比对指令:0x55FD10
AIIdle_dispatchByAIType / 调用方 CMonster__updateAI_tickAndDispatch0x5B0140 / 0x5B1E40
AITYPE Handler:NORMAL / RANGEDDEFENDER / DEFENDER,RESURRECTER Thunk / RANGEDCASTER / CIRCLER / SUPPORT0x5AF830 / 0x5AEDF0 / 0x5B0120 / 0x5AF1F0 / 0x5AF4A0(矩形选点分支:0x5AF70F)/ 0x5AF050
EditorGetAITypes / 网络同步 NetSetAITypeMsg 发送0x4DE0D0 / sub_531700
CCharacter__getPetStance (读取随从/怪物姿态)0x51C990
AIFidget 闲置动作处理例程sub_5B1090
单位属性装载:SIGHT_RADIUSMOTION_RADIUS 难度加成计算sub_51D460
GLOBALS.DAT 全局配置加载 / 全局单例对象 / 难度参数提取sub_641160EASY0x641C2B $\rightarrow$ 0x641C56)/ sub_640CB0() / *(sub_41CE60() + 7048)
CUnit::getSightRange (FOLLOW_RADIUS 唯一读取点)0x51B410
CCharacter::getViewAngle / 索敌调用方 CCharacter__findAITarget (MOTION_RADIUS 唯一读取点 0x522510)0x51B540 / 0x5223A0
追击状态更新:AIHunt_update0x5AE6D0
伤害应用 CBaseUnit::applyDamage / 仇恨广播 CLevel::broadcastAggro / 边到边几何判定0x53D5A0 / 0x5DEEB0 / sub_51A830
DAT 装载例程 (COLLISION_RADIUS / DAMAGE_REACT_RADIUS / VIEW_ANGLE / BRAVERY 缺省值)0x56D380BRAVERY 装载点:0x56D570
六大移动速度属性集中加载例程sub_518F10
BRAVERY 维持逃跑判定函数sub_5239000x523960 / 0x52396E
AI_pickRandomOffensiveSkill (加权随机选技逻辑)0x5ADF00addEntry: 0x5AE05D, pick: 0x5AE07F
CHANCE 独立概率门 / 唯一调用方 CCharacter__canUseSelectedSkillOnsub_6BE050 / 0x524F70
CSkillProperty__ctorFromDat (技能 DAT 硬编码默认值初始化)0x6DC320fldz 初始化:0x6DDFA9 / 0x6DE0BC / 0x6DE1E0TARGETRANGE: 0x6DE05F;冷却强制置零:0x6DE2C2;随从上限:0x6DE6B9
冷却获取分流接口 sub_6DBD70 / 施法调用入口 CSkill__startsub_6DBD70 / 0x6C39E0
CSkill__canUse (NUM_PETS_ALLOWED 校验与阻断入口)0x6BFCE1
浮点转换常量:0.001 / 1000.0 / 0.01flt_2142178 / flt_2141438 / flt_2142654
投射物描述符属性注册 / 逐帧物理运动更新 / 固定时间步长常量sub_4C61C0 / sub_6E4C50 / 0x289494C
关卡填充 CLevel::Populate / 数量生成公式 / 密度系数常量 $K$ / $2^{32}$ 浮点修正sub_5ED9B0 / sub_600810 / flt_2179B4C / flt_213C540
area 累加递增 (瓦片拼装) / CLevel 构造 / isWalkable 通行检测sub_5F36D00x5F3E96 / 0x5F436F)/ sub_5F00200x5F00A7)/ sub_5DB6900x5DB712
Populate 属性名称表 (密度表 / 绝对数量表)sub_1081C20unk_28CF208)/ sub_1081E90unk_28CF4FC = MONSTER_MAX
CLevelTemplateData 构造 / CDungeon::LoadStrata 关卡层级装载sub_60BBE0 / sub_5D7C20
怪物数值等级曲线缩放 / 精英与冠军强化版本sub_55CEE0 / sub_522BE0
动画事件类型全局字典 / 事件装载 / 字符串至下标映射0x330E820(步长 28)/ sub_5AC860 / sub_6871A0
逐帧动画事件收集 / 伤害关键帧消费 / 虚表槽 111 调度0x59C8F0 / 0x561530(有效断点:loc_5615CC)/ 0x56B9D0
受击硬直处理接口 CUnit__applyStagger0x527860

附录 B:支撑源材料与真值账本映射

  • 真值账本 truth/ledger.jsonl
    • C-0140(AITYPE 分派矩阵与回退机制);
    • C-0141(DAMAGE_REACT_RADIUS 边到边仇恨拉群);
    • C-0143(CIRCLER 矩形随机选点);
    • C-0144(VIEW_ANGLE 视野全张角与静态假人);
    • C-0145(MOTION_RADIUS 宝箱怪伪装视距);
    • C-0146(SIGHT_RADIUS 难度全局加成);
    • C-0147(CHANCECAST_ODDS 概率分流);
    • C-0148(远程攻击三级级联调用链路);
    • C-0149(远程物理动力学、技能硬编码默认值与施法者类型分流);
    • C-0150(投射物描述符 55 项属性与结构体偏移);
    • C-0151(仆从召唤上限与 FOLLOW_RADIUS 追击下限);
    • C-0152(Populate 刷怪数量公式、area 物理定义与键名映射表);
    • C-0138(引擎基础默认值校准);
    • C-0043 / C-0038 / C-0121(怪物数值等级曲线缩放机制);
    • C-0062 / C-0080 / C-0081 / C-0082(动画关键帧事件与回退结算);
    • C-0064(移动速度属性加载机制)。
  • 专题逆向研究报告 (原版游戏分析/MONSTER_AI/)AITYPE_全表.mdBRAVERY_怯战阈值.mdGLOBALS按难度分档全表.md受击反应_现状调查.md怪物技能选取规则.md
  • 数据结构与静态配置 (原版游戏分析/reversed_cpp/REGISTRY/)spawn_rules.jsonmissile_descriptor_props.jsonmissile_homing.json
  • 工具链自动化实现参考tools/f2_monster_skills.pytools/f2_missiles.pytools/f2_summons.pytools/m4_populate_counts.py