核心定位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

text
[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 CHANCE55true参与品质插值:数值 = 1 + (2−1) × 品质%/100,随物品等级在 1~2 之间线性变化
MANA STEAL50trueSTEAL_MANA不参与插值:该类型登记了默认曲线,曲线指针非空,插值条件不成立,回落至曲线求值分支

NOTE

上表是「同一份词缀里两个子效果走了两条完全不同的数值路径」的真实案例。判定条件的完整列表见第 7 节 —— 五个曲线指针必须全部为 NULL,插值才会执行。

0.2 可直接套用的骨架

最小可用骨架NAME 是唯一真正的必填项,其余均有可用默认值):

text
[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 语法,实际使用时请删除):

text
[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] 子块执行。 词缀引入的额外逻辑主要分为两类:

  1. 生成控制:决定词缀何时能够被随机抽取或赋予(生成等级范围、权重、难度要求、适用单位类型)。
  2. 数值缩放:决定抽取成功后,其子效果的最终数值如何计算(基于品质百分比进行线性插值)。
text
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 (0x404E4A0x404E63) 的真实形态是:

text
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_STAGE0x64BCA9 构造、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)。

偏移量数据类型字段含义与描述写入来源 / 赋值函数默认初始值
+0x00ptr虚函数表指针 (vftable)构造函数0x219A604
+0x04intCRunicCore 基类内部字段0x4059180
+0x08ptr词缀原型指针 (CAffix*)CAffix__copyFrom 0x7A52370
+0x0C/+0x10ptr + cookie来源技能指针 (CSkill*) 与弱引用句柄CAffix__setSourceSkill 0x7A4FE00 / −1
+0x14/+0x18ptr + cookie来源单位指针 (CBaseUnit*) 与弱引用句柄CAffix__setSource 0x7A48400 / −1
+0x1C/+0x20ptr + cookie当前挂载的 CEffectManager 与弱引用句柄CAffix__attachToMgr_applyEffects 0x7A49EB 写入;CAffix__detachFromHost 0x7A48C0 清空0 / −1
+0x24wstring 28B词缀名称 (NAME),经 toUpperW 转大写存储NAME 配置项 0x7A55B9空字符串
+0x40wstring 28B前缀文本 (PREFIX)PREFIX 配置项 0x7A5796空字符串
+0x5Cwstring 28B后缀文本 (SUFFIX)SUFFIX 配置项 0x7A57DC空字符串
+0x78int生成等级下限 (MIN_SPAWN_RANGE)0x7A56920
+0x7Cint生成等级上限 (MAX_SPAWN_RANGE)0x7A56CD999999
+0x80byte品质百分比 (0–100)CAffix__setLevel 0x7A4457100
+0x84int词缀位阶 (RANK)0x7A581F1
+0x88int占用词缀槽位数 (SLOTS_OCCUPY)0x7A585E1
+0x8Cint允许出现的难度位掩码 (DIFFICULTIES_ALLOWED)0x7A5654−1 (全难度)
+0x90int词缀等级 (Affix Level)CAffix__setLevel 0x7A43F2由构造入参指定
+0x94byte是否播放受击反馈 (PLAY_HITS)0x7A56101 (true)
+0x95byte残留/填充字节,恒为 0仅两处构造路径写入 (0x7A5CC00x7A5E1B),无读取点,且克隆时不复制0
+0x98+0xA4vector<CEffect*>子效果列表 (data / size / cap / growth)[EFFECT] 子块解析 0x7A5A24空列表
+0xA8byte原型可克隆标记(原型对象置 1,克隆出的实例被置 0)CAffix__copyFrom 0x7A524F1
+0xACfloat持续时长 (DURATION)0x7A5750−1.0
+0xB0byte持续时长有效标志 (Has Duration)0x7A58D3 / 0x7A596F0
+0xB4int抽取权重 (WEIGHT)0x7A570D1
+0xB8byte高品质标记 (HIGH_QUALITY)0x7A589C0
+0xBC+0xC8vector<int>[UNITTYPES] 白名单列表,存储单位类型 ID0x7A5AE6空列表
+0xCC+0xD8vector<int>[NOT_UNITTYPES] 黑名单列表,存储单位类型 ID0x7A5BB9空列表
+0xDCptr数据包条目指针(非空时优先走 Pak 打包资源流加载)0x7A554A 读取0

