Comments and strings: the passes' leftovers, and a region's template matched as one name (#391)

The comment and string passes' last leftovers, and one lookup made to match the reference.

**Comments that stated a wrong reference fact**, now stating the right one:
- A cast failure clears the GCD, and a failed cooldown-on-event spell also drops its parked record (`0x6e73cc`).
- `SetUnit` is `0x505d70`; `0x476cb0` is the component clone it reaches.
- Interior sound follows the character, where the listener sits, not the camera.
- Template inheritance re-runs `LoadXML` on the one object against each template (`0x76985c`), and reads `name` and `id` from the instance alone (`0x769770`).
- On the ghost-to-alive edge the reference clears its corpse cache and sends nothing (`0x5ee990`, `0x491f50`).
- The name cache has no high-bit miss. An empty-name answer evicts the record in the reference (`0x55f6f0`).
- The combat-log deferred queue (`0xc4e208`) waits only on an item name. A unit name never waits.
- A flying spline's segments are paced by curve length (`0x454320`, `0x453760`), not by chord length.
- A stop within 3 yd of its stop point keeps the arrow nocked (`0x6019e4`).
- A missile's sound id is at `CMissile+0x40`.
- A nil or unresolved `AssistUnit` token is a gap against `0xb8` ERR_GENERIC_NO_TARGET, not a deviation.

**Strings:**
- Test, log and tool messages lose leftover history wording and placeholder labels.
- The extractor's scan summaries describe the current tint and sequence-clock paths.
- The smoke script's failure text carries no private numbers.

**Line continuations:** twelve log, panic and assert strings get back the `\` continuations they had lost. Each printed a run of spaces mid-message. Fixes #106.

**One lookup:** a region's `inherits=` is looked up as one verbatim name, matched case-insensitively (`0x6ee6f0`, `_strnicmp` at `0x6ee747`), as `framexml::expand` already did for frames. The gate before it split on commas and matched case-sensitively, so a texture or font string whose `inherits=` spelled its template in another case never received the template. `a_region_takes_a_mis_cased_template` fails before this change and passes after.

**Tests:** the quest tests stop loading three FrameXML files that no quest file uses.

**In game:** nothing changes for the stock UI. Log lines read as one sentence, and an addon texture that names its template in another case now gets it.

Verified with `scripts/check.sh` on the changed crates, plus the loader and quest tests with the install present.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-09-26 08:57:47 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 4510a9b284
commit 7a1d799cf7
38 changed files with 140 additions and 123 deletions
+3 -1
View File
@@ -69,7 +69,9 @@ pub(super) fn seed_ui_fixture(
// exits non-zero, as the window-size refusal does.
if script.is_none() {
error!(
"capture: REFUSING this capture — scenario {:?} declares a UI fixture but no script VM exists, so its window cannot be opened and the shot would be a UI-less world wearing the scenario's name.",
"capture: REFUSING this capture — scenario {:?} declares a UI fixture but no script \
VM exists, so its window cannot be opened and the shot would be a UI-less world \
wearing the scenario's name.",
scenario.name
);
exit.write(AppExit::error());
+2 -1
View File
@@ -419,7 +419,8 @@ fn drive(
if home || left <= 0.0 {
if !home {
warn!(
"cinematic: the world did not come back inside the settle budget — revealing anyway"
"cinematic: the world did not come back inside the settle budget — \
revealing anyway"
);
}
cine.settling = None;
@@ -191,7 +191,7 @@ pub(crate) struct MissileSpawn {
pub(crate) ground_aim: Option<Vec3>,
/// The caster's ranged fallback visual, carried to arrival when the caster may be gone.
pub(crate) weapon_visual: Option<u32>,
/// `SpellVisual` field 10: the sound the projectile loops in flight (`CMissile+0x44`).
/// `SpellVisual` field 10: the sound the projectile loops in flight (`CMissile+0x40`).
pub(crate) missile_sound: Option<u32>,
/// Launch at the cast animation's release event (`0x5ffbd0` → `0x60c940`), not at GO; the
/// reference launches at GO only when the cast kit plays no body animation (`0x6e7a70`).
+3 -2
View File
@@ -221,8 +221,9 @@ fn feed_death(
// Ghost to alive: reclaim, spirit healer or an accepted res.
(Some((_, _, true)), false, false) => {
feed.died_at = None;
// The 1.12 client re-queries on the ghost-bit edge (`0x5ee990`), and the not-found
// answer drops the map markers; the server's own push is sent only with a looter
// The not-found answer drops the map markers. The reference sends nothing on this
// edge: its ghost-bit watcher (`0x5ee990`) clears the corpse cache, and `0x491f50`
// queries only while a ghost. The server's own push needs a looter
// (`Map.cpp:3617-3629`).
let _ = net.0.send(ClientCommand::CorpseQuery);
}
+5 -6
View File
@@ -360,7 +360,8 @@ fn debug_panel_ui(
}
}
ui.label(format!(
"animated doodads · {} ({} ticking) · {} material ({hidden} culled, {dim} dimmed) · {} uv",
"animated doodads · {} ({} ticking) · {} material \
({hidden} culled, {dim} dimmed) · {} uv",
anim_hosts.iter().count(),
ticking,
mat_anims.iter().count(),
@@ -595,9 +596,7 @@ fn debug_panel_ui(
.id_salt("stepup_report")
.show(ui, |ui| {
for line in report {
ui.label(
egui::RichText::new(line).small().monospace(),
);
ui.label(egui::RichText::new(line).small().monospace());
}
});
});
@@ -609,8 +608,8 @@ fn debug_panel_ui(
egui::RichText::new(format!(
"{DEV_CHORD}: D panel · P perf · I inspect · M mute · F free-fly"
))
.small()
.color(OVERLAY_TEXT_DIM),
.small()
.color(OVERLAY_TEXT_DIM),
);
// The build; a click copies the full sha.
let build = ui.add(
@@ -327,8 +327,8 @@ pub(in crate::entities) fn build_dressup_preview(
equipment: look.equipment,
emblem: look.emblem,
// No hide flags: `crate::ui_dressup` already left out a hidden worn helm or cloak, as
// `SetUnit` (`0x476cb0`) clones the live display pointers, and a mask here would hide a
// tried-on one.
// `SetUnit` (`0x505d70`) clones the live display pointers (`0x476cb0`), and a mask here
// would hide a tried-on one.
flags: 0,
// The ranged slot here only holds a try-on: `SetUnit` clones the live model, which shows
// a ranged weapon only while ranged-drawn, so `crate::ui_dressup` leaves a worn one out.
+4 -2
View File
@@ -2,7 +2,8 @@
//! guid), a creature's `CMSG_CREATURE_QUERY` (by template entry), a pet's or charm's
//! `CMSG_PET_NAME_QUERY` (by pet number), and which one applies is read off the descriptor as the
//! reference's `GetUnitName` (`0x609210`) reads it. Each key is asked once while in flight, and a
//! negative answer is cached so a bad id never loops.
//! negative answer is cached so a bad id never loops; for a player that is an empty name, which the
//! reference evicts instead (`0x55f6f0`), so its next read asks again.
//!
//! Creature templates persist, as the reference's `creaturecache.wdb` does; player and pet names
//! are never written to disk and are cleared at world-session start, as the reference does.
@@ -509,7 +510,8 @@ pub(crate) mod net {
}
/// `SMSG_INVALIDATE_PLAYER`, which vmangos sends after a rename (`CharacterHandler.cpp:839`):
/// the only per-player eviction, as in the reference, whose name cache has no TTL (`0x555600`).
/// the reference's eviction (`0x555600`) in a name cache with no TTL; its other, an empty-name
/// answer (`0x55f6f0`), is a cached negative here.
fn invalidate_player(guid: u64, names: &mut NameCache) {
debug!("net: invalidating cached name for player {guid:#x}");
names.invalidate_player(guid);
+4 -2
View File
@@ -68,8 +68,10 @@ impl Spline {
}
/// Raw-WoW position, facing (`None` when vertical) and pitch `asin(dz/len)` at `now`, at
/// constant speed. Segments are located by chord length (`0x4532e0`); the evaluator `0x4541b0`
/// lerps a ground path and runs a uniform Catmull-Rom (`0x453580`) on a flying one.
/// constant speed. Segments are located by chord length here in both modes; the reference
/// uses the chord on a ground path only and a flying segment's Catmull-Rom length in 20 steps
/// (`0x454320`, `0x453760`). The evaluator `0x4541b0` lerps a ground path and runs a uniform
/// Catmull-Rom (`0x453580`) on a flying one.
pub(crate) fn sample(&self, now: Instant) -> ([f32; 3], Option<f32>, f32) {
let pts = self.points.as_slice();
if pts.len() < 2 {
+3 -2
View File
@@ -935,8 +935,9 @@ fn monster_move(
}
}
}
// Every spline move un-nocks and drops the weapon hold: the handler `0x6018f0` (opcodes
// 0xDD, 0x2AE) always ends through `0x60d040` (clear `0x400`) and `0x60f530` (un-nock).
// Every spline move un-nocks and drops the weapon hold here. The handler `0x6018f0`
// (opcodes 0xDD, 0x2AE) ends through `0x60d040` (clear `0x400`) and `0x60f530` (un-nock) on
// every path but one: a stop within 3 yd of the stop point jumps past both (`0x6019e4`).
commands.entity(e).remove::<(
crate::creature_anim::NockLatch,
crate::creature_anim::RangedHold,
+2 -1
View File
@@ -115,7 +115,8 @@ pub(super) fn update_attachment(
benilla_assets::trace::line(
"ride",
&format!(
"on {:#x} deck({:8.2},{:7.2},{:8.2}) yaw{:+.3} | feet({:8.2},{:7.2},{:8.2}) local({:7.2},{:6.2},{:7.2}) | grounded={} support={} vy={:+6.2}{}",
"on {:#x} deck({:8.2},{:7.2},{:8.2}) yaw{:+.3} | feet({:8.2},{:7.2},{:8.2}) \
local({:7.2},{:6.2},{:7.2}) | grounded={} support={} vy={:+6.2}{}",
ride.guid,
bpos.x,
bpos.y,
+3 -2
View File
@@ -5,7 +5,8 @@
//! The read that asks takes `&self` (the in-flight set is behind a lock), so read-only systems
//! share the owning resource and its change detection means an answer landed.
//!
//! A negative answer (the server's high-bit miss) is cached as `None` and never re-asked.
//! A negative answer is cached as `None` and never re-asked: an integer-keyed store's high-bit miss
//! (`0x556e67`), or a player's empty name.
use std::collections::{HashMap, HashSet};
use std::hash::Hash;
@@ -86,7 +87,7 @@ impl<K: Copy + Eq + Hash, V> QueryCache<K, V> {
self.generation = self.generation.wrapping_add(1);
}
/// The reference's explicit eviction (a high-bit key, `SMSG_INVALIDATE_PLAYER`): the next
/// The reference's explicit eviction (`SMSG_INVALIDATE_PLAYER` for a player's name): the next
/// read re-asks. Returns whether there was anything to evict.
pub(crate) fn evict(&mut self, key: K) -> bool {
self.pending
+7 -6
View File
@@ -1,13 +1,14 @@
//! WMO interior audio: the `WMOAreaTable` row for the group the camera eye is in (exact group
//! row, then whole-WMO default, then name-set 0), published as an override layer. Zone music,
//! ambience, intro and reverb take its nonzero fields over the terrain `AreaTable` chain; zero
//! fields fall through to it (the reference's rule for a zero interior field is untraced).
//! WMO interior audio: the `WMOAreaTable` row for the group the character is in, where the
//! listener sits (the camera before login), by exact group row, then whole-WMO default, then
//! name-set 0, published as an override layer. Zone music, ambience, intro and reverb take its
//! nonzero fields over the terrain `AreaTable` chain; zero fields fall through to it (the
//! reference's rule for a zero interior field is untraced).
use bevy::prelude::*;
use benilla_world::schedule::WorldStage;
/// The audio FKs of the interior the eye is in; `None` is the open world.
/// The audio FKs of the interior the listener is in; `None` is the open world.
#[derive(Resource, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) struct CurrentInterior(pub(crate) Option<InteriorAudio>);
@@ -23,7 +24,7 @@ pub(crate) struct InteriorAudio {
pub(crate) intro_sound: u32,
}
/// Resolve the eye's interior keys to the audio row; log transitions by interior name.
/// Resolve the listener's interior keys to the audio row; log transitions by interior name.
fn resolve_interior(
world: benilla_world::world_point::WorldPoint,
mut current: ResMut<CurrentInterior>,
+3 -1
View File
@@ -942,7 +942,9 @@ mod tests {
);
assert!(
shut_asked > 0.9,
"gate shut: the METER plane must still read the mix the game produced — asked {shut_asked:.3}. A tap that goes silent with the speakers is the false negative this ordering exists to prevent"
"gate shut: the METER plane must still read the mix the game produced — asked \
{shut_asked:.3}. A tap that goes silent with the speakers is the false negative \
this ordering exists to prevent"
);
}
+7 -6
View File
@@ -598,12 +598,13 @@ fn cast_result(
// arm clears the guard: both clears are the reference's one `0x6e741a call 0x6e4940(0x1c)`.
let in_flight = pending.committed(Instant::now()) == Some(spell_id);
if !success {
// `HandleCastFailed 0x6e1a00`: a failure clears only the GCD armed at send (`0x6e1d83 →
// 0x6e1630`); the spell's own recovery starts at SPELL_GO, which a failed cast never
// reaches. A failing cached auto-repeat spell with reason ≠ 0x17 runs the full local
// cancel (`0x6e1cd9`–`0x6e1cea` → `0x6ea080`, the `SMSG_CANCEL_AUTO_REPEAT` routine). A
// deselect arrives as this failure (vmangos `HandleSetSelectionOpcode` → `Spell::cancel` →
// `SendCastResult(INTERRUPTED)`); target death arrives as [`cancel_auto_repeat`].
// `HandleCastFailed 0x6e1a00` clears the GCD armed at send (`0x6e1d83 → 0x6e1630`), and the
// bit-25 revert below drops a parked record; the spell's own recovery starts at SPELL_GO,
// which a failed cast never reaches. A failing cached auto-repeat spell with reason ≠ 0x17
// runs the full local cancel (`0x6e1cd9`–`0x6e1cea` → `0x6ea080`, the
// `SMSG_CANCEL_AUTO_REPEAT` routine). A deselect arrives as this failure (vmangos
// `HandleSetSelectionOpcode` → `Spell::cancel` → `SendCastResult(INTERRUPTED)`); target
// death arrives as [`cancel_auto_repeat`].
let now = Instant::now();
// Reason 0x17 DONT_REPORT exits before the GCD clear and the display (`6e1ce1`/`6e1cf7` →
// `0x6e224f`). The in-flight clear below still runs, as the reference's `0x6e741a` does
+1 -1
View File
@@ -1190,7 +1190,7 @@ mod tests {
assert_eq!(go_interact_range_sq(GO_TYPE_CHAIR), 9.0);
assert!(
go_interact_range_sq(GO_TYPE_CHAIR) < GO_INTERACT_RANGE_SQ,
"the chair reaches SHORTER than the interim — a longer one would restore the bug"
"a chair's reach is shorter than the default GameObject reach"
);
assert!(!fishing_channel_owned(None, Some(7)));
assert!(!fishing_channel_owned(None, None));
+6 -5
View File
@@ -795,8 +795,9 @@ pub(crate) fn power_word(script: &benilla_ui::script::UiScript, power: u32) -> O
}
/// One endpoint's display name, the reference's `GetObjectName` (`0x6264e0`); `None` until the
/// name cache answers, and the caller retries as the deferred queue at `0xc4e208` does. A streamed
/// unit names through its descriptor (`GetUnitName`, `0x609210`), which is how a pet is named.
/// name cache answers, and the caller retries, where the reference never waits and prints
/// `UNKNOWNOBJECT`. A streamed unit names through its descriptor (`GetUnitName`, `0x609210`), which
/// is how a pet is named.
pub(crate) fn object_name(
guid: u64,
unit: Option<&ObjectStore>,
@@ -1552,9 +1553,9 @@ pub(crate) fn melee_family(
// ────────────────────────── the queued line, awaiting its names ───────────────────────
/// A combat-log line classified at the packet, with only its names outstanding: the reference's
/// `0x629b60` decides while both units are streamed and parks the message on the deferred queue
/// at `0xc4e208` until the name-ready callback `0x6294b0`, so a despawned creature still logs.
/// A combat-log line classified at the packet while both units are streamed, so a despawned
/// creature still logs, with only its names outstanding. The reference's deferred queue
/// (`0xc4e208`, replayed by `0x6294b0`) holds only a line waiting on an item name (`0x55ba30`).
#[derive(Clone, Debug)]
pub(crate) struct PendingCombat {
pub kind: ChatEventKind,
+2 -2
View File
@@ -98,8 +98,8 @@ enum Pending {
},
/// An area discovery (`SMSG_EXPLORATION_EXPERIENCE`), fired at the drain, which holds the VM.
Discovery { area: String, xp: u32 },
/// One combat-log line, classified at the packet, waiting on its two endpoint names: the
/// reference's deferred queue (`0xc4e208`), replayed by the name-ready callback `0x6294b0`.
/// One combat-log line, classified at the packet, waiting on its two endpoint names; the
/// reference's deferred queue (`0xc4e208`) waits only on an item name.
Combat(Box<super::combat::PendingCombat>),
/// A ready event (client-composed lines; name-carrying notices).
Event(ChatEvent),
+1 -1
View File
@@ -132,7 +132,7 @@ mod tests {
assert_eq!(
s.defense_skill,
(0, 0),
"INTERIM — unconfirmed in the reference"
"no pet defense here; the reference answers UnitLevel × 5 and 0 (`0x613680`)"
);
}
@@ -22,9 +22,6 @@ fn questgiver_show_hide_plays_open_and_close_kits() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our scroll kits
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
@@ -85,9 +82,6 @@ fn panel_events_show_exactly_one_child_panel_and_hide_the_others() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our scroll kits
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
@@ -179,9 +173,6 @@ fn detail_panel_reward_grid_follows_the_refs_two_per_row_layout() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our scroll kits
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
@@ -278,9 +269,6 @@ fn reward_panel_choice_click_selects_and_completes_with_zero_based_index() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our scroll kits
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
@@ -358,9 +346,6 @@ fn greeting_goodbye_button_closes_the_window() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our scroll kits
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
@@ -409,9 +394,6 @@ fn detail_panel_action_buttons_resolve_to_real_onscreen_rects() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our scroll kits
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
@@ -535,9 +517,6 @@ fn write_on_still_fades_when_instant_text_is_off() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our scroll kits
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
@@ -612,9 +591,6 @@ fn npc_name_reaches_the_title_bar_on_open_and_on_refresh() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our scroll kits
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
@@ -692,9 +668,6 @@ fn greeting_panel_title_rows_grow_to_their_wrapped_titles() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our scroll kits
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
@@ -787,9 +760,6 @@ fn reward_rows_preview_and_post_without_selecting_the_choice() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our scroll kits
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
@@ -530,7 +530,7 @@ fn the_watch_bar_shows_the_watched_factions_progress_and_swaps_at_max_level() {
s.set_unit("player", Some(player(40)));
assert!(
!shown(&mut s, "ReputationWatchBar"),
"nothing watched: the bar stays down, as it has since it was a stub"
"nothing watched: the bar stays down"
);
// Ironforge is rep slot 20; watching it is a server field, so it rides the push.
+1 -1
View File
@@ -299,7 +299,7 @@ fn player_money_input_reflects_then_offers() {
)
.unwrap(),
("1".into(), "23".into(), "45".into()),
"the echo reflects 1g 23s 45c into the boxes"
"the event fills 1g 23s 45c into the boxes"
);
assert_eq!(
s.take_trade_money(),
@@ -1513,7 +1513,7 @@ pub fn fxuvscan(chain: &mut Chain, prefix: Option<&str>) -> Result<()> {
for c in [FxClass::Invisible, FxClass::Never, FxClass::Unknown] {
listing(
c.label(),
" (frozen@identity — what the effect lane renders today)",
" (frozen@identity — what the effect lane renders)",
|h| h.class,
c,
);
@@ -2442,7 +2442,7 @@ pub fn entityuvscan(chain: &mut Chain, prefix: Option<&str>) -> Result<()> {
] {
listing(
c.label(),
" (frozen@identity — what the entity lane renders today)",
" (frozen@identity — what the entity lane renders)",
|h| h.class,
c,
);
@@ -2521,12 +2521,11 @@ pub fn entityuvscan(chain: &mut Chain, prefix: Option<&str>) -> Result<()> {
println!();
println!(
"============================== M2COLOR TINT ==============================\n\
The same corpus asked about the OTHER channel `entity_variants` leaves unserved.\n\
`build` passes `sub.rgb_anim.as_ref()` unconditionally, so a tint-animating entity\n\
batch is SEEDED at the loop's first key (`model_material`'s `tint.xyz`) — and then\n\
nothing re-samples it, because `doodad_anim::register_tint`'s only call sites are in\n\
`terrain_stream/spawn/assemble.rs`. There is no `play_rgb` flag to flip: the hole is\n\
the missing registration, not a missing argument."
The same corpus asked about the OTHER channel, the keyed M2Color tint. A batch is\n\
seeded at the loop's first key (`model_material`'s `tint.xyz`) and re-sampled only\n\
where `doodad_anim::register_tint` registers it: placed doodads\n\
(`terrain_stream/spawn/assemble.rs`), dressed entity parts (`entities/attach/dress.rs`)\n\
and the glue booth (`portrait/glue_booth.rs`)."
);
for (path, block) in &tint_blocks {
println!(
@@ -2554,7 +2553,7 @@ pub fn entityuvscan(chain: &mut Chain, prefix: Option<&str>) -> Result<()> {
);
}
println!();
println!(" --- the CLOCK the tint fix has to serve ---");
println!(" --- the CLOCK each tint rides ---");
for c in UvClock::ALL {
println!(
" {:<6} {:>5} batch(es) — {}",
@@ -514,9 +514,8 @@ pub fn seqclockscan(chain: &mut Chain, prefix: Option<&str>) -> Result<()> {
eprintln!(
"{scanned} models scanned, {with_seqs} with sequences: {frozen} CLOCKLESS \
({frozen_emitters} emitters) — sequences + per-sequence consumers, not one keyed bone; \
{frozen_go} of them GameObject display models (the hosted lane, frozen at slot 0 t=0). \
{partial} PARTIAL models have unkeyed sequences a clip lookup can never reach; \
{inert} more are boneless with nothing to sample (a clock there is inert)."
{frozen_go} of them GameObject display models. {partial} PARTIAL models key a bone in \
some sequences only; {inert} more key no bone and have nothing to sample."
);
Ok(())
}
@@ -643,9 +642,9 @@ pub fn soundeventscan(chain: &mut Chain, prefix: Option<&str>) -> Result<()> {
}
eprintln!(
"{scanned} models with sequences scanned; {carriers} carry a $DSL/$DSO/$SND marker, \
{hostless} of them on the Static tier (no anim-root entity exists to hang an emitter \
on today); {rest_gated} carry the marker on a REST-posed sequence — the class \
benilla's rig gate never arms, so the marker is unreachable through an AnimationPlayer."
{hostless} of them on the Static tier (the marker alone arms their clock); \
{rest_gated} carry the marker on a REST-posed sequence, which gets no rig, so only the \
rig-free sequence clock reaches it."
);
Ok(())
}
+2 -1
View File
@@ -940,7 +940,8 @@ mod tests {
let swim_delta = 0.75 * (ours_before - reference);
assert!(
swim_delta.abs() > 0.05,
"{label}: the two readings must actually differ, else this test asserts nothing (reference {reference}, form-derived {ours_before})"
"{label}: the two readings must actually differ, else this test asserts nothing \
(reference {reference}, form-derived {ours_before})"
);
worst = worst.max(swim_delta.abs());
}
+6 -5
View File
@@ -922,16 +922,17 @@ mod tests {
{
let first = rot.keys[0].1;
assert!(
rot.keys.iter().all(|(_, q)| q
rot.keys
.iter()
.zip(first)
.all(|(a, b)| (a - b).abs() < 1e-6)),
"UI_{token}'s stage rotation is a PARKED key, not an animation — a moving stage would have to be read off the live joint instead"
.all(|(_, q)| q.iter().zip(first).all(|(a, b)| (a - b).abs() < 1e-6)),
"UI_{token}'s stage rotation is a PARKED key, not an animation — a \
moving stage would have to be read off the live joint instead"
);
let [x, y, z, w] = first;
assert!(
x.abs() < 1e-3 && y.abs() < 1e-3,
"UI_{token}'s stage rotation is a pure YAW about +Z — a stage tipped in pitch or roll would need composing, not summing (got {first:?})"
"UI_{token}'s stage rotation is a pure YAW about +Z — a stage tipped in \
pitch or roll would need composing, not summing (got {first:?})"
);
yaw += 2.0 * f32::atan2(z, w).to_degrees();
}
@@ -183,7 +183,7 @@ pub struct VisualStages {
pub missile_model: u32,
/// Field 9: the index into [`MISSILE_ATTACH_TABLE`].
pub missile_attach: u32,
/// Field 10: the `SoundEntries.dbc` loop the missile plays in flight (`CMissile+0x44`, its
/// Field 10: the `SoundEntries.dbc` loop the missile plays in flight (`CMissile+0x40`, its
/// volume shaped by distance at `0x61d790`), stopped when it arrives.
pub missile_sound: Option<u32>,
/// Field 14: the `SoundEntries.dbc` id the `$TRD` anim event plays at a craft's strike frame
@@ -39,7 +39,8 @@ impl Probe for Speed {
let speed_changes_seen = &cx.world.speed_changes_seen;
ensure!(
speed_changes_seen.len() >= 2,
"--speed: expected 2 force-speed changes (got {}) — did the first ack drop the session, or is the account not GM?",
"--speed: expected 2 force-speed changes (got {}) — did the first ack drop the \
session, or is the account not GM?",
speed_changes_seen.len()
);
let (k1, c1, s1) = speed_changes_seen[0];
@@ -57,7 +58,8 @@ impl Probe for Speed {
"--speed: movement counter must increment across changes (got {c1} then {c2})"
);
println!(
"\n--speed PASS: {k1:?} 7.0->10.5->7.0 yd/s, counters {c1}->{c2}, both acks accepted (stream survived)"
"\n--speed PASS: {k1:?} 7.0->10.5->7.0 yd/s, counters {c1}->{c2}, both acks accepted \
(stream survived)"
);
Ok(())
}
+6 -5
View File
@@ -311,11 +311,12 @@ fn element_from_node(node: roxmltree::Node) -> Element {
}
/// Resolve `inherits` into a materialized [`Element`]: the named template, itself expanded first,
/// then the element on top, as the reference splices a template's nodes before the instance's
/// own (`LoadChildFrames 0x76a060` keeps that order for `<Frames>`). Children are the template's
/// then the element's; the element's attributes override or extend the template's
/// (case-insensitively). `name` and `virtual` splice like any attribute; whether the reference
/// exempts them is untraced. A cycle is skipped with a warning.
/// then the element on top. Children are the template's then the element's; the element's
/// attributes override or extend the template's (case-insensitively). The reference merges
/// nothing: `LoadXML` re-enters itself on the one object against each template, the most distant
/// first, then the instance (`0x76985c`; `<Frames>` repeat the walk, `0x76a060`), and reads `name`
/// and `id` from the instance alone (`PreLoadXML 0x769770`). Here `name` and `virtual` merge like
/// any attribute. A cycle is skipped with a warning.
pub fn expand(
element: &Element,
templates: &HashMap<&str, &Element>,
+5 -5
View File
@@ -428,13 +428,13 @@ impl Loader<'_> {
/// [`Loader::expand`] for a region, only when `inherits=` names a registered element template:
/// a FontString's `inherits=` usually names a font object, which passes through unwarned.
pub(super) fn expand_region(&mut self, el: &Element) -> Element {
let hit = el.attr("inherits").is_some_and(|names| {
// One verbatim name, matched case-insensitively, as `framexml::expand` looks it up.
let hit = el.attr("inherits").is_some_and(|name| {
let model = self.model();
let templates = model.framexml_templates.borrow();
names
.split(',')
.map(str::trim)
.any(|n| templates.contains_key(n))
!name.is_empty()
&& (templates.contains_key(name)
|| templates.keys().any(|k| k.eq_ignore_ascii_case(name)))
});
if hit {
self.expand(el)
+25
View File
@@ -384,6 +384,31 @@ mod loader_tests {
);
}
/// A region's `inherits=` is one name matched case-insensitively (`0x6ee747`), as a frame's
/// is, so a mis-cased template name still reaches the template.
#[test]
fn a_region_takes_a_mis_cased_template() {
let s = UiScript::new().unwrap();
let doc = parse(
r#"<Ui>
<Texture name="MarkTemplate" virtual="true">
<Size><AbsDimension x="32" y="16"/></Size>
</Texture>
<Frame name="MarkHost">
<Layers><Layer level="ARTWORK">
<Texture name="$parentMark" inherits="marktemplate"/>
</Layer></Layers>
</Frame>
</Ui>"#,
);
let report = load(&s, &doc, &no_files);
assert!(report.errors.is_empty(), "errors: {:?}", report.errors);
assert_eq!(
s.eval::<f32>("return MarkHostMark:GetWidth()").unwrap(),
32.0
);
}
#[test]
fn frame_set_all_points_pins_to_parent() {
let mut s = UiScript::new().unwrap();
@@ -301,7 +301,9 @@ fn apply_region_inherits(lua: &Lua, wrapper: &Table, from: &str) -> mlua::Result
if is_template {
let mut model = lua.app_data_mut::<Model>().expect("model");
model.record_warning(format!(
"CreateTexture/CreateFontString: '{from}' is a registered TEMPLATE, not a font object; the region is created but the template's content is not applied (no corpus caller does this)"
"CreateTexture/CreateFontString: '{from}' is a registered TEMPLATE, not a font object; \
the region is created but the template's content is not applied (no corpus caller \
does this)"
));
return Ok(());
}
+1 -1
View File
@@ -789,7 +789,7 @@ mod tests {
s.eval::<Value>("return GetWatchedFactionInfo()")
.unwrap()
.is_nil(),
"nothing watched → one nil, which is the ref's own `if ( name )` gate"
"nothing watched → a nil name, which stock `if ( name )` gates on"
);
s.run("SetWatchedFactionIndex(3)").unwrap(); // Stormwind
@@ -216,7 +216,7 @@ fn a_region_resolves_even_when_its_owner_frame_has_no_rect() {
host:SetWidth(400) host:SetHeight(200)
host:SetPoint("BOTTOMLEFT", 100, 50)
-- the shape that was invisible: owner with NO size and NO SetPoint
-- an owner with NO size and NO SetPoint
bare = CreateFrame("Frame", "BareOwner")
mark = bare:CreateTexture("BareMark", "ARTWORK")
mark:SetWidth(20) mark:SetHeight(10)
@@ -236,7 +236,7 @@ fn a_region_resolves_even_when_its_owner_frame_has_no_rect() {
assert_eq!(
s.eval::<Option<f32>>("return BareOwner:GetLeft()").unwrap(),
None,
"an unpositioned frame has no rect; the fix must not invent one for it"
"an unpositioned frame has no rect"
);
// Host's BOTTOMLEFT (100,50) plus the (5,7) offset.
@@ -253,7 +253,7 @@ fn a_region_resolves_even_when_its_owner_frame_has_no_rect() {
assert_eq!(
s.eval::<Option<f32>>("return SizedMark:GetLeft()").unwrap(),
Some(105.0),
"a sized owner's region must be unaffected by the fix"
"a sized owner's region resolves the same"
);
}
@@ -348,7 +348,8 @@ fn set_inventory_item_renders_full_outside_compare() {
assert(count(tt:SetInventoryItem("player", 16)) == 3,
"occupied: three returns, got " .. count(tt:SetInventoryItem("player", 16)))
local _, _, repairCost = tt:SetInventoryItem("player", 16)
assert(repairCost == 0, "repairCost is a NUMBER — the reference always pushes one; 0 INTERIM")
assert(repairCost == 0,
"repairCost is a NUMBER — the reference always pushes one; 0 as the cost is not fed")
assert(count(tt:SetInventoryItem("player", 5)) == 3,
"empty: three returns too, got " .. count(tt:SetInventoryItem("player", 5)))
local hasItem, _, emptyCost = tt:SetInventoryItem("player", 5)
@@ -994,8 +994,8 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
// AssistUnit(unit) (`0x489b80`): select the unit's `UNIT_FIELD_TARGET`. The shared tail
// (`0x489bb2`-`0x489c07`) returns silently on 0 and selects through `0x489a40`, which leaves
// the selection alone when nothing resolves; no `CanAssist` gate (`0x6066f0`), any unit, and a
// swing only with `assistAttack` set (default "0", `0x48fc50`). Deviation: a nil token is
// silent, because the reference's game message `0xb8` for it has no recoverable text.
// swing only with `assistAttack` set (default "0", `0x48fc50`). A token that resolves nothing
// is silent here, where the reference shows `0xb8` ERR_GENERIC_NO_TARGET (`0x489c0e`).
g.set(
"AssistUnit",
lua.create_function(|lua, token: Option<String>| {
@@ -1046,7 +1046,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
// number is taken as its string. Deviation: among whole-name matches ours picks the nearest,
// because the reference's first-walked pick is order-dependent and reads as a bug. A miss is
// silent here, where the reference shows `0x127` ERR_UNIT_NOT_FOUND, or `0xb8`
// ERR_GENERIC_NO_TARGET for an empty name (#52).
// ERR_GENERIC_NO_TARGET for an empty name.
g.set(
"TargetByName",
lua.create_function(|lua, (name, exact): (Value, Option<Value>)| {
+1 -1
View File
@@ -469,7 +469,7 @@ fn power_bindings_read_the_active_type() {
assert!(
s.eval::<bool>("return UnitPower == nil and UnitPowerMax == nil")
.unwrap(),
"the Era spellings must not linger beside the 1.12 ones"
"UnitPower and UnitPowerMax are not 1.12 globals"
);
// An absent unit is the number 0, never nil: `UnitFrame.lua:122` indexes `ManaBarColor` by it.
assert_eq!(
@@ -565,7 +565,8 @@ pub(super) fn spawn_loaded_placements(
// Once per burst, not per prop: a city WMO can flood thousands in a frame.
let (live, peak) = probes.occupancy();
warn_once!(
"interior-prop probe table full (live {live}, peak {peak}); overflowing props fall back to exterior light"
"interior-prop probe table full (live {live}, peak {peak}); \
overflowing props fall back to exterior light"
);
}
slot
+2 -2
View File
@@ -108,7 +108,7 @@ sup="$(printf '%s\n' "$plain" | sed -n 's/.*logout-smoke: re-entered.*suppressor
printf ' %-24s %s\n' "re-entry suppressors" "${sup:-<unreported>}"
[ "$sup" = "none" ] ||
fail "the re-entered character could not be driven — suppressors: ${sup:-<unreported>} \
(1542: everything the ended session granted its mover dies with it)"
(everything the ended session granted its mover dies with it)"
# vmangos skips the logout root when resting, on a taxi or at account security >= `InstantLogout`
# (`MiscHandler.cpp`, `CMSG_LOGOUT_REQUEST`); probe accounts are GM, so report whether the check
@@ -152,7 +152,7 @@ writes="$(printf '%s\n' "$plain" | grep -cF "saved variables: wrote")"
printf ' %-24s %s\n' "shutdown writes" "$writes"
[ "$writes" -eq "$sessions" ] ||
fail "the shutdown tail wrote $writes time(s), expected $sessions — a session ended without \
saving (1528: the quit root must be observed in \`Last\`, after PostUpdate's exit_on_all_closed)"
saving (the quit root must be observed in \`Last\`, after PostUpdate's exit_on_all_closed)"
# The read-only verdict names each changed file, the whole lead to whatever wrote it.
if [ -n "$install_root" ]; then