- a disarmed fighter swings bare-fisted: `UNIT_FLAG_DISARMED` was read nowhere but a dev banner, and the reference applies it inside `GetWeapon` on the `visFlag == 0` path, so the swing lands on AttackUnarmed; the RE cross-check (nine workers converging byte-for-byte) corrected the first reading twice — disarm hides exactly ONE weapon, main hand first (`5ec28d je 0x5ec2aa`), so a disarmed dual-wielder keeps swinging its dagger; and the weapon comes OFF the hand: the field-change reflex `0x5ff580` detaches a drawn main-hand weapon when the bit goes up, while a stowed one stays frozen on the back; one pure ladder feeds every unarmed-view site of the 82-site `visFlag` census — parry, the sheath machine, the melee sounds (a disarmed punch does not clang like a sword), Heroic Strike greying out, and the Attack button showing `Spell-Reset` with the weapon still equipped (1863) - a quest-log row reads `(Elite)`, `(Dungeon)`, `(Raid)` or `(PvP)`: `QuestInfo.dbc` was never parsed and `GetQuestLogTitle`'s third return was a literal nil; the table is seven rows read out of `patch-2.MPQ`, SPARSE (1 Elite · 21 Life · 41 PvP · 62 Raid · 81 Dungeon · 82 World Event · 83 Legendary — vanilla's 1 is "Elite", not TBC's "Group"), and the base `dbc.MPQ` copy has only four, so a chain stopping short would lose Dungeon, World Event and Legendary; the row gains the reference's own right-flush tag string and its precedence — a finished quest's FAILED/COMPLETE word OVERWRITES the tag — replacing the " (Complete)" we appended to the title; the RE round confirmed the tag path and found the arity bug: six values ALWAYS (`mov eax,6`), the level a NUMBER 0 even on a header, where we gave one nil; the reference's title shrink stays out: an explicit width is a WRAP width here (1873) - the death thud is silent indoors, and that is the content: BOTH the footstep and the thud lookups key on `TerrainType.SoundID`, and TerrainType 10 "None" has SoundID 0 — the footstep table has a row there for 17 classes and `DeathThudLookups` has none, so the floor that creaks underfoot swallows the body that lands on it; 10 075 of 10 299 WMO materials (97.8%) are TerrainType 10 and 694 of 815 WMO roots have no surface that can thud, every inn and tavern among them, the Kharanos inn included — the code was always right and the prose was not; the RE round also confirmed the strict `depth > 2.0` sense and the size-class column, pinned a sound id of 0 as silence, and placed `0x6005d0` as the loot sparkle, not a second sound; `thudcensus` prints all 121 buildings that do thud rather than a head, because a truncated list cannot answer "does THIS one" (1870) - a questgiver's overhead marker follows its rider on and off the mount: the reference re-runs the attach from the `UNIT_FIELD_MOUNTDISPLAYID` watch handler, and benilla picked the slot once at build, relying on a teardown the reseat rework no longer does (B199), so the seat rode at the WRONG slot; `overhead_slot` is now the one definition of the pick (`0x6074c0`) and the marker rebuilds when it moves (1871); and the chat menu's kit was ours overwriting the reference's — `UIMenu.xml` loads earlier on the chain and our `ChatFrame.xml` carried the same kit under the SAME ten names, faithful enough that deleting it changes nothing; nothing ever opened the chat menu, and now a test does; an event census holds the gap between registered and fired events at exactly eighteen, failing on a surprise AND on a quietly gained producer — features not yet built, none a split pair (1869) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
85 lines
3.7 KiB
Rust
85 lines
3.7 KiB
Rust
//! Quest-log **tag names**: the quest template's `Type` → the parenthesised suffix a quest-log row
|
||
//! carries after its title — `(Elite)`, `(Dungeon)`, `(Raid)`, `(PvP)`.
|
||
//!
|
||
//! `QuestInfo.dbc` on 5875 (read here through the client's own patch chain — it ships in
|
||
//! `patch-2.MPQ`) is **7 records × 10 u32 fields, record size 40**: `ID(0)`, `Name(1)`, the 7
|
||
//! remaining loc-block slots, and the loc flag mask at col 9 — the same `ID … Name … loc` shape
|
||
//! `QuestSort.dbc` has, so it reads through the same [`crate::dbc::load_id_name_table`].
|
||
//!
|
||
//! **The whole table**, in file order — these seven ids are the entire tag vocabulary a 1.12
|
||
//! client can render, and they are SPARSE, so the lookup is by id and never by row index:
|
||
//!
|
||
//! | ID | Name |
|
||
//! |----|------|
|
||
//! | 1 | `Elite` |
|
||
//! | 21 | `Life` |
|
||
//! | 41 | `PvP` |
|
||
//! | 62 | `Raid` |
|
||
//! | 81 | `Dungeon` |
|
||
//! | 82 | `World Event` |
|
||
//! | 83 | `Legendary` |
|
||
//!
|
||
//! Note `1 = "Elite"`, not the later expansions' `"Group"` — a 1.12 group quest reads `(Elite)`.
|
||
//!
|
||
//! The key is [`benilla_protocol::messages::QuestTemplate::quest_type`], the 5th `u32` of
|
||
//! `SMSG_QUEST_QUERY_RESPONSE` (vmangos writes `pQuest->GetType()` there, and its
|
||
//! `quest_template.Type` carries exactly these sparse ids). A quest with `Type == 0` — the great
|
||
//! majority — names no row and takes no tag.
|
||
//!
|
||
//! All of that is **§5-verified against the binary** (wow-re `ui/scratch/questlog-title-tag.md`):
|
||
//! `0x4df930` reads the cached template's `+0x10`, bounds-checks it against the table's maxId
|
||
//! (`0xc0d9d0`) and indexes `0xc0d9cc` with no arithmetic on the id; type `0` passes the bounds
|
||
//! and dies on the zero-filled slot, which reaches Lua as `nil` because `lua_pushstring` tail-jumps
|
||
//! to `lua_pushnil` on NULL. **Read this path through the patch chain and nowhere else:** the base
|
||
//! `dbc.MPQ` copy has only four rows (PvP, Life, Elite, Raid; maxId 62), so a truncated chain
|
||
//! silently loses Dungeon, World Event and Legendary while looking right on everything else — the
|
||
//! fixture test's row-count assert is what stands between us and that.
|
||
//!
|
||
//! **The `Type` field has a second reader in the client, and it is not a tag:** the
|
||
//! `PLAYER_QUEST_LOG` field watcher (`0x5dde6b`/`0x5ddf02`) tests it against `1` and fires
|
||
//! TriggerTutorial 40 when an *Elite* quest is accepted. Nothing else renders this table
|
||
//! (`0xc0d9cc` has one reader image-wide), so the tag belongs to the quest-log row and to nothing
|
||
//! else — but `+0x10` is not "the tag id".
|
||
|
||
use std::collections::HashMap;
|
||
|
||
use anyhow::Result;
|
||
|
||
use crate::chain::Chain;
|
||
use crate::dbc::load_id_name_table;
|
||
|
||
/// The resolved `Type → tag name` lookup ([`load_quest_tag_names`]).
|
||
#[derive(Debug, Default)]
|
||
pub struct QuestTagNames(HashMap<u32, String>);
|
||
|
||
impl QuestTagNames {
|
||
/// The tag for a quest template's `Type`, or `None` for a type the table doesn't name —
|
||
/// which includes `0`, the untagged majority. The caller renders nothing for `None`; it must
|
||
/// not substitute an empty string, because the reference's row Lua branches on the tag's
|
||
/// presence, not on its length.
|
||
pub fn resolve(&self, quest_type: u32) -> Option<&str> {
|
||
self.0.get(&quest_type).map(String::as_str)
|
||
}
|
||
|
||
/// How many tags the table names — the load's own sanity check (5875 ships 7).
|
||
pub fn len(&self) -> usize {
|
||
self.0.len()
|
||
}
|
||
|
||
/// Whether the table named nothing at all.
|
||
pub fn is_empty(&self) -> bool {
|
||
self.0.is_empty()
|
||
}
|
||
}
|
||
|
||
/// Load the tag names through the patch chain.
|
||
pub fn load_quest_tag_names(chain: &mut Chain) -> Result<QuestTagNames> {
|
||
Ok(QuestTagNames(load_id_name_table(
|
||
chain,
|
||
"DBFilesClient\\QuestInfo.dbc",
|
||
1,
|
||
10,
|
||
"QuestInfo",
|
||
)?))
|
||
}
|