NOTE

+0x95残留/填充字节,恒为 0,引擎中没有任何代码读取它。三条证据:

  1. 只有两个写入点,写的都是 0。 两处都是 mov [esi+95h], bl,而 ebx 在此之前已被清零 (xor ebx, ebx 分别位于 0x7A5C340x7A5DB5,到写入点之间没有重新赋值)。 两处都夹在一串连续的字段初始化里:+0x88+0x8C+0x90+0x94+0x95+0x98+0x9C (末尾的 +0x98/+0x9C 是效果向量的 data/size,同样靠这个 ebx 写 0)。
  2. 全范围无读取点。0x4010000xA00000 内检索 [reg+95h] 共 20 处命中, 只有上述 2 处属于 CAffix,且均为写入;其余分属 sub_6106A0 簇与 0x7B55000x7B55F00x7B5F900x7B60B0(读 STATDEFAULTVALUESTACKABLE 的另一个子系统)。
  3. 克隆时被跳过。 CAffix__copyFrom (0x7A5200) 逐一复制了其余四个字节字段—— +0x940x7A528B)、+0xA80x7A524F)、+0xB00x7A52B4)、+0xB80x7A52FB)—— 唯独没有 +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 参考手册 中的完全一致,两个类共用同一套基础设施。

cpp
// 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 字节对齐插入的填充,并非引擎使用的字段。对 0x810x830x960x970xA90xAB0xB10xB30xB90xBB 这 14 个偏移在本文档前述扫描范围内做过一次指令级检索,命中数为 0,可以放心按填充处理。


3. 12 个配置键深度解析

[AFFIX] 根数据块仅支持 12 个核心配置键,全部在 CAffix__loadFromDat (0x7A54E0) 函数中按序解析。 引擎的读取顺序为:NAMEPLAY_HITSDIFFICULTIES_ALLOWEDMIN_SPAWN_RANGEMAX_SPAWN_RANGEWEIGHTDURATIONPREFIXSUFFIXRANKSLOTS_OCCUPYHIGH_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 / 0x64FC0D0x64FAA7 / 0x64FC84直接重新从 DAT 文件读取 PREFIXSUFFIX,并不依赖挂载后的 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%

这两个键在引擎内部有两个完全独立的用途,切勿混淆:

  1. 生成筛选条件 (0x7AC8710x7AC891): 判定公式为 MIN_SPAWN_RANGE <= 物品等级 && MAX_SPAWN_RANGE > 物品等级注意:MAX_SPAWN_RANGE 采用开区间(不包含上界)判定。若配置为 50,表示物品等级为 49 时可以生成,但等级达到 50 时将无法随机命中。
  2. 品质线性插值计算 (CAffix__setLevel 0x7A4340): 先将词缀实际等级限制在 [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
显式配置为 00x7AC7E0 处的判定条件 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)。只有「预算不足且池中还有其他候选」才走析构分支 (0x7ACA850x7ACA9D) 并继续抽取
缺省后果默认占用 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。 原版中绝大多数词缀均填写了 -10,实则与缺省无异。若需声明「受时限系统管理但永久生效」,必须显式填写 -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 中,子块解析循环 (0x7A59390x7A5BD6) 根据数据块名称的哈希值进行分发,仅识别以下三个标准块名。拼写错误的子块名称将无法匹配任何分支,整块内容将被引擎静默跳过。

4.1 [EFFECT]

每个 [EFFECT] 子块均会调用 alloc(304) 动态分配一个 CEffect 对象,传入 CEffect__ctorFromDat (call0x7A59F4) 完成反序列化,并追加至偏移 +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] 的解析逻辑在结构上完全相同 (0x7A5A820x7A5AF00x7A5B550x7A5BC3),核心执行流如下:

text
遍历该数据节点下的全部配置值
  id = unittypeNameToId(配置文本字符串)
  if (id != -1) {
      push_back(id 存入目标向量列表)
  }
  // 若 id == -1 则直接静默丢弃,不输出报错

