unitxp: inSight and behind — four macro conditionals that silently never worked

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-10-25 17:03:30 -06:00
co-authored by Claude Opus 5
parent 1e9ef80c41
commit 38a6b6f149
7 changed files with 299 additions and 2 deletions
@@ -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,
}
@@ -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,
}
+106
View File
@@ -801,6 +801,14 @@ fn feed_self_movement(
fn feed_unit_reach(
script: Option<NonSendMut<UiScript>>,
// 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<benilla_world::collision::WorldCollision>,
tokens: UnitTokens,
selection: Res<Selection>,
self_q: Query<(&Transform, &ObjectStore), With<SelfPlayer>>,
@@ -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)));
}
}
+1 -1
View File
@@ -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.
@@ -70,6 +70,37 @@ fn distance_between(lua: &Lua, args: &[Value]) -> Option<f64> {
/// 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::<Model>().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", <sub>)`. 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::<bool>(r#"return UnitXP("inSight", "player", "target") == true"#)
.unwrap());
assert!(s
.eval::<bool>(r#"return UnitXP("inSight", "player", "mouseover") ~= true"#)
.unwrap());
assert_eq!(
s.eval::<Option<bool>>(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::<bool>(r#"return UnitXP("behind", "player", "target") == true"#)
.unwrap());
assert_eq!(
s.eval::<Option<bool>>(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::<Option<bool>>(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::<Option<bool>>(&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,
},
+9
View File
@@ -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.
+16 -1
View File
@@ -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`)