Hello World

吞风吻雨葬落日 欺山赶海踏雪径

0%

WOW 3.3.5a 物品附魔 SpellItemEnchantment.dbc 解析

接着上次 DBC 文件格式那篇,这次钻进一张具体又特别实用的表:SpellItemEnchantment.dbc。魔兽里凡是叫得上名字的「物品附魔」——磨刀石、武器油、毒药、萨满武器附魔、工程附魔、珠宝宝石属性、附魔专业配方、装备自带的随机词条——全在这张表里。改私服加附魔、写随机附魔脚本、排查宝石激活条件,最后都得回到这张表读记录。

这张表管什么

SpellItemEnchantment.dbc所有物品附魔的唯一权威定义表。一条记录 = 一种附魔效果,2656 条记录覆盖了整个 3.3.5a 的附魔体系。

它被三处引用:

  1. 物品模板 item_template 的随机属性 RandomProperty / RandomSuffix,以及装备自带的 enchantedLocation,都通过附魔 ID 指向这里。
  2. 法术效果 SPELL_EFFECT_ENCHANT_ITEM / SPELL_EFFECT_ENCHANT_ITEM_PRISMATIC,附魔专业配方卷轴施法时按 ID 施加附魔。
  3. 服务端运行时 sSpellItemEnchantmentStore.LookupEntry(ID),装备穿上/脱下时查这张表算属性。

所以理解附魔 = 理解这张表的 38 个字段怎么读。

文件头与整体结构

文件格式就是标准的 WDBC(上一篇讲过),三段式:

1
2
3
4
5
6
7
+----------------------+
| Header (20 bytes) |
+----------------------+
| Records block | 2656 × 152 bytes
+----------------------+
| String block | 99756 bytes
+----------------------+

本表的关键头部数值:

字段 说明
magic WDBC 校验用
RecordCount 2656 记录总数
FieldCount 38 每条记录 38 个字段
RecordSize 152 = 38 × 4,纯 uint32 表
StringBlockSize 99756 16 种语言的名称都在这里

RecordSize == FieldCount × 4,说明这张表没有 1 字节字段,每列都是 4 字节,解析器可以放心按 uint32 切。

读头沿用老套路:

1
2
3
4
5
6
7
import struct
from pathlib import Path

data = Path("SpellItemEnchantment.dbc").read_bytes()
magic, n, fields, rec_size, str_size = struct.unpack_from("<4sIIII", data, 0)
assert magic == b"WDBC"
# 2656 38 152 99756

38 字段总览

按记录内的下标(uint32 序号)排:

下标 CSV 列名 类型 说明
0 ID uint32 附魔 ID(主键)
1 Charges uint32 使用次数(0 = 永久/无限)
2-4 Effect_1/_2/_3 uint32 3 个效果的类型
5-7 EffectPointsMin_1/_2/_3 uint32 3 个效果的最小数值
8-10 EffectPointsMax_1/_2/_3 uint32 3 个效果的最大数值(服务端不加载)
11-13 EffectArg_1/_2/_3 uint32 3 个效果的参数
14-29 Name_Lang_xx string×16 16 个语言的名称偏移
30 Name_Lang_Mask uint32 名称语言掩码(服务端不加载)
31 ItemVisual uint32 物品视觉效果 ID
32 Flags uint32 附魔标志位
33 Src_ItemID uint32 源物品 ID(宝石附魔用)
34 Condition_Id uint32 附魔生效条件
35 RequiredSkillID uint32 所需专业/技能
36 RequiredSkillRank uint32 所需专业技能等级
37 MinLevel uint32 最低角色等级

一条记录里真正承载效果语义的只有 4 组Effect / EffectPointsMin / EffectArg(各 3 个槽),加上一个 Charges。其余字段要么是显示/本地化,要么是使用门槛。把这 4 组读懂,附魔就懂了八成。

服务端怎么读它:format 串

AzerothCore 用 DBCfmt.h 里的 format 字符串描述每列怎么读:

1
char constexpr SpellItemEnchantmentfmt[] = "niiiiiiixxxiiissssssssssssssssxiiiiiii";

逐位拆:

字符 对应字段 含义
0 n ID 主键,建索引
1 i charges Charges
2-4 iii type[3] Effect 类型
5-7 iii amount[3] EffectPointsMin
8-10 xxx 跳过 EffectPointsMax
11-13 iii spellid[3] EffectArg
14-29 s×16 description[16] 名称(偏移→char*
30 x 跳过 Name_Lang_Mask
31 i aura_id ItemVisual
32 i slot Flags
33 i GemID Src_ItemID
34 i EnchantmentCondition Condition_Id
35 i requiredSkill RequiredSkillID
36 i requiredSkillValue RequiredSkillRank
37 i requiredLevel MinLevel

对应 DBCStructure.h 里的结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define MAX_SPELL_ITEM_ENCHANTMENT_EFFECTS 3

struct SpellItemEnchantmentEntry
{
uint32 ID; // 0
uint32 charges; // 1
uint32 type[MAX_SPELL_ITEM_ENCHANTMENT_EFFECTS]; // 2-4
uint32 amount[MAX_SPELL_ITEM_ENCHANTMENT_EFFECTS]; // 5-7
//uint32 amount2[...] // 8-10 (未加载)
uint32 spellid[MAX_SPELL_ITEM_ENCHANTMENT_EFFECTS]; // 11-13
char const* description[16]; // 14-29
//uint32 descriptionFlags; // 30 (未加载)
uint32 aura_id; // 31
uint32 slot; // 32
uint32 GemID; // 33
uint32 EnchantmentCondition; // 34
uint32 requiredSkill; // 35
uint32 requiredSkillValue; // 36
uint32 requiredLevel; // 37
};

两个被注释掉的字段是关键坑(见后文)。

字段逐一详解

总览表只给了字段名,真正改附魔、排查问题时要的是每个字段的含义、用途、取值、示例。下面按字段下标逐个拆。

字段 0:ID —— 附魔主键

全表唯一标识符,业务主键。几乎所有附魔逻辑的入口都靠它:

  • 物品模板 item_template 的随机属性 RandomProperty / RandomSuffix、装备自带 enchantedLocation 通过它引用附魔。
  • 法术效果 SPELL_EFFECT_ENCHANT_ITEM / SPELL_EFFECT_ENCHANT_ITEM_PRISMATIC 按它施加附魔。
  • 服务端 sSpellItemEnchantmentStore.LookupEntry(ID) 全局查询。

示例:1 = Rockbiter 3(石化武器 3 级)、7 = Deadly Poison(致命毒药)、2686 = +8 Strength(+8 力量宝石)。ID 不连续,索引表大小通常是 max(id)+1,不是行号。

字段 1:Charges —— 使用次数

「使用型」附魔效果的可触发次数:

  • 0 = 永久 / 被动 / 无限(绝大多数附魔)。
  • >0 = 触发 N 次后消失(工程附魔、毒药等消耗型)。

示例:1003 Venomhide Poison Charges=15(击中 15 次后消失)。仅对 COMBAT_SPELL / USE_SPELL 类型有意义,属性类附魔恒为 0。

字段 2-4:Effect_1 / _2 / _3 —— 效果类型

3 个独立效果槽的类型,决定同槽 EffectPointsMinEffectArg 的语义。完整 9 种类型枚举与 2656 条记录的分布见后文「Effect 类型」一节。

这里只要记住:一条附魔可以同时挂多种效果,各占一个槽。比如一件装备可以同时「+8 力量」+「击中概率触发暗影箭」,就是 Effect_1=STAT、Effect_2=COMBAT_SPELL 各填一槽。

字段 5-7:EffectPointsMin_1 / _2 / _3 —— 效果数值

对应效果的数值(最小值),语义随 Effect 类型变:

Effect 类型 PointsMin 含义
STAT 属性加成点数(+8 力量 → 8)
DAMAGE 武器伤害增加量(+3 Damage → 3)
RESISTANCE 抗性点数(+8 护甲 → 8)
COMBAT_SPELL 通常 0(由 SpellID 决定)
EQUIP_SPELL 通常 0(法术自身决定)
TOTEM 每秒伤害基数(实际值 = amount × 武器速度)

可为负数:如 27 Sundered 破甲 PointsMin = -10(减少护甲)。DBC 里存的是 uint32,服务端按 int32 解释,所以自己解析时要用 <i 而不是 <I,否则破甲类附魔会读成一个巨大的正数。

字段 8-10:EffectPointsMax_1 / _2 / _3 —— 效果最大值(服务端不加载)

理论上用于范围随机附魔(min~max),但 AzerothCore 完全不读这三个字段:format 串里是 xxx,结构体里对应成员 amount2 被整段注释掉。服务端实际数值恒等于 EffectPointsMin。

也就是说:想靠改 DBC 实现「随机范围数值附魔」(比如 +5~+10 力量),在 3.3.5 服务端不会生效,得自己写代码支持。这个字段是纯客户端/理论保留。

字段 11-13:EffectArg_1 / _2 / _3 —— 效果参数

效果的具体指向,是整张表最容易踩坑的字段:语义随 Effect 类型变。STAT 时是 ItemModType,RESISTANCE 时是抗性学校,COMBAT_SPELL / EQUIP_SPELL / USE_SPELL 时才是 SpellID。完整对照表与 ItemModType 取值见后文「EffectArg 的多义性」一节。

源码成员名叫 spellid,但只有法术类 Effect 它才是 SpellID——这个名字本身就是个坑。

字段 14-29:Name_Lang_xx —— 16 个语言名称

附魔的显示名称,按 16 个语言槽位存储。记录里存的是字符串块偏移(uint32),0 表示该语言未提供名称,客户端会回退到 enUS。

下标 语言 下标 语言
14 enUS 22 zhTW
15 enGB 23 esES
16 koKR 24 esMX
17 frFR 25 ruRU
18 deDE 26 ptPT
19 enCN 27 ptBR
20 zhCN 28 itIT
21 enTW 29 Unk(保留)

中文端特殊处理(重要):国服 3.3.5 汉化端常把中文字符串实际写进 deDE 槽位(下标 18)而非标准 zhCN(下标 20)。做中英 DBC 合并时(如 merge_spellitemenchantment_dbc.py),脚本只把英文 enUS 名称追加到字符串块末尾、更新 enUS 槽位(下标 14),保留 deDE 槽位的中文字符串不动。处理本地化前务必先确认中文到底落在哪个槽,否则会读到空名或英文名。

字段 30:Name_Lang_Mask —— 名称语言掩码(服务端不加载)

位掩码,标记哪些语言槽位有效(非空)。本表几乎所有记录都是 16712190服务端不读取(format 里是 x),纯客户端字段,用于决定某语言未提供时回退到哪种语言。改它对服务端逻辑毫无影响。

字段 31:ItemVisual —— 物品视觉效果

CSV 列名 ItemVisual,源码成员 aura_id。指向 ItemVisual.dbc 的 ID,决定附魔后在角色身上的视觉特效(火焰、冰霜、闪电光晕)。

服务端只在构建物品更新数据包 SMSG_UPDATE_OBJECT 时透传给客户端,自身不解释其值:

1
*data << uint32(enchant ? enchant->aura_id : 0);

所以改这个字段只影响外观光效,不影响任何数值。示例:Rockbiter=61、Flametongue 3=32(火焰光效)、Reinforced=0(无特效)。

字段 32:Flags —— 附魔标志位

CSV 列名 Flags,源码成员 slot(历史遗留命名,实际语义是 flags 位掩码)。位定义(Item.h):

1
2
3
4
5
6
7
enum EnchantmentSlotMask
{
ENCHANTMENT_CAN_SOULBOUND = 0x01, // 附魔后物品会绑定
ENCHANTMENT_UNK1 = 0x02,
ENCHANTMENT_UNK2 = 0x04,
ENCHANTMENT_UNK3 = 0x08
};

服务端检查(Item.cpp):

1
2
if (enchantEntry->slot & ENCHANTMENT_CAN_SOULBOUND)
return true; // 该附魔会使物品变为灵魂绑定

常见取值:0 = 普通附魔无标志;1 = 施加后灵魂绑定(多数消耗型附魔:磨刀石、武器油、毒药)。字段名 slot 极易和装备上的附魔槽位枚举 EnchantmentSlot 混淆,看注释别看名字

字段 33:Src_ItemID —— 源物品 ID

CSV 列名 Src_ItemID,源码成员 GemID。对宝石附魔(珠宝加工镶嵌)指向宝石的 item_template.entry,服务端用它数「装备上有几颗同类宝石」来判断 meta 宝石激活条件:

1
2
3
4
5
uint32 gemid = enchantEntry->GemID;
if (gemid) {
ItemTemplate const* gemProto = sObjectMgr->GetItemTemplate(gemid);
// 取宝石颜色,判断 meta 宝石激活条件
}

非宝石附魔此字段为 0。示例:

ID 名称 Src_ItemID 含义
2686 +8 Strength 23233 宝石物品 23233 的属性
2687 +8 Agility 23234 宝石物品 23234
2688 +8 Stamina 23235 宝石物品 23235

字段 34:Condition_Id —— 附魔条件

指向 SpellItemEnchantmentCondition.dbc 的 ID,定义该附魔生效所需的复杂条件(如「装备至少 4 颗蓝色宝石」)。条件表支持 5 组比较:颜色、比较运算符、比较颜色、数值、逻辑连接符。0 = 无条件,附魔总是生效。

ID 名称 Condition_Id
2689 +8 mana every 5 sec. 28
2704 +12 Strength if 4 blue gems equipped 3
2827 +14 Crit and 1% Spell Reflect 64
2829 +24 AP and Minor Run Speed Increase 63

meta 宝石「为什么没激活」类问题,最终都要回到这个条件表查。

字段 35:RequiredSkillID —— 所需专业

源码成员 requiredSkill,指向 SkillLine.dbc 的专业技能 ID。服务端校验专业门槛(Item.cpp):

1
2
3
if (enchantEntry->requiredSkill &&
player->GetSkillValue(enchantEntry->requiredSkill) < enchantEntry->requiredSkillValue)
return false; // 专业技能等级不足,附魔不生效

常见值:0 = 无专业要求(普通附魔、宝石属性);164 = 锻造;333 = 附魔;755 = 珠宝加工。高级珠宝专属宝石(如 BoP 橙色宝石)会限 RequiredSkillID=755

字段 36:RequiredSkillRank —— 所需专业等级

源码成员 requiredSkillValue,与 RequiredSkillID 配合,指定专业的等级下限。取值 1~450(WLK 上限 450)。示例:RequiredSkillID=755, RequiredSkillRank=350 → 需珠宝加工 350 才生效。

字段 37:MinLevel —— 最低角色等级

源码成员 requiredLevel,使用此附魔的物品要求的最低角色等级。附魔可能抬高物品需求等级(Item.cpp):

1
2
if (enchantEntry->requiredLevel > level)
level = enchantEntry->requiredLevel; // 附魔越高,装备门槛越高

多数记录为 0(无等级加成),高级配方(TBC/WLK 顶级)可能设为 60/70/80。这也是为什么同一件装备打了高级附魔后,小号反而戴不上去。


Effect 类型:附魔效果的分发器

3 个 Effect 槽位的取值来自 DBCEnums.hItemEnchantmentType 枚举,它决定 EffectPointsMinEffectArg 怎么解释

1
2
3
4
5
6
7
8
9
10
11
12
enum ItemEnchantmentType
{
ITEM_ENCHANTMENT_TYPE_NONE = 0, // 无效果
ITEM_ENCHANTMENT_TYPE_COMBAT_SPELL = 1, // 近战击中时概率施放法术
ITEM_ENCHANTMENT_TYPE_DAMAGE = 2, // 增加武器伤害(磨刀石 +X Damage)
ITEM_ENCHANTMENT_TYPE_EQUIP_SPELL = 3, // 装备时施放法术(光环 Buff)
ITEM_ENCHANTMENT_TYPE_RESISTANCE = 4, // 增加某系抗性
ITEM_ENCHANTMENT_TYPE_STAT = 5, // 增加属性(力量/敏捷/爆击等级…)
ITEM_ENCHANTMENT_TYPE_TOTEM = 6, // 萨满图腾增强(石化武器)
ITEM_ENCHANTMENT_TYPE_USE_SPELL = 7, // 右键使用时施放法术(主动)
ITEM_ENCHANTMENT_TYPE_PRISMATIC_SOCKET = 8 // 添加一个棱彩色孔(无色孔)
};

2656 条记录里 Effect_1 的分布,能直观看出这张表的主体是什么:

1
2
3
4
5
6
7
8
5  STAT           1566 条  ← 绝对主体,属性加成类(含绝大多数宝石)
3 EQUIP_SPELL 582 条 装备光环
4 RESISTANCE 315 条 抗性
1 COMBAT_SPELL 93 条 击中触发(毒药、武器油)
2 DAMAGE 39 条 武器伤害(磨刀石)
6 TOTEM 36 条 萨满武器附魔
7 USE_SPELL 少量 主动使用
8 PRISMATIC_SOCKET 少量 棱彩孔

属性加成类(STAT)占了近 60%,因为珠宝加工每一颗宝石的属性都是一条独立记录。

EffectArg 的多义性(最关键的坑)

EffectArg(结构体里叫 spellid)名字像法术 ID,但它的语义随 Effect 类型变。脱离 Effect 单看 EffectArg 一定会读错:

Effect 类型 EffectArg 含义 引用
COMBAT_SPELL(1) 触发法术的 SpellID Spell.dbc
EQUIP_SPELL(3) 装备时施加的光环 SpellID Spell.dbc
USE_SPELL(7) 右键使用时施放的 SpellID Spell.dbc
RESISTANCE(4) 抗性学校索引(0=物理护甲…) SpellSchools 枚举
STAT(5) 属性类型(ItemModType) ItemModType 枚举
DAMAGE(2) 通常为 0
TOTEM(6) 通常为 0
PRISMATIC_SOCKET(8) 通常为 0

STAT 时 EffectArg = ItemModType

最常见的分支,EffectArg 是属性类型枚举(ItemTemplate.h)。节选实战中最常遇到的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ITEM_MOD_MANA                 = 0,  // 法力值
ITEM_MOD_HEALTH = 1, // 生命值
ITEM_MOD_AGILITY = 3, // 敏捷
ITEM_MOD_STRENGTH = 4, // 力量
ITEM_MOD_INTELLECT = 5, // 智力
ITEM_MOD_SPIRIT = 6, // 精神
ITEM_MOD_STAMINA = 7, // 耐力
ITEM_MOD_DEFENSE_SKILL_RATING = 12, // 防御技能等级
ITEM_MOD_DODGE_RATING = 13, // 躲闪等级
ITEM_MOD_PARRY_RATING = 14, // 招架等级
ITEM_MOD_BLOCK_RATING = 15, // 格挡等级
ITEM_MOD_HIT_RATING = 31, // 综合命中等级
ITEM_MOD_CRIT_RATING = 32, // 综合爆击等级
ITEM_MOD_RESILIENCE_RATING = 35, // 韧性等级
ITEM_MOD_HASTE_RATING = 36, // 急速等级
ITEM_MOD_EXPERTISE_RATING = 37, // 精通等级
ITEM_MOD_ATTACK_POWER = 38, // 攻击强度
ITEM_MOD_RANGED_ATTACK_POWER = 39, // 远程攻击强度
ITEM_MOD_MANA_REGENERATION = 43, // 每5秒法力回复
ITEM_MOD_SPELL_POWER = 45, // 法术强度
ITEM_MOD_HEALTH_REGEN = 46, // 每5秒生命回复
ITEM_MOD_SPELL_PENETRATION = 47, // 法术穿透

所以一条「+8 力量宝石」的记录就是:Effect=5(STAT)EffectPointsMin=8EffectArg=4(ITEM_MOD_STRENGTH)

一组真实记录对照

从 CSV 抽几条典型记录,把 Effect / PointsMin / EffectArg 串起来看:

ID 名称 Effect PointsMin EffectArg 解读
1 Rockbiter 3 6 (TOTEM) 6 0 萨满石化武器,每秒 6 点伤害基数
2 Frostbrand 1 1 (COMBAT_SPELL) 0 8034 击中时施放 Spell 8034
7 Deadly Poison 1 (COMBAT_SPELL) 30 2818 30 PPM 触发 Spell 2818
13 Sharpened (+3 Dmg) 2 (DAMAGE) 3 0 磨刀石 +3 武器伤害
15 Reinforced (+8 Arm) 4 (RESISTANCE) 8 0 +8 物理护甲
24 +5 Mana 5 (STAT) 5 0 +5 法力值(ITEM_MOD_MANA=0)
28 +4 All Resistances 3 (EQUIP_SPELL) 0 7438 装备时施放 Spell 7438(全抗光环)
2686 +8 Strength 5 (STAT) 8 4 +8 力量(宝石属性)
3319 (Prismatic Socket) 8 (PRISMATIC_SOCKET) 0 0 添加一个棱彩孔

注意 EffectPointsMin 可以是负数——比如破甲类附魔存 -10(DBC 里是 uint32,服务端按 int32 解释)。

服务端怎么应用效果

装备穿上时,PlayerStorage.cpp_ApplyItemMods 遍历 3 个效果槽,按 type 分发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for (uint8 s = 0; s < MAX_SPELL_ITEM_ENCHANTMENT_EFFECTS; ++s)
{
uint32 enchant_display_type = pEnchant->type[s];
uint32 enchant_amount = pEnchant->amount[s];
uint32 enchant_spell_id = pEnchant->spellid[s];

switch (enchant_display_type)
{
case ITEM_ENCHANTMENT_TYPE_NONE: break;
case ITEM_ENCHANTMENT_TYPE_COMBAT_SPELL: /* CastItemCombatSpell 处理 */ break;
case ITEM_ENCHANTMENT_TYPE_DAMAGE: /* 更新伤害修正 */ break;
case ITEM_ENCHANTMENT_TYPE_EQUIP_SPELL: /* 施加/移除光环 */ break;
case ITEM_ENCHANTMENT_TYPE_RESISTANCE: /* 修改抗性 */ break;
case ITEM_ENCHANTMENT_TYPE_STAT: /* 按 enchant_spell_id 修改属性 */ break;
case ITEM_ENCHANTMENT_TYPE_TOTEM: /* 萨满石化武器 */ break;
case ITEM_ENCHANTMENT_TYPE_USE_SPELL: /* CastItemUseSpell 处理 */ break;
case ITEM_ENCHANTMENT_TYPE_PRISMATIC_SOCKET:/* 增加棱彩孔,无数值 */ break;
}
}

这就是为什么附魔表用「3 槽 + type 分发」而不是定长字段——一条附魔可以同时给「+8 力量 + 概率触发法术」两种效果,各占一个槽。

常见坑

字段详解里已分散提过,这里收成一张速查表,排查时按图索骥:

  1. 字段 8-10 EffectPointsMax 服务端用 xxx 跳过:改 DBC 想做随机范围附魔不生效,数值恒等于 EffectPointsMin。
  2. 字段 30 Name_Lang_Mask 服务端用 x 跳过:纯客户端字段,决定回退语言。
  3. 中文端 zhCN 常落在 deDE 槽(下标 18):合并本地化前先确认中文槽位,合并脚本常把 LOCALE_ZHCN 硬编码成 4。
  4. 字段 11-13 EffectArg 必须配合 Effect 解释:Effect=5 是 ItemModType,=4 是抗性学校,=1/3/7 才是 SpellID。
  5. 字段 31 ItemVisual(aura_id)是纯视觉:改它只影响光效,不影响数值。
  6. 字段 32 源码名 slot 实为 Flags:跟装备 EnchantmentSlot 枚举是两回事,看注释别看名字。
  7. EffectPointsMin 可为负:破甲类存负数,解析要用 <i 不能用 <I,否则读到巨大正数。

Python 解析示例

接上一篇的 parse_dbc 思路,写一个针对附魔表的解析器,把每条记录解成可读的三效果描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#!/usr/bin/env python3
"""SpellItemEnchantment.dbc → 可读附魔效果列表。"""
import struct
from pathlib import Path

FMT = "niiiiiiixxxiiissssssssssssssssxiiiiiii" # 来自 DBCfmt.h

EFFECT_NAME = {
0: "NONE", 1: "COMBAT_SPELL", 2: "DAMAGE", 3: "EQUIP_SPELL",
4: "RESISTANCE", 5: "STAT", 6: "TOTEM", 7: "USE_SPELL", 8: "PRISMATIC_SOCKET",
}

# Effect=5(STAT) 时 EffectArg 用到的 ItemModType 节选
STAT_NAME = {
0: "法力值", 1: "生命值", 3: "敏捷", 4: "力量", 5: "智力",
6: "精神", 7: "耐力", 12: "防御等级", 13: "躲闪等级", 14: "招架等级",
15: "格挡等级", 31: "命中等级", 32: "爆击等级", 35: "韧性等级",
36: "急速等级", 37: "精通等级", 38: "攻击强度", 45: "法术强度",
}


def field_offsets(fmt: str):
"""按 format 计算每列磁盘偏移:b/X 占 1 字节,其余 4 字节。"""
offs, pos = [], 0
for ch in fmt:
offs.append(pos)
pos += 1 if ch in ("b", "X") else 4
return offs, pos


def parse_enchantments(path: str):
data = Path(path).read_bytes()
magic, n, fields, rec_size, str_size = struct.unpack_from("<4sIIII", data, 0)
assert magic == b"WDBC"
assert len(FMT) == fields

offs, total = field_offsets(FMT)
assert total == rec_size

str_base = 20 + n * rec_size
string_block = data[str_base:str_base + str_size]

def cstring(off):
if off <= 0 or off >= len(string_block):
return ""
end = string_block.find(b"\x00", off)
raw = string_block[off:] if end < 0 else string_block[off:end]
return raw.decode("utf-8", errors="replace")

def u32(rec_base, col):
return struct.unpack_from("<I", data, rec_base + offs[col])[0]

rows = []
for i in range(n):
base = 20 + i * rec_size
eid = u32(base, 0)
effects = []
for s in range(3): # 3 个效果槽
t = u32(base, 2 + s) # Effect 类型
amt = struct.unpack_from("<i", data, base + offs[5 + s])[0] # 按 int32 读,支持负数
arg = u32(base, 11 + s) # EffectArg
if t == 0:
continue
if t == 5: # STAT:把 arg 翻译成属性名
arg_desc = STAT_NAME.get(arg, f"mod{arg}")
effects.append(f"{EFFECT_NAME[t]} {amt:+d} {arg_desc}")
elif t in (1, 3, 7): # 法术类:arg 是 SpellID
effects.append(f"{EFFECT_NAME[t]} amt={amt} spell={arg}")
else:
effects.append(f"{EFFECT_NAME[t]} {amt:+d} arg={arg}")
rows.append({
"id": eid,
"name": cstring(u32(base, 14)), # enUS 槽位
"charges": u32(base, 1),
"effects": effects,
})
return rows


if __name__ == "__main__":
for r in parse_enchantments("SpellItemEnchantment.dbc")[:30]:
print(f"[{r['id']:>5}] {r['name']:<24} charges={r['charges']:<3} | {' / '.join(r['effects'])}")

跑出来大致是:

1
2
3
4
5
6
7
[    1] Rockbiter 3              charges=0   | TOTEM +6 arg=0
[ 2] Frostbrand 1 charges=0 | COMBAT_SPELL amt=0 spell=8034
[ 7] Deadly Poison charges=0 | COMBAT_SPELL amt=30 spell=2818
[ 13] Sharpened (+3 Dmg) charges=0 | DAMAGE +3 arg=0
[ 15] Reinforced (+8 Arm) charges=0 | RESISTANCE +8 arg=0
[ 24] +5 Mana charges=0 | STAT +5 法力值
[ 2686] +8 Strength charges=0 | STAT +8 力量

排查「为什么这颗宝石没生效」「这条随机词条给了什么属性」时,比起翻 SQL 表,直接把 .dbc 拉出来跑一遍这个脚本更快。

和随机附魔脚本的关系

仓库里的 Bin/lua_scripts/RandomEnchants.lua(Eluna 脚本)实现随机附魔系统,脚本里那些附魔 ID——比如 {343, 0, 0, "+8 敏捷", "敏捷"} 里的 343——全部指向这张表的 ID 字段。item:SetEnchantment(enchId, slot) 最终也是回到 sSpellItemEnchantmentStore 查记录。

也就是说:Lua 脚本只负责分配和过滤 ID,效果的真实数值/类型永远由 DBC 里的 Effect / EffectPointsMin / EffectArg 决定。加新附魔词条时,先确认对应 ID 在 DBC 里确实存在且语义正确,否则脚本配了也触发不出效果。

小结

  • 一条附魔 = 3 个效果槽,每槽由 Effect(类型)+ EffectPointsMin(数值)+ EffectArg(参数)三字段决定。
  • EffectArg 是不是 SpellID 取决于 Effect:STAT 时是 ItemModType,RESISTANCE 时是抗性学校,只有 1/3/7 时才是 SpellID。
  • EffectPointsMax(8-10)和 Name_Lang_Mask(30)服务端用 xxx/x 跳过,纯客户端字段。
  • 中文端 zhCN 常实际存在 deDE 槽位,做本地化合并前先确认。
  • 源码字段名 slot 实为 Flags、spellid 实为 EffectArg,都是历史遗留,看注释别看名字。

排查顺序:解析 .dbc 确认 ID/Effect/Arg → 对照 ItemModType 翻译属性 → 对照 Spell.dbc 翻译法术 → 最后才去看运行时日志。