必须明确以下三项核心规则:

  1. 内存中存储的是类型 ID 而非名称字符串: 当单位类型名拼写错误时,unittypeNameToId (0x643EB0) 返回 −1,该配置项将被静默丢弃 (0x7A5AC3 / 0x7A5B96)。这会导致不对称的异常表现:
    • 白名单拼写错误:导致有效适用范围变窄;
    • 黑名单拼写错误:导致排除规则失效,词缀被错误赋予本应被禁用的单位。
  2. 块内键名不参与语义判定: 解析器直接遍历该节点的值列表集合(*(node + 32)*(node + 36)),完全不校验每行的键名(Key)。因此,<STRING>UNITTYPE:PLAYER<STRING>UNITTYPES:PLAYER 具有完全相同的解析效果。
  3. 同名子块会自动合并: 在全局索引构建阶段 (0x7AD264),引擎通过 CDataGroup__collectChildBlocksNamed (0x679430) 收集所有同名子块。即使在一个 DAT 文件中声明了多个 [UNITTYPES] 块,其包含的规则在索引中均会被汇总合并。

用量统计:[UNITTYPES] 在原版中有 2586 个,MOD 库中有 15783 个;[NOT_UNITTYPES] 在原版中有 1093 个,MOD 库中有 2692 个。原版中共有 1463 份词缀未包含 [UNITTYPES] 块(MOD 库中达 32423 份)—— 该设计有其特定用途,详见下一节。


5. 空白名单的两套相反语义

在引擎不同执行路径中,「白名单列表为空」的处理逻辑截然相反。这是词缀系统开发中最容易产生混淆的设计点:

1. 掉落生成期筛选 (CEffectGroupMgr__rollAffixesForItem 0x7AC6F9)

text
v14 = (白名单列表大小 == 0);        // 标记是否为空表
遍历白名单列表:
    若当前单位类型命中任意一项 -> 判定通过
若未命中任何一项 且 !v14:
    跳过当前词缀 (剔除出候选集)

表现:当白名单为空时,v14 为真,词缀正常进入候选随机池。即:未配置白名单 = 面向所有单位类型开放

2. 适用性检测与镶嵌判定 (CAffix__appliesToUnitType 0x7A44D0)

text
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整型数值00x7AD0DC
MAX_SPAWN_RANGE整型数值9999990x7AD145
WEIGHT整型数值10x7AD18A
DIFFICULTIES_ALLOWED整型位掩码−10x7AD1E7
[UNITTYPES] 集合合并同名块后的 ID 列表空列表0x7AD264
[NOT_UNITTYPES] 集合合并同名块后的 ID 列表空列表0x7AD2A3

索引在内存中采用并行数组(SoA)而非对象指针数组存储。 当调用 CEffectGroupMgr__rollAffixesForItem (0x7AC620) 生成装备词缀时,引擎按下列条件过滤索引数据:

筛选分两遍进行,中间隔着一层按单位类型缓存的候选列表:

第一遍——构建候选列表(结果按 this[7] 的类型树缓存,同类型复用):

  1. 白名单校验:命中白名单任一项即通过;若白名单为空亦默认通过(0x7AC6F90x7AC745)。
  2. 黑名单校验:命中黑名单任一项则直接否决(0x7AC76D0x7AC7D0)。
  3. 权重非零校验WEIGHT 必须大于 0(0x7AC7E0)——这一步与前两步同遍,不是最后才查。

第二遍——遍历候选列表

  1. 等级区间校验MIN_SPAWN_RANGE <= 物品等级 && MAX_SPAWN_RANGE > 物品等级(上界为开区间,0x7AC871 / 0x7AC891)。
  2. 难度掩码校验(1 << 当前游戏难度) & DIFFICULTIES_ALLOWED 必须非零 (0x7AC8B5)。
  3. 第二单位类型的追加白名单校验:调用方传入了第二个 UNITTYPE id(第 6 个实参非 0)时,再过一次白名单(0x7AC8C10x7AC907)。镶嵌等场景会用到。

