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:
@@ -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`);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user