Files
benilla-octo/crates/benilla-protocol/src/messages/world_state.rs
T
SamandClaude Opus 5 c48d8c6b7e other players walk the world, and NPCs speak your name
- Other players walk the world. A watched player's motion between server updates now runs through
  the same grounded movement resolve as your own character — same capsule, same colliders, same
  step-up and ground election — so they follow terrain every frame instead of every packet, and
  stop at a wall instead of walking into it and being snapped back. Their jumps are swept against
  the world too, and a standstill jump steered mid-air reports that steer, the one mid-air press
  the reference client broadcasts rather than defers.
- The move relay rebuilt: relayed moves ride a per-unit chain on real time, a due move never jumps
  ahead of its unit's queue, and SMSG_COMPRESSED_MOVES — the batched carrier the server sends under
  load — is decoded rather than skipped. Our own send path now matches the reference client's:
  facing streams while moving, Nagle is off so each packet is one write, and the strafe axis nets so
  left and right can never ride the same packet.
- NPC and quest text speaks the whole $-macro grammar, carved from the reference bytes — your name,
  class and race, the gendered forms, and the server's live world states — and the same
  substitution feeds gossip, quest text, the quest log and unit frames. Greetings are a weighted
  draw, rolled per NPC per open, and a quest giver with no quest to give is no longer clickable,
  where it used to open a dialog holding nothing but a placeholder greeting.
- Spell tooltips finish the reference's line law: cast time and cooldown behind the real passive
  gate, the required tool or form, the reagents block, the chance-to-X line, a multi-bit
  equipped-item requirement named properly instead of by raw bits, and a buff's dispel class.
- The UI layout engine got a dense, reusable anchor solver, and a change gate that skips the resolve
  entirely on frames where no layout input moved — in practice, almost every idle frame. Pets are
  named by their pet number rather than a creature-template entry, so the client asks the query the
  server can actually answer. And the asset extractor grew a stray-geometry census for tracking
  down geometry that renders where it should not.

This snapshot replaces the six dated `sync` commits that stood here before it, whose subjects said
nothing but the date. If you cloned before now: `git fetch && git reset --hard origin/main`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 14:57:54 +07:00

