Files
benilla-octo/crates/benilla-formats/src/quest_info.rs
T
SamandClaude Fable 5.1 766395116c a disarmed fighter punches, and the quest log tags its elite rows
- 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>
2026-09-04 20:49:33 +07:00

85 lines
3.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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",
)?))
}