Comments in benilla-ui: one line of fact each, no history, no record numbers (#143)
Part of #41: the comments of `benilla-ui` (the Lua UI engine, its FrameXML loader and its widgets) are rewritten to the rule in `docs/METHOD.md`, which is what the code does and the reference fact behind it, in a line. 43,140 comment lines become 15,161. - **The mechanical strip.** The citations that were only a private record number go first, and each edit is shown to be a pure removal. - **Wrong facts corrected.** The rewrite corrects the comments that stated wrong facts. Among them: - `0x64a480` is the case-sensitive compare (a byte `repz cmpsb`), and `0x64a4c0` the case-insensitive one. - A frame's id sits at `+0xb0`, not `+0xb4`. `EnableKeyboard`/`IsKeyboardEnabled` are `0x776ec0`/`0x776f90`. - A region's unpinned edge has no fallback to its owner's rect: the owner supplies only the scale. - `$parent` expands to the nearest named ancestor, or "Top" when none is named (`0x76c5b0`), not simply to the parent. - A StatusBar fills from its internal normalised fraction, not from `GetValue()`. - The addresses of `StartDuel` and `StartDuelUnit` were swapped. - Many "untraced" notes now give the traced reference fact: the drag threshold, the per-grip sizing edges, the empty-`autoFocus` skip, and `SetTexCoordModifiesRect`'s default. - Stock FrameXML line citations are checked against the stock 1.12 files and corrected. `ref-` file names become the stock file names. - **Deviation labels.** "Deviation:" now marks only a choice with a reason. A surface that exists only for the retired Era addon target says so plainly: it is not 1.12. - **Verification.** Every file strips to identical code, and `cargo fmt --check` is clean. Nine second readers compared every file old against new, restored the facts the rewrite had dropped, and checked each new citation against the code, the stock files, vmangos and the reference. Behaviour differences the rewrite surfaced are filed as their own issues (#131–#142), and more are being validated. The comments here state what the code does. String literals are untouched; they are #92. 🤖 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:
co-authored by
Claude Opus 5.5
parent
8505187668
commit
fd386e75ae
@@ -1,15 +1,7 @@
|
||||
//! The cursor payload (decision 0216, slice 1) — the client's payload-mode global `[0xb4d900]`
|
||||
//! as a typed enum, and the drag-gesture mechanics (`RegisterForDrag`/`OnDragStart`/`OnDragStop`/
|
||||
//! `OnReceiveDrag`) that transition it. One seam for every surface — bags, the paper doll
|
||||
//! ([`doll`], decision 0208 phase 1b), action bars ([`bar`], decision 0216 §7/0218 §4), and the
|
||||
//! spellbook ([`super::spellbook`]'s `PickupSpell`, slice 5 — the [`CursorSpell`] producer) — so
|
||||
//! sounds, `CURSOR_UPDATE`, grid events, and lock display can't drift apart per window.
|
||||
//!
|
||||
//! [`container`](super::container)'s `pickup_container_item`, [`doll`]'s
|
||||
//! `pickup_inventory_item`, and [`bar`]'s `pickup_action`/`place_action` (the surface-specific
|
||||
//! transition bodies) all route through [`queue_cursor_update`]/[`queue_lock_changed`], the one
|
||||
//! place those events fire from. [`bar`], [`doll`], and [`drag`] are split out purely for size —
|
||||
//! this file keeps the payload types, the shared transition seam, and `install`.
|
||||
//! The cursor payload: the client's payload-mode global `[0xb4d900]` as a typed enum, and the drag
|
||||
//! gestures ([`drag`]) that change it. Bags, the paper doll ([`doll`]), the action bars ([`bar`])
|
||||
//! and the spellbook all transition it through [`queue_cursor_update`] and [`queue_lock_changed`],
|
||||
//! so sounds, `CURSOR_UPDATE`, grid events and lock display fire from one place.
|
||||
|
||||
use mlua::{Lua, Value};
|
||||
|
||||
@@ -27,28 +19,17 @@ pub(crate) use drag::{
|
||||
abandon_drag, arm_drag, maybe_start_drag, take_drag, DragGesture, DragRelease,
|
||||
};
|
||||
|
||||
/// The sentinel bag id that folds the player's EQUIPPED slots into the ONE payload space
|
||||
/// (decision 0216 §1, extended to the paper doll by decision 0208 phase 1b): a
|
||||
/// [`CursorItem`]/[`super::container::ContainerMove`] whose `bag` is `EQUIPMENT_BAG` addresses
|
||||
/// `slot` as a 1-based live-API inventory slot id — `GetInventorySlotInfo`'s own numbering
|
||||
/// (HeadSlot=1 … TabardSlot=19, `char_stats::SLOT_INFO`'s convention), not a bag's contents.
|
||||
/// Disjoint from every real bag id — including the ERA's own negative container ids, which are
|
||||
/// real API surface (−1 = `BANK_CONTAINER`, decision 0604; −2 = the keyring) — so a `match` on
|
||||
/// `bag` can never confuse the spaces (its original value −1 collided with the bank the day the
|
||||
/// bank landed; the sentinel is engine-internal and never crosses the Lua boundary as a value,
|
||||
/// verified across the XML fleet at the change). Ammo (id 0) stays OUT of this space — a named
|
||||
/// deferral ([`doll::pickup_inventory_item`]'s own range guard).
|
||||
/// Sentinel bag id for the player's equipped slots, whose `slot` is a 1-based inventory slot id
|
||||
/// (`GetInventorySlotInfo`'s, HeadSlot = 1). Never a Lua value, and disjoint from every real bag
|
||||
/// id, -1 the bank and -2 the keyring included. Ammo (slot 0) is outside it.
|
||||
pub const EQUIPMENT_BAG: i64 = -100;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
// The payload
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// What the cursor carries — the client's payload-mode global [0xb4d900] as a typed enum
|
||||
/// (1 = live item, 3 = spell, **4 = pet action**, **8 =
|
||||
/// macro**, **10 = stabled pet**; our Action arm is the client's bar-slot pickup; the money arm
|
||||
/// is mode 2 ([`CursorMoney`], 1962/1965) and the preview arm stays unbuilt). One transition seam
|
||||
/// for every surface, so sounds, CURSOR_UPDATE, and lock display can't drift apart per window (decision 0216).
|
||||
/// What the cursor carries, by the client's payload mode `[0xb4d900]`: 1 item, 2 money, 3 spell,
|
||||
/// 4 pet action, 5 vendor row, 8 macro, 10 stabled pet; `Action` is the bar-slot pickup.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum CursorPayload {
|
||||
Item(CursorItem),
|
||||
@@ -56,125 +37,69 @@ pub enum CursorPayload {
|
||||
Action(CursorAction),
|
||||
Macro(CursorMacro),
|
||||
PetAction(CursorPetAction),
|
||||
/// A vendor row grabbed off the merchant window — **mode 5** (see [`CursorMerchantItem`]).
|
||||
///
|
||||
/// Deliberately NOT `Item`: the reference's `CursorHasItem 0x4895d0` answers truthy for modes
|
||||
/// **1 and 9 only**, so all thirteen stock `CursorHasItem()` gates (TradeFrame, MailFrame,
|
||||
/// PlayerFrame, TargetFrame, PetFrame, StaticPopup, the keyring…) exclude a vendor cursor.
|
||||
/// Making this an `Item` variant would silently open every one of them.
|
||||
/// Mode 5, a vendor row. Not an `Item`: `CursorHasItem` (`0x4895d0`) is true for modes 1 and 9
|
||||
/// only, so all thirteen stock `CursorHasItem()` gates stay shut while one is held.
|
||||
Merchant(CursorMerchantItem),
|
||||
/// A pet picked up from the stable window — **mode 10** (see [`CursorStablePet`]).
|
||||
///
|
||||
/// Mode 10 was recorded as "class/talent ability (DBC)" until the bytes corrected it (decision
|
||||
/// 1677): `0x495020` is the stabled-pet grab, `[0xb4d900] = 10` is written at exactly one site
|
||||
/// image-wide, and it is inside it. benilla built the stable's drag frame-locally on the
|
||||
/// strength of that earlier reading — this variant is what puts it back on the shared cursor,
|
||||
/// so a stable pet dropped on the world or another window clears through the same path as
|
||||
/// every other payload.
|
||||
/// Mode 10, a pet taken from the stable; its grab `0x495010` is the only writer of mode 10.
|
||||
StablePet(CursorStablePet),
|
||||
/// Coins picked up off a money frame — **mode 2** (see [`CursorMoney`]).
|
||||
/// Mode 2, coins taken off a money frame.
|
||||
Money(CursorMoney),
|
||||
}
|
||||
|
||||
/// Copper held on the cursor — payload **mode 2**, the client's `[0xb4e2f0]` amount written by
|
||||
/// `0x494cc0` from `PickupPlayerMoney 0x48abc0` with `LOOTWINDOWCOINSOUND`. The purse is never
|
||||
/// debited by the pickup:
|
||||
/// the stock `MoneyTypeInfo["PLAYER"]` shows `GetMoney() - GetCursorMoney() - …`, the subtraction
|
||||
/// being how the held coins leave the display while the money stays yours (1962).
|
||||
/// Copper held on the cursor, mode 2 (`[0xb4e2f0]`, set by `PickupPlayerMoney`, `0x48abc0`). The
|
||||
/// purse is not debited; the stock money frame subtracts `GetCursorMoney()` (`MoneyFrame.lua:17`).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CursorMoney {
|
||||
pub copper: u32,
|
||||
}
|
||||
|
||||
/// A vendor row held on the cursor — payload **mode 5**, set by `PickupMerchantItem 0x4fb760`.
|
||||
///
|
||||
/// **A buy-on-drop cursor, not an item cursor.** The reference writes three globals and this
|
||||
/// mirrors all three: the item entry (`0xb4b41c`), the display id that drives the icon
|
||||
/// (`0xb4d8ec` — we carry the resolved texture path, which is the same thing one hop later), and
|
||||
/// the **0-based** vendor row (`0xb4b420`). Dropping it on a bag slot sends
|
||||
/// `CMSG_BUY_ITEM_IN_SLOT 0x1a3`; dropping it on the world, pressing ESC, or the vendor window
|
||||
/// closing is a plain clear with **no packet**.
|
||||
///
|
||||
/// No Lua predicate reports it. `CursorHasItem`, `CursorHasSpell`, `CursorHasMoney` and
|
||||
/// `CursorCanGoInSlot` are all nil for mode 5 — no registered binding exposes it at all.
|
||||
/// A vendor row held on the cursor, mode 5, set by `PickupMerchantItem` (`0x4fb760`). A drop on a
|
||||
/// bag slot is the buy (`CMSG_BUY_ITEM_IN_SLOT`, `0x1a3`); in the reference a drop on the world,
|
||||
/// ESC or the vendor closing clears it with no packet. No registered binding reports mode 5 to Lua.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CursorMerchantItem {
|
||||
/// The row's item template entry — `0xb4b41c`, and what the buy addresses.
|
||||
/// The row's item entry (`0xb4b41c`), which the buy names.
|
||||
pub item_id: u32,
|
||||
/// The vendor row this came from, **0-based** (`0xb4b420` = `trunc(arg) - 1`). Kept because
|
||||
/// the reference keeps it: a second `PickupMerchantItem` naming the same row is a toggle-off,
|
||||
/// which is the only thing that reads it back.
|
||||
/// The 0-based vendor row (`0xb4b420` = `trunc(arg) - 1`): a second `PickupMerchantItem` on the
|
||||
/// same row drops the grab.
|
||||
pub row: u32,
|
||||
/// The icon drawn at the mouse. The reference stores `ItemDisplayInfo`'s id (`0xb4d8ec`) and
|
||||
/// resolves the art from it; we hold the already-resolved path, which is the same value one
|
||||
/// hop later and the shape every other payload here uses.
|
||||
/// The icon at the mouse, resolved from the display id the reference stores (`0xb4d8ec`).
|
||||
pub texture: Option<String>,
|
||||
}
|
||||
|
||||
/// A pet held on the cursor from the stable window — payload **mode 10** (`0x495020`, the grab
|
||||
/// `0x495010`).
|
||||
///
|
||||
/// Mode 10 was recorded as "class/talent ability (DBC)" until the bytes corrected it (decision
|
||||
/// 1677): `[0xb4d900] = 10` is written at exactly one site image-wide and it is inside the
|
||||
/// stabled-pet grab, whose id global `[0xb4e300]` holds a **stable index**, not a spell. benilla
|
||||
/// first built the stable's drag
|
||||
/// frame-locally on the strength of that earlier reading; this puts it on the shared cursor.
|
||||
/// A pet held on the cursor from the stable window, mode 10, set by the grab `0x495010`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CursorStablePet {
|
||||
/// The slot the grab came from, in the Lua index space `PickupStablePet` takes (`0` = the
|
||||
/// summoned pet, `1..=2` a stable slot) — the client's `[0xb4e300]`.
|
||||
/// `[0xb4e300]`, in `PickupStablePet`'s index space: 0 the summoned pet, 1..=2 a stable slot.
|
||||
pub slot: u8,
|
||||
/// The family icon drawn at the mouse. Not merely decoration: a **non-empty icon path is the
|
||||
/// grab's own gate** (`0x495010` refuses without one), so a payload of this shape always has
|
||||
/// one.
|
||||
/// The family icon at the mouse, never empty: the grab refuses a pet without one (`0x495010`).
|
||||
pub texture: String,
|
||||
}
|
||||
|
||||
/// The item currently held on the cursor (`PickupContainerItem`/`SplitContainerItem`/
|
||||
/// `PickupInventoryItem` set it). The real client's drag state: it names *where the item was
|
||||
/// picked from* (so a second click routes the swap) and carries the icon the app draws at the
|
||||
/// mouse. Purely transient — cleared on place/cancel; the server's field updates do the actual
|
||||
/// move.
|
||||
/// An item held on the cursor: where it was picked from, so the next click routes the swap, and
|
||||
/// the icon at the mouse. The server's field updates do the actual move.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CursorItem {
|
||||
/// Live-API bag id the item was picked up from (0 = backpack, 1..=4 an equipped bag,
|
||||
/// [`EQUIPMENT_BAG`] the player's own equipped slots).
|
||||
/// The source bag id in the Lua API's numbering, or [`EQUIPMENT_BAG`].
|
||||
pub bag: i64,
|
||||
/// 1-based slot the item was picked up from.
|
||||
/// The 1-based source slot.
|
||||
pub slot: u32,
|
||||
/// The picked item's template entry (`GetCursorInfo`'s itemID).
|
||||
pub item_id: u32,
|
||||
/// The icon texture path to draw at the mouse (`Interface\Icons\…`); `None` if unresolved.
|
||||
pub texture: Option<String>,
|
||||
/// The item link (`GetCursorInfo`'s itemLink), when known.
|
||||
pub link: Option<String>,
|
||||
/// A split carry (`SplitContainerItem` picked up `n` of the stack); `None` = the whole stack.
|
||||
/// `Some(n)` for a split of `n`, `None` for the whole stack.
|
||||
pub count: Option<u32>,
|
||||
/// The item's quality (0..6) at pickup, carried so `DELETE_ITEM_CONFIRM` (a world drop) can
|
||||
/// report it without a container round-trip.
|
||||
/// Quality (0..6) at pickup, for a world drop's `DELETE_ITEM_CONFIRM`.
|
||||
pub quality: Option<u32>,
|
||||
/// The 1-based live-API inventory slot ids this item could be EQUIPPED into (empty = not
|
||||
/// equippable), captured at pickup from the source's own `equip_slots` (the container slot's
|
||||
/// or the doll slot view's — decision 0208 phase 1b's "the fit rule").
|
||||
/// [`doll::pickup_inventory_item`] reads it to decide a place-onto-doll-slot's fit;
|
||||
/// [`doll::cursor_can_go_in_slot`] serves it straight to `CURSOR_UPDATE`'s highlight.
|
||||
/// The 1-based inventory slot ids the item can be equipped into, empty if none: the paper
|
||||
/// doll's place and `CursorCanGoInSlot` read it.
|
||||
pub equip_slots: Vec<u8>,
|
||||
/// Whether this item may be placed on an ACTION-BAR slot, captured at pickup from the
|
||||
/// source's own `bar_placeable` — `PlaceAction`'s only item filter (decision 0666).
|
||||
/// Whether `PlaceAction` accepts it, its only item filter.
|
||||
pub bar_placeable: bool,
|
||||
}
|
||||
|
||||
/// How many items the cursor is actually holding, for a payload picked up from a bag.
|
||||
///
|
||||
/// [`CursorItem::count`] is `None` for a **whole-stack** pickup — it records only what a *split*
|
||||
/// carry took — so reading it as `unwrap_or(1)` answers 1 for a stack of twenty. That is a
|
||||
/// display wart on a mail attachment and a real arithmetic error on an auction deposit, which is
|
||||
/// computed per stack: it would quote a twentieth of what the server then charges. So the true
|
||||
/// size comes back from the source slot, which still holds the stack (a pickup locks the slot, it
|
||||
/// does not empty it).
|
||||
///
|
||||
/// Falls back to 1 only when the source slot is genuinely unreadable — a bag the app has not
|
||||
/// pushed, or a slot that changed under us.
|
||||
/// How many items the cursor holds. A whole-stack pickup has `count: None`, so the size is read
|
||||
/// from the source slot, which a pickup locks but does not empty; 1 if that slot is unreadable.
|
||||
pub(super) fn held_count(model: &super::Model, item: &CursorItem) -> u32 {
|
||||
if let Some(n) = item.count {
|
||||
return n.max(1);
|
||||
@@ -187,23 +112,19 @@ pub(super) fn held_count(model: &super::Model, item: &CursorItem) -> u32 {
|
||||
.map_or(1, |s| s.count.max(1))
|
||||
}
|
||||
|
||||
/// A spell payload ([`super::spellbook`]'s `PickupSpell` produces it): the spellbook slot it was
|
||||
/// picked from, its book (`"spell"`/`"pet"`, the Era `GetCursorInfo` shape), and the resolved
|
||||
/// spell id.
|
||||
/// A spell payload, mode 3, from `PickupSpell` on the player's spellbook.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CursorSpell {
|
||||
pub book_slot: u32,
|
||||
pub book_type: String,
|
||||
pub spell_id: u32,
|
||||
pub texture: Option<String>,
|
||||
/// `Attributes & 0x40` (`SPELL_ATTR_PASSIVE`) — a passive cannot go on the action bar
|
||||
/// (`PlaceAction`'s other filter, `0x4e63ad`; decision 0666).
|
||||
/// `Attributes & 0x40` (`SPELL_ATTR_PASSIVE`): `PlaceAction` refuses a passive (`0x4e63ad`).
|
||||
pub passive: bool,
|
||||
}
|
||||
|
||||
/// An action-slot payload ([`bar`]'s `PickupAction`/`place_action` hop produces it): the source
|
||||
/// action slot, the packed action's kind byte (SPELL/MACRO/ITEM — decision 0216's
|
||||
/// `CMSG_SET_ACTION_BUTTON` type byte), and the action id itself.
|
||||
/// An action-bar payload from `PickupAction`: the source slot, the action's type byte as
|
||||
/// `CMSG_SET_ACTION_BUTTON` packs it (spell, macro or item) and its id.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CursorAction {
|
||||
pub src_slot: u32,
|
||||
@@ -212,72 +133,45 @@ pub struct CursorAction {
|
||||
pub texture: Option<String>,
|
||||
}
|
||||
|
||||
/// A pet-bar payload ([`pet`]'s `PickupPetAction` produces it) — the client's cursor mode **4**,
|
||||
/// whose payload global `[0xb4e2f8]` is the picked slot's **packed word, copied verbatim** (its
|
||||
/// sole writer `0x494f0c` is a plain `mov edx,[edi]; mov [0xb4e2f8],edx`; decision 1010).
|
||||
///
|
||||
/// The word is the payload, and that is the whole design: the drop trampoline `0x4bce00` forwards
|
||||
/// this dword into the assign core without reading a field of it, so a slot's contents after any
|
||||
/// accepted drop is a word that already existed elsewhere in client state. Nothing is encoded at
|
||||
/// drop time and nothing here needs decoding.
|
||||
///
|
||||
/// This payload is **pet-bar-only**. `PlaceAction` refuses it (`PlaceAction 0x4e62e0`'s
|
||||
/// accept table: pet actions and class abilities are the refused modes, macros are not), and
|
||||
/// nothing else produces it — which is also why a pet bar can only ever be *rearranged*, never
|
||||
/// populated from the spellbook: its contents are the server's.
|
||||
/// A pet-bar payload, mode 4, from `PickupPetAction` or the pet spellbook: the packed action word
|
||||
/// copied verbatim into `[0xb4e2f8]` (`0x494f0c`) and dropped undecoded (`0x4bce00`). Pet bar
|
||||
/// only: `PlaceAction` refuses it (`0x4e62e0`).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CursorPetAction {
|
||||
/// The 1-based pet bar slot it was picked from.
|
||||
/// The 1-based pet bar slot it came from, 0 from the spellbook.
|
||||
pub src_slot: u32,
|
||||
/// The slot's packed word, verbatim.
|
||||
/// The packed action word, verbatim.
|
||||
pub packed: u32,
|
||||
/// `Attributes & 0x40` for a spell word — carried because the assign core's one source filter
|
||||
/// tests it at DROP time, not at pickup (`0x4bc9f8`).
|
||||
/// `Attributes & 0x40` of a spell word, tested at the drop, not the pickup (`0x4bc9f8`).
|
||||
pub passive: bool,
|
||||
pub texture: Option<String>,
|
||||
}
|
||||
|
||||
/// A macro payload ([`super::macros`]'s `PickupMacro` produces it) — the client's cursor mode
|
||||
/// **8**, whose payload global is the bare macro id (`[0xb4e2fc]`, the ONE non-item/non-spell
|
||||
/// payload `PlaceAction 0x4e62e0` accepts).
|
||||
/// Unlike every other arm this one carries no source slot to swap back to: a macro lives in the
|
||||
/// macro table, not in the surface it was dragged from, so a refused place just leaves it held.
|
||||
/// A macro payload from `PickupMacro`, mode 8: the bare macro id (`[0xb4e2fc]`), which
|
||||
/// `PlaceAction` accepts (`0x4e62e0`). It has no source slot, so a refused place leaves it held.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CursorMacro {
|
||||
/// The 1-based macro index (1..=18 account, 19..=36 character — [`super::macros`]'s space).
|
||||
/// The 1-based macro index: 1..=18 account, 19..=36 character.
|
||||
pub index: u32,
|
||||
pub texture: Option<String>,
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
// The one transition seam: CURSOR_UPDATE / ITEM_LOCK_CHANGED / DELETE_ITEM_CONFIRM
|
||||
// The transition events
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Queue `CURSOR_UPDATE` — fired on EVERY payload transition (pickup, place/clear, cancel,
|
||||
/// `ClearCursor`, `DeleteCursorItem`, a world-drop clear). One push per transition.
|
||||
///
|
||||
/// Also derives `ACTIONBAR_SHOWGRID`/`ACTIONBAR_HIDEGRID` (decision 0216 §7) off
|
||||
/// [`Model::cursor_grid_shown`]'s mirror against the CURRENT `model.cursor` (already the
|
||||
/// post-transition state at every call site — every caller mutates `model.cursor` before calling
|
||||
/// this): a None→Some edge (any bar-droppable arm, any surface — bags/doll/actions alike) shows the
|
||||
/// bar's drop grid, Some→None hides it, Some→Some (the action hop) touches neither, so one
|
||||
/// gesture never churns HIDE+SHOW. This is the one seam every pickup/place/clear already routes
|
||||
/// through, so no call site needs to know about grid events at all.
|
||||
/// Queue `CURSOR_UPDATE`, once per payload transition, plus a grid event when what the bars can
|
||||
/// take changes. Call it after `model.cursor` holds the new payload; an action hop keeps the grid
|
||||
/// up rather than hiding and reshowing it.
|
||||
pub(crate) fn queue_cursor_update(model: &mut Model) {
|
||||
model
|
||||
.pending_events
|
||||
.push(("CURSOR_UPDATE".to_string(), Vec::new()));
|
||||
// The two grids are derived off DIFFERENT predicates, because the reference fires them from
|
||||
// different places and the payload spaces do not overlap (decision 1010):
|
||||
//
|
||||
// - the ACTION bar's grid follows "is anything held that could land there" — every arm except
|
||||
// the pet one, which `PlaceAction` refuses outright, and the vendor row (mode 5): its grab
|
||||
// goes through the shared setter `0x4950f0`, whose `ACTIONBAR_SHOWGRID` branch is taken for
|
||||
// mode 7 alone (`0x495106`/`0x49513a`);
|
||||
// - the PET bar's grid follows the pet payload alone. The reference fires `PET_BAR_SHOWGRID`
|
||||
// from inside the pet-action pickup builder itself (`0x494f28`), not from a shared cursor
|
||||
// transition — so a spell on the cursor lights the action bar's empty slots and leaves the
|
||||
// pet bar alone, which is also the only honest answer: you cannot drop it there.
|
||||
// The action bar's grid shows for any payload but a pet action, which `PlaceAction` refuses,
|
||||
// or a vendor row, whose grab `0x4950f0` fires `ACTIONBAR_SHOWGRID` for mode 7 only
|
||||
// (`0x495106`, `0x49513a`). The reference fires it (`0x4e58c0`) only from the item, spell and
|
||||
// macro setters and that mode-7 grab, so coins and a stabled pet show it here and not there.
|
||||
// The pet bar's follows the pet action alone (`0x494f28`).
|
||||
let pet_held = matches!(model.cursor, Some(CursorPayload::PetAction(_)));
|
||||
let bar_held = model.cursor.is_some()
|
||||
&& !pet_held
|
||||
@@ -302,9 +196,7 @@ pub(crate) fn queue_cursor_update(model: &mut Model) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Queue `ITEM_LOCK_CHANGED(bag, slot)` — a pickup locking a source slot, or a place/cancel/
|
||||
/// clear/destroy unlocking it (decision 0216 §4: the engine-derived held-here lock,
|
||||
/// `container.rs`'s `GetContainerItemInfo` `held_here` check).
|
||||
/// Queue `ITEM_LOCK_CHANGED(bag, slot)` for a source slot that a pickup locks or a put-down frees.
|
||||
pub(crate) fn queue_lock_changed(model: &mut Model, bag: i64, slot: u32) {
|
||||
model.pending_events.push((
|
||||
"ITEM_LOCK_CHANGED".to_string(),
|
||||
@@ -312,10 +204,8 @@ pub(crate) fn queue_lock_changed(model: &mut Model, bag: i64, slot: u32) {
|
||||
));
|
||||
}
|
||||
|
||||
/// Queue `DELETE_ITEM_CONFIRM(name, quality)` for an item payload dropped on the world (the popup
|
||||
/// flow transcribed from `StaticPopup.lua`, decision 0216 §3). `name` is parsed out of the link's
|
||||
/// `[...]` segment (empty string with no link); the payload is left untouched here — the popup's
|
||||
/// `OnAccept`/`OnCancel` (`DeleteCursorItem`/`ClearCursor`) own clearing it.
|
||||
/// Queue `DELETE_ITEM_CONFIRM(name, quality)` for an item dropped on the world. The payload stays
|
||||
/// held: the popup's `DeleteCursorItem` or `ClearCursor` clears it.
|
||||
fn queue_delete_item_confirm(model: &mut Model, item: &CursorItem) {
|
||||
let name = item_link_name(item.link.as_deref());
|
||||
let quality = item.quality.unwrap_or(0);
|
||||
@@ -325,8 +215,7 @@ fn queue_delete_item_confirm(model: &mut Model, item: &CursorItem) {
|
||||
));
|
||||
}
|
||||
|
||||
/// Parse an item link's display name out of its `|h[Name]|h` segment; empty with no link or an
|
||||
/// unrecognized shape.
|
||||
/// An item link's name, from its `|h[Name]|h` segment; empty for no link or another shape.
|
||||
pub(super) fn item_link_name(link: Option<&str>) -> String {
|
||||
link.and_then(|l| l.split_once('['))
|
||||
.and_then(|(_, rest)| rest.split_once(']'))
|
||||
@@ -334,15 +223,11 @@ pub(super) fn item_link_name(link: Option<&str>) -> String {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// `ClearCursor()` — drops whatever the cursor holds, any arm (the right-click/ESC cancel). An
|
||||
/// item payload also un-locks its source slot; an already-empty cursor is not a transition (no
|
||||
/// event).
|
||||
/// `ClearCursor()`: drops any payload and frees an item's source slot; an empty cursor fires
|
||||
/// nothing.
|
||||
pub(crate) fn clear_cursor(model: &mut Model) {
|
||||
// **An armed gift wrap dies with any cancel** (decision 1934), and this is the FIRST thing
|
||||
// the reference's `ClearCursor 0x495190` does — `0x5edf10` is its opening act and it is
|
||||
// **ungated by either parameter**, including `ClearCursor(0)`, the flavour that deliberately
|
||||
// keeps an ordinary held item's lock. So all 70 of its call sites cancel a wrap. The paper
|
||||
// unlocks and the cursor mode goes back to the base.
|
||||
// Any clear first cancels an armed gift wrap, whatever its parameters (`0x495190` opens with
|
||||
// `0x5edf10`): the wrapping paper unlocks and the cursor mode returns to the base.
|
||||
if let Some(wrap) = model.pending_wrap.take() {
|
||||
queue_lock_changed(model, wrap.bag, wrap.slot);
|
||||
model.ui_cursor = None;
|
||||
@@ -359,11 +244,9 @@ pub(crate) fn clear_cursor(model: &mut Model) {
|
||||
| CursorPayload::Macro(_)
|
||||
| CursorPayload::PetAction(_)
|
||||
| CursorPayload::StablePet(_)
|
||||
// Mode 2 — coins go back where they never left (the purse was not debited).
|
||||
// Coins: the purse was never debited, so nothing goes back.
|
||||
| CursorPayload::Money(_)
|
||||
// Mode 5 clears like every other non-item payload, and clearing it sends NO packet —
|
||||
// a vendor cursor abandoned on the world, on ESC, or by the window closing costs
|
||||
// nothing. `ClearCursor`'s own mode-5 arm is `0x49525f`.
|
||||
// A vendor row clears with no packet (`0x49525f`).
|
||||
| CursorPayload::Merchant(_),
|
||||
) => {
|
||||
queue_cursor_update(model);
|
||||
@@ -372,23 +255,10 @@ pub(crate) fn clear_cursor(model: &mut Model) {
|
||||
}
|
||||
}
|
||||
|
||||
/// **The SELL fork's cursor clear** — `0x494b60 SetCursorItem(0, …)`, which is *not* `ClearCursor`
|
||||
/// and differs from [`clear_cursor`] in exactly one way that a player can see.
|
||||
///
|
||||
/// Both drop the payload and fire `CURSOR_UPDATE`. `ClearCursor` also un-locks the source slot;
|
||||
/// this does **not**: `0x494b60`'s mode-1 arm is entered with `param1 = 0`, which skips `0x495420`,
|
||||
/// so the sold item's slot stays **greyed** until the server's own inventory update removes it.
|
||||
/// Leaving the lock set is the point: the item is gone from the cursor but not yet
|
||||
/// gone from the bag, and the grey is what says so.
|
||||
///
|
||||
/// Shared by the two verbs that sell off the cursor — `PickupMerchantItem`'s sell fork
|
||||
/// (`0x4fb760`) and the interact ladder's vendor arm (`0x5df5d0`) — because they are the same
|
||||
/// clear in the binary, and two copies of it is how one of them quietly grows a different
|
||||
/// behaviour. Returns the item so the caller can address the packet by its slot.
|
||||
///
|
||||
/// A non-item payload is left exactly where it was: the fork answers for **mode 1 only**
|
||||
/// (`GetCursorItem 0x494c60`), so a spell, a macro or a vendor row on the cursor is not a sale
|
||||
/// and must not be dropped by asking.
|
||||
/// The sell fork's clear, `SetCursorItem(0, …)` (`0x494b60`), shared by `PickupMerchantItem`
|
||||
/// (`0x4fb760`) and the interact ladder's vendor arm (`0x5df5d0`): unlike [`clear_cursor`] it
|
||||
/// leaves the source slot locked, grey until the server removes the item (the mode-1 arm skips
|
||||
/// `0x495420`). Any other payload is no sale and stays held (`GetCursorItem`, `0x494c60`).
|
||||
pub(crate) fn take_cursor_item_for_sale(model: &mut Model) -> Option<CursorItem> {
|
||||
let item = match model.cursor.take() {
|
||||
Some(CursorPayload::Item(item)) => item,
|
||||
@@ -401,48 +271,29 @@ pub(crate) fn take_cursor_item_for_sale(model: &mut Model) -> Option<CursorItem>
|
||||
Some(item)
|
||||
}
|
||||
|
||||
/// What the app's world pick resolves under the cursor this frame — the reference's click-time
|
||||
/// pick state `[this+0x350]` (0 = nothing / 1 = terrain / 2 = object; `0x481f60`, decisions
|
||||
/// 0571 + 0574). Fed per frame by the app ([`super::UiScript::set_world_pick`]; stays
|
||||
/// [`WorldPick::Nothing`] in tests/captures). Routes the left world-drop
|
||||
/// ([`world_drop_click`]): over an `Object` no payload drops at all (the object leg `0x492ce0`
|
||||
/// clears only the displayId-PREVIEW gate `[0xb4b41c]` — an arm benilla doesn't carry — and
|
||||
/// dispatches SELECT/INTERACT with the payload still held); over `Terrain` an ITEM drops (the
|
||||
/// `DELETE_ITEM_CONFIRM` popup via `0x5e0320`) and a spell/action payload clears silently
|
||||
/// (decision 0843's deliberate divergence — the reference keeps it there); over `Nothing` the
|
||||
/// item pops and any other payload silently clears (`0x492d30`).
|
||||
/// What is under the cursor in the world this frame: the reference's click-time pick state
|
||||
/// `[this+0x350]` (`0x481f60`). The app feeds it; tests and captures leave it `Nothing`.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
|
||||
pub enum WorldPick {
|
||||
/// No world hit at all (sky / beyond the world) — the reference's state 0.
|
||||
/// State 0: no world hit (the sky).
|
||||
#[default]
|
||||
Nothing,
|
||||
/// The cursor ray hits the world (terrain/WMO/doodad) but no unit or GameObject — state 1.
|
||||
/// State 1: terrain, a WMO or a doodad, but no unit or GameObject.
|
||||
Terrain,
|
||||
/// A unit or GameObject is the pick — state 2.
|
||||
/// State 2: a unit or GameObject.
|
||||
Object,
|
||||
}
|
||||
|
||||
/// A world drop: a completed left CLICK on the game world while a payload is held — press and
|
||||
/// release both on the world (the world frame, or no frame at all where none is loaded — the
|
||||
/// caller's `over_world`, decision 2089), no drag (the byte-verified trigger, decision 0218: the
|
||||
/// client's `0x495300` runs on the WorldFrame click release only; a drag released over the world
|
||||
/// routes as a drag and keeps carrying) — routed by [`Model::world_pick`] (`0x481f60`; decisions
|
||||
/// 0571 + 0574):
|
||||
/// A left click on the world with a payload held, press and release both over the world and no
|
||||
/// drag (`0x495300`); returns whether it was consumed. Over an `Object` (`0x492ce0`) nothing
|
||||
/// drops and the click selects. Over `Terrain` (`0x492c90`) or `Nothing` (`0x492d30`) an item
|
||||
/// raises `DELETE_ITEM_CONFIRM(name, quality)` (`0x5e0320`) and stays held for the popup to clear;
|
||||
/// a spell, action, macro or pet action clears. Coins, a vendor row and a stabled pet stay held
|
||||
/// here, where the reference puts a vendor row down on every leg and clears any non-item payload
|
||||
/// over `Nothing` (`0x492e04`).
|
||||
///
|
||||
/// - `Object`: NOTHING drops — the object leg (`0x492ce0`) keeps every real payload and
|
||||
/// dispatches SELECT instead.
|
||||
/// - `Terrain`: an item payload fires `DELETE_ITEM_CONFIRM(name, quality)` and STAYS held (the
|
||||
/// reference popup's `OnAccept` calls `DeleteCursorItem`, `OnCancel` `ClearCursor`, and its
|
||||
/// `OnUpdate` auto-hides when the cursor empties — the engine must not pre-clear); a
|
||||
/// spell/action payload clears silently — a DELIBERATE divergence (decision 0843, the
|
||||
/// director's call): the reference's terrain leg keeps a non-item payload on the left click
|
||||
/// (`0x492c90`/`0x5e0320` clear non-items only on the right button), which leaves a spell
|
||||
/// stuck to the cursor with no left-handed way off it.
|
||||
/// - `Nothing`: the item pops the same popup; any other payload clears silently (`0x492d30`'s
|
||||
/// non-item arm — here the reference agrees).
|
||||
///
|
||||
/// Returns whether the click was consumed as a drop (an empty cursor or an `Object` pick
|
||||
/// consume nothing).
|
||||
/// Deviation: on `Terrain` the reference keeps a spell, action, macro or pet action on the left
|
||||
/// button; we clear them, because otherwise a spell has no left-click way off the cursor.
|
||||
pub(crate) fn world_drop_click(model: &mut Model) -> bool {
|
||||
match (&model.cursor, model.world_pick) {
|
||||
(Some(CursorPayload::Item(item)), WorldPick::Terrain | WorldPick::Nothing) => {
|
||||
@@ -466,12 +317,9 @@ pub(crate) fn world_drop_click(model: &mut Model) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// `SplitContainerItem(bag, slot, count)` — a stack-split pickup (`StackSplitFrame.lua`'s
|
||||
/// `SplitStack`): only from an EMPTY cursor, onto a resolved and unlocked slot, `count >= 1`.
|
||||
/// `count` at or past the whole stack degrades to a plain whole-stack pickup (`count: None` — the
|
||||
/// client never "splits" a full stack; vmangos rejects `count == stack` as cheat). Returns
|
||||
/// whether a payload was picked up (the repaint gate, matching `pickup_container_item`'s
|
||||
/// contract).
|
||||
/// `SplitContainerItem(bag, slot, count)`: picks up `count` of an unlocked stack onto an empty
|
||||
/// cursor. A count of the whole stack or more is a plain pickup, since vmangos refuses to split a
|
||||
/// whole stack or more (`Player.cpp:11139`, `:11147`). Returns whether anything was picked up.
|
||||
pub(crate) fn split_container_item(model: &mut Model, bag: i64, slot: u32, count: i64) -> bool {
|
||||
if model.cursor.is_some() || count < 1 {
|
||||
return false;
|
||||
@@ -501,9 +349,8 @@ pub(crate) fn split_container_item(model: &mut Model, bag: i64, slot: u32, count
|
||||
true
|
||||
}
|
||||
|
||||
/// `DeleteCursorItem()` — the delete popup's `OnAccept` (decision 0216 §3): an Item payload
|
||||
/// queues its `(bag, slot, count)` destroy (`count == 0` = the whole stack) and clears; any other
|
||||
/// payload (or an empty cursor) is a no-op — the client's contract for a stale/mismatched confirm.
|
||||
/// `DeleteCursorItem()`, the delete popup's accept: a held item queues its `(bag, slot, count)`
|
||||
/// destroy, count 0 for the whole stack, and clears; anything else is a no-op.
|
||||
pub(crate) fn delete_cursor_item(model: &mut Model) {
|
||||
let Some(CursorPayload::Item(item)) = &model.cursor else {
|
||||
return;
|
||||
@@ -522,9 +369,7 @@ pub(crate) fn delete_cursor_item(model: &mut Model) {
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
impl super::UiScript {
|
||||
/// The item currently held on the cursor, or `None` — the Item-arm projection of
|
||||
/// [`UiScript::cursor_payload`], kept for the app's existing extract/sound watchers
|
||||
/// (`ui_script/extract.rs`, `sound/ui.rs`), which only ever cared about items.
|
||||
/// The held item, if the payload is one.
|
||||
pub fn cursor_item(&self) -> Option<CursorItem> {
|
||||
match self.model_ref().cursor.clone() {
|
||||
Some(CursorPayload::Item(item)) => Some(item),
|
||||
@@ -532,36 +377,28 @@ impl super::UiScript {
|
||||
}
|
||||
}
|
||||
|
||||
/// The full cursor payload, any arm — what the app's hardware-cursor drive
|
||||
/// (`benilla::cursor`) and capture stand-in draw, and what the world-click consumers gate on.
|
||||
/// The cursor payload, any arm.
|
||||
pub fn cursor_payload(&self) -> Option<CursorPayload> {
|
||||
self.model_ref().cursor.clone()
|
||||
}
|
||||
|
||||
/// Feed: what the app's world pick resolves under the cursor this frame
|
||||
/// ([`Model::world_pick`], decisions 0571 + 0574) — routes [`world_drop_click`]'s legs
|
||||
/// (object keeps everything, terrain drops items only, nothing drops any arm).
|
||||
/// Feed the world pick under the cursor this frame ([`WorldPick`]).
|
||||
pub fn set_world_pick(&mut self, pick: WorldPick) {
|
||||
self.model_mut().world_pick = pick;
|
||||
}
|
||||
|
||||
/// The sell fork's take-and-clear, for the app-side interact ladder — see
|
||||
/// [`take_cursor_item_for_sale`]. `None` for an empty cursor or any non-item payload, and in
|
||||
/// that case the payload is left untouched.
|
||||
/// [`take_cursor_item_for_sale`], for the app's interact ladder.
|
||||
pub fn take_cursor_item_for_sale(&mut self) -> Option<CursorItem> {
|
||||
take_cursor_item_for_sale(&mut self.model_mut())
|
||||
}
|
||||
|
||||
/// `ClearCursor()`'s Rust seam — drops whatever the cursor holds, any arm, silently (fires
|
||||
/// `CURSOR_UPDATE` + the item source un-lock, exactly the Lua `ClearCursor`). The app's
|
||||
/// world-click router calls it for a clean RIGHT-click over empty world (`0x492c90`/
|
||||
/// `0x492d30`'s action-4 leg: `ClearCursor 0x495190(1,1)` unconditionally — decision 0571).
|
||||
/// `ClearCursor()` from Rust, which the app calls on a right-click over empty world: both
|
||||
/// reference legs there (`0x492c90`, `0x492d30`) call `ClearCursor(1, 1)` (`0x495190`).
|
||||
pub fn clear_cursor_payload(&mut self) {
|
||||
clear_cursor(&mut self.model_mut());
|
||||
}
|
||||
|
||||
/// Test-only: set the cursor payload directly, bypassing every Lua binding — drives the
|
||||
/// Spell/Action arms in tests before a spellbook/action-bar slice can produce them.
|
||||
/// Test-only: set the payload directly, bypassing the Lua bindings.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn set_cursor_for_test(&mut self, payload: CursorPayload) {
|
||||
self.model_mut().cursor = Some(payload);
|
||||
@@ -569,65 +406,53 @@ impl super::UiScript {
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
// Lua globals: GetCursorInfo / CursorHasItem / CursorHasSpell / ClearCursor / SplitContainerItem /
|
||||
// DeleteCursorItem — all top-level (the real client's `GetCursorInfo` &c. are not namespaced).
|
||||
// The targeting cursor's item pick and the enchant confirms
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
impl super::UiScript {
|
||||
/// Arm (or disarm) **the targeting cursor's item half** — the engine mirror of
|
||||
/// `TargetingWantsItem 0x6e6330`. While armed, a bag click ([`super::container`]) or a
|
||||
/// paper-doll click ([`doll::pickup_inventory_item`]) queues its `(bag, slot)` into the pick
|
||||
/// list instead of running the cursor gesture, exactly as the reference's two pickup
|
||||
/// functions reroute (`0x4f9b30` @ `4f9c54`, `0x4c7300` @ `4c76df`). The app owns the word:
|
||||
/// it arms on a resolved item-targeted cast and clears on the bind, a cancel, or ESC
|
||||
/// (decision 0923 — before it, this was the CraftFrame's private enchant pick).
|
||||
/// Arm or disarm the targeting cursor's item half (`TargetingWantsItem`, `0x6e6330`): while
|
||||
/// armed, a bag or paper-doll click queues its `(bag, slot)` as a pick instead of picking up,
|
||||
/// as the reference's two pickups do (`0x4f9b30` at `0x4f9c54`, `0x4c7300` at `0x4c76df`).
|
||||
pub fn set_item_pick_armed(&mut self, armed: bool) {
|
||||
self.model_mut().item_pick_armed = armed;
|
||||
}
|
||||
|
||||
/// Push what `SpellCanTargetUnit` answers — whether the standing targeting word could bind a
|
||||
/// unit at all (see [`crate::script::Model::spell_can_target_unit`]).
|
||||
/// Push what `SpellCanTargetUnit` answers: whether the armed targeting cast can take a unit.
|
||||
pub fn set_spell_can_target_unit(&mut self, can: bool) {
|
||||
self.model_mut().spell_can_target_unit = can;
|
||||
}
|
||||
|
||||
/// Drain the `(bag, slot)` clicks the armed item half consumed since the last call. A doll
|
||||
/// click reports as [`EQUIPMENT_BAG`] + its 1-based inventory slot — the ONE bag space, so
|
||||
/// the app resolves both seams with one lookup.
|
||||
/// Drain the `(bag, slot)` picks since the last call; a paper-doll pick reports as
|
||||
/// [`EQUIPMENT_BAG`] and its 1-based inventory slot.
|
||||
pub fn take_item_picks(&mut self) -> Vec<(i64, u32)> {
|
||||
std::mem::take(&mut self.model_mut().item_picks)
|
||||
}
|
||||
|
||||
/// Drain the enchant-confirm popups' answers since the last call (decision 0928). Both are
|
||||
/// answers to the *same* pick the app parked, which is why they share one queue.
|
||||
/// Drain the enchant-confirm answers since the last call; both answer the pick the app parked.
|
||||
pub fn take_enchant_confirms(&mut self) -> Vec<EnchantConfirm> {
|
||||
std::mem::take(&mut self.model_mut().enchant_confirms)
|
||||
}
|
||||
}
|
||||
|
||||
/// A Yes on one of the enchant-apply confirms — the two Lua globals `StaticPopup.lua` calls from
|
||||
/// `BIND_ENCHANT`'s and `REPLACE_ENCHANT`'s `OnAccept` (decision 0928). Plain intents; the app
|
||||
/// holds the item guid they answer for (the reference's `0xb4e3c0/0xb4e3c4`).
|
||||
/// A Yes on an enchant-apply confirm (`StaticPopup.lua:1245`, `:1257`); the app holds the item
|
||||
/// guid it answers for (the reference's `0xb4e3c0`/`0xb4e3c4`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum EnchantConfirm {
|
||||
/// `BindEnchant()` — `0x48d2e0`, which re-invokes the enchant-apply gate `0x495d60` over the
|
||||
/// parked guid with its "already confirmed" parameter set. So this is NOT a send: the gate
|
||||
/// runs again and may raise the *replace* popup next.
|
||||
/// `BindEnchant()` (`0x48d2e0`): reruns the enchant-apply gate `0x495d60` as already
|
||||
/// confirmed, so it may raise the replace popup next rather than cast.
|
||||
Bind,
|
||||
/// `ReplaceEnchant()` — `0x48d300`, which re-resolves the parked guid and calls the ordinary
|
||||
/// target binder `0x6e5b40` **directly**, skipping the gate. There is no
|
||||
/// `CMSG_REPLACE_ENCHANT` in 5875: the popup is pure client-side gating over the same cast.
|
||||
/// `ReplaceEnchant()` (`0x48d300`): binds the parked guid through `0x6e5b40`, skipping the
|
||||
/// gate. There is no replace packet: the popup only gates the same cast.
|
||||
Replace,
|
||||
}
|
||||
|
||||
// The paper-doll globals (`PickupInventoryItem` &c.) install from [`doll`]; the action-bar
|
||||
// globals (`PickupAction`/`PlaceAction`) install from [`bar`].
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
// Lua globals
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let g = lua.globals();
|
||||
|
||||
// BindEnchant() / ReplaceEnchant() — the two enchant-confirm popups' OnAccept (StaticPopup.lua
|
||||
// `BIND_ENCHANT` l.1240 and `REPLACE_ENCHANT` l.1252). Both queue an intent the app answers
|
||||
// over its parked item guid; neither sends anything from here.
|
||||
// `BindEnchant()`, `ReplaceEnchant()`: queue an intent for the app; nothing is sent from here.
|
||||
for (name, answer) in [
|
||||
("BindEnchant", EnchantConfirm::Bind),
|
||||
("ReplaceEnchant", EnchantConfirm::Replace),
|
||||
@@ -642,10 +467,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
}
|
||||
|
||||
// GetCursorInfo() → per arm: Item ("item", itemID, itemLink); Spell ("spell", book_slot,
|
||||
// book_type, spell_id) — the Era shape; Action ("action", src_slot). Empty ⇒ all nils. A
|
||||
// fixed 4-return shape (padded with nil past each arm's own fields) so one function signature
|
||||
// covers every arm; callers destructuring fewer names simply ignore the tail.
|
||||
// `GetCursorInfo()` is not a 1.12 verb. It answers ("item", id, link), ("spell", book slot,
|
||||
// book, spell id), ("action", slot), ("macro", index) or ("petaction", slot), padded with nil
|
||||
// to four values; any other payload, or none, is four nils.
|
||||
g.set(
|
||||
"GetCursorInfo",
|
||||
lua.create_function(|lua, ()| {
|
||||
@@ -675,40 +499,24 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
Value::Nil,
|
||||
Value::Nil,
|
||||
)),
|
||||
// The Era `GetCursorInfo` shape for a macro: the kind word + the macro index.
|
||||
Some(CursorPayload::Macro(m)) => Ok((
|
||||
Value::String(lua.create_string("macro")?),
|
||||
Value::Integer(i64::from(m.index)),
|
||||
Value::Nil,
|
||||
Value::Nil,
|
||||
)),
|
||||
// The pet arm follows the Action arm's shape — kind word + the slot it came from.
|
||||
// Nothing in the shipped 1.12 UI reads it (the pet bar's own drag never asks what
|
||||
// it is carrying); it is here so the ONE payload space stays fully describable.
|
||||
Some(CursorPayload::PetAction(p)) => Ok((
|
||||
Value::String(lua.create_string("petaction")?),
|
||||
Value::Integer(i64::from(p.src_slot)),
|
||||
Value::Nil,
|
||||
Value::Nil,
|
||||
)),
|
||||
// Mode 10 — the stabled-pet grab (decision 1677). Reported as nothing: the
|
||||
// reference's `GetCursorInfo` arms are not carved for it, the stable window never
|
||||
// asks, and inventing a type string would put a guess where an addon could read it.
|
||||
Some(CursorPayload::StablePet(_)) => {
|
||||
Ok((Value::Nil, Value::Nil, Value::Nil, Value::Nil))
|
||||
}
|
||||
// Mode 2 — coins (1962). Reported as nothing for the same reason: the money arm
|
||||
// of `GetCursorInfo` is not carved, and `GetCursorMoney` is the reference's own
|
||||
// way of asking. The same gap closes once that arm is carved.
|
||||
Some(CursorPayload::Money(_)) => {
|
||||
Ok((Value::Nil, Value::Nil, Value::Nil, Value::Nil))
|
||||
}
|
||||
// Mode 5 — the vendor grab. Reported as nothing, for the same reason and with a
|
||||
// stronger warrant: **no registered binding in the 5875 image exposes mode 5 to
|
||||
// Lua at all.** `CursorHasItem`, `CursorHasSpell`, `CursorHasMoney` and
|
||||
// `CursorCanGoInSlot` are each nil for it, by census. A vendor cursor is invisible
|
||||
// from Lua and that invisibility is load-bearing — it is what keeps all thirteen
|
||||
// stock `CursorHasItem()` gates closed while one is held.
|
||||
Some(CursorPayload::Merchant(_)) => {
|
||||
Ok((Value::Nil, Value::Nil, Value::Nil, Value::Nil))
|
||||
}
|
||||
@@ -770,8 +578,7 @@ mod tests {
|
||||
use super::{CursorAction, CursorItem, CursorPayload, CursorSpell};
|
||||
use crate::script::UiScript;
|
||||
|
||||
/// A one-item, one-slot backpack (mirrors `container::tests::backpack` closely enough for
|
||||
/// these payload-shape tests, which don't need its in-flight slot).
|
||||
/// A backpack holding a five-stack in slot 1.
|
||||
fn one_item_backpack() -> crate::script::container::ContainerState {
|
||||
let mut slots = std::collections::HashMap::new();
|
||||
slots.insert(
|
||||
@@ -803,24 +610,14 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// **The sell clear is not `ClearCursor`** — decision 1914, and the difference is the one a
|
||||
/// player sees.
|
||||
///
|
||||
/// `0x494b60 SetCursorItem(0, …)` drops the payload and fires `CURSOR_UPDATE`, but its mode-1
|
||||
/// arm skips `0x495420` with `param1 = 0`, so it never un-locks the source slot: the item is
|
||||
/// off the cursor and the bag slot stays **greyed** until the server's inventory update takes
|
||||
/// it away. `ClearCursor` un-locks. Firing `ITEM_LOCK_CHANGED` here would un-grey a slot whose
|
||||
/// item is still sitting in it awaiting the server, which is the visible wrong.
|
||||
///
|
||||
/// The second half is the blast radius: the fork answers for **mode 1 only**, so asking a
|
||||
/// spell or a vendor row must not drop it.
|
||||
/// `SetCursorItem(0, …)` (`0x494b60`) skips `ClearCursor`'s unlock (`0x495420`) and takes only
|
||||
/// a held item.
|
||||
#[test]
|
||||
fn the_sell_clear_keeps_the_source_slot_locked() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_container(0, Some(one_item_backpack()));
|
||||
|
||||
// Pick the item up, then drain whatever the pickup queued so the assert below sees only
|
||||
// what the SELL clear itself fires.
|
||||
// Drain the pickup's events so only the sell clear's remain.
|
||||
s.eval::<()>("PickupContainerItem(0, 1)").unwrap();
|
||||
assert!(s.cursor_item().is_some(), "the pickup did not take");
|
||||
s.model_mut().pending_events.clear();
|
||||
@@ -848,7 +645,6 @@ mod tests {
|
||||
`SetCursorItem(0, …)`'s) — got {fired:?}"
|
||||
);
|
||||
|
||||
// Mode 1 only: every other payload is left exactly where it was.
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_cursor_for_test(CursorPayload::Spell(CursorSpell {
|
||||
book_slot: 1,
|
||||
@@ -871,14 +667,12 @@ mod tests {
|
||||
fn get_cursor_info_and_has_checks_per_arm() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
|
||||
// Empty: all nils, both Has* false.
|
||||
assert!(s
|
||||
.eval::<bool>("local k = GetCursorInfo() return k == nil")
|
||||
.unwrap());
|
||||
assert!(!s.eval::<bool>("return CursorHasItem()").unwrap());
|
||||
assert!(!s.eval::<bool>("return CursorHasSpell()").unwrap());
|
||||
|
||||
// Item arm.
|
||||
s.set_cursor_for_test(CursorPayload::Item(CursorItem {
|
||||
bar_placeable: true,
|
||||
bag: 0,
|
||||
@@ -900,7 +694,6 @@ mod tests {
|
||||
assert!(s.eval::<bool>("return CursorHasItem()").unwrap());
|
||||
assert!(!s.eval::<bool>("return CursorHasSpell()").unwrap());
|
||||
|
||||
// Spell arm — the Era 4-tuple shape.
|
||||
s.set_cursor_for_test(CursorPayload::Spell(CursorSpell {
|
||||
passive: false,
|
||||
book_slot: 3,
|
||||
@@ -920,7 +713,6 @@ mod tests {
|
||||
assert!(!s.eval::<bool>("return CursorHasItem()").unwrap());
|
||||
assert!(s.eval::<bool>("return CursorHasSpell()").unwrap());
|
||||
|
||||
// Action arm.
|
||||
s.set_cursor_for_test(CursorPayload::Action(CursorAction {
|
||||
src_slot: 12,
|
||||
kind: 0,
|
||||
@@ -950,10 +742,7 @@ mod tests {
|
||||
assert!(s.cursor_payload().is_none());
|
||||
}
|
||||
|
||||
/// The world-drop pick routing (decisions 0571 + 0574 — `0x481f60`, amended by 0843): a
|
||||
/// spell/action payload clears silently on BOTH empty-world legs — terrain included, the
|
||||
/// 0843 divergence (the reference keeps it on terrain; the director wants the left click to
|
||||
/// dismiss) — while an item keeps its byte-faithful popup flow.
|
||||
/// Clearing a spell on terrain is the deviation documented on `world_drop_click`.
|
||||
#[test]
|
||||
fn world_drop_terrain_and_nothing_both_clear_a_spell_payload() {
|
||||
use super::{world_drop_click, WorldPick};
|
||||
@@ -977,7 +766,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// An item drops (the popup path) on BOTH empty-world legs, never over an object.
|
||||
let item = CursorPayload::Item(CursorItem {
|
||||
bar_placeable: true,
|
||||
bag: 0,
|
||||
|
||||
Reference in New Issue
Block a user