diff --git a/crates/benilla-formats/src/spells/catalog_tests.rs b/crates/benilla-formats/src/spells/catalog_tests.rs index 50961862..36c63a54 100644 --- a/crates/benilla-formats/src/spells/catalog_tests.rs +++ b/crates/benilla-formats/src/spells/catalog_tests.rs @@ -483,14 +483,16 @@ fn real_spell_catalog_reads_usable_walk_columns() { assert!(!cat.get(plain).unwrap().needs_combo_points(), "{plain}"); } - // Auto Shot: bows/guns/crossbows. Slow Fall: one Light Feather. + // Auto Shot. The realm added **thrown** to its weapon mask: `0x5000c` carries subclass bits + // 2 (Bow), 3 (Gun), 16 (Thrown) and 18 (Crossbow), where 1.12's `0x4000c` had the three + // ranged ones and not thrown. So a thrown weapon auto-attacks at range here. let auto_shot = cat.get(75).unwrap(); assert_eq!( ( auto_shot.equipped_item_class, auto_shot.equipped_item_subclass_mask ), - (2, 0x4000c) + (2, 0x5000c) ); assert_eq!(cat.get(130).unwrap().reagents[0], (17056, 1), "Slow Fall"); @@ -699,7 +701,7 @@ fn real_spell_catalog_classifies_combat_initiation() { ); } - // 36 shipped rows carry bit 20, every one an opener, a positional strike or Judgement. + // 38 shipped rows carry bit 20, every one an opener, a positional strike or Judgement. let deferred: Vec<(u32, &str)> = cat .iter() .filter(|(_, d)| d.initiates_auto_attack_at_go()) @@ -707,8 +709,9 @@ fn real_spell_catalog_classifies_combat_initiation() { .collect(); assert_eq!( deferred.len(), - 36, - "5875 ships 36 INITIATE_COMBAT_POST_CAST rows, got {}: {:?}", + 38, + "the realm ships 38 INITIATE_COMBAT_POST_CAST rows (36 in 5875 — the two extra are its \ + own Shiv and Shred ranks), got {}: {:?}", deferred.len(), deferred ); @@ -725,6 +728,9 @@ fn real_spell_catalog_classifies_combat_initiation() { "Judgement", "Pounce", "Ravage", + // The realm gave Shiv the deferred start too, which is of a piece with the rest of + // the class: it is a rogue strike that opens from stealth range. + "Shiv", "Shred", "Test Stab R50", ], @@ -733,7 +739,7 @@ fn real_spell_catalog_classifies_combat_initiation() { } /// `modalNextSpell`, column 38, which `0x6e7447` reads to start Auto Shot after a hunter shot: -/// nonzero on 57 of 22357 rows, 52 of them naming spell 75. +/// nonzero on 77 of the realm's rows, 67 of them naming spell 75 (57 and 52 in 5875). #[test] fn real_spell_catalog_reads_modal_next_spell() { let data = crate::wow_data_or_skip!(); @@ -773,14 +779,14 @@ fn real_spell_catalog_reads_modal_next_spell() { .collect(); assert_eq!( chained.len(), - 57, - "5875 ships 57 rows with a non-zero column 38, got {}", + 77, + "the realm ships 77 rows with a non-zero column 38 (57 in 5875), got {}", chained.len() ); let to_auto_shot = chained.iter().filter(|&&(_, next)| next == 75).count(); assert_eq!( - to_auto_shot, 52, - "52 of the 57 name Auto Shot (the rest: three (TEST) bow shot rows → 59, two Minigun → 23675)" + to_auto_shot, 67, + "67 of the 77 name Auto Shot (52 of 57 in 5875)" ); // Nothing points at a spell that chains onward: one hop, in data. for &(id, next) in &chained { @@ -939,10 +945,12 @@ fn real_prospecting_effect_is_absent_from_5875() { } } // A scan that read nothing would pass too, so pin the row count. + // 28046 rows now, where 5875 had 22357 reaching this scan — the realm's additions. Still + // three slots apiece, which is the shape this is really guarding. assert_eq!( slots, - 22357 * 3, - "every effect slot of every 5875 row was read" + 28046 * 3, + "every effect slot of every shipped row was read" ); } @@ -975,10 +983,13 @@ fn real_item_target_family_and_its_gate_columns() { } } assert_eq!( - item_only, 363, - "Targets == 0x10 — the enchant/poison family" + item_only, 465, + "Targets == 0x10 — the enchant/poison family (363 in 5875)" + ); + assert_eq!( + locked_only, 118, + "Targets == 0x4000 — the OPEN_LOCK family (103 in 5875)" ); - assert_eq!(locked_only, 103, "Targets == 0x4000 — the OPEN_LOCK family"); // The OPEN_LOCK implicit arm: `cast_target_mask` ORs `0x800` for arm 23 and `TF_UNIT` for arm // 25, so a lock word is `0x4000` or `0x4800`, nonzero under both `& 0x4010` and `& 0x4800`, @@ -989,8 +1000,8 @@ fn real_item_target_family_and_its_gate_columns() { } assert_eq!( arms.values().sum::(), - 103, - "every OPEN_LOCK row is counted" + 118, + "every OPEN_LOCK row is counted (103 in 5875)" ); assert_eq!( arms.get(&25), @@ -1110,7 +1121,10 @@ fn real_spell_catalog_cost_columns() { "Life Tap {id}" ); } - // Bloodrage: a percentage-only health cost. + // **Bloodrage is free on this realm.** In 1.12 it was the percentage-only health cost — + // power type `0xFFFF_FFFE` (health) with `mana_cost_pct` 20 — and here all three columns are + // zero, so it takes no health and no mana. Not a column slip: the Life Tap loop just above + // reads `0xFFFF_FFFE` out of the same column and passes. let bloodrage = cat.get(2687).unwrap(); assert_eq!( ( @@ -1118,13 +1132,17 @@ fn real_spell_catalog_cost_columns() { bloodrage.mana_cost, bloodrage.mana_cost_pct ), - (0xFFFF_FFFE, 0, 20) + (0, 0, 0), + "the realm charges nothing for Bloodrage" ); // Health Funnel: flat health plus per second (the `_PER_TIME` line), and channeled. let funnel = cat.get(755).unwrap(); + // The realm retuned it: 3 health up front and 6 per second, where 1.12 took 11 and 5. The + // power type is still health, so the column is read. assert_eq!( (funnel.power_type, funnel.mana_cost, funnel.mana_per_second), - (0xFFFF_FFFE, 11, 5) + (0xFFFF_FFFE, 3, 6), + "the realm's Health Funnel costs" ); assert!(funnel.tooltip_channeled()); // The cast cell's attribute arms: Heroic Strike (next melee; 150 on the wire is "15 Rage"), @@ -1165,7 +1183,10 @@ fn real_spell_catalog_cost_columns() { "manaPerSecondPerLevel on {id}" ); } - assert_eq!(per_level_rows, 72, "the manaCostPerlevel population"); + assert_eq!( + per_level_rows, 77, + "the manaCostPerlevel population (72 in 5875)" + ); let dark_offering = cat.get(7154).unwrap(); assert_eq!( ( @@ -1338,19 +1359,23 @@ fn real_channel_bar_name_law() { } let channeled: Vec<_> = cat.iter().filter(|(_, d)| d.tooltip_channeled()).collect(); - assert_eq!(channeled.len(), 323, "channeled rows in the shipped file"); + assert_eq!( + channeled.len(), + 427, + "channeled rows in the shipped file (323 in 5875)" + ); assert_eq!( channeled .iter() .filter(|(_, d)| d.channel_bar_own_name()) .count(), - 9, - "only nine channeled rows print their own name" + 14, + "fourteen channeled rows print their own name (nine in 5875)" ); assert_eq!( channeled.iter().filter(|(_, d)| d.no_channel_bar()).count(), - 2, - "only the two Blood Siphons suppress the bar" + 6, + "six rows suppress the bar, where 5875 had only the two Blood Siphons" ); } @@ -1378,29 +1403,39 @@ fn real_spell_family_columns_carry_the_modifier_gate() { assert_eq!( families, [ - (0, 18243), - (1, 47), - (3, 563), - (4, 350), - (5, 466), - (6, 493), - (7, 450), - (8, 316), - (9, 393), - (10, 362), - (11, 500), - (13, 174), + (0, 22387), + (1, 54), + (3, 679), + (4, 487), + (5, 604), + (6, 653), + (7, 620), + (8, 435), + (9, 637), + (10, 551), + (11, 729), + (13, 207), + // Two keys outside vmangos's `SpellFamilyNames` set, and both are the realm's own + // authoring rather than a column we misread: 57091 "Flamewave" and 57092 "Flamewave + // Effect" carry 64, and 57089 "Grellfire" carries `0x100000` — a family *name* field + // filled in as if it were a bitmask. All three have zero family flags, so nothing the + // modifier gate walks ever reads them. + (64, 2), + (1_048_576, 1), ] .into_iter() .collect::>(), - "SpellFamilyName histogram — the vmangos SpellFamilyNames set, and only it" + "SpellFamilyName histogram — vmangos's set plus three custom rows with junk names" ); assert_eq!( popcounts.values().skip(1).sum::() - popcounts[&1], - 322, - "rows setting MORE than one family bit — the reader's sum is live" + 466, + "rows setting MORE than one family bit (322 in 5875) — the reader's sum is live" ); - assert_eq!(max_bit, 35, "the highest family bit index in the file"); + // 53, where 5875's highest was 35: the realm's own spells reach further up the pair. Still + // inside the 64-bit pair, so the high dword being live is the thing this guards and it still + // is — a reader that dropped the high half would answer 31. + assert_eq!(max_bit, 53, "the highest family bit index in the file"); // Frostbolt spans one dword, Cleanse both, Cure Poison only the high one. for (id, family, bits) in [ diff --git a/crates/benilla-formats/src/spells/duration.rs b/crates/benilla-formats/src/spells/duration.rs index 98cf9b6d..50bbf1ca 100644 --- a/crates/benilla-formats/src/spells/duration.rs +++ b/crates/benilla-formats/src/spells/duration.rs @@ -137,7 +137,9 @@ mod tests { let short = durations.get(1).expect("row 1"); assert_eq!((short.base_ms, short.per_level_ms), (10_000, 0)); - assert_eq!(durations.len(), 82, "5875 ships 82 SpellDuration rows"); + // 86, where 5875 shipped 82: the realm adds durations for its own spells. 4 fields + // either way, which is what the decode depends on. + assert_eq!(durations.len(), 86, "the realm ships 86 SpellDuration rows"); // Row 427: a negative base with a positive per-level term, permanent under neither test. let scaling = durations.get(427).expect("row 427");