通过全部筛选的词缀将被送入 WeightedRandom__addEntry(索引, WEIGHT) (0x7AC91F),并通过 WeightedRandom__pick (0x7AC966) 完成加权随机抽取。 抽取命中后,引擎才会按需构造词缀原型 (sub_7A5C10 0x7AC9E9,构造完成后存入内存缓存),随后通过 CAffix__ctorFromRecord (0x7ACA60) 复制出运行时实例并执行定级计算,最后校验 SLOTS_OCCUPY 确定装备预算是否足以容纳。

TIP

在执行第一道筛选前,系统还配备了一层类型级缓存:this[7] 维护了一棵基于单位类型 ID 索引的树结构 (0x7AC6600x7AC686)。同一单位类型在后续生成词缀时,将直接复用先前计算完成的候选列表。


7. 词缀数值的真实计算公式(纠正纯随机误区)

在玩家社区与 MOD 制作中长期存在一个误区,即认为装备词缀的最终数值是在 MINMAX 之间进行均匀随机。逆向代码表明事实并非如此。

CAffix__setLevel 完成品质百分比计算后,将立即调用 CAffix__applyQualityPercentToEffects (0x7A4180)。该函数遍历词缀下的所有子效果,当同时满足以下全部条件时执行数值重写:

text
1. MAX 槽数值 (+0xE0) != 0
2. MIN 槽数值 (+0xDC) != MAX 槽数值
3. 该效果绑定的 5 条数值曲线指针 (+0xF0..+0x100) 均为空 (NULL)
4. 全局查找表 g_EffectAffixModifies_239[TYPE] 为真 (true)
5. 效果类型 TYPE 既不是 59,也不是 163

满足上述条件后,将执行如下插值计算:

text
v = MIN + (MAX − MIN) × 品质百分比 / 100
槽0 = 槽1 = v    // 调用 CEffect__setSlotAndReroll,将 MIN 和 MAX 两个数值槽同时覆盖为 v

这意味着,子效果的 MINMAX 数值槽在挂载前已被折叠写入为相同的确定值 v。随后在调用 CEffect__rollAndCacheValue 进行数值抽取时,由于上限与下限均为 v,最终数值必然稳定落在 v因此,词缀效果的数值本质上是依据词缀等级在 MINMAX 之间进行的精确线性插值,而非随机波动。

NOTE

全局数组 g_EffectAffixModifies_239 位于 0x28A1510(共 239 字节),直接映射自 EFFECTSLIST.DAT 中各效果类型的 AFFIX_MODIFIES 配置列。若某效果类型在该列标记为 false,则不会执行上述插值逻辑MINMAX 将原样保留,后续进入常规的区间随机分支。

子效果的内部等级(LEVEL)亦在该阶段最终确立 (0x7A42AE0x7A4314):

text
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
0x7A5C36CAffix__ctorFromDatFile 0x7A5C10 (从文件构造原型)
0x7A5DB7CAffix__ctorFromRecord 0x7A5D90 (实例克隆)

克隆函数 CAffix__ctorFromRecord 的全部 4 处调用方如下:

调用地址所在函数业务场景
0x404266createAffixByNameAndLevel 0x404200按名称与等级创建词缀
0x6554ADsub_655230运行时业务逻辑
0x7ACA5BCEffectGroupMgr__rollAffixesForItem 0x7AC620装备随机掉落生成
0x7B01FEsub_7B0170动态挂载处理

上述调用链均不在存档读写流程中。 负责装备存档反序列化的函数 CItemSaveState_read (0x5A9920) 与 CItemSaveState_ctor (0x5A96B0) 既不访问 CAffix 虚表,亦不调用任何词缀构造函数。装备存档中实际持久化保存的只有具体的 CEffect 对象(通过 0x5AA3210x5AB984 调用 CEffect__save_read)。

核心结论CAffix 仅作为生成阶段的一次性包装容器存在。它在完成子效果实例化、计算并锁定品质插值、将各 CEffect 挂载至宿主管理器后,其生命周期便告结束。真正写入存档并随角色/物品持久化保存的是各个具体的 CEffect 对象,而非 CAffix 本身。 类似地,PREFIXSUFFIX 亦不存入存档,装备名称是在界面展示时由 sub_64F770 依据当前 DAT 定义动态拼接合成的。


