- Area triggers exist now, so portals work: every teleport pad, dungeon entrance and enter-a-place quest volume reports the reference's own way — one exit latch, first containing volume on the map wins, a sphere or a yawed box. Verified live: Rut'theran to Darnassus, the Deadmines worldport, and a level-gated entrance refusing with the server's own "You must be at least level 10 to enter." - GameObjects animate. Every animated object arms its idle the moment it goes live (doors and chests used to flash bind pose for a frame), a missing rest pose is remapped the way the reference remaps it instead of playing nothing — the Ahn'Qiraj gate's root tangle stood frozen for want of a Closed clip — and the state machine covers the verified twenty of thirty-one object types instead of three: fourteen hundred spawns' worth of traps, goobers, books and questgivers now change pose off the wire. - The flickering tent is still. Cloth you can see from both sides is authored as two reversed copies of the same sheet, and the real client backface-culls each copy from its wrong side so exactly one covers any pixel; we drew every building batch two-sided, which left the winner to floating-point noise, flickering while the camera crept. WMO materials now honor their cull flag. - A sweep of long-standing visual bugs: effects respect the far-clip wall — a ferry's deck lanterns were drawing from 7,000 yards away; the night-elf eye glow goes out in first person instead of leaving two orbs burning in mid-air; the goblins' stray white hair tuft is gone (a duplicated table key resolved last-wins where the client scans first-wins); lava and slime finally have their own submerged view — the dense fiery orange and dense green of the real thing, off two fixed lighting rows nothing in the zone data points at; and the far-horizon band overlaps the detailed terrain instead of leaving a strip of sky at lowered view distance. - Two caught at the keyboard: the backpack icon greys instead of vanishing while the escape menu is up, and the world map is a real panel now — it can no longer open over the menu, and ESC closes it first like everything else. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
123 lines
4.5 KiB
Rust
123 lines
4.5 KiB
Rust
//! GameObject display resolution: `displayId` → world model path.
|
|
//!
|
|
//! A GameObject's `GAMEOBJECT_DISPLAYID` indexes **GameObjectDisplayInfo.dbc**, whose `modelName`
|
|
//! column is a direct path — mostly `.mdx`/`.mdl` (M2: chests, mailboxes, doodads) with a few `.wmo`
|
|
//! (large structures). Unlike creatures there's no model-data indirection or skins, so the catalog is
|
|
//! just `displayId → path`; the renderer dispatches the path through [`crate::load_object_model`].
|
|
//!
|
|
//! Layout verified against build 5875: 12 fields (ID@0, modelName@1 string, Sound[10]@2..11).
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use crate::Chain;
|
|
use anyhow::{Context, Result};
|
|
use benilla_dbc::{FieldType, Schema, SchemaField};
|
|
|
|
use crate::dbc::{parse, str_at, u32_at};
|
|
|
|
const GAMEOBJECT_DISPLAY_INFO: &str = "DBFilesClient\\GameObjectDisplayInfo.dbc";
|
|
|
|
/// `displayId → model path` from GameObjectDisplayInfo.dbc.
|
|
pub struct GameObjectCatalog {
|
|
models: HashMap<u32, String>,
|
|
}
|
|
|
|
impl GameObjectCatalog {
|
|
/// The model path for a GameObject display id (`.mdx`/`.mdl`/`.wmo`), or `None`.
|
|
pub fn model_path(&self, display_id: u32) -> Option<&str> {
|
|
self.models.get(&display_id).map(String::as_str)
|
|
}
|
|
|
|
/// Number of display entries (diagnostics).
|
|
pub fn len(&self) -> usize {
|
|
self.models.len()
|
|
}
|
|
|
|
/// Whether the catalog is empty.
|
|
pub fn is_empty(&self) -> bool {
|
|
self.models.is_empty()
|
|
}
|
|
|
|
/// Every `(displayId, model path)` pair, unordered — the corpus-sweep entry point for "which
|
|
/// models can a GameObject ever be, and which display ids reach each one".
|
|
pub fn iter(&self) -> impl Iterator<Item = (u32, &str)> {
|
|
self.models.iter().map(|(&id, p)| (id, p.as_str()))
|
|
}
|
|
}
|
|
|
|
/// GameObjectDisplayInfo.dbc — 12 fields in build 5875: ID, modelName, then 10 sound refs.
|
|
fn schema() -> Schema {
|
|
let mut s = Schema::new("GameObjectDisplayInfo");
|
|
s.add_field(SchemaField::new("ID", FieldType::UInt32));
|
|
s.add_field(SchemaField::new("ModelName", FieldType::String));
|
|
for i in 0..10 {
|
|
s.add_field(SchemaField::new(format!("Sound{i}"), FieldType::UInt32));
|
|
}
|
|
s
|
|
}
|
|
|
|
/// Load GameObjectDisplayInfo.dbc from the patch chain into a [`GameObjectCatalog`].
|
|
pub fn load_gameobject_catalog(chain: &mut Chain) -> Result<GameObjectCatalog> {
|
|
let bytes = chain
|
|
.read_file(GAMEOBJECT_DISPLAY_INFO)
|
|
.with_context(|| format!("reading {GAMEOBJECT_DISPLAY_INFO}"))?;
|
|
let rs = parse(&bytes, schema(), "GameObjectDisplayInfo")?;
|
|
let mut models = HashMap::with_capacity(rs.records().len());
|
|
for r in rs.records() {
|
|
if let (Some(id), Some(path)) = (u32_at(r, 0), str_at(&rs, r, 1)) {
|
|
models.insert(id, path);
|
|
}
|
|
}
|
|
Ok(GameObjectCatalog { models })
|
|
}
|
|
|
|
/// The per-display **sound-kit slots** (`Sound0..9` — the other 10 columns of the same table):
|
|
/// `{0 Stand, 1 Open, 2 Loop, 3 Close, 4 Destroy, 5 Opened, 6..9 Custom}` → SoundEntries.
|
|
/// Loaded separately from the model catalog so the audio consumer doesn't reach into the
|
|
/// renderer's cache (decision 0070); only displays with at least one non-zero slot are kept
|
|
/// (most of the 1638 rows are silent props).
|
|
pub struct GameObjectSounds {
|
|
sounds: HashMap<u32, [u32; 10]>,
|
|
}
|
|
|
|
/// The `Sound[10]` slot indices with recorded meanings (wowdev, vanilla family).
|
|
pub mod go_sound_slot {
|
|
pub const OPEN: usize = 1;
|
|
pub const CLOSE: usize = 3;
|
|
}
|
|
|
|
impl GameObjectSounds {
|
|
/// The 10 sound-kit slots for a display id; `None` when the display has no sounds at all.
|
|
pub fn slots(&self, display_id: u32) -> Option<&[u32; 10]> {
|
|
self.sounds.get(&display_id)
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.sounds.len()
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.sounds.is_empty()
|
|
}
|
|
}
|
|
|
|
/// Load the sound slots off GameObjectDisplayInfo.dbc.
|
|
pub fn load_gameobject_sounds(chain: &mut Chain) -> Result<GameObjectSounds> {
|
|
let bytes = chain
|
|
.read_file(GAMEOBJECT_DISPLAY_INFO)
|
|
.with_context(|| format!("reading {GAMEOBJECT_DISPLAY_INFO}"))?;
|
|
let rs = parse(&bytes, schema(), "GameObjectDisplayInfo")?;
|
|
let mut sounds = HashMap::new();
|
|
for r in rs.records() {
|
|
let Some(id) = u32_at(r, 0) else { continue };
|
|
let mut slots = [0u32; 10];
|
|
for (i, slot) in slots.iter_mut().enumerate() {
|
|
*slot = u32_at(r, 2 + i).unwrap_or(0);
|
|
}
|
|
if slots.iter().any(|&s| s != 0) {
|
|
sounds.insert(id, slots);
|
|
}
|
|
}
|
|
Ok(GameObjectSounds { sounds })
|
|
}
|