核心定位:
CAffix是挂载在单位或装备上的「词缀实例」在内存中的对象表达。 位于MEDIA/AFFIXES/目录下的每个.DAT文件,其根级[AFFIX]数据块即构成一份独立的词缀定义。本文档为该底层类的权威技术参考手册。分析依据:基于 IDA 对 32 位可执行文件
Torchlight2.exe(基址0x400000)的逐指令反汇编分析。文中所有0x……均为绝对内存地址。 配置用量数据来自实机样本统计:覆盖原版E:/Torchlight 2/MEDIA中 16084 个.DAT文件的 4049 份词缀定义,以及 MOD 库D:/WizardProject/TL2中 165753 个.DAT文件的 48203 份词缀定义。 统计口径为「包含该配置键的[AFFIX]根块数量」,不计入SETS/*.DAT等套装文件中的引用式[AFFIX]子块(原版 391 个、MOD 库 1523 个)。关联文档:效果底层的具体实现参见 CEffect 参考手册;词缀的挂载与调度侧参见 CEffectManager 参考手册;从技能开发视角使用词缀的方法参见 TL2 技能设计指南。
0. 配置样例速览
先给出一份取自原版数据的完整词缀文件(内容原文照录,仅将缩进统一为 4 个空格),再给出可直接套用的骨架。
0.1 完整实例:MEDIA/AFFIXES/ITEMS/OFTHESAVANT.DAT
[AFFIX]
<STRING>NAME:OFTHESAVANT
<TRANSLATE>PREFIX:Savant [ITEM]
<INTEGER>RANK:18
<INTEGER>MIN_SPAWN_RANGE:25
<INTEGER>MAX_SPAWN_RANGE:999
<FLOAT>DURATION:0
<INTEGER>WEIGHT:140
<INTEGER>SLOTS_OCCUPY:3
[UNITTYPES]
<STRING>UNITTYPE:HELMET
[/UNITTYPES]
[EFFECT]
<STRING>NAME:OFTHESAVANT CRITICAL CHANCE
<STRING>ACTIVATION:PASSIVE
<STRING>DURATION:0
<STRING>TYPE:CRITICAL CHANCE
<FLOAT>MIN:1
<FLOAT>MAX:2
[/EFFECT]
[EFFECT]
<STRING>NAME:OFTHESAVANT MANA STEAL
<STRING>ACTIVATION:PASSIVE
<STRING>DURATION:0
<STRING>TYPE:MANA STEAL
<FLOAT>MIN:15
<FLOAT>MAX:25
[/EFFECT]
[/AFFIX]这份文件几乎把词缀系统的每个机制都用上了一遍,逐条解读如下:
WEIGHT:140—— 非 0,因此会进入随机掉落候选池。原版 4049 份词缀中有 3247 份写的是WEIGHT:0,那些只能由[AFFIXES]块按名引用,本例属于少数派。MIN_SPAWN_RANGE:25/MAX_SPAWN_RANGE:999—— 生成筛选条件为25 <= 物品等级 && 物品等级 < 999。注意上界是开区间。这两个值同时还是品质插值的分母来源。SLOTS_OCCUPY:3—— 抽中后一次性消耗 3 点槽位预算。若剩余预算不足 3 且候选池里还有别的词缀,它会被当场析构并重新抽取;若它已是池中最后一个候选,则即使预算不足也照样收下(预算被扣成负数)。[UNITTYPES]只写了HELMET—— 白名单非空,因此只有头盔类装备能摇到它。此处若把HELMET拼错,该项会被静默丢弃,白名单退化为空表;而空表在掉落筛选中意味着「对所有类型开放」,词缀会扩散到全部装备上(详见第 5 节)。DURATION:0(根块) —— 归一化后落入「其余」分支,存-1.0且+0xB0 = 0,与不写该键完全等价。原版 100% 的词缀都写了这个键,绝大多数写的正是0或-1。- 两个
[EFFECT]子块的 MIN 与 MAX 不相等,这正是品质插值机制的触发前提。二者的行为却并不相同:
| 子效果 | EFFECTSLIST.DAT 行号 | AFFIX_MODIFIES | 默认曲线 | 实际数值行为 |
|---|---|---|---|---|
CRITICAL CHANCE | 55 | true | 无 | 参与品质插值:数值 = 1 + (2−1) × 品质%/100,随物品等级在 1~2 之间线性变化 |
MANA STEAL | 50 | true | STEAL_MANA | 不参与插值:该类型登记了默认曲线,曲线指针非空,插值条件不成立,回落至曲线求值分支 |
NOTE
上表是「同一份词缀里两个子效果走了两条完全不同的数值路径」的真实案例。判定条件的完整列表见第 7 节 —— 五个曲线指针必须全部为 NULL,插值才会执行。
0.2 可直接套用的骨架
最小可用骨架(NAME 是唯一真正的必填项,其余均有可用默认值):
[AFFIX]
<STRING>NAME:MYMOD_SHARP
<TRANSLATE>PREFIX:Sharp [ITEM]
<INTEGER>MIN_SPAWN_RANGE:10
<INTEGER>MAX_SPAWN_RANGE:60
<INTEGER>WEIGHT:100
<INTEGER>SLOTS_OCCUPY:1
[UNITTYPES]
<STRING>UNITTYPE:1HSWORD
[/UNITTYPES]
[EFFECT]
<STRING>NAME:MYMOD_SHARP CRIT
<STRING>ACTIVATION:PASSIVE
<STRING>TYPE:CRITICAL CHANCE
<FLOAT>MIN:2
<FLOAT>MAX:5
[/EFFECT]
[/AFFIX]完整骨架(右侧 ← 为注解,并非 DAT 语法,实际使用时请删除):
[AFFIX]
# ---- 身份 ----
<STRING>NAME:MYMOD_SHARP ← 唯一必填项。缺省将变为 "NA",导致按名引用全部失效
<TRANSLATE>PREFIX:Sharp [ITEM] ← 物品名前缀词,缺省则不追加
# <TRANSLATE>SUFFIX:of Sharpness ← 后缀词,原版仅 12 份词缀使用
# ---- 生成期筛选 ----
<INTEGER>MIN_SPAWN_RANGE:10 ← 缺省 0
<INTEGER>MAX_SPAWN_RANGE:60 ← 缺省 999999;开区间上界,想覆盖 60 级需写 61
<INTEGER>WEIGHT:100 ← 缺省 1;写 0 则永不随机抽取,但仍可按名引用
# <INTEGER>DIFFICULTIES_ALLOWED:-1 ← 缺省 -1(全难度);是位掩码,不是难度编号
# ---- 生成期预算与位阶 ----
<INTEGER>SLOTS_OCCUPY:1 ← 缺省 1;写 0 表示不占用预算
<INTEGER>RANK:1 ← 缺省 1;未发现业务读取点
# ---- 运行时 ----
<FLOAT>DURATION:-1 ← 缺省 -1.0;写 -1 与不写等价,限时需填正数
# <BOOL>PLAY_HITS:true ← 缺省即为 true。注意不是 PLAY_HIT
# <BOOL>HIGH_QUALITY:false ← 缺省 false;未发现业务读取点
# ---- 适用范围:白名单为空在两条代码路径上语义相反,务必显式声明 ----
[UNITTYPES]
<STRING>UNITTYPE:1HSWORD ← 块内的键名不参与判定,写 UNITTYPE 或 UNITTYPES 均可
<STRING>UNITTYPE:1HAXE ← 但类型名本身必须精确:原版没有 SWORD,只有 1HSWORD / 2HSWORD
[/UNITTYPES]
# [NOT_UNITTYPES]
# <STRING>UNITTYPE:BOW
# [/NOT_UNITTYPES]
# ---- 效果本体,可声明多个 ----
[EFFECT]
<STRING>NAME:MYMOD_SHARP CRIT
<STRING>ACTIVATION:PASSIVE
<STRING>TYPE:CRITICAL CHANCE ← 该类型无默认曲线,MIN/MAX 不等即触发品质插值
<FLOAT>MIN:2
<FLOAT>MAX:5
[/EFFECT]
[/AFFIX]1. 运行架构与生命周期
在引擎底层,词缀本质上是效果(Effect)的容器,并外加了一层生成期的元数据控制。词缀对象自身不产生直接的游戏逻辑效果,所有实际逻辑均由其包含的 [EFFECT] 子块执行。 词缀引入的额外逻辑主要分为两类:
- 生成控制:决定词缀何时能够被随机抽取或赋予(生成等级范围、权重、难度要求、适用单位类型)。
- 数值缩放:决定抽取成功后,其子效果的最终数值如何计算(基于品质百分比进行线性插值)。
MEDIA/AFFIXES/**/*.DAT 中的 [AFFIX] 根数据块
│
├─ 游戏启动阶段: 词缀池索引构建 (CEffectGroupMgr__buildAffixIndex 0x7ACE70)
│ 仅抽取 6 个核心字段与 2 个单位类型表构建全局检索索引,此时不构造 CAffix 对象
│
└─ 运行时按需调用:
CAffix__ctorFromDatFile (0x7A5C10) → CAffix__loadFromDat (0x7A54E0) 构造「词缀原型」CAffix
└─ CAffix__ctorFromRecord (0x7A5D90) 从原型克隆实例并进行定级计算
├─ CAffix__setLevel (0x7A4340) 计算品质百分比 (Quality Percent)
│ └─ CAffix__applyQualityPercentToEffects (0x7A4180) 按品质重写所有子效果的 MIN/MAX 数值
└─ CEffectMgr__addAffix (0x7AFF..) 将生成的各子效果挂载至宿主效果管理器装备词缀的随机抽取与生成属于掉落系统的核心流程,其主要入口函数为 CEffectGroupMgr__rollAffixesForItem (0x7AC620),采用 __thiscall 调用约定。
CAUTION
函数名与早期结论均需更正。 该函数的 this 不是单位指针,而是全局的词缀库单例。 调用点 CEquipment__applyRandomAffixes (0x404E4A–0x404E63) 的真实形态是:
call sub_6488C0 ; 取 CGameData
mov eax, [eax+4Ch] ; → 词缀库单例
...
push esi ; esi = [item+0x1AC] = 物品的 UNITTYPE id(整数,不是指针)
mov ecx, eax ; this = 词缀库单例;单位侧只以 UNITTYPE id 作栈参传入
call CEffectGroupMgr__rollAffixesForItem这个单例的类名是 CEffectGroupManager(vtable 0x219AA80,构造 0x7ACB00), 由 CGameData__ctor_loadAllData_STAGE 在 0x64BCA9 构造、0x64BCC3 存入 CGameData+0x4C。 本文第 6 节提到的那组「并行数组」就是它的字段。
因此:「引擎里没有 CAffixMgr 这个类」这句话字面上仍然成立(确实没有叫这个名字的类), 但由此推出的「词缀池不是单例」是错的 —— 词缀池确实是个全局单例,只是类名叫 CEffectGroupManager。 该类的完整字段图见 CEffectDisplayMgr 参考手册 第 2 节。
本文档重点剖析第 6 节中的五道生成筛选流程,不展开复杂的全套掉落上下文。
2. 内存数据结构 (CAffix Memory Layout)
CAffix 的 RTTI 类继承关系为 CAffix : CRunicCore,虚函数表地址位于 0x219A604,单个对象在内存中占用 224 字节 (0xE0)。
涉及对象生命周期与字段交叉核对的核心函数包括:CAffix__loadFromDat (0x7A54E0)、CAffix__ctorFromDatFile (0x7A5C10,从文件路径构建原型)、CAffix__copyFrom (0x7A5200)、CAffix__ctorFromRecord (0x7A5D90,实例克隆) 以及析构函数 (0x7A5070)。
| 偏移量 | 数据类型 | 字段含义与描述 | 写入来源 / 赋值函数 | 默认初始值 |
|---|---|---|---|---|
+0x00 | ptr | 虚函数表指针 (vftable) | 构造函数 | 0x219A604 |
+0x04 | int | CRunicCore 基类内部字段 | 0x405918 | 0 |
+0x08 | ptr | 词缀原型指针 (CAffix*) | CAffix__copyFrom 0x7A5237 | 0 |
+0x0C/+0x10 | ptr + cookie | 来源技能指针 (CSkill*) 与弱引用句柄 | CAffix__setSourceSkill 0x7A4FE0 | 0 / −1 |
+0x14/+0x18 | ptr + cookie | 来源单位指针 (CBaseUnit*) 与弱引用句柄 | CAffix__setSource 0x7A4840 | 0 / −1 |
+0x1C/+0x20 | ptr + cookie | 当前挂载的 CEffectManager 与弱引用句柄 | CAffix__attachToMgr_applyEffects 0x7A49EB 写入;CAffix__detachFromHost 0x7A48C0 清空 | 0 / −1 |
+0x24 | wstring 28B | 词缀名称 (NAME),经 toUpperW 转大写存储 | NAME 配置项 0x7A55B9 | 空字符串 |
+0x40 | wstring 28B | 前缀文本 (PREFIX) | PREFIX 配置项 0x7A5796 | 空字符串 |
+0x5C | wstring 28B | 后缀文本 (SUFFIX) | SUFFIX 配置项 0x7A57DC | 空字符串 |
+0x78 | int | 生成等级下限 (MIN_SPAWN_RANGE) | 0x7A5692 | 0 |
+0x7C | int | 生成等级上限 (MAX_SPAWN_RANGE) | 0x7A56CD | 999999 |
+0x80 | byte | 品质百分比 (0–100) | CAffix__setLevel 0x7A4457 | 100 |
+0x84 | int | 词缀位阶 (RANK) | 0x7A581F | 1 |
+0x88 | int | 占用词缀槽位数 (SLOTS_OCCUPY) | 0x7A585E | 1 |
+0x8C | int | 允许出现的难度位掩码 (DIFFICULTIES_ALLOWED) | 0x7A5654 | −1 (全难度) |
+0x90 | int | 词缀等级 (Affix Level) | CAffix__setLevel 0x7A43F2 | 由构造入参指定 |
+0x94 | byte | 是否播放受击反馈 (PLAY_HITS) | 0x7A5610 | 1 (true) |
+0x95 | byte | 残留/填充字节,恒为 0 | 仅两处构造路径写入 (0x7A5CC0、0x7A5E1B),无读取点,且克隆时不复制 | 0 |
+0x98…+0xA4 | vector<CEffect*> | 子效果列表 (data / size / cap / growth) | [EFFECT] 子块解析 0x7A5A24 | 空列表 |
+0xA8 | byte | 原型可克隆标记(原型对象置 1,克隆出的实例被置 0) | CAffix__copyFrom 0x7A524F | 1 |
+0xAC | float | 持续时长 (DURATION) | 0x7A5750 | −1.0 |
+0xB0 | byte | 持续时长有效标志 (Has Duration) | 0x7A58D3 / 0x7A596F | 0 |
+0xB4 | int | 抽取权重 (WEIGHT) | 0x7A570D | 1 |
+0xB8 | byte | 高品质标记 (HIGH_QUALITY) | 0x7A589C | 0 |
+0xBC…+0xC8 | vector<int> | [UNITTYPES] 白名单列表,存储单位类型 ID | 0x7A5AE6 | 空列表 |
+0xCC…+0xD8 | vector<int> | [NOT_UNITTYPES] 黑名单列表,存储单位类型 ID | 0x7A5BB9 | 空列表 |
+0xDC | ptr | 数据包条目指针(非空时优先走 Pak 打包资源流加载) | 0x7A554A 读取 | 0 |
NOTE
+0x95 是残留/填充字节,恒为 0,引擎中没有任何代码读取它。三条证据:
- 只有两个写入点,写的都是 0。 两处都是
mov [esi+95h], bl,而ebx在此之前已被清零 (xor ebx, ebx分别位于0x7A5C34与0x7A5DB5,到写入点之间没有重新赋值)。 两处都夹在一串连续的字段初始化里:+0x88→+0x8C→+0x90→+0x94→+0x95→+0x98→+0x9C(末尾的+0x98/+0x9C是效果向量的 data/size,同样靠这个ebx写 0)。 - 全范围无读取点。 在
0x401000–0xA00000内检索[reg+95h]共 20 处命中, 只有上述 2 处属于CAffix,且均为写入;其余分属sub_6106A0簇与0x7B5500/0x7B55F0/0x7B5F90/0x7B60B0(读STAT/DEFAULTVALUE/STACKABLE的另一个子系统)。 - 克隆时被跳过。
CAffix__copyFrom(0x7A5200) 逐一复制了其余四个字节字段——+0x94(0x7A528B)、+0xA8(0x7A524F)、+0xB0(0x7A52B4)、+0xB8(0x7A52FB)—— 唯独没有+0x95。活字段不会被这样漏掉。
至此 CAffix 的 224 字节结构表全部字段均已定性。
内存中存在的三组 (指针, cookie) 结构属于统一的引擎弱引用管理机制:通过 sub_4059A0 注册对象并获取句柄 Cookie,析构时通过 sub_405950 按 Cookie 注销,初始值统一置为 −1。此设计与 CEffect 内部的弱引用机制(+0x40/+0x44 及 +0x48/+0x4C)完全一致。
偏移 +0xA4、+0xC8、+0xD8 在部分早期分析中曾被误认为是独立未知字段,实则为 core::vector 四元组结构 (data, size, cap, growth) 中的容量增长步长分量(Growth)。CAffix__copyFrom 会将该分量统一初始化为「源向量元素数 + 1」。
2.1 C++ 结构体声明
下列声明由上表逐字段还原而来,偏移量与 sizeof 均已通过 static_assert 自校验。其中 core_vector / wstring28 / CRunicCore 三个辅助类型的定义与 CEffect 参考手册 中的完全一致,两个类共用同一套基础设施。
// RTTI: CAffix : CRunicCore vtable @ 0x219A604 sizeof == 0xE0 (224)
struct CAffix : CRunicCore {
/* +0x08 */ CAffix* prototype; // 克隆来源;applyQualityPercentToEffects 从中读取原始 MIN/MAX
/* +0x0C */ CSkill* sourceSkill;
/* +0x10 */ int sourceSkillCookie; // 弱引用注册号,初值 -1
/* +0x14 */ CBaseUnit* sourceUnit;
/* +0x18 */ int sourceUnitCookie; // 弱引用注册号,初值 -1
/* +0x1C */ CEffectManager* host; // 当前挂载的效果管理器
/* +0x20 */ int hostCookie; // 弱引用注册号,初值 -1
/* +0x24 */ wstring28 name; // NAME,缺省 "NA",经 toUpperW
/* +0x40 */ wstring28 prefix; // PREFIX
/* +0x5C */ wstring28 suffix; // SUFFIX
/* +0x78 */ int minSpawnRange; // MIN_SPAWN_RANGE,缺省 0
/* +0x7C */ int maxSpawnRange; // MAX_SPAWN_RANGE,缺省 999999,开区间上界
/* +0x80 */ unsigned char qualityPercent; // 0..100,由 CAffix__setLevel 计算
/* +0x81 */ unsigned char _pad81[3];
/* +0x84 */ int rank; // RANK,缺省 1;未发现业务读取点
/* +0x88 */ int slotsOccupy; // SLOTS_OCCUPY,缺省 1
/* +0x8C */ int difficultiesAllowed; // DIFFICULTIES_ALLOWED 位掩码,缺省 -1
/* +0x90 */ int level; // 词缀等级,由构造入参指定
/* +0x94 */ unsigned char playHits; // PLAY_HITS,缺省 1
/* +0x95 */ unsigned char _pad95; // 残留/填充:恒为 0,无读取点,克隆时不复制
/* +0x96 */ unsigned char _pad96[2];
/* +0x98 */ core_vector<CEffect*> effects; // [EFFECT] 子块解析结果
/* +0xA8 */ unsigned char copyable; // 原型为 1,克隆出的实例被置 0
/* +0xA9 */ unsigned char _padA9[3];
/* +0xAC */ float duration; // DURATION,已归一化
/* +0xB0 */ unsigned char durationValid; // DURATION 是否有效
/* +0xB1 */ unsigned char _padB1[3];
/* +0xB4 */ int weight; // WEIGHT,缺省 1;0 表示不进随机池
/* +0xB8 */ unsigned char highQuality; // HIGH_QUALITY,缺省 0;未发现业务读取点
/* +0xB9 */ unsigned char _padB9[3];
/* +0xBC */ core_vector<int> unitTypes; // [UNITTYPES],存单位类型 ID
/* +0xCC */ core_vector<int> notUnitTypes; // [NOT_UNITTYPES],存单位类型 ID
/* +0xDC */ void* packageEntry; // 非空时走 Pak 资源流加载
};
static_assert(sizeof(CAffix) == 0xE0, "CAffix layout");
static_assert(offsetof(CAffix, name) == 0x24, "");
static_assert(offsetof(CAffix, minSpawnRange) == 0x78, "");
static_assert(offsetof(CAffix, effects) == 0x98, "");
static_assert(offsetof(CAffix, unitTypes) == 0xBC, "");
static_assert(offsetof(CAffix, notUnitTypes) == 0xCC, "");NOTE
五个 _padNN 数组是编译器为满足 4 字节对齐插入的填充,并非引擎使用的字段。对 0x81–0x83、0x96–0x97、0xA9–0xAB、0xB1–0xB3、0xB9–0xBB 这 14 个偏移在本文档前述扫描范围内做过一次指令级检索,命中数为 0,可以放心按填充处理。
3. 12 个配置键深度解析
[AFFIX] 根数据块仅支持 12 个核心配置键,全部在 CAffix__loadFromDat (0x7A54E0) 函数中按序解析。 引擎的读取顺序为:NAME → PLAY_HITS → DIFFICULTIES_ALLOWED → MIN_SPAWN_RANGE → MAX_SPAWN_RANGE → WEIGHT → DURATION → PREFIX → SUFFIX → RANK → SLOTS_OCCUPY → HIGH_QUALITY。
注:用量统计分别对应原版 4049 份与 MOD 库 48203 份独立词缀定义中的有效配置数量。
3.1 基础身份标识
NAME (<STRING>)
| 属性 | 说明 |
|---|---|
| 写入目标 | 偏移 +0x24,经 toUpperW 转换为全大写后存入 std::wstring |
| 读取地址 | 0x7A5597 |
| 默认值 | 缺省回退值为字符串 "NA"(压栈默认参数 0x7A5580) |
| 缺省后果 | 词缀名称变为 "NA";后续其他数据块在 [AFFIXES] 中通过名称引用该词缀时将无法找到 |
| 错误后果 | 自由字符串格式,无前置格式校验。由于内存中强制以大写存储,外部引用时小写书写亦可匹配 |
| 实际用量 | 原版 4049 / MOD 库 48203 —— 两方使用率均为 100% |
在 AFFIXES 目录下的所有定义文件中,NAME 均完整提供,无一遗漏。这是 12 个键中唯一严格不可或缺的核心主键。
PREFIX / SUFFIX (均为 <TRANSLATE>)
| 属性 | 说明 |
|---|---|
| 写入目标 | 偏移 +0x40 (PREFIX) / +0x5C (SUFFIX) |
| 读取地址 | 0x7A578D (PREFIX) / 0x7A57D3 (SUFFIX) |
| 默认值 | 均为全局空字符串地址 unk_3B97D44(即 L"") |
| 缺省后果 | 保持为空字符串;装备生成该词缀时不会在名称前后拼接词缀修饰词 |
| 错误后果 | 仅作为本地化显示文本使用,无特殊逻辑校验 |
| 实际用量 | PREFIX:原版 526 / MOD 库 769;SUFFIX:原版 12 / MOD 库 179 |
NOTE
物品名称的实时生成采用了独立机制:函数 sub_64F770 会在展示阶段(0x64F994 / 0x64FC0D 及 0x64FAA7 / 0x64FC84)直接重新从 DAT 文件读取 PREFIX 与 SUFFIX,并不依赖挂载后的 CAffix 内存对象(详见第 8 节)。
3.2 生成期筛选配置(核心 4 键)
以下 4 个键具有一个共同机制:在引擎中会被读取两次。第一次读取发生在对象构造期解析到 CAffix 字段中(CAffix__loadFromDat),第二次则在启动时提取至全局词缀池索引结构(CEffectGroupMgr__buildAffixIndex 0x7ACE70,详见第 6 节)。两处读取的默认缺省值完全一致。
MIN_SPAWN_RANGE / MAX_SPAWN_RANGE (均为 <INTEGER>)
| 属性 | 说明 |
|---|---|
| 写入目标 | 偏移 +0x78 (MIN) / +0x7C (MAX) |
| 读取地址 | 对象解析:0x7A5692 / 0x7A56CD;索引构建:0x7AD0DC / 0x7AD145 |
| 默认值 | 0 / 999999 |
| 缺省后果 | 采用默认区间 [0, 999999],即任意等级的物品均满足生成条件 |
| 错误后果 | getInt 解析失败时返回默认传入参数 |
| 实际用量 | 两个键均为原版 4049 / MOD 库 48203 —— 两方覆盖率均为 100% |
这两个键在引擎内部有两个完全独立的用途,切勿混淆:
- 生成筛选条件 (
0x7AC871–0x7AC891): 判定公式为MIN_SPAWN_RANGE <= 物品等级 && MAX_SPAWN_RANGE > 物品等级。 注意:MAX_SPAWN_RANGE采用开区间(不包含上界)判定。若配置为 50,表示物品等级为 49 时可以生成,但等级达到 50 时将无法随机命中。 - 品质线性插值计算 (
CAffix__setLevel0x7A4340): 先将词缀实际等级限制在[MIN, MAX]区间内,再计算品质百分比: $$\text{品质百分比} = \operatorname{round}\left(\min\left(100, \frac{\text{当前等级} - \text{MIN}}{\text{MAX} - \text{MIN}} \times 100\right)\right)$$
针对品质计算公式,存在两个特殊的边界情况:
MIN == MAX:计算时发生除以 0,FPU 指令fidiv(0x7A4416) 产生QNaN。随后的浮点比较指令test ah, 41h判定为无序状态,恰好落入「取 100.0」分支 (0x7A443B),最终计算结果恒为 100% 品质。原版数据中未出现此写法,MOD 库中有 8 处。MIN > MAX(上下限反写):当前等级首先被限制为MAX。分子在进行无符号计算时得到极大的正整数,除以负的差值后产生极大的负数,经__ftol2_sse浮点转整型溢出后返回0x80000000,截取低字节得到 0。最终计算结果恒为 0% 品质,所有子效果数值均取MIN初始值。
WEIGHT (<INTEGER>)
| 属性 | 说明 |
|---|---|
| 写入目标 | 偏移 +0xB4 |
| 读取地址 | 对象解析:0x7A570D;索引构建:0x7AD18A;随机池消费:WeightedRandom__addEntry 0x7AC91F |
| 默认值 | 1 |
| 缺省后果 | 权重默认为 1,可正常进入加权随机掉落池 |
| 错误后果 | 解析失败时回退至默认值 1 |
| 显式配置为 0 | 在 0x7AC7E0 处的判定条件 if (*v27) 将直接跳过该词缀 —— 永远不会被随机抽取命中,但依然支持通过 [AFFIXES] 块按名称显式引用 |
| 实际用量 | 原版 4049 / MOD 库 48203 —— 两方使用率均为 100% |
TIP
统计显示,原版 4049 份词缀中存在 3247 份显式配置了 WEIGHT:0(MOD 库中占比亦高达 45436 / 48203)。这意味着官方数据中约 75% 的词缀定义并不参与常规装备掉落,而是专门供技能、被动效果或怪物模板按名称定向引用。
DIFFICULTIES_ALLOWED (<INTEGER>)
| 属性 | 说明 |
|---|---|
| 写入目标 | 偏移 +0x8C |
| 读取地址 | 对象解析:0x7A5654;索引构建:0x7AD1E7 |
| 默认值 | 保持构造初始值 −1(二进制全 1,表示允许所有游戏难度) |
| 字段类型 | 难度位掩码 (Bitmask),并非难度等级序号 |
| 生效流程 | 生成阶段校验 (0x7AC8B5):(1 << 当前难度) & DIFFICULTIES_ALLOWED;挂载生效阶段校验 ( CEffectMgr__addAffix 0x7AFFF6 等三处):test [esi+8Ch], edx,若掩码不匹配则直接析构并释放该词缀对象 |
| 缺省后果 | 值为 −1,在所有难度下均有效 |
| 错误后果 | 若误写为难度序号(例如配置为 2),将被视为二进制掩码 0b10,导致该词缀仅在难度等级 1 下生效 |
| 实际用量 | 原版仅 1 处使用 / MOD 库 0 处使用 |
3.3 生成期槽位预算与位阶
SLOTS_OCCUPY (<INTEGER>)
| 属性 | 说明 |
|---|---|
| 写入目标 | 偏移 +0x88 |
| 读取地址 | 0x7A585E |
| 默认值 | 1 |
| 生效流程 | 在 CEffectGroupMgr__rollAffixesForItem (0x7ACA6A / 0x7ACAC2) 中:随机抽取命中后检查剩余槽位预算。接收条件是 预算足够 || 候选池已抽空(0x7ACA7C)——候选池抽空时即便预算不足也会被收下,预算被扣成负数 (0x7ACAC8)。只有「预算不足且池中还有其他候选」才走析构分支 (0x7ACA85–0x7ACA9D) 并继续抽取 |
| 缺省后果 | 默认占用 1 个词缀预算槽位 |
| 错误后果 | 解析失败回退为 1。若配置为 0,表示不消耗生成预算,单件装备上理论可挂载任意多个该词缀 |
| 实际用量 | 原版 4003 / MOD 库 48119 |
注意:此配置键并不进入全局词缀池索引(第 6 节),仅在抽取命中后作为后置预算校验,不影响候选池构建。
RANK (<INTEGER>)
| 属性 | 说明 |
|---|---|
| 写入目标 | 偏移 +0x84 |
| 读取地址 | 0x7A57F4 |
| 默认值 | 1 |
| 缺省 / 错误 | 偏移 +0x84 保持初始值 1 |
| 实际用量 | 原版 4038 / MOD 库 48121 |
RANK 键在整套可执行文件中仅有两处读取点:CAffix__loadFromDat (0x7A57F4) 以及 sub_6D4020 (0x6D45B2,该处属于非词缀的独立 DAT 解析器)。在针对 CAffix 核心逻辑的扫描范围内,偏移 +0x84 仅在对象克隆及文件构造时被复制传递,未发现任何实际业务读取逻辑,亦不参与词缀索引构建。现有逆向证据表明该字段对引擎逻辑无实际影响(保留为未定论状态)。
3.4 运行时与视觉控制
DURATION (<FLOAT>)
| 属性 | 说明 |
|---|---|
| 写入目标 | 偏移 +0xAC (float),同时同步更新 +0xB0 的有效标志位 |
| 读取地址 | 0x7A5750 |
| 默认值 | −1.0 |
| 归一化分支 | 在 0x7A58C1 处分流:1. 数值等于 −999 → 存储为 −1.0,且置标志位 +0xB0 = 1(表示永久有效但具有显式时长标记);2. 数值 > 0 → 原样存储正数秒,且置标志位 +0xB0 = 1;3. 其他情况(包含默认的 −1.0 及 ≤ 0)→ 存储为 −1.0,且置标志位 +0xB0 = 0(无时限标记) |
| 缺省后果 | 值为 −1.0,+0xB0 = 0(无时限) |
| 消费场景 | +0xB0 标志位在 sub_7BFD10 (0x7C0033) 及 sub_6C4DD0 (0x6C518F) 中被读取,两处均紧随 CAffix__setLevel 之后调用 |
| 实际用量 | 原版 4049 / MOD 库 48197 |
IMPORTANT
显式配置 DURATION:-1 与完全不写此项在底层等价,两者均会产生 +0xB0 = 0。 原版中绝大多数词缀均填写了 -1 或 0,实则与缺省无异。若需声明「受时限系统管理但永久生效」,必须显式填写 -999;若需声明有限时长,则必须填写大于 0 的正浮点数。
PLAY_HITS (<BOOL>)
| 属性 | 说明 |
|---|---|
| 写入目标 | 偏移 +0x94 (byte) |
| 读取地址 | 0x7A5610(键名静态字符串位于 0x219A6CC) |
| 默认值 | 1 (true) —— 12 个键中唯一默认值为 true 的配置项 |
| 消费场景 | CAffix__attachToMgr_applyEffects (0x7A4F08),通过 movzx ecx, byte ptr [edi+94h] 作为受击反馈开关传递至底层效果应用逻辑 |
| 缺省后果 | 保持为 1 (true) |
| 实际用量 | 原版 0 / MOD 库 0(所有正确拼写的数据块为 0 处) |
WARNING
该键在官方与社区 MOD 中遭遇了极为广泛的拼写笔误:原版有 19 处、MOD 库有 301 处误写为 PLAY_HIT(缺少末尾的 S)。 由于底层字符串哈希计算函数 hashStringW (0x4C9FE0) 严格匹配键名,PLAY_HIT 无法被识别,直接被引擎忽略并回退为默认值 1 (true)。这导致上述 320 处试图关闭受击反馈的配置实际上全部未能生效。
HIGH_QUALITY (<BOOL>)
| 属性 | 说明 |
|---|---|
| 写入目标 | 偏移 +0xB8 (byte) |
| 读取地址 | 0x7A589C |
| 默认值 | 0 (false) |
| 缺省 / 错误 | 保持初始值 0 |
| 实际用量 | 原版 64 / MOD 库 112 |
与 RANK 类似,HIGH_QUALITY 键名字符串仅在 CAffix__loadFromDat 中被引用一次。偏移 +0xB8 仅在克隆函数中被复制,无业务读取点且不进入词缀索引。
4. 三个子数据块
在 CAffix__loadFromDat 中,子块解析循环 (0x7A5939–0x7A5BD6) 根据数据块名称的哈希值进行分发,仅识别以下三个标准块名。拼写错误的子块名称将无法匹配任何分支,整块内容将被引擎静默跳过。
4.1 [EFFECT]
每个 [EFFECT] 子块均会调用 alloc(304) 动态分配一个 CEffect 对象,传入 CEffect__ctorFromDat (call 在 0x7A59F4) 完成反序列化,并追加至偏移 +0x98 的子效果列表中 (0x7A5A24)。关于该数据块的具体键值配置,请查阅 CEffect 参考手册。
用量统计:原版 4049 份词缀中共包含 6445 个 [EFFECT] 子块;MOD 库 48203 份词缀中包含 92571 个。
CAUTION
根块与子块配置项严禁混淆。 样本库中发现有数十个 MOD 错误地将 TYPE / ACTIVATION / MIN / MAX / 字符串型 DURATION 直接配置在 [AFFIX] 根块上 —— 这类写法均属无效配置,根块解析器只会匹配前述 12 个键,其余内容均会被直接丢弃。
4.2 [UNITTYPES] 与 [NOT_UNITTYPES]
白名单 [UNITTYPES] 与黑名单 [NOT_UNITTYPES] 的解析逻辑在结构上完全相同 (0x7A5A82–0x7A5AF0 与 0x7A5B55–0x7A5BC3),核心执行流如下:
遍历该数据节点下的全部配置值
id = unittypeNameToId(配置文本字符串)
if (id != -1) {
push_back(id 存入目标向量列表)
}
// 若 id == -1 则直接静默丢弃,不输出报错必须明确以下三项核心规则:
- 内存中存储的是类型 ID 而非名称字符串: 当单位类型名拼写错误时,
unittypeNameToId(0x643EB0) 返回 −1,该配置项将被静默丢弃 (0x7A5AC3/0x7A5B96)。这会导致不对称的异常表现:- 白名单拼写错误:导致有效适用范围变窄;
- 黑名单拼写错误:导致排除规则失效,词缀被错误赋予本应被禁用的单位。
- 块内键名不参与语义判定: 解析器直接遍历该节点的值列表集合(
*(node + 32)与*(node + 36)),完全不校验每行的键名(Key)。因此,<STRING>UNITTYPE:PLAYER与<STRING>UNITTYPES:PLAYER具有完全相同的解析效果。 - 同名子块会自动合并: 在全局索引构建阶段 (
0x7AD264),引擎通过CDataGroup__collectChildBlocksNamed(0x679430) 收集所有同名子块。即使在一个 DAT 文件中声明了多个[UNITTYPES]块,其包含的规则在索引中均会被汇总合并。
用量统计:[UNITTYPES] 在原版中有 2586 个,MOD 库中有 15783 个;[NOT_UNITTYPES] 在原版中有 1093 个,MOD 库中有 2692 个。原版中共有 1463 份词缀未包含 [UNITTYPES] 块(MOD 库中达 32423 份)—— 该设计有其特定用途,详见下一节。
5. 空白名单的两套相反语义
在引擎不同执行路径中,「白名单列表为空」的处理逻辑截然相反。这是词缀系统开发中最容易产生混淆的设计点:
1. 掉落生成期筛选 (CEffectGroupMgr__rollAffixesForItem 0x7AC6F9)
v14 = (白名单列表大小 == 0); // 标记是否为空表
遍历白名单列表:
若当前单位类型命中任意一项 -> 判定通过
若未命中任何一项 且 !v14:
跳过当前词缀 (剔除出候选集)表现:当白名单为空时,v14 为真,词缀正常进入候选随机池。即:未配置白名单 = 面向所有单位类型开放。
2. 适用性检测与镶嵌判定 (CAffix__appliesToUnitType 0x7A44D0)
v10 = 0;
if (白名单非空) {
若当前单位命中白名单中任意一项 -> v10 = 1;
}
if (!需要校验黑名单) return v10;
if (黑名单非空 && 命中黑名单中任意一项) return 0;
return v10;表现:当白名单为空时,判定循环不会执行,v10 保持为 0,函数直接返回 0(判定不适用)。该逻辑主要服务于 CSocketable__tagEffectsByHostItemType(宝石镶嵌类型过滤)及技能关联效果校验。
NOTE
原版中 1463 份未配置 [UNITTYPES] 的词缀,绝大多数均显式配置了 WEIGHT:0。它们属于专供技能或实体模板直接按名调用的专用词缀,不会参与掉落筛选流程。
与白名单不同,黑名单在两处逻辑中的语义完全一致:命中即被排除,空黑名单则不产生任何拦截。
6. 词缀池的单位类型筛选机制
游戏启动期间,CEffectGroupMgr__buildAffixIndex (0x7ACE70) 会扫描 AFFIXES 目录下的所有 .DAT 文件,并在内存中构建高效的二进制检索索引。 索引系统仅提取以下 6 个核心字段与 2 个类型列表,其余配置项一律不进入索引:
| 索引字段 | 数据来源 | 缺省默认值 | 提取逻辑地址 |
|---|---|---|---|
| 文件相对路径 | 从路径中的 "MEDIA" 关键字截断(路径不含 "MEDIA" 将被跳过) | 无 | 0x7ACFA2 |
NAME | 转大写后的文本 | "NA" | 0x7AD05C |
MIN_SPAWN_RANGE | 整型数值 | 0 | 0x7AD0DC |
MAX_SPAWN_RANGE | 整型数值 | 999999 | 0x7AD145 |
WEIGHT | 整型数值 | 1 | 0x7AD18A |
DIFFICULTIES_ALLOWED | 整型位掩码 | −1 | 0x7AD1E7 |
[UNITTYPES] 集合 | 合并同名块后的 ID 列表 | 空列表 | 0x7AD264 |
[NOT_UNITTYPES] 集合 | 合并同名块后的 ID 列表 | 空列表 | 0x7AD2A3 |
索引在内存中采用并行数组(SoA)而非对象指针数组存储。 当调用 CEffectGroupMgr__rollAffixesForItem (0x7AC620) 生成装备词缀时,引擎按下列条件过滤索引数据:
筛选分两遍进行,中间隔着一层按单位类型缓存的候选列表:
第一遍——构建候选列表(结果按 this[7] 的类型树缓存,同类型复用):
- 白名单校验:命中白名单任一项即通过;若白名单为空亦默认通过(
0x7AC6F9–0x7AC745)。 - 黑名单校验:命中黑名单任一项则直接否决(
0x7AC76D–0x7AC7D0)。 - 权重非零校验:
WEIGHT必须大于 0(0x7AC7E0)——这一步与前两步同遍,不是最后才查。
第二遍——遍历候选列表:
- 等级区间校验:
MIN_SPAWN_RANGE <= 物品等级 && MAX_SPAWN_RANGE > 物品等级(上界为开区间,0x7AC871/0x7AC891)。 - 难度掩码校验:
(1 << 当前游戏难度) & DIFFICULTIES_ALLOWED必须非零 (0x7AC8B5)。 - 第二单位类型的追加白名单校验:调用方传入了第二个 UNITTYPE id(第 6 个实参非 0)时,再过一次白名单(
0x7AC8C1–0x7AC907)。镶嵌等场景会用到。
通过全部筛选的词缀将被送入 WeightedRandom__addEntry(索引, WEIGHT) (0x7AC91F),并通过 WeightedRandom__pick (0x7AC966) 完成加权随机抽取。 抽取命中后,引擎才会按需构造词缀原型 (sub_7A5C10 0x7AC9E9,构造完成后存入内存缓存),随后通过 CAffix__ctorFromRecord (0x7ACA60) 复制出运行时实例并执行定级计算,最后校验 SLOTS_OCCUPY 确定装备预算是否足以容纳。
TIP
在执行第一道筛选前,系统还配备了一层类型级缓存:this[7] 维护了一棵基于单位类型 ID 索引的树结构 (0x7AC660–0x7AC686)。同一单位类型在后续生成词缀时,将直接复用先前计算完成的候选列表。
7. 词缀数值的真实计算公式(纠正纯随机误区)
在玩家社区与 MOD 制作中长期存在一个误区,即认为装备词缀的最终数值是在 MIN 与 MAX 之间进行均匀随机。逆向代码表明事实并非如此。
当 CAffix__setLevel 完成品质百分比计算后,将立即调用 CAffix__applyQualityPercentToEffects (0x7A4180)。该函数遍历词缀下的所有子效果,当同时满足以下全部条件时执行数值重写:
1. MAX 槽数值 (+0xE0) != 0
2. MIN 槽数值 (+0xDC) != MAX 槽数值
3. 该效果绑定的 5 条数值曲线指针 (+0xF0..+0x100) 均为空 (NULL)
4. 全局查找表 g_EffectAffixModifies_239[TYPE] 为真 (true)
5. 效果类型 TYPE 既不是 59,也不是 163满足上述条件后,将执行如下插值计算:
v = MIN + (MAX − MIN) × 品质百分比 / 100
槽0 = 槽1 = v // 调用 CEffect__setSlotAndReroll,将 MIN 和 MAX 两个数值槽同时覆盖为 v这意味着,子效果的 MIN 与 MAX 数值槽在挂载前已被折叠写入为相同的确定值 v。随后在调用 CEffect__rollAndCacheValue 进行数值抽取时,由于上限与下限均为 v,最终数值必然稳定落在 v。 因此,词缀效果的数值本质上是依据词缀等级在 MIN 到 MAX 之间进行的精确线性插值,而非随机波动。
NOTE
全局数组 g_EffectAffixModifies_239 位于 0x28A1510(共 239 字节),直接映射自 EFFECTSLIST.DAT 中各效果类型的 AFFIX_MODIFIES 配置列。若某效果类型在该列标记为 false,则不会执行上述插值逻辑,MIN 与 MAX 将原样保留,后续进入常规的区间随机分支。
子效果的内部等级(LEVEL)亦在该阶段最终确立 (0x7A42AE–0x7A4314):
if (词缀等级 != 0) {
子效果 LEVEL = 规范化后的词缀等级; // 小于 0 或大于 1000 均被重置为 0
} else if (子效果 LEVEL == 0) {
子效果 LEVEL = 1;
}技能所赋予的词缀等级来源于技能配置项 AFFIXLEVEL。当配置为 -1 时,解析器将在 0x6C7412 处自动将其替换为该技能等级对应的 LEVEL_REQUIRED 数值。
8. 存档与序列化机制
8.1 CAffix 不进入存档
逆向调用链分析表明,CAffix 类的虚表地址 0x219A604 在整个游戏代码中仅存在 3 处引用,且均位于 CAffix 自身的构造与析构函数中:
| 引用地址 | 所在函数 |
|---|---|
0x7A508F | 析构函数 sub_7A5070 |
0x7A5C36 | CAffix__ctorFromDatFile 0x7A5C10 (从文件构造原型) |
0x7A5DB7 | CAffix__ctorFromRecord 0x7A5D90 (实例克隆) |
克隆函数 CAffix__ctorFromRecord 的全部 4 处调用方如下:
| 调用地址 | 所在函数 | 业务场景 |
|---|---|---|
0x404266 | createAffixByNameAndLevel 0x404200 | 按名称与等级创建词缀 |
0x6554AD | sub_655230 | 运行时业务逻辑 |
0x7ACA5B | CEffectGroupMgr__rollAffixesForItem 0x7AC620 | 装备随机掉落生成 |
0x7B01FE | sub_7B0170 | 动态挂载处理 |
上述调用链均不在存档读写流程中。 负责装备存档反序列化的函数 CItemSaveState_read (0x5A9920) 与 CItemSaveState_ctor (0x5A96B0) 既不访问 CAffix 虚表,亦不调用任何词缀构造函数。装备存档中实际持久化保存的只有具体的 CEffect 对象(通过 0x5AA321 与 0x5AB984 调用 CEffect__save_read)。
核心结论:CAffix 仅作为生成阶段的一次性包装容器存在。它在完成子效果实例化、计算并锁定品质插值、将各 CEffect 挂载至宿主管理器后,其生命周期便告结束。真正写入存档并随角色/物品持久化保存的是各个具体的 CEffect 对象,而非 CAffix 本身。 类似地,PREFIX 与 SUFFIX 亦不存入存档,装备名称是在界面展示时由 sub_64F770 依据当前 DAT 定义动态拼接合成的。
8.2 修改词缀定义对旧存档的影响
- 已存在于装备上的属性:绝对保持不变 装备上的词缀已转化为独立的
CEffect实例,其 5 个数值槽、最终计算值、等级和时长均已固化存储在存档数据流中,读取时不会重新计算(详见 CEffect 的存档一节)。因此,即使在 MOD 中修改MIN/MAX、改动子效果配置、甚至直接删除词缀.DAT文件,旧存档中已有装备的各项属性均不会发生任何变化。 - 装备显示名称可能会改变 由于
PREFIX/SUFFIX是在游戏运行时根据当前 DAT 文件动态重构的,如果 MOD 调整了词缀的前后缀翻译,旧装备的名称会同步更新;若删除了对应词缀定义,名称中的前后缀将消失(但装备内在属性依旧存在)。 - 新掉落的装备将采用全新定义 词缀池全局索引在每次游戏启动时都会重新构建,后续新掉落或新生成的装备均遵循最新的筛选规则与插值公式。
- 修改
EFFECTSLIST.DAT具有破坏性 存档中记录的效果类型为行号索引而非字符串名称。一旦改动了EFFECTSLIST.DAT的总行数或排列顺序,旧存档中所有物品与技能的效果类型将发生大面积错位。
9. 配置速查表
[AFFIX] 12 个核心配置键 类型 默认值 缺省或错误处理
──────────────────────────────────────────────────────────────────────────
NAME <STRING> "NA" 为 "NA" 时外部按名引用失效
PREFIX <TRANSLATE> 空 装备名称不拼接前缀词
SUFFIX <TRANSLATE> 空 装备名称不拼接后缀词
MIN_SPAWN_RANGE <INTEGER> 0 生成等级下限 (含)
MAX_SPAWN_RANGE <INTEGER> 999999 生成等级上限 (开区间上界,不含)
RANK <INTEGER> 1 无业务读取点 (未定论)
SLOTS_OCCUPY <INTEGER> 1 占用词缀槽位;配 0 表示不消耗预算
DIFFICULTIES_ALLOWED <INTEGER> -1 难度位掩码,-1 为全难度;非难度序号
PLAY_HITS <BOOL> true ← 唯一默认值为 true 的配置键
DURATION <FLOAT> -1.0 -999: 永久且设标志位;>0: 限时秒数;其余: 无时限
WEIGHT <INTEGER> 1 配 0: 不参与随机掉落,但可按名引用
HIGH_QUALITY <BOOL> false 无业务读取点 (未定论)
三大子数据块
──────────────────────────────────────────────────────────────────────────
[EFFECT] 支持声明多个,每个对应构造一个 CEffect 效果实例
[UNITTYPES] 适用单位白名单;忽略块内键名;拼写错误的项静默丢弃
[NOT_UNITTYPES] 禁用单位黑名单;忽略块内键名;拼写错误的项静默丢弃
空白名单的两套相反语义
──────────────────────────────────────────────────────────────────────────
掉落生成筛选 (0x7AC6F9) 白名单为空 = 对所有单位类型开放
适用性检测 (0x7A44D0) 白名单为空 = 一律判定为不适用
品质插值与最终数值
──────────────────────────────────────────────────────────────────────────
品质% = round(min(100, (当前等级 − MIN_SPAWN) / (MAX_SPAWN − MIN_SPAWN) × 100))
MIN == MAX → 品质恒为 100%
MIN > MAX → 品质恒为 0%
子效果数值 = MIN + (MAX − MIN) × 品质% / 100,随后 MIN 与 MAX 槽均被覆写为该值
生效前提: MAX != 0 且 MIN != MAX 且五条曲线均为空 且 AFFIX_MODIFIES 为真 且 TYPE 非 59/16310. 常见排错与排坑指南
Q1:配置了新词缀,但在游戏中完全无法随机掉落?
- 按照第 6 节的五道筛选关卡依次排查:
- 检查是否误配置了
WEIGHT:0(原版约 75% 的词缀为定向引用词缀,权重为 0 不参与随机抽取); - 检查
MAX_SPAWN_RANGE:该参数为开区间上界,若希望 50 级装备能够生成,必须配置为 51 或更高。
- 检查是否误配置了
Q2:添加的词缀 .DAT 文件未被引擎识别?
- 检查文件路径结构:
CEffectGroupMgr__buildAffixIndex(0x7ACFA2) 要求资源路径中必须包含"MEDIA"字段,否则整条记录将被直接跳过。
Q3:配置了 [UNITTYPES] 白名单后,词缀未能正确生效?
- 若单位类型名拼写错误,该项会被底层静默丢弃。如果白名单中的所有项均被丢弃导致列表变为空白,掉落系统仍会正常放行(空白 = 开放),但后续的
CAffix__appliesToUnitType判定却会判定为不适用,导致镶嵌或附着逻辑失效。
Q4:配置的 [NOT_UNITTYPES] 黑名单未能成功拦截单位?
- 同上,黑名单中的单位名称拼写错误会导致该项被静默忽略,从而使排除规则失效,词缀错误地出现在不该适用的单位上。
Q5:词缀子效果的数值始终固定在最小值或最大值?
- 检查
MIN_SPAWN_RANGE与MAX_SPAWN_RANGE:若两者数值相等,品质百分比计算除以 0 会触发保护分支,导致品质恒为 100%(取MAX);若上下限反写(MIN > MAX),品质计算溢出导致品质恒为 0%(取MIN)。
Q6:希望词缀数值在 MIN 到 MAX 之间随机变动,但实际每次数值均完全一致?
- 属于引擎正常设计机制(第 7 节)。词缀系统默认会按照等级对数值进行品质线性插值,并在挂载前将
MIN与MAX槽锁死为同一个计算值。若需实现纯随机,必须使子效果打破插值条件(例如为效果关联曲线,或选用AFFIX_MODIFIES为false的效果类型)。
Q7:显式配置了 PLAY_HITS:false,但受击表现依然无法关闭?
- 检查键名拼写是否误写为
PLAY_HIT(缺少末尾的 S)。键名不匹配将直接回退至默认值true。
Q8:在 [AFFIX] 根块中配置了 TYPE、MIN、DURATION,但未产生任何效果?
[AFFIX]根块仅支持 12 个基础元数据键,所有与效果类型、数值、持续时间相关的属性必须写在[EFFECT]子数据块内部。
Q9:MOD 升级后,旧存档中的装备名称改变了,但装备属性未同步更新?
- 属于正常现象。装备属性在生成时已作为
CEffect快照固化在存档中(第 8 节),而装备名称则是每次在界面展示时根据 DAT 文件实时重构的。