Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 47 additions & 7 deletions docs/data-ingestion.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,13 +330,53 @@ dump.
records what its rows were checked against, because "hand-authored" is not the same as "unverified"
and the two must not be conflated.

**Reference source.** `DBFilesClient/Spell.dbc`, read out of a locally owned 1.12.1 client install
(`Data/dbc.MPQ` + `patch*.MPQ` patch chain, build 5875), through the same `wow-mpq` + `wow_dbc` read
path `importer/src/dbc.rs` already uses. **Revision:** the client's own shipped DBC — there is no
upstream revision to pin beyond the build number, and `wow_dbc` hard-asserts the 5875 record shape, so
a wrong-version client fails the read rather than producing silent drift. The probe was a throwaway
example, run once, deleted after; it printed derived facts (id, name, rank subtext, base points) and
wrote nothing. **No client bytes entered the repository tree**, matching the firewall above.
**Reference source.** The Operator supplies build 5875 client archives. The importer reads
`DBFilesClient/Spell.dbc` in memory through `wow-mpq`. `importer/src/spell_dbc.rs` checks the 173-field,
692-byte layout and reads the columns shared by spell and item enchantment import. Other DBC tables
still use `wow_dbc`. No client bytes enter the repository.

The Spell layout follows the pinned [SpellEntry definition](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Server/DBCStructure.h).
`wow_dbc` 0.3 omits column 21, InterruptFlags. Its named fields stay shifted through the effect arrays.
An extra field restores alignment at the localized name, column 120.

| Consumed fields | Build 5875 columns |
|---|---|
| ID, school, dispel, mechanic | 0, 1, 4, 5 |
| Attributes, AttributesEx1, AttributesEx2, stance mask | 6, 7, 8, 11 |
| Explicit target flags | 13 |
| Required spell focus, cast time, recovery times | 15, 18, 19, 20 |
| Aura interrupt, Proc mask, chance, charges | 22, 24, 25, 26 |
| Maximum level, spell level, duration, power, cost, range, stack count | 27, 29, 30, 31, 32, 36, 39 |
Comment thread
Mathih13 marked this conversation as resolved.
| Reagents and counts, equipment class and masks | 42..49, 50..57, 58..60 |
| Effects, die sides, base dice, dice scaling, level scaling, base points | 61..63, 64..66, 67..69, 70..72, 73..75, 76..78 |
| Mechanics, target A, target B, radii, auras, amplitudes | 79..81, 82..84, 85..87, 88..90, 91..93, 94..96 |
| Multipliers, chain targets, item references, miscellaneous values, triggered spells, combo scaling | 97..99, 100..102, 103..105, 106..108, 109..111, 112..114 |
| English name, family, complete 64-bit family mask | 120, 160, 161..162 |

Spell output retains its existing magnitude convention, base points plus one, and auxiliary-table
range, radius, duration and cast-time resolution. Item enchantments retain base points plus base dice.
The normalized input preserves all three slots and raw aura numbers. Unsupported effects remain
`E_SCRIPTED`, and unsupported item enchantment effects remain Stat Kind 0.

[Target definitions](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Spells/SpellTargetDefines.h)
define 6 as enemy and 21 and 57 as friendly units. The pinned
[target classification](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Spells/SpellTargets.cpp)
marks codes 9, 10, 17, 18, 22, 32, 39, 41 through 44, 46 through 50, 53, 55 and 63 as locations.
When a location in target A has a companion target B, target B decides the Module's unit routing.
Code 45 is a friendly chain unit and code 26 is a lock, so neither uses that rule. Code 0 means no
implicit target. For a `(0, 0)` pair, the importer uses compatible Spell header target flags, then
the source effect's default target. An unsupported effect keeps the scripted fallback. The importer
translates vanilla damage and movement interrupt bits into the Module's compact mask. Gouge's control
and Stealth's speed penalty come from the third source effect. Their former synthetic additions are
removed. The Human Spirit maps directly to its percentage aura. Evocation's existing periodic-mana
correction now addresses source slot 0.
The authored seal marker and Power Word: Shield's linked Weakened Soul rule remain because their
Module behavior is not an equivalent DBC effect.

Every World Import Profile loads the same full Spell catalogue. After this importer change, each
World Shard and Instance Pool needs the normal Spell reimport and curated class-spell pass. Rebuild
Base Snapshots and review Package Deltas that name affected effect indices before replaying them.
Publishing the Module alone does not correct stored rows. No live Realm update is part of this change.