8.2 修改词缀定义对旧存档的影响

  1. 已存在于装备上的属性:绝对保持不变 装备上的词缀已转化为独立的 CEffect 实例,其 5 个数值槽、最终计算值、等级和时长均已固化存储在存档数据流中,读取时不会重新计算(详见 CEffect 的存档一节)。因此,即使在 MOD 中修改 MIN/MAX、改动子效果配置、甚至直接删除词缀 .DAT 文件,旧存档中已有装备的各项属性均不会发生任何变化
  2. 装备显示名称可能会改变 由于 PREFIX/SUFFIX 是在游戏运行时根据当前 DAT 文件动态重构的,如果 MOD 调整了词缀的前后缀翻译,旧装备的名称会同步更新;若删除了对应词缀定义,名称中的前后缀将消失(但装备内在属性依旧存在)。
  3. 新掉落的装备将采用全新定义 词缀池全局索引在每次游戏启动时都会重新构建,后续新掉落或新生成的装备均遵循最新的筛选规则与插值公式。
  4. 修改 EFFECTSLIST.DAT 具有破坏性 存档中记录的效果类型为行号索引而非字符串名称。一旦改动了 EFFECTSLIST.DAT 的总行数或排列顺序,旧存档中所有物品与技能的效果类型将发生大面积错位。

9. 配置速查表

text
[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/163

10. 常见排错与排坑指南

Q1:配置了新词缀,但在游戏中完全无法随机掉落?

  • 按照第 6 节的五道筛选关卡依次排查:
    1. 检查是否误配置了 WEIGHT:0(原版约 75% 的词缀为定向引用词缀,权重为 0 不参与随机抽取);
    2. 检查 MAX_SPAWN_RANGE:该参数为开区间上界,若希望 50 级装备能够生成,必须配置为 51 或更高。

Q2:添加的词缀 .DAT 文件未被引擎识别?

  • 检查文件路径结构:CEffectGroupMgr__buildAffixIndex (0x7ACFA2) 要求资源路径中必须包含 "MEDIA" 字段,否则整条记录将被直接跳过。

Q3:配置了 [UNITTYPES] 白名单后,词缀未能正确生效?

  • 若单位类型名拼写错误,该项会被底层静默丢弃。如果白名单中的所有项均被丢弃导致列表变为空白,掉落系统仍会正常放行(空白 = 开放),但后续的 CAffix__appliesToUnitType 判定却会判定为不适用,导致镶嵌或附着逻辑失效。

Q4:配置的 [NOT_UNITTYPES] 黑名单未能成功拦截单位?

  • 同上,黑名单中的单位名称拼写错误会导致该项被静默忽略,从而使排除规则失效,词缀错误地出现在不该适用的单位上。

Q5:词缀子效果的数值始终固定在最小值或最大值?

  • 检查 MIN_SPAWN_RANGEMAX_SPAWN_RANGE:若两者数值相等,品质百分比计算除以 0 会触发保护分支,导致品质恒为 100%(取 MAX);若上下限反写(MIN > MAX),品质计算溢出导致品质恒为 0%(取 MIN)。

Q6:希望词缀数值在 MIN 到 MAX 之间随机变动,但实际每次数值均完全一致?

  • 属于引擎正常设计机制(第 7 节)。词缀系统默认会按照等级对数值进行品质线性插值,并在挂载前将 MINMAX 槽锁死为同一个计算值。若需实现纯随机,必须使子效果打破插值条件(例如为效果关联曲线,或选用 AFFIX_MODIFIESfalse 的效果类型)。

Q7:显式配置了 PLAY_HITS:false,但受击表现依然无法关闭?

  • 检查键名拼写是否误写为 PLAY_HIT(缺少末尾的 S)。键名不匹配将直接回退至默认值 true

Q8:在 [AFFIX] 根块中配置了 TYPEMINDURATION,但未产生任何效果?

  • [AFFIX] 根块仅支持 12 个基础元数据键,所有与效果类型、数值、持续时间相关的属性必须写在 [EFFECT] 子数据块内部。

Q9:MOD 升级后,旧存档中的装备名称改变了,但装备属性未同步更新?

  • 属于正常现象。装备属性在生成时已作为 CEffect 快照固化在存档中(第 8 节),而装备名称则是每次在界面展示时根据 DAT 文件实时重构的。