From 38a6b6f149774503f802bd5d98aee5b47f9ad15a Mon Sep 17 00:00:00 2001 From: grom Date: Sun, 25 Oct 2026 17:03:30 -0600 Subject: [PATCH] =?UTF-8?q?unitxp:=20inSight=20and=20behind=20=E2=80=94=20?= =?UTF-8?q?four=20macro=20conditionals=20that=20silently=20never=20worked?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not a gap: a defect. Defining `UnitXP` at all is what makes SuperCleveRoidMacros believe the whole family is present — `hasUnitXP = pcall(UnitXP, "nop", "nop")` (`Init.lua:274`), which succeeds here because the dispatcher's fall-through does not raise — so four macro conditionals have been running against commands this client did not implement: `[insight]`, `[noinsight]`, `[behind]`, `[nobehind]`, each with a count mode too. The fall-through answers the stock `UnitXP(unit)` getter's 0, and every one of those conditionals compares against `true` LITERALLY (`== true`, `~= true`). So `[insight]` and `[behind]` could never fire while `[noinsight]` and `[nobehind]` always did, with nothing to notice it by. That is why both commands answer a boolean rather than 1.12's `1`/nil flag shape, and why a pair they cannot measure answers nil rather than a fabricated `false`: the two directions are read separately, so "cannot say" has to be distinguishable from "no". `inSight`'s blocker is gone, and it was gone before this: the row said "needs a real raycast with no current binding-layer access", and the nameplate-occlusion work built exactly that raycast. This is the same body-layer ray from the player's chest instead of the camera's eye, fed into the per-token `UnitReach` record `distanceBetween` already reads — only the app can trace the world, which is the whole reason the command sat blocked in the binding. `behind` this file called *unconfirmed* ("treat as unconfirmed until seen elsewhere"). It is now seen elsewhere: ten call sites, with the argument order documented by its own readers, so `behind(A, B)` asks whether A stands in B's rear arc. The arc is the rear HEMISPHERE, which is what this realm's server means by behind — vmangos tests `!HasInArc(M_PI, attacker)`, the same rule that accepts or refuses Backstab — taken flat, so a ledge above a mob's back counts and directly overhead does not. Two things my own tests caught, both worth keeping: - A two-argument `inSight` answered `true`. These are recognised commands, so they now match on the NAME alone and say nil when the arguments make no pair; the arity fall-through is right for an unrecognised command and wrong here, because neither name is a unit token so the stock getter can only answer a meaningless 0 — which is truthy in Lua, i.e. a confident "yes, in sight". - A `#[cfg(test)]` module placed mid-file hid every `fire_event` after it from `event_shape_gate`, which truncates each file at the first one: six unit-field bridge events read as unproduced. The test moved to the file's own tests module, where the one-test-module-per-file convention that gate assumes puts it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- .../src/ui_script/honor_frame_tests.rs | 3 + .../src/ui_script/inspect_tests.rs | 3 + crates/benilla-app/src/ui_unit.rs | 106 ++++++++++++ crates/benilla-app/src/vplates.rs | 2 +- crates/benilla-ui/src/script/compat/unitxp.rs | 161 ++++++++++++++++++ crates/benilla-ui/src/script/inspect.rs | 9 + docs/ADDON_COMPAT.md | 17 +- 7 files changed, 299 insertions(+), 2 deletions(-) diff --git a/crates/benilla-app/src/ui_script/honor_frame_tests.rs b/crates/benilla-app/src/ui_script/honor_frame_tests.rs index b2d8e578..01e7cba3 100644 --- a/crates/benilla-app/src/ui_script/honor_frame_tests.rs +++ b/crates/benilla-app/src/ui_script/honor_frame_tests.rs @@ -307,6 +307,9 @@ use std::collections::HashMap; /// A live, inspectable unit at squared distance `d2`. fn reach(d2: f64) -> benilla_ui::script::UnitReach { benilla_ui::script::UnitReach { + in_sight: true, + player_at_its_back: false, + at_player_back: false, dist_sq: d2, inspectable: true, } diff --git a/crates/benilla-app/src/ui_script/inspect_tests.rs b/crates/benilla-app/src/ui_script/inspect_tests.rs index 0adb9f5f..3494da2a 100644 --- a/crates/benilla-app/src/ui_script/inspect_tests.rs +++ b/crates/benilla-app/src/ui_script/inspect_tests.rs @@ -13,6 +13,9 @@ use benilla_ui::script::{ /// non-distance refusals and only `CanInspect` reads it; these tests are about distance. fn reach(dist_sq: f64) -> UnitReach { UnitReach { + in_sight: true, + player_at_its_back: false, + at_player_back: false, dist_sq, inspectable: true, } diff --git a/crates/benilla-app/src/ui_unit.rs b/crates/benilla-app/src/ui_unit.rs index ae87fe3a..a3ba6173 100644 --- a/crates/benilla-app/src/ui_unit.rs +++ b/crates/benilla-app/src/ui_unit.rs @@ -801,6 +801,14 @@ fn feed_self_movement( fn feed_unit_reach( script: Option>, + // UnitXP_SP3's `inSight`: the same body-layer ray the nameplate occlusion casts, from the + // player's chest rather than the camera's eye. Here rather than in the binding because only + // the app can trace the world — the whole reason that command sat blocked. + // + // `Option`, like `feed_self_movement`'s world point: a UI-only harness builds no physics world + // at all, and without one nothing can be in the way, which is also `camera_can_see`'s own + // answer for a ray it declines to cast. + collide: Option, tokens: UnitTokens, selection: Res, self_q: Query<(&Transform, &ObjectStore), With>, @@ -848,6 +856,18 @@ fn feed_unit_reach( benilla_ui::script::UnitReach { dist_sq: dist_sq(tf.translation, self_tf.translation), inspectable, + // Chest to chest: `camera_can_see` lifts and backs off the *target* end + // itself, so lifting the eye here puts both ends at body height and neither + // unit's own floor can count as the thing in the way. + in_sight: collide.as_ref().is_none_or(|c| { + crate::vplates::camera_can_see( + c, + self_tf.translation + Vec3::Y * crate::vplates::OCCLUSION_AIM_LIFT, + tf.translation, + ) + }), + player_at_its_back: in_rear_arc(tf, self_tf.translation), + at_player_back: in_rear_arc(self_tf, tf.translation), }, ); } @@ -855,6 +875,27 @@ fn feed_unit_reach( script.set_unit_reach(reach); } +/// Whether `other` stands in `subject`'s **rear arc**: UnitXP_SP3's `behind`. +/// +/// The rear **hemisphere**, which is what this realm's own server means by behind: vmangos tests +/// it as `!target->HasInArc(M_PI, attacker)` — a π arc centred on the facing, so anything outside +/// it is behind — and that is the rule the server already enforces for the abilities that care +/// (Backstab's "You must be behind your target"). The mod publishes no arc of its own, so matching +/// the authority that will accept or refuse the cast is the one choice that cannot disagree with +/// the game. +/// +/// Flat: the dot is taken in the horizontal plane, so standing on a ledge above a mob is not +/// "behind" it. A unit exactly beside the subject (dot 0) is **not** behind, as the strict +/// inequality in the server's own test is not. +fn in_rear_arc(subject: &Transform, other: Vec3) -> bool { + let facing = subject.forward().as_vec3(); + let to_other = other - subject.translation; + let flat = |v: Vec3| Vec3::new(v.x, 0.0, v.z); + let (facing, to_other) = (flat(facing), flat(to_other)); + // A zero-length separation (the subject and the other at one point) is not behind. + to_other.length_squared() > 0.0 && facing.dot(to_other) < 0.0 +} + /// [`feed_units`]' stores and change tracking, grouped to stay under Bevy's 16-parameter limit. #[derive(bevy::ecs::system::SystemParam)] pub(crate) struct UnitStores<'w, 's> { @@ -2513,6 +2554,20 @@ mod tests { .unwrap() } + // UnitXP_SP3's two player-anchored predicates ride the same record, so one assertion + // through this harness proves the feed carries them and the binding reads them; the arc + // law itself is `the_rear_arc_is_the_servers_own_half_circle` below. + assert!( + ask( + BOAR, + 0, + 15.0, + r#"return type(UnitXP("behind", "player", "target")) == "boolean" + and type(UnitXP("inSight", "player", "target")) == "boolean""# + ), + "both answer a boolean for a live token — nil would mean the record never arrived, \ + and the macro conditionals compare against `true` literally" + ); assert!( ask( BOAR, @@ -3347,4 +3402,55 @@ mod tests { app.update(); assert_eq!(left(&mut app), 2, "the next world's departure fires again"); } + + /// The rear **hemisphere**, which is what this realm's server means by behind + /// (`!HasInArc(M_PI, ...)`), taken flat and strictly: beside is not behind, and above is not + /// behind either. + #[test] + fn the_rear_arc_is_the_servers_own_half_circle() { + // A unit at the origin facing north. Bevy's identity rotation looks down −Z, and WoW's + // north is −Z here too, so "in front" is −Z and "behind" is +Z. + let facing_north = Transform::from_xyz(0.0, 0.0, 0.0); + let at = |x: f32, y: f32, z: f32| Vec3::new(x, y, z); + + assert!( + in_rear_arc(&facing_north, at(0.0, 0.0, 5.0)), + "directly behind" + ); + assert!( + !in_rear_arc(&facing_north, at(0.0, 0.0, -5.0)), + "directly in front" + ); + assert!( + !in_rear_arc(&facing_north, at(5.0, 0.0, 0.0)), + "exactly beside is NOT behind: the server's own test is a strict one, and a rogue \ + standing on the flank gets \"You must be behind your target\"" + ); + assert!( + in_rear_arc(&facing_north, at(5.0, 0.0, 0.1)), + "a hair past the flank is behind — the hemisphere, not a narrow cone" + ); + assert!( + in_rear_arc(&facing_north, at(0.0, 30.0, 5.0)), + "the test is flat: standing on a ledge above a mob's back is still behind it" + ); + assert!( + !in_rear_arc(&facing_north, at(0.0, 30.0, 0.0)), + "…and directly overhead is not, which a 3-D dot would have called behind or in front \ + by whichever way the model happened to tilt" + ); + assert!( + !in_rear_arc(&facing_north, at(0.0, 0.0, 0.0)), + "the same point is not behind itself" + ); + + // Turning the subject turns the arc with it. + let facing_east = Transform::from_xyz(0.0, 0.0, 0.0) + .with_rotation(Quat::from_rotation_y(-std::f32::consts::FRAC_PI_2)); + assert!( + in_rear_arc(&facing_east, at(-5.0, 0.0, 0.0)), + "facing east (+X), behind is −X" + ); + assert!(!in_rear_arc(&facing_east, at(5.0, 0.0, 0.0))); + } } diff --git a/crates/benilla-app/src/vplates.rs b/crates/benilla-app/src/vplates.rs index 4487d359..d0722814 100644 --- a/crates/benilla-app/src/vplates.rs +++ b/crates/benilla-app/src/vplates.rs @@ -410,7 +410,7 @@ struct PlateWorld<'w, 's> { /// ray aimed there grazes the ground it stands on and reports every unit on a slope as hidden; /// this lifts the target to roughly chest height on a humanoid, which is also about where the /// plate itself floats. -const OCCLUSION_AIM_LIFT: f32 = 1.5; +pub(crate) const OCCLUSION_AIM_LIFT: f32 = 1.5; /// The ray stops short of the unit by this much, so geometry the unit is standing against — the /// floor under its feet, the wall at its back — cannot count as the thing hiding it. diff --git a/crates/benilla-ui/src/script/compat/unitxp.rs b/crates/benilla-ui/src/script/compat/unitxp.rs index 4323977f..865993fe 100644 --- a/crates/benilla-ui/src/script/compat/unitxp.rs +++ b/crates/benilla-ui/src/script/compat/unitxp.rs @@ -70,6 +70,37 @@ fn distance_between(lua: &Lua, args: &[Value]) -> Option { /// tab-target history is keyed to its own two sides, and threading a third cycle through it is its /// own piece of work), `worldBoss` (no classification data reaches here) and `rangeCone` (it tunes /// the scan's own cone constant, which is not a Lua-settable knob today). +/// A player-anchored two-unit predicate: `read` is handed the other unit's [`UnitReach`] record +/// and whether `"player"` was the **first** of the two arguments, which is what tells the two +/// `behind` directions apart. +/// +/// `Value::Nil` when neither side is `"player"`, when both are, or when the other side has no +/// record this frame — a unit out of range, or a token naming nothing. Nil rather than `false`: +/// both readers test `== true` and `~= true` separately, so "cannot say" has to be distinguishable +/// from "no", and a fabricated `false` would make `[noinsight]` fire on a unit we simply have no +/// trace for. +fn player_anchored( + lua: &Lua, + pair: &[Value], + read: impl FnOnce(&crate::script::UnitReach, bool) -> bool, +) -> Value { + let (Some(a), Some(b)) = (pair.first().and_then(as_str), pair.get(1).and_then(as_str)) else { + return Value::Nil; + }; + let is_player = |t: &str| t.eq_ignore_ascii_case("player"); + let (other, player_first) = match (is_player(&a), is_player(&b)) { + (true, false) => (b, true), + (false, true) => (a, false), + // Both sides the player, or neither: nothing to measure against. + _ => return Value::Nil, + }; + let model = lua.app_data_ref::().expect("model app_data"); + match model.unit_reach.get(&other.to_ascii_lowercase()) { + Some(r) => Value::Boolean(read(r, player_first)), + None => Value::Nil, + } +} + fn target_command(lua: &Lua, sub: Option<&str>) -> Value { use crate::script::SelectionRequest as R; let request = match sub { @@ -146,6 +177,46 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> { "distanceBetween" => { distance_between(lua, &args[1..]).map_or(Value::Nil, Value::Number) } + // `inSight(a, b)` and `behind(a, b)`, both **player-anchored**: one side has to + // be `"player"`, because what the app feeds is a per-token record measured + // against the player ([`UnitReach`]) and neither a ray between two mobs nor a + // mob's arc around another mob is computed anywhere. Every call site in grom's + // corpus is player-anchored — `SuperCleveRoidMacros/Conditionals.lua` passes + // `("player", unit)` or `(unit, "player")` at all eighteen of them — and a pair + // this cannot answer is **nil**, which both of its readers test for by name + // (`== true` / `~= true`), rather than a fabricated `false`. + // + // **Booleans, not the 1.12 `1`/nil flag shape.** The conditionals compare against + // `true` literally (`UnitXP("inSight", "player", unit) == true`), so a `1` here + // would read as "not in sight" at every site — the exact silent wrong answer the + // fall-through was already giving them, since an unrecognised command answers the + // stock XP getter's `0` and `0 == true` is false. That is what made `[insight]` + // and `[behind]` never fire and `[noinsight]`/`[nobehind]` always fire: the addon + // decides the whole family is present with `pcall(UnitXP, "nop", "nop")` + // (`Init.lua:274`), which succeeds here because the fall-through does not raise. + // Matched on the **name alone**, not on the argument count: these two are + // recognised commands, so a call that cannot be answered has to say so rather + // than fall through to the stock XP getter. That fall-through is right for an + // unrecognised *command* — the mod's detour reaches the original — and wrong here, + // because neither name is a unit token, so the getter can only answer a + // meaningless `0`, which is **truthy in Lua**: a two-argument `inSight` would have + // claimed line of sight. (`"target"` below keeps its arity guard, where the + // collision with the stock one-argument getter is real and deliberate.) + "inSight" => player_anchored(lua, args.get(1..3).unwrap_or(&[]), |r, _| r.in_sight), + // `behind(A, B)` asks whether **A stands in B's rear arc** — the order its own + // readers document: `[behind]` is "Player is behind target" and passes + // `("behind", "player", unit)` (`Conditionals.lua:7381`). + "behind" => { + player_anchored(lua, args.get(1..3).unwrap_or(&[]), |r, player_first| { + if player_first { + // ("behind", "player", unit): the player is at the unit's back. + r.player_at_its_back + } else { + // ("behind", unit, "player"): the unit is at the player's back. + r.at_player_back + } + }) + } // `"target"` is ambiguous: stock 1.12's `UnitXP("target")` is the target unit's // XP, while UnitXP_SP3's is `UnitXP("target", )`. Disambiguated on the second // argument, so the stock one-argument getter keeps answering — the same way the @@ -199,6 +270,93 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> { mod tests { use crate::script::{UiScript, UnitReach}; + /// `inSight` and `behind`, **in the shapes `SuperCleveRoidMacros/Conditionals.lua` reads + /// them** — which is the whole point of this pair, because that file is grom's macro + /// conditional evaluator and four conditionals hang off these two commands: `[insight]`, + /// `[noinsight]`, `[behind]`, `[nobehind]`, each with a count mode as well. + /// + /// Every one of them compares against `true` **literally** (`== true`, `~= true`), so the + /// answer has to be a boolean and not 1.12's `1`/nil flag: a `1` reads as "no" at every site. + /// Before this, the two commands fell through to the stock `UnitXP(unit)` getter and answered + /// the integer 0, so `[insight]` and `[behind]` could never fire while `[noinsight]` and + /// `[nobehind]` always did — and the addon had no way to tell, because it decides the family + /// is present with `pcall(UnitXP, "nop", "nop")` (`Init.lua:274`), which succeeds. + #[test] + fn insight_and_behind_answer_the_booleans_the_macro_conditionals_compare_against() { + let mut s = UiScript::new().unwrap(); + s.set_unit_reach(std::collections::HashMap::from([ + ( + "target".to_string(), + UnitReach { + dist_sq: 25.0, + inspectable: false, + in_sight: true, + player_at_its_back: true, + at_player_back: false, + }, + ), + ( + "mouseover".to_string(), + UnitReach { + dist_sq: 400.0, + inspectable: false, + in_sight: false, + player_at_its_back: false, + at_player_back: true, + }, + ), + ])); + + // `[insight]` / `[noinsight]`, transcribed (`Conditionals.lua:7482`, `:7503`). + assert!(s + .eval::(r#"return UnitXP("inSight", "player", "target") == true"#) + .unwrap()); + assert!(s + .eval::(r#"return UnitXP("inSight", "player", "mouseover") ~= true"#) + .unwrap()); + assert_eq!( + s.eval::>(r#"return UnitXP("inSight", "player", "mouseover")"#) + .unwrap(), + Some(false), + "a boolean, not the 1.12 flag shape: `== true` is what reads it" + ); + + // `[behind]`, "Player is behind target" (`:7381`), and its mirror (`:5326`), which is the + // other argument order and the other unit's arc. + assert!(s + .eval::(r#"return UnitXP("behind", "player", "target") == true"#) + .unwrap()); + assert_eq!( + s.eval::>(r#"return UnitXP("behind", "target", "player")"#) + .unwrap(), + Some(false), + "the order is `A is behind B`: the player is at the target's back, not the other way" + ); + assert_eq!( + s.eval::>(r#"return UnitXP("behind", "mouseover", "player")"#) + .unwrap(), + Some(true), + "and the mouseover is at the player's back, which is the mirrored field" + ); + + // A pair this cannot measure is nil, not a fabricated `false`: both readers test the two + // directions separately, so `[noinsight]` must not fire on a unit we have no record for. + for call in [ + r#"UnitXP("inSight", "player", "party3")"#, + r#"UnitXP("behind", "player", "party3")"#, + r#"UnitXP("inSight", "target", "mouseover")"#, + r#"UnitXP("behind", "player", "player")"#, + r#"UnitXP("inSight", "player")"#, + ] { + assert_eq!( + s.eval::>(&format!("return {call}")).unwrap(), + None, + "{call} is nil" + ); + } + assert!(s.errors().is_empty(), "handler errors: {:?}", s.errors()); + } + /// The occlusion toggle, as `UnitXP_SP3_Addon.lua:107-115` drives it. The **truthy return** is /// half the contract: that function warns "UnitXP_SP3.dll failed to execute" whenever the /// result is falsy, and the dispatcher's fall-through answers the integer 0 — truthy in Lua — @@ -251,6 +409,9 @@ mod tests { s.set_unit_reach(HashMap::from([( "target".to_string(), UnitReach { + in_sight: true, + player_at_its_back: false, + at_player_back: false, dist_sq: 25.0, inspectable: false, }, diff --git a/crates/benilla-ui/src/script/inspect.rs b/crates/benilla-ui/src/script/inspect.rs index b97f2ca5..97e4ed7f 100644 --- a/crates/benilla-ui/src/script/inspect.rs +++ b/crates/benilla-ui/src/script/inspect.rs @@ -44,6 +44,15 @@ pub struct UnitReach { /// (`MiscHandler.cpp:945-956`). Only `CanInspect` reads it: `CheckInteractDistance` is a pure /// distance test, so a creature or an enemy player still answers it. pub inspectable: bool, + /// UnitXP_SP3's `UnitXP("inSight", "player", unit)`: nothing solid between the player and this + /// unit. The same body-layer ray the nameplate occlusion casts (`vplates::camera_can_see`), + /// from the player's chest instead of the camera's eye. + pub in_sight: bool, + /// `UnitXP("behind", "player", unit)`: the **player** stands in this unit's rear arc — the + /// backstab condition, and the one `[behind]` asks for. + pub player_at_its_back: bool, + /// `UnitXP("behind", unit, "player")`: this unit stands in the **player's** rear arc. + pub at_player_back: bool, } /// What the app has resolved for the unit currently being inspected. diff --git a/docs/ADDON_COMPAT.md b/docs/ADDON_COMPAT.md index cf96a444..c596317b 100644 --- a/docs/ADDON_COMPAT.md +++ b/docs/ADDON_COMPAT.md @@ -179,13 +179,28 @@ in this file on the strength of the namreeb research. pepo's v2 has its own ## UnitXP_SP3 — confirmed (open source, multiple mirrors) +**The two commands built last were a defect, not a gap.** Defining `UnitXP` at all is what makes +`SuperCleveRoidMacros` believe the whole family is present — `CleveRoids.hasUnitXP = pcall(UnitXP, +"nop", "nop")` (`Init.lua:274`), which succeeds here because the dispatcher's fall-through does not +raise — so four macro conditionals ran against commands this client did not implement: +`[insight]`, `[noinsight]`, `[behind]`, `[nobehind]`, each with a count mode as well. The +fall-through answers the stock `UnitXP(unit)` getter's `0`, and every one of those conditionals +compares against `true` **literally** (`== true`, `~= true`), so `[insight]` and `[behind]` could +never fire while `[noinsight]` and `[nobehind]` always did. The addon had no way to notice. That is +why both commands answer a **boolean** rather than 1.12's `1`/nil flag shape, and why a pair they +cannot measure answers **nil** rather than a fabricated `false` — the two directions are read +separately, so "cannot say" has to be distinguishable from "no". + + | Area | Status | Notes | |---|---|---| | `UnitXP(command, ...)` dispatcher | done | `compat/unitxp.rs`. No documented detection global for this one — defining `UnitXP` at all is what an addon's `if UnitXP then` finds. **Collision note**: stock 1.12 already has its own single-argument `UnitXP(unit)` (experience points, `PLAYER_XP`); the mod shadows it on a real install exactly as a detour falls through to the original, so this dispatcher's unrecognised-command branch now carries that stock getter's exact behaviour (moved out of `script/unit/bindings.rs`, which no longer registers `UnitXP` itself — only `UnitXPMax` stayed there, untouched) | | `distanceBetween(unit1, unit2)` | done | player side: `inspect.rs`'s `UnitReach::dist_sq` (works whenever one side is `"player"`). Both-non-player side: `UnitState.position` (player/target/targettarget/npc/mouseover only, same reach as SuperWoW's `UnitPosition`) | | `target("nearestEnemy")` | done | queues `SelectionRequest::NearestEnemy` (`script/unit/mod.rs`) onto the same selection queue `TargetUnit`/`TargetLastEnemy` use, so a UnitXP target and a stock one commit through one path in call order. Handled by `target::scan::target_nearest_enemy`, which reuses `scan::best_enemy` over the **existing** candidate scan the tab-target and attack-order paths already run — so "nearest" means exactly what it does there (off-centre-plus-distance score, fighting-me bonus, on-screen first), not a second notion of nearest. Its own message+system rather than an arm in the drain: pulling the whole `TargetScan` into that shared system broke a UI-only test harness and cost +16 ambiguity pairs, the same reason `AttackNearestRequest` is already split out | | `target("nextMarkedEnemyInCycle"/"previousMarkedEnemyInCycle")` | done | `SelectionRequest::MarkCycle { forward }`, handled in the selection drain (`target/click.rs`) off `GroupState::raid_targets`, walked slot-7-to-0 so the order is skull-first as the README documents; steps from whichever marked unit is held, wrapping, and enters at the skull (forward) or star (back) end when nothing marked is held. Only marks on a streamed unit are in the cycle | -| `inSight`, `target` distance-cycles/`worldBoss`/`rangeCone`, `notify` | todo | confirmed from `MarcelineVQ/UnitXP_SP3`'s own `README.md` (the authoritative command reference — more precise than this session's earlier research pass, which guessed at `AoE`/`meleeAutoAttack`/`behind`/`timer` variants this README doesn't mention; treat those as unconfirmed until seen elsewhere). `inSight` needs a real raycast with no current binding-layer access; `nextEnemyConsideringDistance`/`nextEnemyInCycle` (the 41-yd distance-banded cycles) would need a third cycle threaded through `TabHistory`, which is keyed to its own two sides; `worldBoss` needs classification data that doesn't reach here; `rangeCone` tunes the scan's own cone constant, not a Lua-settable knob today; `notify` needs OS-level taskbar-flash/system-sound | +| `inSight(a, b)` | **done** | the blocker this row used to carry was "needs a real raycast with no current binding-layer access" — and the raycast now exists: the nameplate-occlusion work built `vplates.rs`'s `camera_can_see` over `WorldCollision::ray_body`. `inSight` is the same body-layer ray from the **player's chest** instead of the camera's eye (`camera_can_see` lifts and backs off the target end itself, so both ends sit at body height and neither unit's own floor can be the thing in the way). Computed app-side into the per-token `UnitReach` record `distanceBetween` already reads, because only the app can trace the world. **Player-anchored**: one side must be `"player"`, which is what all eighteen call sites in grom's corpus pass | +| `behind(a, b)` | **done** | this file called it *unconfirmed* — "this session's earlier research pass guessed at `AoE`/`meleeAutoAttack`/`behind`/`timer` variants this README doesn't mention; treat those as unconfirmed until seen elsewhere". It is now **seen elsewhere**: ten call sites in `SuperCleveRoidMacros/Conditionals.lua`, with the argument order documented by its own readers (`[behind]` is "Player is behind target" and passes `("behind", "player", unit)`), so `behind(A, B)` asks whether **A stands in B's rear arc**. The arc is the rear **hemisphere**, which is what this realm's server means by behind — vmangos tests `!HasInArc(M_PI, attacker)`, the same rule that accepts or refuses Backstab — taken flat, so a ledge above a mob's back still counts and directly overhead does not. `in_rear_arc` in `ui_unit.rs`, with its own test | +| `target` distance-cycles, `worldBoss`, `rangeCone`, `notify` | todo | `nextEnemyConsideringDistance`/`nextEnemyInCycle` (the 41-yd distance-banded cycles) would need a third cycle threaded through `TabHistory`, which is keyed to its own two sides; `worldBoss` needs classification data that does not reach here; `rangeCone` tunes the scan's own cone constant, not a Lua-settable knob today; `notify` needs OS-level taskbar-flash/system-sound. **Nothing in grom's corpus calls any of them** — the measured call counts are `distanceBetween` 21, `behind` 10, `inSight` 8, `target` 5 | | `modernNameplateDistance` | **built** | The command's name says distance; its own checkbox says what it does — **"Proper Nameplates Occlusion"** (`UnitXP_SP3_Addon.xml:151`), with three sibling options in that panel gated on it ("Need Proper Nameplates Occlusion to be also enabled"). So it is line-of-sight culling, not reach: benilla's reach is `NameplateRange`/`NP_NameplateDistance`, which have their own rows. 1.12 draws a plate for any unit in range whether or not a wall stands between, which is why a pull through a wall shows its whole room. Stored as the new `NameplateOcclusion` CVar rather than as private state, so it persists, appears in `/console`, and reaches the engine through the one `CvarChanged` path every other nameplate knob uses. **Two readers, as the reach CVars have**: `vplates.rs`'s `VPlateMode::occlusion` (V-key plates) and `nameplates.rs`'s `NameConfig::occlusion` (overhead names), sharing one `camera_can_see` — they must agree, because a unit with a plate has no overhead name and vice versa, so culling only one would swap a unit's plate for a floating name instead of hiding it. The ray is cast from the **camera**, not the player: the plate is drawn where the camera projects it, and in third person the two origins disagree often. It aims `OCCLUSION_AIM_LIFT` above the unit's origin (a transform sits at the feet, so a ray aimed there skims the ground and hides every unit on a slope) and stops `OCCLUSION_BACKOFF` short (so the floor under the unit's feet cannot count as the thing hiding it). Against the body layer only, so a unit standing in front of another cannot hide it — geometry occlusion, as the mod's is, not depth sorting. **The return value is part of the fix**: the addon warns "UnitXP_SP3.dll failed to execute" on a falsy result (`UnitXP_SP3_Addon.lua:108-113`), and the dispatcher's fall-through answers the integer 0, which is truthy in Lua — so before this the toggle reported success and changed nothing | ## ClassicAPI — confirmed (open source, `brues-code/ClassicAPI`)