**Not used, deliberately:** cmangos `spell_group` / `spell_group_stack_rules`. That dataset would
answer the rule questions directly, but it is GPL-licensed content this project ships zero rows of, and
Expand Down
2 changes: 1 addition & 1 deletion importer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ wow-m2 = "=0.7.0"
wow-mpq = "=0.7.0"
wow-wmo = "=0.7.0"
wow_dbc = { version = "0.3", features = ["vanilla"] }
# The Spell.dbc importer matches on the typed `AuraMod` enum + reads `Attributes` flags off `SpellRow`.
# The Spell.dbc importer maps numeric aura columns through the typed `AuraMod` enum.
# Those types live in wow_world_base; wow_dbc 0.3 pins it to =0.2.0, so we depend on the SAME version
# (same type instance) to name the variants by hand. Pure parser, ships ZERO Blizzard data.
wow_world_base = { version = "=0.2.0", features = ["vanilla"] }
Expand Down
80 changes: 11 additions & 69 deletions importer/src/item_property.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Derive Random Property catalogues from the Operator's client and ClassicDB dump.

use crate::spell_dbc::{read_spells, Spell};
use crate::{dbc, field, parse_table, push_insert, sql_text};
use anyhow::{bail, Context, Result};
use lyracore_shared::item_property as kind;
Expand All @@ -19,65 +20,6 @@ struct Effect {
school_mask: u32,
}

#[derive(Default)]
struct EquipSpell {
id: u32,
equipped_item_class: u32,
effect: [u32; 3],
effect_die_sides: [i32; 3],
effect_base_dice: [u32; 3],
effect_dice_per_level: [f32; 3],
effect_real_points_per_level: [f32; 3],
effect_base_points: [i32; 3],
effect_aura: [u32; 3],
effect_misc_value: [u32; 3],
effect_points_per_combo: [f32; 3],
}

/// Read build 5875 column positions. wow_dbc 0.3 omits InterruptFlags and shifts the
/// equipment fields and effect arrays, so its named SpellRow fields cannot define this catalogue.
fn read_equip_spells(bytes: &[u8]) -> Result<Vec<EquipSpell>> {
if bytes.len() < 20 || &bytes[..4] != b"WDBC" {
bail!("Spell.dbc has no WDBC header");
}
let header = |at: usize| u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap()) as usize;
let (count, fields, size, strings) = (header(4), header(8), header(12), header(16));
if fields != 173 || size != 692 {
bail!("Spell.dbc requires the build 5875 layout: 173 fields, 692 bytes per row");
}
let records_end = count.checked_mul(size).and_then(|n| n.checked_add(20));
if records_end.and_then(|n| n.checked_add(strings)) != Some(bytes.len()) {
bail!("Spell.dbc record and string lengths do not match its header");
}
let mut ids = BTreeSet::new();
bytes[20..records_end.unwrap()]
.chunks_exact(size)
.map(|row| {
// Every referenced column fits the checked fixed-width record.
let column =
|at: usize| u32::from_le_bytes(row[at * 4..at * 4 + 4].try_into().unwrap());
let array = |at: usize| std::array::from_fn(|i| column(at + i));
let id = column(0);
if !ids.insert(id) {
bail!("duplicate Spell.dbc row {id}");
}
Ok(EquipSpell {
id,
equipped_item_class: column(58),
effect: array(61),
effect_die_sides: array(64).map(|n| n as i32),
effect_base_dice: array(67),
effect_dice_per_level: array(70).map(f32::from_bits),
effect_real_points_per_level: array(73).map(f32::from_bits),
effect_base_points: array(76).map(|n| n as i32),
effect_aura: array(91),
effect_misc_value: array(106),
effect_points_per_combo: array(112).map(f32::from_bits),
})
})
.collect()
}

