the server's broadcasts land, and a ghost walks through closed doors

- the four world broadcasts decode and land — a shutdown countdown, the civilian-kill alarm, a defense-tower capture and the whisper-cap refusal all reached benilla as unhandled opcodes; `SMSG_SERVER_MESSAGE` carries a row id and a fill, never a finished line, so `ServerMessages.dbc` ships (the five sentences a shutdown has, plus the client's own `[%d]: %s` row-missing fallback, which is mechanism, not an error path); the two defense broadcasts are CHANNEL lines, not system lines — delivered once per joined channel whose row carries DEFENSE, the LocalDefense copy zone-matched against the PARENT area while the text names the packet's own — so a character in neither channel sees nothing, faithfully, and since nothing auto-joins WorldDefense in 1.12 an Eastern Plaguelands tower capture reaches only players standing there unless they typed /join; the wow-re cross-check then made the length prefix load-bearing — the client borrows a pointer into the receive buffer and composes NULL on an overrun, so an overrunning length is a refusal here too: the packet drops with a log line, nothing displayed
- `BankButtonIDToInvSlotID` is carved: `isBag` is a TYPE test, not truthiness — `lua_isnumber` refuses every non-number tag without ever loading the payload, so `true` takes the same arm as `nil` while `0` and `"1"` take the bag arm — the function is TOTAL (our out-of-range 0 was an invention; the only compares in all 118 bytes are on `lua_isnumber`'s return), and the +59/+39 arithmetic stands confirmed, byte-identical to `ContainerIDToInventoryID`'s second arm
- a deliberate right-drag turn no longer stands you while seated — the observation was right and our attribution was wrong: the body-facing commit is refused by two independent gates and the stand arm is skipped outright while the button is held; what stands you is the RELEASE, a press under 200 ms with under 2.25 degrees of yaw dispatched as a right-CLICK whose interact reaches `SetStandState(0)` — this lands the deletion half, with the click's own stand held for one more wow-re answer
- a ghost walks through closed doors — the whole mechanism is one instruction image-wide, reading bit 15 through the sign flag to answer "not a collision candidate" for a DOOR when the traced body is a player in ghost form; the bit is a property of whose trace it is, not of the door, so the exclusion rides the two mover-only casts and the set is empty on every living frame — the particle snap, the precipitation probe, the mouse pick and the creature conform keep the door's collider untouched, exactly as the binary leaves them

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-31 20:19:32 +07:00
co-authored by Claude Fable 5
parent 14bd5ea1a4
commit b8758d0b21
27 changed files with 1457 additions and 70 deletions
+105 -35
View File
@@ -131,29 +131,52 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
// BankButtonIDToInvSlotID(id, isBag) — the pure button→live-slot map (module doc).
//
// **The bag arm takes the CONTAINER id 5..=10, not a bag number 1..=6**, and getting that
// wrong reads six slots off the end of the band. The reference's own file is the oracle
// (0675) and says so four times over: `BankFrame.xml` gives `BankFrameBag1..BankFrameBag6`
// the ids **5..10** (the generic buttons `BankFrameItem1..24` take 1..24);
// `ButtonInventorySlot` hands `this:GetID()` straight in; and both
// `UpdateBagButtonHighlight(id)` (`"BankFrameBag"..(id - NUM_BAG_SLOTS)`) and
// `BankFrameItemButton_UpdateLock` (`(this:GetID() - 4) > GetNumBankSlots()`) subtract 4 from
// that same id to get back to 1..6. So the bag arm is `id + 59` — byte-identical to
// `ContainerIDToInventoryID`'s carved second arm (`t = id - 1; t >= 4 → t + 60`,
// `crate::script::container`), which is the same map under the other name.
// CARVED (`0x4f8530`, 118 bytes; wow-re `system/ui/scratch/bank-button-invslot-law.md`, §5
// trio + orchestrator byte arbitration 2026-08-31). Three findings, and two of them corrected
// this binding:
//
// Out-of-range answers 0 (the reference binding is total over its buttons; our XML never asks
// outside them — 0 is the visible "wired wrong" tell rather than a silent misroute).
// 1 · **The arithmetic is `+59` / `+39`, and the bag arm takes the CONTAINER id 5..10** — not
// a bag number 1..6, which is what this used to compute. `0x4f8575 dec esi` runs BEFORE the
// branch and `0x4f8587 inc esi` AFTER it, so the pair cancels and the constants stand as
// written (`0x4f857f add esi,0x3b` / `0x4f8584 add esi,0x27`). The bag arm is numerically
// identical to `ContainerIDToInventoryID 0x4f94e0`'s own `id >= 5` arm — two functions, two
// instruction sequences, one constant. The reference's file pins the same numbering four
// times over: `BankFrame.xml` gives `BankFrameBag1` `id="5"`, `ButtonInventorySlot` hands
// `this:GetID()` straight in, and `UpdateBagButtonHighlight`/`BankFrameItemButton_UpdateLock`
// both subtract 4 from it to get back to 1..6.
//
// 2 · **`isBag` is a TYPE test, not a truthiness test.** `0x4f8576 call 0x6f34d0` is
// `lua_isnumber(L, 2)`, which accepts tag 3 or a tag-4 string `luaO_str2d` fully consumes,
// and refuses every other tag at `0x6f7c35` without ever loading the value. So **`true`
// takes the same arm as `nil`, `false` and a missing argument**, while `0` and `"1"` take
// the BAG arm. `BankFrame.lua:24` writes `this.isBag = 1` — a NUMBER — so the stock UI is
// correct either way; a client that models the flag as a boolean diverges the moment an
// addon passes `true`, and would then resolve the six bank bags to 44..49, colliding with
// `BankFrameItem5..10`.
//
// 3 · **Total, with no range check of any kind.** The only `test`/`cmp` in all 118 bytes are
// on `lua_isnumber`'s return value: no clamp, no mask, no `nil`. This used to answer 0
// outside 1..24 / 5..10 as a "wired wrong" tell; that was our invention, and the reference
// simply returns the arithmetic. Its immediate neighbour `GetNumBankSlots 0x4f85b0` carries
// a real `cmp esi,6; jl`, so a bound would have been visible here if one existed.
//
// Argument 1 is shape A ([`binding_abi::number_arg`]): `lua_isnumber` gated, truncated toward
// zero by the `0x40a2b0` ftol, low dword only, and raising the `.data` usage string verbatim
// — which names only `buttonID`, the reference's own omission of the second parameter.
g.set(
"BankButtonIDToInvSlotID",
lua.create_function(|_, (id, is_bag): (u32, Option<Value>)| {
let is_bag = is_bag.is_some_and(|v| v.as_boolean().unwrap_or(true));
let live = match (is_bag, id) {
(false, 1..=24) => 39 + id,
(true, 5..=10) => 59 + id,
_ => 0,
};
Ok(i64::from(live))
lua.create_function(|lua, (id, is_bag): (Value, Option<Value>)| {
let n = super::binding_abi::number_arg(
lua,
id,
"Usage: BankButtonIDToInvSlotID(buttonID)",
)?;
// Finding 2: `lua_isnumber(L, 2)`, asked of the VALUE's type — never its truthiness.
let is_bag = is_bag
.and_then(|v| lua.coerce_number(v).ok().flatten())
.is_some();
let step = if is_bag { 59 } else { 39 };
Ok(i64::from(n.wrapping_add(step)))
})?,
)?;
@@ -326,12 +349,11 @@ mod tests {
/// The button→live-slot map: item buttons 1..24 → 40..63; bag buttons — **whose ids are the
/// container ids 5..10, not bag numbers** — → 64..69 (live id − 1 = the wire slot: bank items
/// 39..62, bank bags 63..68). The reference calls it with `this.isBag` = 1 or nil, so
/// nil/false both mean "item button".
/// 39..62, bank bags 63..68).
///
/// The bag arm's numbering is the reference file's, checked here because getting it wrong
/// reads six slots off the end of the band and draws an empty bag row: `BankFrame.xml` gives
/// `BankFrameBag1` `id="5"`, and `ButtonInventorySlot` hands that id straight in.
/// The bag arm's numbering is checked here because getting it wrong reads six slots off the
/// end of the band and draws an empty bag row — which is what this binding did until the
/// `0x4f8530` carve.
#[test]
fn bank_button_to_inv_slot() {
let s = UiScript::new().unwrap();
@@ -366,21 +388,69 @@ mod tests {
.unwrap()
);
}
// false is "not a bag" (nil-or-false truthiness), out of range answers 0.
assert_eq!(
s.eval::<i64>("return BankButtonIDToInvSlotID(2, false)")
.unwrap(),
41
);
}
/// The three things the `0x4f8530` carve corrected, each of which this binding had wrong or
/// invented (wow-re `scratch/bank-button-invslot-law.md`).
#[test]
fn bank_button_to_inv_slot_follows_the_carved_abi() {
let s = UiScript::new().unwrap();
// **`isBag` is a TYPE test** (`lua_isnumber(L,2)`), not a truthiness test. `true` is not a
// number, so it takes the ITEM arm exactly as nil and false do; `0` and `"1"` are numbers
// and take the BAG arm. The stock UI is safe either way — `BankFrame.lua:24` writes
// `this.isBag = 1` — but an addon passing `true` would otherwise land the six bank bags on
// 44..49, on top of `BankFrameItem5..10`.
for (call, want) in [
("BankButtonIDToInvSlotID(5, true)", 44),
("BankButtonIDToInvSlotID(5, false)", 44),
("BankButtonIDToInvSlotID(5, nil)", 44),
("BankButtonIDToInvSlotID(5, {})", 44),
("BankButtonIDToInvSlotID(5)", 44),
("BankButtonIDToInvSlotID(5, 0)", 64),
("BankButtonIDToInvSlotID(5, \"1\")", 64),
] {
assert_eq!(
s.eval::<i64>(&format!("return {call}")).unwrap(),
want,
"{call}"
);
}
// **Total — no range check of any kind.** Answering 0 outside the button ranges was ours,
// not the reference's; the only compares in the body are on `lua_isnumber`'s result.
assert_eq!(
s.eval::<i64>("return BankButtonIDToInvSlotID(25)").unwrap(),
0
64
);
assert_eq!(
s.eval::<i64>("return BankButtonIDToInvSlotID(4, 1)")
s.eval::<i64>("return BankButtonIDToInvSlotID(-100)")
.unwrap(),
0,
"4 is an EQUIPPED bag, not a bank one"
-61
);
// Argument 1 is shape A: `lua_isnumber` gated, truncated toward ZERO by the `0x40a2b0`
// ftol (not floored), and raising the `.data` usage string — which names only `buttonID`,
// the reference's own omission of the second parameter.
assert_eq!(
s.eval::<i64>("return BankButtonIDToInvSlotID(-2.9)")
.unwrap(),
37,
"trunc toward zero gives -2, not -3"
);
assert_eq!(
s.eval::<i64>("return BankButtonIDToInvSlotID(\"7\")")
.unwrap(),
46,
"a numeric string passes the gate"
);
let err = s
.eval::<i64>("return BankButtonIDToInvSlotID({})")
.unwrap_err()
.to_string();
assert!(
err.contains("Usage: BankButtonIDToInvSlotID(buttonID)"),
"the reference's own usage string, verbatim: {err}"
);
}
}