- show-helm / show-cloak preferences dress the world body at last: the preference is `PLAYER_FLAGS` 0x400/0x800 — a public descriptor field, so it dresses every player you can see — and equipment resolve now reads it in both places the helm resolves (geosets and attachments) and the one the cloak does; character select has honoured the same stored bit since day one, which is why the two disagreed - the toggles reach the wire (`CMSG_TOGGLE_HELM`/`CMSG_TOGGLE_CLOAK`), the `ShowHelm`/`ShowingHelm`/`ShowCloak`/`ShowingCloak` script API exists, the dressing room follows the world, and Options → Interface finally has the two rows - the minimap's interior draw starts compositing into the client's own 256² target, and the interior gate reads the client's indoor byte — the ground work for the black-lines fix that lands next - `WOW_DRESS_CENSUS` reports what the wire asked for vs what is on the body Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
220 lines
9.6 KiB
Rust
220 lines
9.6 KiB
Rust
//! The **equipment-display** API surface — `ShowHelm`/`ShowingHelm`, `ShowCloak`/`ShowingCloak`
|
|
//! (decision 1472): the player's two "don't draw this piece of my gear" preferences.
|
|
//!
|
|
//! Two bits, and they are not the UI's to keep. The preference lives server-side in the character's
|
|
//! own `PLAYER_FLAGS` (`HIDE_HELM 0x400` / `HIDE_CLOAK 0x800`), which is a **public** descriptor
|
|
//! field — so it dresses our body on every client that can see us, and every other player's on
|
|
//! ours. That is why there is no CVar and no saved variable behind this row: the wire flag is the
|
|
//! single source of truth, it is per-character rather than per-install, and it is already persisted
|
|
//! by whoever we are logged into.
|
|
//!
|
|
//! **The setter is a set; the wire verb is a flip.** `CMSG_TOGGLE_HELM`/`CMSG_TOGGLE_CLOAK` carry no
|
|
//! body and have no target-state form at all (vmangos `HandleShowingHelmOpcode` is a bare
|
|
//! `ToggleFlag`), so `ShowHelm(v)` compares `v` against what we currently believe and queues a flip
|
|
//! only on a difference. The belief is updated **optimistically at the call** and overwritten only
|
|
//! when the *wire* bit actually moves ([`super::UiScript::set_worn_display`], pushed on the
|
|
//! descriptor edge and not per frame) — without that, the value would snap back to the stale
|
|
//! descriptor for the length of the round trip and a second click inside that window would compute
|
|
//! the wrong flip.
|
|
//!
|
|
//! The 1.12 panel calls the setter with the **strings** `"1"` / `"0"` (`UIOptionsFrame_Save`
|
|
//! l.286-297, `value.setFunc(value.value)`), so the argument is read *numerically* — plain Lua
|
|
//! truthiness would make `"0"` mean "show" and the reference's own Options panel could never turn
|
|
//! a helm off. See [`shown_arg`].
|
|
|
|
use mlua::{Lua, Value};
|
|
|
|
use super::Model;
|
|
|
|
/// Which of the two worn-display preferences a queued flip is for. One entry per
|
|
/// `CMSG_TOGGLE_HELM`/`CMSG_TOGGLE_CLOAK` the app should send.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum WornDisplay {
|
|
/// The head slot — `PLAYER_FLAGS_HIDE_HELM`.
|
|
Helm,
|
|
/// The back slot — `PLAYER_FLAGS_HIDE_CLOAK`.
|
|
Cloak,
|
|
}
|
|
|
|
impl super::UiScript {
|
|
/// Drain the worn-display flips queued since the last call — one `CMSG_TOGGLE_HELM` /
|
|
/// `CMSG_TOGGLE_CLOAK` each. A list rather than a count because the two slots are distinct
|
|
/// packets; within one slot, two flips in a frame are two sends, exactly like the PvP toggle.
|
|
pub fn take_worn_display_toggles(&mut self) -> Vec<WornDisplay> {
|
|
std::mem::take(&mut self.model_mut().worn_display_toggles)
|
|
}
|
|
|
|
/// Push the wire truth for both preferences — the app calls this **on a `PLAYER_FLAGS` edge**,
|
|
/// never per frame, so an optimistic flip made by [`install`]'s setters survives until the
|
|
/// server's answer actually arrives (see the module doc).
|
|
pub fn set_worn_display(&mut self, helm_shown: bool, cloak_shown: bool) {
|
|
let mut model = self.model_mut();
|
|
model.helm_shown = helm_shown;
|
|
model.cloak_shown = cloak_shown;
|
|
}
|
|
|
|
/// What the VM currently believes — the app's own read, for the drain's compare and for tests.
|
|
pub fn worn_display(&self) -> (bool, bool) {
|
|
let model = self.model_ref();
|
|
(model.helm_shown, model.cloak_shown)
|
|
}
|
|
}
|
|
|
|
/// The setter's argument convention: **numeric**, not Lua-truthy. `nil`/`false`/`0`/`"0"` read as
|
|
/// "hide"; `true`, any non-zero number and any non-numeric string read as "show".
|
|
///
|
|
/// The distinction is load-bearing rather than pedantic. 1.12's own Interface panel hands this
|
|
/// binding the *string* `"0"` to mean off (`UIOptionsFrame_Save`), and in Lua 5.0 `"0"` is truthy —
|
|
/// so a plain `lua_toboolean` reading would make the reference's own Show Helm checkbox a one-way
|
|
/// switch. Numeric-string coercion is what makes it work, and it is the same shape
|
|
/// [`super::action::truthy_nonzero`] already carries for `UseAction`'s `checkCursor`.
|
|
fn shown_arg(v: &Value) -> bool {
|
|
match v {
|
|
Value::Nil => false,
|
|
Value::Boolean(b) => *b,
|
|
Value::Integer(i) => *i != 0,
|
|
Value::Number(n) => *n != 0.0,
|
|
// A numeric string coerces like Lua's own arithmetic does; a non-numeric one is not a
|
|
// value this API is ever handed, and reads as "show" rather than silently hiding gear.
|
|
Value::String(s) => s
|
|
.to_str()
|
|
.ok()
|
|
.and_then(|t| t.trim().parse::<f64>().ok())
|
|
.is_none_or(|n| n != 0.0),
|
|
_ => true,
|
|
}
|
|
}
|
|
|
|
/// Ask for a state, and queue the flip only if we are not already in it.
|
|
fn want(model: &mut Model, which: WornDisplay, show: bool) {
|
|
let held = match which {
|
|
WornDisplay::Helm => &mut model.helm_shown,
|
|
WornDisplay::Cloak => &mut model.cloak_shown,
|
|
};
|
|
if *held == show {
|
|
return;
|
|
}
|
|
*held = show; // optimistic — the wire edge overwrites it when the server answers
|
|
model.worn_display_toggles.push(which);
|
|
}
|
|
|
|
/// Register the equipment-display globals.
|
|
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
|
let g = lua.globals();
|
|
|
|
for (name, which) in [
|
|
("ShowHelm", WornDisplay::Helm),
|
|
("ShowCloak", WornDisplay::Cloak),
|
|
] {
|
|
g.set(
|
|
name,
|
|
lua.create_function(move |lua, v: Value| {
|
|
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
|
want(&mut model, which, shown_arg(&v));
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
}
|
|
|
|
// The getters the Options row reads its checked state from — `1`/`nil`, the 1.12 boolean
|
|
// return the panel feeds straight into `SetChecked`.
|
|
for (name, which) in [
|
|
("ShowingHelm", WornDisplay::Helm),
|
|
("ShowingCloak", WornDisplay::Cloak),
|
|
] {
|
|
g.set(
|
|
name,
|
|
lua.create_function(move |lua, ()| {
|
|
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
|
let shown = match which {
|
|
WornDisplay::Helm => model.helm_shown,
|
|
WornDisplay::Cloak => model.cloak_shown,
|
|
};
|
|
Ok(shown.then_some(1u32))
|
|
})?,
|
|
)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::WornDisplay;
|
|
use crate::script::UiScript;
|
|
|
|
/// The argument convention, and the one that would silently break the feature: 1.12's own
|
|
/// Options panel calls the setter with the **string** `"0"` to mean "hide", and in Lua 5.0 that
|
|
/// string is truthy. A plain truthiness reading would make Show Helm a one-way switch — on
|
|
/// forever, off never — with no error anywhere to say so.
|
|
#[test]
|
|
fn the_setter_reads_its_argument_numerically_so_the_string_zero_hides() {
|
|
let mut s = UiScript::new().unwrap();
|
|
assert!(
|
|
s.eval::<bool>("return ShowingHelm() == 1").unwrap(),
|
|
"shown by default"
|
|
);
|
|
|
|
s.run(r#"ShowHelm("0")"#).unwrap();
|
|
assert!(s.eval::<bool>("return ShowingHelm() == nil").unwrap());
|
|
assert_eq!(s.take_worn_display_toggles(), vec![WornDisplay::Helm]);
|
|
|
|
s.run(r#"ShowHelm("1")"#).unwrap();
|
|
assert!(s.eval::<bool>("return ShowingHelm() == 1").unwrap());
|
|
assert_eq!(s.take_worn_display_toggles(), vec![WornDisplay::Helm]);
|
|
|
|
// The other spellings the API is handed elsewhere in 1.12: a bare number and nil.
|
|
s.run("ShowCloak(0)").unwrap();
|
|
assert!(s.eval::<bool>("return ShowingCloak() == nil").unwrap());
|
|
s.run("ShowCloak(1)").unwrap();
|
|
assert!(s.eval::<bool>("return ShowingCloak() == 1").unwrap());
|
|
s.run("ShowCloak(nil)").unwrap();
|
|
assert!(s.eval::<bool>("return ShowingCloak() == nil").unwrap());
|
|
assert_eq!(
|
|
s.take_worn_display_toggles(),
|
|
vec![WornDisplay::Cloak, WornDisplay::Cloak, WornDisplay::Cloak]
|
|
);
|
|
}
|
|
|
|
/// The wire verb is a blind flip, so **asking for the state we are already in must send
|
|
/// nothing** — otherwise the Options window's Defaults button, which writes every row on the
|
|
/// page unconditionally, would invert whichever preference was already at its default.
|
|
#[test]
|
|
fn asking_for_the_state_we_are_already_in_sends_nothing() {
|
|
let mut s = UiScript::new().unwrap();
|
|
s.run("ShowHelm(1) ShowCloak(1)").unwrap();
|
|
assert!(
|
|
s.take_worn_display_toggles().is_empty(),
|
|
"both already shown — no packet"
|
|
);
|
|
|
|
s.run("ShowHelm(0)").unwrap();
|
|
assert_eq!(s.take_worn_display_toggles(), vec![WornDisplay::Helm]);
|
|
s.run("ShowHelm(0)").unwrap();
|
|
assert!(
|
|
s.take_worn_display_toggles().is_empty(),
|
|
"the belief moved at the first call, so the second is a no-op"
|
|
);
|
|
}
|
|
|
|
/// The descriptor is the truth: a wire push overrides whatever the VM believed. This is the
|
|
/// edge the app feeds — it is why the app pushes on a `PLAYER_FLAGS` CHANGE and not per frame.
|
|
#[test]
|
|
fn the_wire_push_overrides_the_optimistic_belief() {
|
|
let mut s = UiScript::new().unwrap();
|
|
s.run("ShowHelm(0)").unwrap();
|
|
let _ = s.take_worn_display_toggles();
|
|
assert_eq!(s.worn_display(), (false, true));
|
|
|
|
// The server answers something else entirely (another client toggled it, or our flip
|
|
// crossed a login) — the descriptor wins, and the next ask is computed from it.
|
|
s.set_worn_display(true, false);
|
|
assert_eq!(s.worn_display(), (true, false));
|
|
s.run("ShowHelm(0)").unwrap();
|
|
assert_eq!(
|
|
s.take_worn_display_toggles(),
|
|
vec![WornDisplay::Helm],
|
|
"re-asked against the server's value, not the stale belief"
|
|
);
|
|
}
|
|
}
|