fn resistance_kind(school: u32) -> Option<u8> {
match school {
0 => Some(kind::ARMOR),
Expand Down Expand Up @@ -122,7 +64,7 @@ fn aura_kinds(aura: u32, misc: u32) -> Vec<(u8, u32)> {
}
}

fn spell_effects(slot: u8, spell: &EquipSpell) -> Result<Vec<Effect>> {
fn spell_effects(slot: u8, spell: &Spell) -> Result<Vec<Effect>> {
let mut mapped = Vec::new();
for subeffect in 0..3 {
if spell.effect[subeffect] == 0 {
Expand All @@ -134,7 +76,7 @@ fn spell_effects(slot: u8, spell: &EquipSpell) -> Result<Vec<Effect>> {
&& spell.effect_dice_per_level[subeffect] == 0.0
&& spell.effect_points_per_combo[subeffect] == 0.0
&& spell.effect_die_sides[subeffect] <= 1
&& spell.equipped_item_class == u32::MAX;
&& spell.equipment[0] == u32::MAX;
let kinds = if flat {
aura_kinds(aura, spell.effect_misc_value[subeffect])
} else {
Expand Down Expand Up @@ -174,14 +116,14 @@ fn spell_effects(slot: u8, spell: &EquipSpell) -> Result<Vec<Effect>> {
pub(crate) fn catalogue_sql(chain: &mut PatchChain) -> Result<(Vec<String>, BTreeSet<u32>, u64)> {
let properties: ItemRandomProperties = dbc::read_table(chain)?;
let enchantments: SpellItemEnchantment = dbc::read_table(chain)?;
let spells = read_equip_spells(&chain.read_file("DBFilesClient\\Spell.dbc")?)?;
let spells = read_spells(&chain.read_file("DBFilesClient\\Spell.dbc")?)?;
catalogue_rows_sql(&properties, &enchantments, &spells)
}

fn catalogue_rows_sql(
properties: &ItemRandomProperties,
enchantments: &SpellItemEnchantment,
spells: &[EquipSpell],
spells: &[Spell],
) -> Result<(Vec<String>, BTreeSet<u32>, u64)> {
for (name, count) in [
("ItemRandomProperties.dbc", properties.rows().len()),
Expand Down Expand Up @@ -471,7 +413,7 @@ mod tests {
let at = 20 + column * 4;
bytes[at..at + 4].copy_from_slice(&value.to_le_bytes());
}
let spells = read_equip_spells(&bytes).unwrap();
let spells = read_spells(&bytes).unwrap();
let effects = spell_effects(0, &spells[0]).unwrap();
assert_eq!(
effects,
Expand Down Expand Up @@ -500,7 +442,7 @@ mod tests {
]
);
bytes[12..16].copy_from_slice(&688u32.to_le_bytes());
assert!(read_equip_spells(&bytes).is_err());
assert!(read_spells(&bytes).is_err());
}

// Authored zero records let tests exercise the real DBC parser without client bytes.
Expand All @@ -513,7 +455,7 @@ mod tests {
T::read(&mut std::io::Cursor::new(bytes)).unwrap()
}

fn catalogues() -> (ItemRandomProperties, SpellItemEnchantment, Vec<EquipSpell>) {
fn catalogues() -> (ItemRandomProperties, SpellItemEnchantment, Vec<Spell>) {
let mut properties: ItemRandomProperties = synthetic_table(16);
properties.rows[0].id.id = 509_0001;
properties.rows[0].spell_item_enchantment = [509_0002, 0, 0, 0, 0];
Expand All @@ -523,7 +465,7 @@ mod tests {
enchantments.rows[0].enchantment_type = [5, 5, 77];
enchantments.rows[0].effect_arg = [7, 4, 0];
enchantments.rows[0].effect_points_min = [7, 9, 11];
(properties, enchantments, vec![EquipSpell::default()])
(properties, enchantments, vec![Spell::default()])
}

#[test]
Expand Down Expand Up @@ -593,10 +535,10 @@ mod tests {

#[test]
fn equip_spell_expands_stats_and_retains_unmapped_effects() {
let mut spell = EquipSpell::default();
let mut spell = Spell::default();
let spell = &mut spell;
spell.id = 509_0003;
spell.equipped_item_class = u32::MAX;
spell.equipment[0] = u32::MAX;
spell.effect = [6, 6, 2];
spell.effect_aura[0] = 29;
spell.effect_base_dice = [1; 3];
Expand Down
34 changes: 34 additions & 0 deletions importer/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,19 @@ mod nav;
mod pack_client;
mod package_delta;
mod spell;
mod spell_dbc;
mod spell_snapshot;
mod talent;
mod terrain;
mod ui_transform;
mod vmap;
mod world_import_scope;

#[cfg(test)]
// The shared Wasm builder requires this package directly beneath the workspace root.
#[path = "../../module/tests/support/mod.rs"]
mod standalone_support;

use std::collections::HashMap;
use std::io::Read;
use std::process::Command;
Expand Down Expand Up @@ -7862,6 +7868,34 @@ mod tests {
}
}

#[test]
#[ignore = "requires pinned LYRACORE_CLASSIC_DB_SQL and build 5875 LYRACORE_TEST_DBC"]
fn pinned_items_plan_counts_templates_and_property_catalogues() {
let path = std::env::var("LYRACORE_CLASSIC_DB_SQL").unwrap();
let dbc = std::env::var("LYRACORE_TEST_DBC").unwrap();
let dump = read_dump(&path).unwrap();
let args = parse_args_from([
"--dump",
path.as_str(),
"--dbc",
dbc.as_str(),
"--world-profile",
"alliance-eastern",
"--family",
"items",
])
.unwrap();
let plan = build_dump_plan(&dump, &args, &None, &None).unwrap();
let (templates, properties, enchantments, pool_members) = (17_718, 2_012, 1_685, 27_892);
assert_eq!(
plan.stamps,
vec![(
"items",
templates + properties + enchantments + pool_members
)]
);
}

#[test]
#[ignore = "requires LYRACORE_CLASSIC_DB_SQL pointing at the pinned decompressed SQL or gzip"]
fn pinned_alliance_single_plan_is_apply_ready_with_relay_static_dependencies() {
Expand Down
Loading