Files
benilla-octo/crates/benilla-ui/src/script/minimap.rs
T
SamandClaude Fable 5 d4690cc67c a cinematic holds your body, and all seven chat windows exist
- a cinematic holds the body while the camera flies: the streaming focus follows the eye — a Tauren's intro opens 1741 yards from the body — so the body's own ground unloaded under it and it fell (z=59.4 to −62 in four seconds, the server yanking it back); it now takes the same hold a teleport takes, released when the focus comes home to a resident tile, and the loading screen stops covering the fly-by, because "the player's ground is missing" is not a load while the eye is deliberately elsewhere; the letterbox measurement prints as an ordinary info line, and scripts/cine.sh plays any one cinematic and prints what the client did to the world while the camera was away
- a cinematic cuts the zone music: the reference asserts exactly what /console EnableMusic 0 asserts, through the same setter — a cut, not a fade, the music pump dead for the duration, resume at +3.000 s — and benilla asserts the runtime flag, never the CVar, or quitting during a 102-second intro would save your music off for good; ambience is deliberately untouched (it has the analogous flag and the cinematic never writes it); the narration has no category at all — its flags route it around every volume slider but not around the Enable Sound Effects gate, so unchecking that silences it — and the listener follows the cinematic camera for the duration
- addons can walk a UI they did not build: GetChildren/GetNumChildren/GetRegions/GetNumRegions land — the basis of the whole reskin genre, and the wall four of the twenty most-installed 1.12 addons died on: pfUI drew nothing before and now paints 495 quads, ShaguDPS 71 → 566, pfQuest 3 → 498 — with the reference's own quirks: the title region is NOT in the list (its creation path never reaches the region linker), hidden regions are returned and counted, oldest first; the Model widget gets its Lua surface (SetModel, SetSequence and siblings, read out of the shipped image, where SetRotation and SetFacing are two names for one slot); and the Minimap carries the nine engine-created Model children no XML declares, built before the frame descent, the ninth being the player arrow Questie indexes as ({Minimap:GetChildren()})[9]
- all seven chat windows come from one template, tabs and scroll buttons included — whether a window is shown has nothing to do with whether its globals resolve, and pfUI reads every piece of that furniture for all seven, unguarded; isDocked only on windows 1 and 2 (the chat-cache's own DOCKED byte), SELECTED_CHAT_FRAME seated beside DEFAULT_CHAT_FRAME (the window you last picked vs the one FrameXML prints to), and the four per-window fields ChatFrame_OnLoad seats; two more one-name walls fell — a Button's label answers to its second spelling NormalText, which KLHThreatMeter's column headers die without, and ABANDON_QUEST_WITH_ITEMS is declared because a dialog table is read at load whatever the client later shows, leaving Questie with zero errors of ours — and the addon harness gains --probe/--eval, one addon's VM answering a question in ~5 s instead of a 90 s corpus run

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 19:50:08 +07:00

186 lines
8.9 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.
//! The `Minimap` method surface — the zoom API the FrameXML zoom buttons drive
//! (`MinimapZoomIn`/`MinimapZoomOut` → `Minimap:SetZoom(Minimap:GetZoom() ± 1)`).
//!
//! Grounded in wow-re's byte-verified minimap node: `get_zoom_levels` (`0x6da9a0`) returns the
//! constant 6, `set_zoom` clamps at 5 and marks the tile grid dirty, and the zoom index feeds the
//! `zoom_to_scale` tables (`0x6da9b0`). The engine core carries only the index
//! ([`MinimapState`]); the app renderer maps it to a world radius and draws the tiles
//! (decision 0203). The two model attrs (`minimapArrowModel=`/`minimapPlayerModel=`) are modeled
//! only as far as the Lua surface can see them: [`apply_model_attrs`] names the nine engine
//! children the ctor built, and the app still draws every arrow from its own art.
//!
//! **The ping** (decision 1596) is the same split, one rung further out: the two methods here are
//! pure seam — `PingLocation` parks a click and `GetPingPosition` reads back what the app
//! published — because the ping's only stored form is a WORLD point the app pins it to, and the
//! engine core has no world. Nothing here holds the ping's position, its lifetime, or its art;
//! putting any of that in Lua is what made the first attempt flaky.
//!
//! **The level persists** (decision 1131). The client keeps two halves: the live indices
//! (`0x86f698` outdoor / `0x86f69c` indoor — our [`MinimapState`]) and the two CVar objects
//! `minimapZoom`/`minimapInsideZoom` (both registered default `"3"`), which are what `Config.wtf`
//! actually stores. `set_zoom` writes *both*; the minimap reset path (`0x6d9008`–`0x6d901f`)
//! re-seeds the live index from the CVar's parsed int. benilla runs the same two halves at the same
//! seam: `SetZoom` below writes the index and the CVar, and the app seeds the widget from the CVar
//! table once, when the in-game UI materializes ([`super::UiScript::set_minimap_zoom`]).
//!
//! The methods live in their own registry table, consulted by the frame `__index` dispatcher only
//! for Minimap frames — the same duck-typing posture as StatusBar's.
use mlua::{Lua, Table};
use super::object::frame_handle_of;
use super::Model;
use crate::widget::{KindState, MinimapState, MINIMAP_ZOOM_LEVELS};
/// Registry key of the Minimap method table (the MAXCSTACK discipline: Lua-side root, named key).
pub(super) const REG_MINIMAP_METHODS: &str = "__benilla_minimap_methods";
/// Run `f` over a frame's Minimap state under one short write borrow. Errors if `this` is not a
/// live Minimap (unreachable through the kind dispatcher, but the method table is a plain Lua
/// value — a caller can fish it out and misapply it).
fn with_minimap<T>(
lua: &Lua,
this: &Table,
f: impl FnOnce(&mut MinimapState) -> T,
) -> mlua::Result<T> {
let h = frame_handle_of(lua, this)?;
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
let frame = model
.arena
.frame_mut(h)
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?;
match &mut frame.kind_state {
KindState::Minimap(m) => Ok(f(m)),
_ => Err(mlua::Error::runtime("not a Minimap")),
}
}
/// `CMinimap::LoadXML 0x4ee2b0`'s model half: assign the two `<Minimap>` model attributes to the
/// nine engine children the ctor already built. `minimapArrowModel` (engine default
/// [`crate::widget::MINIMAP_DEFAULT_ARROW_MODEL`]) goes to children **1–8** via `0x4ee170`'s two loops
/// (`0x4ee1b7` over the five at `+0x320`, `0x4ee204` over the three at `+0x314`);
/// `minimapPlayerModel` (default [`crate::widget::MINIMAP_DEFAULT_PLAYER_MODEL`]) goes to child **9 alone**, via
/// `0x4ee260`.
///
/// The split is what makes the nine *distinguishable* from Lua: without it `GetModel()` is `""` on
/// all nine and nothing in the tuple says which one is the player arrow.
///
/// Runs before the `<Frames>` descent, because `0x4ee2b0` does its own work and only then chains to
/// `CSimpleFrame::LoadXML 0x76a2f0`, which is what recurses into the children.
pub(crate) fn apply_model_attrs(
lua: &Lua,
this: &Table,
arrow: &str,
player: &str,
) -> mlua::Result<()> {
let h = frame_handle_of(lua, this)?;
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
let Some(children) = model.arena.frame(h).map(|f| f.children.clone()) else {
return Ok(());
};
for (i, child) in children
.into_iter()
.take(crate::widget::MINIMAP_ENGINE_CHILDREN)
.enumerate()
{
let path = if i + 1 == crate::widget::MINIMAP_ENGINE_CHILDREN {
player
} else {
arrow
};
if let Some(frame) = model.arena.frame_mut(child) {
if let KindState::Model(state) = &mut frame.kind_state {
state.path = Some(path.to_string());
}
}
}
Ok(())
}
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
let m = lua.create_table()?;
m.set(
"GetZoom",
// Reads whichever index is live — the indoor one (`0x86f69c`) while inside a WMO, else the
// outdoor one (`0x86f698`). The client's `get_zoom_index` routes on the same flag.
lua.create_function(|lua, this: Table| with_minimap(lua, &this, |m| m.active_zoom()))?,
)?;
m.set(
"SetZoom",
lua.create_function(|lua, (this, zoom): (Table, f64)| {
// The client's set_zoom clamps into [0, levels-1] (`0x6daa10`: clamp at 5). A negative
// or fractional Lua number truncates like lua_tonumber → int. It writes the index the
// inside flag selects, so the +/- buttons zoom the map you're actually looking at and
// each mode keeps its own level.
let clamped = (zoom.max(0.0) as u8).min(MINIMAP_ZOOM_LEVELS - 1);
let inside = with_minimap(lua, &this, |m| {
m.set_active_zoom(clamped);
m.inside
})?;
// …and it persists the level in the same breath: `set_zoom` writes the live index AND
// `CVar::Set`s the matching CVar (`minimapInsideZoom` indoors, `minimapZoom` out), which
// is the whole reason a zoom level survives a restart. The borrow above is released
// before this one — both reach the same `Model` app_data.
super::cvars::set_from_engine(
&mut lua.app_data_mut::<Model>().expect("model app_data"),
if inside {
"minimapInsideZoom"
} else {
"minimapZoom"
},
clamped.to_string(),
);
Ok(())
})?,
)?;
m.set(
"GetZoomLevels",
lua.create_function(|lua, this: Table| {
// Validate the receiver like every kind method (the constant is per the client's
// `get_zoom_levels`, but a non-Minimap receiver is still a caller bug).
with_minimap(lua, &this, |_| MINIMAP_ZOOM_LEVELS)
})?,
)?;
m.set(
"PingLocation",
// The minimap click (our own `Minimap_OnClick`, and any addon that replaced it): centre-
// relative offsets in **UI units**, x right / y up — exactly `GetCursorPosition()` minus
// `Minimap:GetCenter()`, both of which are UI-space. Parked, not converted: the app owns
// the view scale, and it drains this in the SAME frame it draws the map so the click
// resolves against the geometry the player actually clicked on (decision 1596).
//
// The receiver is validated but the value lives on the model — one pending click, not one
// per Minimap widget.
lua.create_function(|lua, (this, x, y): (Table, f32, f32)| {
with_minimap(lua, &this, |_| ())?;
lua.app_data_mut::<Model>()
.expect("model app_data")
.minimap_ping_request = Some((x, y));
Ok(())
})?,
)?;
m.set(
"GetPingPosition",
// The live ping's normalized offsets from the widget centre (fractions of the widget
// side, x right / y up — the `MINIMAP_PING` event's own arg2/arg3 space), recomputed by
// the app from the ping's world point every frame one is live. So a caller polling this
// while walking sees the value MOVE, which is the whole point: the ping is pinned to the
// world, not to the map.
//
// Returns **nil, nil** when no ping is live, rather than `(0, 0)`: zero is a real answer
// meaning "the ping is under your feet", and handing it back for "there is no ping" is a
// lie a caller cannot tell from the truth (1203).
lua.create_function(|lua, this: Table| {
with_minimap(lua, &this, |_| ())?;
let ping = lua
.app_data_ref::<Model>()
.expect("model app_data")
.minimap_ping;
Ok(ping.map_or((None, None), |(x, y)| (Some(x), Some(y))))
})?,
)?;
lua.set_named_registry_value(REG_MINIMAP_METHODS, m)?;
Ok(())
}