the action bars are the reference's own, and a gift can be wrapped

- the action-bar cluster is the reference's own: `MainMenuBar.xml`, `ActionBarFrame.xml`, `MultiActionBars.xml` and `BonusActionBarFrame.xml` run off the player's own chain, and our `ActionBar.xml` (2047 lines), `MultiBars.xml` and `StanceBar.xml` are deleted — three, since the stock bonus-bar file declares the shapeshift bar too; one engine verb, `ChangeActionBarPage`, fires `ACTIONBAR_PAGE_CHANGED` and nothing else; one load blocker: stock `MultiActionBars.lua` writes the options window's table from its OnLoad, the reference's own load-order hack, so our options window moves to the reference's toc slot; and ten things the reference does that ours did not: an empty button is HIDDEN while showgrid is 0, the bonus overlay sits one pixel right of the main buttons, every slide takes one frame more than its time, and `ActionButton_ShowGrid` ghosts EVERY ring, occupied included (1938)
- a gift can be wrapped: right-clicking wrapping paper fell through to plain use and sent `CMSG_USE_ITEM`, so nothing happened — you could receive a gift and never give one; the wrap is a displayed-cursor MODE plus a stashed slot, never a payload, so `CursorHasItem()` stays false and every stock gate on it stays shut: right-click arms it, a left-click on a bag slot sends `CMSG_WRAP_ITEM`, and the client applies no eligibility test — the server's `ERR_CANT_WRAP_*` reasons are the error line (1934); a duration-limited item counts down: `SMSG_ITEM_TIME_UPDATE` parks a deadline and the tooltip's `ITEM_DURATION` line runs the enchant countdown's bucket ladder, so "Duration: 1 days" is what the reference renders; an update for an item we do not hold is DROPPED, no queue (1933); and a renamed crowd-control refusal names its mechanic — "Can't do that while stunned" — off `SpellMechanic.dbc` (1943)
- a creature paths on a boat: `SMSG_MONSTER_MOVE_TRANSPORT` carries the transport's guid and deck-local coordinates, and is the attach/detach edge; a deck sample lands on the rider or nowhere, never as a world write a few yards from the map origin; and "there is no terrain under a boat" turned out wrong: the ground probe admits GameObject meshes — a boat's `.wmo` is a GameObject, so the probe hits the deck (1936); `MSG_MOVE_TIME_SKIPPED` is read, not dropped — the reference adds the lag to the observed unit's last-wire stamp, so dropping it scheduled their next packet `lag` ms late, a hitch for every player who stalled zoning in near us; and our own settle hold reports once (1935); and the tradeskill tool order was backwards — focus FIRST, so every two-tool recipe had read "Blacksmith Hammer, Anvil" for the reference's "Anvil, Blacksmith Hammer"; the focus's `hasTool` is 1.0 in the reference too (1942)
- the crowd's second sitting, instruments first: the probe line carries the tail — cpu p95/p99 and the six worst frames — because a mean under budget hides the one frame in twenty over it (raid legs railed at 16.67 with p95 at 18–20 ms) and whether that frame was work or a wait is the first question about a stutter, and the visible meshes are counted by lane; then two cuts — a terrain tile furnishes as 4×4 cells of 4×4 chunks, one mesh, one entity, one draw per 133-yd cell instead of per 33-yd chunk (a city horizon is over a thousand chunks a frame, and a cell changes nothing a pixel can see; Stormwind 10.55 → 10.32 `cpu_ms`), and an authored point light is our own `WorldPointLight`, not Bevy's `PointLight`, which was only a data carrier while Bevy ran its whole light lane over every lamp, ~2 % of the alone frame; the sitting's record is 1945, and the code says so (1945)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-04 20:54:27 +07:00
co-authored by Claude Fable 5.1
parent 72e78ebc92
commit 2f7cfcabf5
107 changed files with 3407 additions and 3490 deletions
+180 -2
View File
@@ -105,6 +105,13 @@ pub struct ContainerSlot {
/// hands over the row's name plus the three facts the line law needs to place and paint it.
/// Empty = unenchanted, or the enchant DBC never loaded. Decisions 0915/0920.
pub enchants: Vec<super::EnchantView>,
/// **The instance's remaining LIFETIME in milliseconds** — a duration-limited item counting
/// down to its own destruction (a conjured stone, a holiday gift, a timed quest item). Its
/// only feed is `SMSG_ITEM_TIME_UPDATE`, parked as an absolute deadline and recomputed on
/// read ([`crate::items::Items::duration_remaining_display_ms`] in benilla-app), floored to
/// the whole second so the snapshot moves once a second rather than every frame.
/// `None` = no timer, which is nearly every item. Decision 1933.
pub duration_ms: Option<u64>,
/// The petition this item names, when it is a signable charter — **line 3 of the tooltip's
/// emission law**, between the NAME and the green `ITEM_SIGNABLE` line.
///
@@ -263,6 +270,47 @@ impl super::UiScript {
std::mem::take(&mut self.model_mut().container_destroys)
}
/// **Arm the gift-wrap cursor** on the paper at `(bag, slot)` — the app's call when a
/// right-click takes the reference's begin-wrap arm (`ItemInfo::begins_gift_wrap`).
///
/// Three acts, exactly the three `0x5edea0` performs (wow-re
/// `object-layer/scratch/gift-wrap-law.md`): the paper's slot reads **locked**, the displayed
/// cursor becomes **mode 2** (`Interface\Cursor\Cast.blp` — the same art the cast cursor
/// uses, table `0x853b88` slot 2), and **nothing goes on the wire**. What completes it is the
/// next left-click on a container slot. Decision 1934.
pub fn arm_gift_wrap(&mut self, bag: i64, slot: u32) {
let mut model = self.model_mut();
model.pending_wrap = Some(PendingWrap { bag, slot });
model.ui_cursor = Some(UiCursorMode::Cast);
model.ui_cursor_dirty = true;
cursor::queue_lock_changed(&mut model, bag, slot);
}
/// **Cancel an armed gift wrap and nothing else** — disarm, unlock the paper, reset the
/// cursor. Deliberately narrower than `ClearCursor`, which also drops any held payload:
/// what wow-re records is that a right-click "uses the item and cancels the wrap", not by
/// which of the two routes, so this touches only the half the finding actually names.
pub fn cancel_gift_wrap(&mut self) -> Option<PendingWrap> {
let mut model = self.model_mut();
let wrap = model.pending_wrap.take()?;
cursor::queue_lock_changed(&mut model, wrap.bag, wrap.slot);
model.ui_cursor = None;
model.ui_cursor_dirty = true;
Some(wrap)
}
/// Is a gift wrap armed, and on which slot? Read by the app to decide whether a right-click
/// is cancelling one.
pub fn gift_wrap_armed(&self) -> Option<PendingWrap> {
self.model_ref().pending_wrap
}
/// Drain the `(giftBag, giftSlot, itemBag, itemSlot)` quads a completed wrap queued — the app
/// resolves each pair to the wire's bag/slot addressing and sends `CMSG_WRAP_ITEM`.
pub fn take_container_wraps(&mut self) -> Vec<(i64, u32, i64, u32)> {
std::mem::take(&mut self.model_mut().container_wraps)
}
/// The mode the last FrameXML cursor call armed — `None` after a `ResetCursor`. This is the
/// *value* of the most recent write; whether a write happened at all is
/// [`Self::take_cursor_write`], and that is what the app acts on (decision 1061).
@@ -285,6 +333,19 @@ impl super::UiScript {
}
}
/// **The armed gift wrap** — which piece of wrapping paper the next container click will spend.
///
/// The reference holds this as globals beside the displayed-cursor mode rather than as a cursor
/// *payload*; the distinction is load-bearing and is why this is its own type instead of a
/// [`cursor::CursorPayload`] variant (see [`super::Model::pending_wrap`]).
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct PendingWrap {
/// The paper's container id, in the same bag space every cursor verb speaks.
pub bag: i64,
/// The paper's 1-based slot within that container.
pub slot: u32,
}
/// A **displayed-cursor override** the FrameXML cursor family arms (wow-re cursor-system.md §7): the
/// single "displayed mode" (`0xbe2c2c`) the real client swaps to while a UI element wants a non-base
/// cursor, restored to the base mode by `ResetCursor`. The app maps each to the matching
@@ -330,6 +391,49 @@ pub enum UiCursorMode {
///
/// Returns whether the caller should repaint (the source-slot lock or the held payload changed).
fn pickup_container_item(model: &mut super::Model, bag: i64, slot: u32) -> bool {
// **The gift-wrap completion, ahead of the whole gesture** (decision 1934). `0x4f9b30` is the
// *only* entry to the wrap sender `0x5edfc0` image-wide — which is why the paper doll cannot
// finish a wrap and an equipped item is therefore unwrappable — and it takes this branch
// before it looks at the cursor at all.
if let Some(wrap) = model.pending_wrap {
// An empty or unresolved slot **bails and leaves the wrap armed** — the reference's own
// edge. Nothing is spent and the cursor keeps its mode, so the next click can still land
// on a real item.
if !model
.containers
.get(&bag)
.and_then(|c| c.slots.get(&slot))
.is_some_and(|s| s.item_id != 0)
{
return false;
}
// **The paper has to still be there.** `0x5edfef` bails when the stashed gift GUID no
// longer resolves, and — the edge worth transcribing — it then sends *and clears*
// nothing: the wrap stays armed and the paper's slot stays locked. So this returns
// without touching either.
if !model
.containers
.get(&wrap.bag)
.and_then(|c| c.slots.get(&wrap.slot))
.is_some_and(|s| s.item_id != 0)
{
return false;
}
// No eligibility test of any kind. The wrap arm reads each item's `ITEM_FIELD_CONTAINED`
// and nothing else — not flags, not stack count, not bind state — and the client ships six
// `ERR_CANT_WRAP_*` strings for the server to answer with (`SMSG_INVENTORY_CHANGE_FAILURE`
// reasons 43-48). Same law as the openable click (decision 0896): gating locally would eat
// the click in silence and cost the player their error line.
model.pending_wrap = None;
model.container_wraps.push((wrap.bag, wrap.slot, bag, slot));
// The paper unlocks and the cursor resets **immediately**, not on the server's answer:
// `0x5edfc0` calls `0x5eded0` at `0x5ee0aa`, right after the send. The target is never
// locked at all.
cursor::queue_lock_changed(model, wrap.bag, wrap.slot);
model.ui_cursor = None;
model.ui_cursor_dirty = true;
return true;
}
match model.cursor.take() {
None => {
let picked = model
@@ -653,7 +757,14 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
let held = matches!(
&model.cursor,
Some(CursorPayload::Item(c)) if c.bag == bag && c.slot == slot
);
) ||
// An armed gift wrap locks its paper for as long as it stands (`0x4953e0` at
// `0x5edeb5`) — the same dimming a held item's source slot shows, and released by
// the send or by any cancel rather than by the server (decision 1934).
matches!(
&model.pending_wrap,
Some(w) if w.bag == bag && w.slot == slot
);
(
model
.containers
@@ -858,7 +969,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
#[cfg(test)]
mod tests {
use super::{ContainerMove, ContainerSlot, ContainerState};
use super::{ContainerMove, ContainerSlot, ContainerState, PendingWrap, UiCursorMode};
use crate::script::UiScript;
fn backpack() -> ContainerState {
@@ -866,6 +977,7 @@ mod tests {
slots.insert(
1,
ContainerSlot {
duration_ms: None,
petition: None,
already_bound: false,
bar_placeable: true,
@@ -903,6 +1015,71 @@ mod tests {
}
}
/// **The gift-wrap state machine** (decision 1934), all four of its edges. Armed by the app's
/// right-click on a wrapper, the arm locks the paper and paints cursor mode 2 and sends
/// nothing; the next LEFT-click on a container slot spends it; an empty slot bails and leaves
/// it armed; and any `ClearCursor` cancels it.
#[test]
fn an_armed_gift_wrap_spends_on_the_next_container_click() {
let mut s = UiScript::new().unwrap();
s.set_container(0, Some(backpack()));
// The arm: nothing on the wire, the paper reads locked, the cursor is mode 2, and the
// held-item predicate stays FALSE — this is a cursor mode, not a payload.
s.arm_gift_wrap(0, 1);
assert_eq!(s.gift_wrap_armed(), Some(PendingWrap { bag: 0, slot: 1 }));
assert!(s.take_container_wraps().is_empty(), "arming sends nothing");
assert_eq!(s.take_cursor_write(), Some(Some(UiCursorMode::Cast)));
assert!(
s.eval::<bool>("local _, _, locked = GetContainerItemInfo(0, 1) return locked")
.unwrap(),
"the paper dims while the wrap stands"
);
assert!(
!s.eval::<bool>("return CursorHasItem()").unwrap(),
"the wrap writes no payload, so every stock CursorHasItem() gate stays shut"
);
// A click on an EMPTY slot bails and leaves the wrap armed — the reference's own edge.
s.eval::<()>("PickupContainerItem(0, 9)").unwrap();
assert_eq!(s.gift_wrap_armed(), Some(PendingWrap { bag: 0, slot: 1 }));
assert!(s.take_container_wraps().is_empty());
// A click on a real item spends it: the paper's pair leads, as the wire does.
s.eval::<()>("PickupContainerItem(0, 4)").unwrap();
assert_eq!(s.take_container_wraps(), vec![(0, 1, 0, 4)]);
assert_eq!(s.gift_wrap_armed(), None);
assert_eq!(
s.take_cursor_write(),
Some(None),
"the cursor resets on the send, not on the server's answer"
);
assert!(
s.cursor_payload().is_none(),
"the completing click is spent on the wrap, not on picking the target up"
);
}
/// Any `ClearCursor` cancels an armed wrap — the reference tests it FIRST inside
/// `ClearCursor 0x495190` and does so ungated by either parameter, so all 70 of its call
/// sites cancel (decision 1934).
#[test]
fn clear_cursor_cancels_an_armed_gift_wrap() {
let mut s = UiScript::new().unwrap();
s.set_container(0, Some(backpack()));
s.arm_gift_wrap(0, 1);
let _ = s.take_cursor_write();
s.eval::<()>("ClearCursor()").unwrap();
assert_eq!(s.gift_wrap_armed(), None);
assert_eq!(s.take_cursor_write(), Some(None), "and the mode goes back");
assert!(
!s.eval::<bool>("local _, _, locked = GetContainerItemInfo(0, 1) return locked")
.unwrap(),
"the paper unlocks with the cancel — the lock never waited on a server"
);
// …and the next container click is an ordinary pickup again.
s.eval::<()>("PickupContainerItem(0, 1)").unwrap();
assert!(s.take_container_wraps().is_empty());
assert!(s.cursor_payload().is_some(), "a plain pickup, as before");
}
#[test]
fn container_snapshot_reads() {
let mut s = UiScript::new().unwrap();
@@ -1032,6 +1209,7 @@ mod tests {
state.slots.insert(
5,
ContainerSlot {
duration_ms: None,
petition: None,
already_bound: false,
bar_placeable: true,