71 more key commands, and the quest windows wear the reference's names

- the binding registry goes 130 → 201 of the client's 228 live commands: a coverage gate makes registered ∪ recorded-absent exactly the 1.12 surface, each absence naming the Lua globals whose arrival refutes it — which is how 37 rows whose mechanism had already landed came home (the keyring, the pet book, the raid tab, the six action-bar pages, both vertical multibars) — and then four mechanisms bring 71 more: the five camera views with the reference's default table and its sixteen camera CVars read out of the binary (NextView/PrevView do NOT wrap), self-cast riding UseAction's third argument with autoSelfCast a real CVar at last, the friendly target cone / assist / last-hostile as one cycler with a side (the two sides use different deadness tests on purpose, and assist neither requires an assistable target nor swings), and REPLY2 — reply to the last person you told; the friendly cone also stops borrowing the enemy side's in-combat score bonus, which would have shoved a healer targeting you to the head of every cycle
- the key names close their gaps: NUMPADEQUALS, F13-F24 and BUTTON6+ were keys the binding window listed and the file saved that the dispatcher could never press — named both ways now, with the namer and the codec structurally unable to disagree again; platform= becomes the OS gate it really is (off a Mac build the iTunes-remote category never even exists), and the Mac 1.12 slice settles that its F13 IS print-screen while F14/F15 map to UNKNOWN on purpose and stay unbindable; and the last-enemy stamp gains the four conjuncts it shipped without — you resolve, are not dead, not a ghost, not mounted, and the new target is alive-or-feigning — so TargetLastEnemy can no longer land somewhere the real client's never would
- the quest windows wear the reference's names: ~40 of the quest window's globals were ours (the reference's name usually sitting in the comment beside them), its two six-item reward pools merge into the reference's single ten-item run with the update function ported line-for-line, the four panels and the quest log take REAL scroll frames (the thumb drags now), the gossip window gets its greeting panel, the two mail panes' inert painted-at-rest scrollbars become real ones, the Skills detail pane nests as the reference has it, and nine private constants take their real names; the one that broke a feature detect: our QuestLogCount — a name 1.12 doesn't publish — made pfUI conclude it was running on TBC and die in that branch
- an EditBox is born with its constructor's five regions (the text, three selection quads and the caret), so GetRegions answers the reference's own count and authored regions start at index 6 — and a bare <NormalTexture/> now builds a live blank texture, since the element is what creates the region, not its file=; OpacityFrame lands, its first customer having arrived; and the equip / auto-equip / use bind confirms complete the soulbind arc — three different predicates with no quality leg (borrowing the loot arm's would have silenced the confirm on every white bind-on-equip item), Accept re-issuing the original action with the reference's suppress flag because 1.12 ships no confirm packet, and the soulstone use going through the same gate

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-31 20:16:49 +07:00
co-authored by Claude Fable 5
parent 542c2ac04b
commit 8ae42cc7ab
69 changed files with 6829 additions and 1479 deletions
+84 -2
View File
@@ -7,7 +7,7 @@ use mlua::{Lua, Value};
use super::super::Model;
use super::{
check_unit_token, classification_word, grey_band, level_reads_unknown, pick_unit_token,
power_token, with_unit,
power_token, with_unit, SelectionRequest,
};
/// The two class ids `GetComboPoints 0x51a190` accepts — the literals `4` and `0xb` it compares
@@ -889,12 +889,94 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
lua.create_function(|lua, token: Option<String>| {
if let Some(token) = token {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
model.target_requests.push(token);
model.selection_requests.push(SelectionRequest::Unit(token));
}
Ok(())
})?,
)?;
// AssistUnit(unit) — select the *token's own* target (`0x489b80`; wow-re
// `object-layer/scratch/targeting-by-name.md` PART C, §5-cross-checked). The ASSISTTARGET
// binding's body is `AssistUnit("target")`, and `/assist`'s bare form is the same call.
//
// The shared assist tail (`0x489bb2`–`0x489c07`, byte-identical to `AssistByName`'s) is three
// steps and no more: read `UNIT_FIELD_TARGET` off the basis (`[[obj+0x110]+0x28]`), bail
// **silently** if it is zero, then hand the guid to the select-if-resolves helper `0x489a40`.
// Four things it deliberately is not:
//
// * **not gated by `CanAssist`** — a whole-binary census of the 25 `call 0x6066f0` sites puts
// none of them on this path (VERIFIED negative), so `AssistUnit("target")` on a hostile
// creature assists it, and that is the common case in play.
// * **not players-only** — unlike `AssistByName`'s typemask `0x10`, the token resolver takes
// whatever the token names.
// * **not a deselect on failure** — an unresolvable token, a basis with no target, and a
// target that is not streamed all leave the current selection exactly where it was
// (`0x489a40`'s arm 3 is a bare `ret`).
// * **not an attack** — the tail's swing leg is gated on the `assistAttack` CVar, whose
// registered default is `"0"` (VERIFIED at the registration bytes `0x48fc50`). Stock assist
// selects and does not swing; the leg becomes reachable only if benilla grows the CVar.
//
// A nil/absent unit is ignored here rather than queued, like `TargetUnit`'s: the reference
// emits game-message `0xb8` for the token it cannot parse, and that id→string table is
// runtime-populated BSS wow-re could not statically recover — the same known deviation
// `TargetByName` already carries, and silence is better than an invented line.
g.set(
"AssistUnit",
lua.create_function(|lua, token: Option<String>| {
if let Some(token) = token {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
model
.selection_requests
.push(SelectionRequest::Assist(token));
}
Ok(())
})?,
)?;
// TargetLastEnemy() — re-select the last **attackable** unit that was targeted (`0x489b45`
// reads the pair `[0xb4e2e8]/[0xb4e2ec]`, which `SetSelection 0x493540` stamps at `0x49377d`
// alongside the plain last-target pair `TargetLastTarget` reads). The TARGETLASTHOSTILE
// binding (default `G`) is its only shipped caller.
//
// Takes no argument and routes through the same select-if-resolves helper, so a remembered
// guid whose unit has since streamed out is a silent no-op rather than a deselect. The app
// owns the memory itself (`crate::target::scan`'s `LastEnemy`) — the VM holds no guids.
g.set(
"TargetLastEnemy",
lua.create_function(|lua, ()| {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
model.selection_requests.push(SelectionRequest::LastEnemy);
Ok(())
})?,
)?;
// TargetNearestFriend([reverse]) — the friendly half of the TAB scan. `0x489aa0` is
// byte-for-byte `TargetNearestEnemy`'s shim with one changed immediate: both fetch the
// optional Lua arg #1 as the reverse flag (`0x6f1c10`, default 0) and call the one cycler
// `0x493f60(ecx = reverse, edx = mode)` — `edx = 1` for enemy, **`2` for friend** (3 and 4 are
// the party and raid siblings). Everything downstream is the same code; only the
// per-candidate filter `0x493e40` forks on the mode, and mode 2's arm (`0x493eca`) is
// `CanAssist(player, cand)` then `UNIT_FIELD_HEALTH > 0`.
//
// TARGETNEARESTFRIEND (`CTRL-TAB`) calls it bare; TARGETPREVIOUSFRIEND (`CTRL-SHIFT-TAB`)
// calls `TargetNearestFriend(1)` — 1.12's own `Bindings.xml` comments the argument
// `-- 1 (or "true") means reverse!`, so the truthiness test below takes a number OR a boolean.
// A numeric 0 is forward, matching `0x6f1c10`'s "absent == 0" reading.
g.set(
"TargetNearestFriend",
lua.create_function(|lua, reverse: Option<Value>| {
let reverse = match reverse {
None | Some(Value::Nil) | Some(Value::Boolean(false)) => false,
Some(Value::Integer(n)) => n != 0,
Some(Value::Number(n)) => n != 0.0,
Some(_) => true,
};
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
model.target_nearest_friend_requests.push(reverse);
Ok(())
})?,
)?;
// TargetByName(name [, exactMatch]) — select by NAME through the shared resolver `0x493aa0`
// (`0x489d60`; wow-re `object-layer/scratch/targeting-by-name.md`, a §5 trio + two pairs).
// The app already owns that resolver for `/target` (decision 0886, `crate::target::by_name`);
+35 -4
View File
@@ -24,6 +24,27 @@ use mlua::Lua;
use super::Model;
/// One queued **selection ask** from Lua, in call order — the app resolves it to a guid and
/// commits it through the one SetSelection tail.
///
/// The three verbs share one queue because the reference shares one *function*: `TargetUnit`
/// (`0x4899d0`), `AssistUnit` (`0x489b80`), `TargetLastEnemy` and `TargetLastTarget` all reach
/// selection through the "select if it resolves" helper `0x489a40`, whose three arms are the same
/// for every caller — resolve the guid and commit, else fall back to the group roster, else a
/// **silent return** (wow-re `object-layer/scratch/selection-attack-seam.md` §1). Splitting them
/// into a queue each would also lose their relative order, which a macro can observe
/// (`TargetUnit("party1"); AssistUnit("target")` is not the same as the reverse).
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SelectionRequest {
/// `TargetUnit(unit)` — select the unit the token names.
Unit(String),
/// `AssistUnit(unit)` — select the token's own `UNIT_FIELD_TARGET`; a basis with no target is
/// a silent no-op (the reference's shared assist tail bails before any send).
Assist(String),
/// `TargetLastEnemy()` — select the last *attackable* unit that was committed.
LastEnemy,
}
/// One unit-token's game-state snapshot, pushed by the app each frame and read by the `Unit*`
/// bindings. Plain data (no mlua handles, no ECS types) — the engine-free seam decision 0068 §3
/// draws between the app's net/ECS feed and the Lua API.
@@ -450,10 +471,20 @@ impl super::UiScript {
model.combo_target = target;
}
/// Drain the unit tokens `TargetUnit` queued since the last call — the app resolves each to a
/// streamed entity and commits the selection (the reference's `TargetUnit` → SetSelection path).
pub fn take_target_requests(&mut self) -> Vec<String> {
std::mem::take(&mut self.model_mut().target_requests)
/// Drain the selection asks queued since the last call — the app resolves each to a guid and
/// commits it through the one SetSelection tail. See [`SelectionRequest`] for why the three
/// verbs share one ordered queue.
pub fn take_selection_requests(&mut self) -> Vec<SelectionRequest> {
std::mem::take(&mut self.model_mut().selection_requests)
}
/// Drain the `TargetNearestFriend([reverse])` presses queued since the last call — one `bool`
/// per call, the cycle direction (`false` forward, `true` backward). Its own queue rather than
/// a [`SelectionRequest`] variant because it names no unit: the reference runs it through the
/// TAB cycler `0x493f60(reverse, mode 2)` and straight into `SetSelection`, never through the
/// select-if-resolves helper the other three share.
pub fn take_target_nearest_friend_requests(&mut self) -> Vec<bool> {
std::mem::take(&mut self.model_mut().target_nearest_friend_requests)
}
/// Drain the `(name, exactMatch)` pairs `TargetByName` queued since the last call — the app
+52 -3
View File
@@ -276,16 +276,65 @@ fn corpse_can_attack_and_green_range_bindings() {
#[test]
fn target_unit_queues_the_token_for_the_app_to_resolve() {
use crate::script::SelectionRequest;
let mut s = UiScript::new().unwrap();
// Nothing queued until a call lands.
assert!(s.take_target_requests().is_empty());
assert!(s.take_selection_requests().is_empty());
// Each call queues its raw token, in order; a nil token is ignored.
s.eval::<()>(r#"TargetUnit("player")"#).unwrap();
s.eval::<()>(r#"TargetUnit("target")"#).unwrap();
s.eval::<()>(r#"TargetUnit(nil)"#).unwrap();
assert_eq!(s.take_target_requests(), vec!["player", "target"]);
assert_eq!(
s.take_selection_requests(),
vec![
SelectionRequest::Unit("player".into()),
SelectionRequest::Unit("target".into()),
]
);
// The drain is a take — a second read is empty.
assert!(s.take_target_requests().is_empty());
assert!(s.take_selection_requests().is_empty());
}
/// The three verbs share ONE queue, and the queue keeps their call order — which is the whole
/// reason it is one queue (`0x489a40` is one function for all of them, and a macro can observe
/// the order). A nil `AssistUnit` argument is dropped like `TargetUnit`'s, and
/// `TargetLastEnemy()` names no unit at all.
#[test]
fn the_selection_queue_carries_all_three_verbs_in_call_order() {
use crate::script::SelectionRequest;
let mut s = UiScript::new().unwrap();
s.eval::<()>(r#"TargetUnit("party1")"#).unwrap();
s.eval::<()>(r#"AssistUnit("target")"#).unwrap();
s.eval::<()>(r#"AssistUnit(nil)"#).unwrap();
s.eval::<()>("TargetLastEnemy()").unwrap();
assert_eq!(
s.take_selection_requests(),
vec![
SelectionRequest::Unit("party1".into()),
SelectionRequest::Assist("target".into()),
SelectionRequest::LastEnemy,
]
);
assert!(s.take_selection_requests().is_empty());
}
/// `TargetNearestFriend`'s reverse flag, read exactly as `0x6f1c10(idx 1, default 0)` does:
/// absent or nil is forward, and 1.12's own `Bindings.xml` note — *"1 (or \"true\") means
/// reverse!"* — is why a boolean and a number both count. A numeric `0` is forward.
#[test]
fn target_nearest_friend_queues_its_reverse_flag() {
let mut s = UiScript::new().unwrap();
assert!(s.take_target_nearest_friend_requests().is_empty());
s.eval::<()>("TargetNearestFriend()").unwrap();
s.eval::<()>("TargetNearestFriend(1)").unwrap();
s.eval::<()>("TargetNearestFriend(true)").unwrap();
s.eval::<()>("TargetNearestFriend(0)").unwrap();
s.eval::<()>("TargetNearestFriend(nil)").unwrap();
assert_eq!(
s.take_target_nearest_friend_requests(),
vec![false, true, true, false, false]
);
assert!(s.take_target_nearest_friend_requests().is_empty());
}
#[test]