your corpse answers the mouse, and a cinematic's last holes close

- a corpse is a body you can hover, cursor and click: it enters the same ray pass as units with the winner switched on type at publish (the reference's own shape — one trace over every object), the cursor classifier is transcribed whole (lootable-by-me → the pickup cursor, grayed by range; else the insignia leg behind the learned-skill latch; your own body falls through to the clear), and right-click is "not mounted AND lootable" → CMSG_LOOT; CORPSE_FIELD_FLAGS bit 5 is the PvP insignia flag, not "reclaimable" as an old note had it; the widening also caught that a GameObject anywhere on screen could steal the click from a nearer corpse, and that a left-click on a body cleared your target as if you had clicked ground
- the cinematic sweep closes eleven holes: a shot plays its sequence band's WIDTH, not its end — two shipped fly-bys band away from zero and ran long with their opening frames frozen (the Night Elf intro measured 102333 → 102000 ms); CMSG_NEXT_CINEMATIC_CAMERA announces every shot, the first included — the old "between shots" reading meant it was sent NEVER, since every 1.12 sequence row carries exactly one camera; the ack invariant is stated once (one CMSG_COMPLETE_CINEMATIC per accepted trigger, over-acking tolerated, under-acking never — a dropped ack despawns the world around you until relog); the hardware cursor hides for the shot; and the music cut lands in the frame that starts it — it was one frame late, exactly long enough for a first login's zone fanfare to start under the narration and be stamped as played
- RunBinding exists, and a fly-by stops handing the world its mouse: CinematicFrame's OnKeyDown hands the SCREENSHOT chord back to its binding through RunBinding, a global we never had, so every press during a fly-by raised and took no picture; and the mouse was fully live under a shot — right-drag turned the character, clicks picked targets through the flying camera, both buttons walked the body — so the pointer now reads as over the UI while a shot plays
- AdvanceTime and ReplaceIconTexture are carved off the bytes: AdvanceTime really is a no-op in the reference (its one reachable call is mov eax,1; ret), ReplaceIconTexture is a material swap over a model's type-14 texture slots that stores nothing on the widget, and SetModel(nil) RAISES — the "documented clear" our binding claimed was invented, not read; and the addon roster now says WHOSE package is short when a .toc names files the folder doesn't hold — nine of the corpus's twenty-nine load failures are the addon's own manifest, which the real client also shrugs off ("Couldn't open %s", carry on), so they stop reading as our defects

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-31 19:55:36 +07:00
co-authored by Claude Fable 5
parent f6f2af3da9
commit 6e3a67e039
28 changed files with 1237 additions and 130 deletions
@@ -155,6 +155,79 @@ fn a_player_model_is_a_model_plus_three_and_the_chain_runs_one_way() {
);
}
/// **`SetModel(nil)` raises; `ClearModel()` clears. They are not the same verb.**
///
/// This binding asserted the opposite until 2026-08-30 — "the documented clear, and how the corpus
/// writes 'no model'" — which was invented, not read. `0x76d950` is shape A (decision 1717's
/// taxonomy): `lua_isstring` gates the argument and `Usage: %s:SetModel("file")` is raised on
/// anything that is not a string or a number. `ClearModel 0x76db20` is a separate table entry.
///
/// The same gate on `ReplaceIconTexture 0x76ed70`, whose *whole* observable behaviour it is: the
/// swap itself lands on a `CM2Model` we do not have, and with no CM2Model the reference drops the
/// call and never replays it.
///
/// A NUMBER is accepted by both — `lua_isstring` takes tags 3|4 and the client renders it to
/// decimal text — which is the half a `Value::String`-only match would get wrong.
#[test]
fn the_string_setters_gate_their_argument_and_a_number_is_a_string() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(r#"g = CreateFrame("Model", "GateM", UIParent)"#)
.unwrap();
s.run(r#"GateM:SetModel("Interface\\Buttons\\A.mdx")"#)
.unwrap();
for bad in ["nil", "{}", "true", "print", ""] {
assert!(
s.run(&format!("GateM:SetModel({bad})")).is_err(),
"SetModel({bad}) must raise — it is not the clear"
);
}
// ...and none of those raises disturbed the path that was already set.
assert_eq!(
s.eval::<String>("return GateM:GetModel()").unwrap(),
r"Interface\Buttons\A.mdx",
"a raised setter leaves the pane alone"
);
// A number IS a string to `lua_isstring`, and the client renders it to decimal text.
s.run("GateM:SetModel(42)").unwrap();
assert_eq!(s.eval::<String>("return GateM:GetModel()").unwrap(), "42");
// ClearModel is the clear, takes no argument, and empties the pane.
s.run("GateM:ClearModel()").unwrap();
assert_eq!(
s.eval::<Option<String>>("return GateM:GetModel()").unwrap(),
None
);
// `ReplaceIconTexture` — same gate, and that gate is all of it. A good argument is accepted
// and changes NOTHING observable: the swap targets the CM2Model's type-14 textures, and a
// pane with no CM2Model drops the call (the reference's own `[widget+0x318] == 0` leg).
for bad in ["nil", "{}", "true", ""] {
assert!(
s.run(&format!("GateM:ReplaceIconTexture({bad})")).is_err(),
"ReplaceIconTexture({bad}) must raise"
);
}
s.run(r#"GateM:SetModel("Interface\\Buttons\\A.mdx")"#)
.unwrap();
s.run(r#"GateM:ReplaceIconTexture("Interface\\Icons\\INV_Misc_QuestionMark")"#)
.unwrap();
assert_eq!(
s.eval::<String>("return GateM:GetModel()").unwrap(),
r"Interface\Buttons\A.mdx",
"it is a MATERIAL swap, not a content setter — the pane's model is untouched"
);
// `AdvanceTime` takes nothing, returns nothing, and does nothing — verified, not stubbed.
assert_eq!(
s.eval::<usize>("return table.getn({ GateM:AdvanceTime() })")
.unwrap(),
0,
"AdvanceTime pushes no return value"
);
}
/// **The whole block, both directions** — decision 1718's rule applied to the two model tables.
///
/// The lists below are the reference's OWN enumerations, transcribed entry-for-entry from
@@ -203,10 +276,11 @@ fn the_two_model_tables_are_the_references_own() {
const PLAYERMODEL_3: [&str; 3] = ["SetUnit", "RefreshUnit", "SetRotation"];
/// The subset of [`MODEL_23`] this client does not build. Named, not stubbed (1134 §4) — their
/// bodies are uncarved and they have no corpus caller. Moving a name OUT of here means the
/// verb was actually implemented.
const UNBUILT: [&str; 7] = [
"AdvanceTime",
"ReplaceIconTexture",
/// verb was actually implemented — as `AdvanceTime` and `ReplaceIconTexture` were, once
/// wow-re carved them (`ui/scratch/model-advancetime-replaceicontexture.md`). The five left
/// are the fog near/far set, whose `ClearFog` semantics over the colour/near/far triple nobody
/// has read; guessing that clear would read as knowledge.
const UNBUILT: [&str; 5] = [
"SetFogNear",
"GetFogNear",
"SetFogFar",