109 lines
5.3 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.
//! World states — the server's key/value scoreboard table (`SMSG_INIT_WORLD_STATES` `0x2C2`,
//! `SMSG_UPDATE_WORLD_STATE` `0x2C3`). It is what the NPC-text `$<n>w`/`$<n>e` tokens read, and it
//! is what a battleground scoreboard is built from; benilla uses the first today.
//!
//! Both wires are settled from two independent sources that agree:
//!
//! - **The reference binary** (wow-re `system/ui/scratch/questtext-macro-expander.md` §3): the one
//! handler `0x48f690` holds the only two callers of the table setter `0x4c5870`, registered
//! against exactly these two opcodes at `0x48f515-0x48f52f`. `0x2C2` is *two leading dwords*
//! (passed to the unexamined `0x4c5650`), a `u16` count, then `count × (id:u32, value:u32)`;
//! `0x2C3` is a single such pair.
//! - **vmangos's writers** (`Player::SendInitWorldStates`, `Player.cpp:8219-8293`, and
//! `WorldPackets::Misc::UpdateWorldState::AppendBodyTo`, `Misc.cpp:685-694`): `u32 mapId`,
//! `u32 zoneId` (the zone dword is `#if SUPPORTED_CLIENT_BUILD > CLIENT_BUILD_1_11_2`, so 1.12.1
//! gets it), the `u16` count back-patched at the end, then the pairs; and `u32 field, u32 value`
//! respectively (also build-gated — a pre-1.8.4 client would read `u16`s).
//!
//! **The count includes a `(0, 0)` terminator.** vmangos starts `count = 1` and appends
//! `uint32(0) << uint32(0)` last, *"to prevent repeating audio bug"* — so the pair run is read
//! verbatim, terminator and all, exactly as the reference's count loop does. There is no
//! terminator special case on either side and we add none: id 0 simply lands in the table as 0.
//!
//! Ids and values are **raw dwords** on the wire and stay raw here. That matters: the reference's
//! `$<n>e` token reads the table at the *negated* key, so a text asking for `$2077e` looks up
//! `0xFFFFF7E3` — a key only reachable if nothing has re-interpreted the field on the way in.
use std::io;
use crate::wire::{read_u16_le, read_u32_le};
/// `SMSG_INIT_WORLD_STATES` — the whole table for a zone, sent on login and on every zone change
/// (vmangos `Player::UpdateZone` → `SendInitWorldStates`).
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct InitWorldStates {
/// The map the states are scoped to. The reference hands this and `zone` to `0x4c5650` and
/// nothing else; what that call does with them is **not recorded** (see [`read_init_world_states`]).
pub map: u32,
pub zone: u32,
/// `(id, value)` verbatim, including the trailing `(0, 0)` terminator.
pub states: Vec<(u32, u32)>,
}
/// Read `SMSG_INIT_WORLD_STATES`: `u32 map, u32 zone, u16 count`, then `count` `(id, value)` pairs.
///
/// The count is trusted only as far as the body allows — a truncated run stops at the short read
/// like every other reader here, rather than pre-allocating on a hostile length.
pub(in crate::messages) fn read_init_world_states(r: &mut &[u8]) -> io::Result<InitWorldStates> {
let map = read_u32_le(r)?;
let zone = read_u32_le(r)?;
let count = read_u16_le(r)?;
let mut states = Vec::new();
for _ in 0..count {
states.push((read_u32_le(r)?, read_u32_le(r)?));
}
Ok(InitWorldStates { map, zone, states })
}
/// Read `SMSG_UPDATE_WORLD_STATE`: one `(id, value)` dword pair.
pub(in crate::messages) fn read_update_world_state(r: &mut &[u8]) -> io::Result<(u32, u32)> {
Ok((read_u32_le(r)?, read_u32_le(r)?))
}
#[cfg(test)]
mod tests {
use super::*;
/// A vmangos-shaped `SMSG_INIT_WORLD_STATES`: map, zone, a count that *includes* the trailing
/// `(0, 0)` terminator, then the pairs. The terminator is data, not a sentinel we strip.
#[test]
fn init_reads_every_pair_including_the_terminator() {
let mut body = Vec::new();
body.extend_from_slice(&0u32.to_le_bytes()); // map 0 (Eastern Kingdoms)
body.extend_from_slice(&1519u32.to_le_bytes()); // zone (Stormwind)
body.extend_from_slice(&3u16.to_le_bytes()); // 2 real pairs + the terminator
for (id, value) in [(2264u32, 7u32), (2263u32, 0u32), (0, 0)] {
body.extend_from_slice(&id.to_le_bytes());
body.extend_from_slice(&value.to_le_bytes());
}
let got = read_init_world_states(&mut body.as_slice()).unwrap();
assert_eq!(got.map, 0);
assert_eq!(got.zone, 1519);
assert_eq!(got.states, vec![(2264, 7), (2263, 0), (0, 0)]);
}
/// A count longer than the body is a short read, not a panic or a giant allocation.
#[test]
fn init_truncated_run_errors() {
let mut body = Vec::new();
body.extend_from_slice(&0u32.to_le_bytes());
body.extend_from_slice(&0u32.to_le_bytes());
body.extend_from_slice(&u16::MAX.to_le_bytes());
body.extend_from_slice(&1u32.to_le_bytes());
assert!(read_init_world_states(&mut body.as_slice()).is_err());
}
/// The update wire is the bare pair, and both halves stay raw dwords — a value with the top bit
/// set survives as-is, so the `%d` render downstream is the only place a sign appears.
#[test]
fn update_reads_a_raw_dword_pair() {
let mut body = Vec::new();
body.extend_from_slice(&2264u32.to_le_bytes());
body.extend_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
assert_eq!(
read_update_world_state(&mut body.as_slice()).unwrap(),
(2264, 0xFFFF_FFFF)
);
}
}