superwow: CanLootUnit, GetSpeed and UnitNameplate
Three of the DLL's own manifest, and two of them needed no new feed at all — which is the point worth recording, because the row that called this family "todo, blocked: each needs new app-side state" was wrong about two of the three. `CanLootUnit` is the loot sparkle's test read through the token map: `UnitState` already carries `dead` and raw `dynamic_flags`, so a corpse that sparkles answers 1 and there is one source of truth for "has loot for me". The bit is per-viewer on the server, so "can" means can. `UnitNameplate` goes through the SAME `plate_frame` wrapper as ClassicAPI's `C_NamePlate.GetNamePlateForUnit`, welded by a test that asserts both names hand back one object — an addon may feature-detect either, and two different frames for one plate is the kind of bug that only shows up as an anchor in the wrong place. `GetSpeed` is the one that needed a field: the posture snapshot gained `speed`, filled from `net::current_speed`, the reference's own `CMovement::GetCurrentSpeed 0x7c4c90`. What could not be settled is written down rather than guessed over — upstream publishes no prose and nothing in the corpus calls it, so "current" is a reading, taken because it is the more informative of the two and because it is the only function in the client named after speed that a hook would return. Standing still it is 0, as that getter is. `SelfMovement` loses its `Eq` (an f32 field); nothing compared two snapshots — the feed assigns a fresh one each tick. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -759,7 +759,10 @@ fn feed_self_movement(
|
||||
// `Option`, like `player` and the queries: a UI-only harness has no world at all, and the
|
||||
// indoor bit is the one field that needs one. Everything else still feeds without it.
|
||||
world: Option<benilla_world::world_point::WorldPoint>,
|
||||
self_q: Query<&ObjectStore, With<SelfPlayer>>,
|
||||
// The speed block is `Option` on its own account, not just for a harness: it arrives with the
|
||||
// create block's LIVING half, so there is a window at world entry where the player exists and
|
||||
// its speeds do not.
|
||||
self_q: Query<(&ObjectStore, Option<&crate::net::UnitSpeeds>), With<SelfPlayer>>,
|
||||
// The camera rig, for `IsMouselooking()`. `Option` for the same reason `world` is: a UI-only
|
||||
// harness builds no camera, and every other field still feeds without one.
|
||||
rig: Option<Res<crate::player::CameraControl>>,
|
||||
@@ -769,7 +772,10 @@ fn feed_self_movement(
|
||||
};
|
||||
use crate::creature_anim::move_flags;
|
||||
let flags = player.as_deref().map_or(0, |p| p.move_flags());
|
||||
let store = self_q.iter().next();
|
||||
let (store, speeds) = match self_q.iter().next() {
|
||||
Some((store, speeds)) => (Some(store), speeds),
|
||||
None => (None, None),
|
||||
};
|
||||
script.set_self_movement(benilla_ui::script::SelfMovement {
|
||||
swimming: flags & move_flags::SWIMMING != 0,
|
||||
mounted: store.is_some_and(|s| s.0.unit_mount_display_id() > 0),
|
||||
@@ -785,6 +791,10 @@ fn feed_self_movement(
|
||||
| move_flags::TURN_RIGHT)
|
||||
!= 0,
|
||||
shapeshift_form: store.map_or(0, |s| s.0.unit_shapeshift_form()),
|
||||
// SuperWoW's `GetSpeed()`: the reference's own `CMovement::GetCurrentSpeed` walk over our
|
||||
// speed block and the same move flags every field above reads, so the number cannot
|
||||
// disagree with `swimming` or `moving` about what the player is doing this tick.
|
||||
speed: speeds.map_or(0.0, |s| crate::net::current_speed(&s.0, flags)),
|
||||
mouselooking: rig.is_some_and(|r| r.mouselooking()),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -55,7 +55,8 @@ mod map;
|
||||
mod math_ext;
|
||||
mod merchant_frame;
|
||||
mod misc;
|
||||
mod nameplate;
|
||||
// `pub(in …compat)` for its `plate_frame`, which SuperWoW's `UnitNameplate` shares.
|
||||
pub(in crate::script::compat) mod nameplate;
|
||||
mod new_items;
|
||||
mod player_cache;
|
||||
mod player_info;
|
||||
|
||||
@@ -28,8 +28,13 @@ use crate::script::object::frame_wrapper;
|
||||
use crate::script::unit::{guid_token, token_guid};
|
||||
use crate::script::Model;
|
||||
|
||||
/// A plate's Lua frame value, by the arena id its handle maps to.
|
||||
fn plate_frame(lua: &Lua, frame: crate::widget::FrameHandle) -> mlua::Result<Table> {
|
||||
/// A plate's Lua frame value, by the arena id its handle maps to. Shared with SuperWoW's
|
||||
/// `UnitNameplate` (`compat/superwow.rs`), which is the same lookup under the older name — one
|
||||
/// wrapper, so the two cannot hand back different objects for one plate.
|
||||
pub(in crate::script::compat) fn plate_frame(
|
||||
lua: &Lua,
|
||||
frame: crate::widget::FrameHandle,
|
||||
) -> mlua::Result<Table> {
|
||||
let id = lua
|
||||
.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
@@ -271,6 +276,26 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// SuperWoW's `UnitNameplate("unit")` is this same lookup under the older name, and hands
|
||||
/// back the **same object** — an addon that feature-detects one and falls back to the other
|
||||
/// must not get two different frames for one plate.
|
||||
#[test]
|
||||
fn unit_nameplate_is_the_same_frame_as_the_namespaced_getter() {
|
||||
let s = script();
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
r#"return UnitNameplate("target") ~= nil
|
||||
and UnitNameplate("target") == C_NamePlate.GetNamePlateForUnit("target")"#
|
||||
)
|
||||
.unwrap());
|
||||
assert!(
|
||||
s.eval::<bool>(r#"return UnitNameplate("pet") == nil"#)
|
||||
.unwrap(),
|
||||
"a unit with no plate up is nil, not a raise"
|
||||
);
|
||||
assert!(s.eval::<bool>("return UnitNameplate() == nil").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retiring_the_plates_empties_the_lists() {
|
||||
let mut s = script();
|
||||
|
||||
@@ -113,6 +113,7 @@ mod tests {
|
||||
rooted: false,
|
||||
moving: false,
|
||||
shapeshift_form: 3,
|
||||
speed: 0.0,
|
||||
});
|
||||
|
||||
assert_eq!(s.eval::<i64>("return IsSwimming()").unwrap(), 1);
|
||||
|
||||
@@ -218,6 +218,77 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// is dropped by the same rule every routed line follows, which is upstream's behaviour too —
|
||||
// the function appends to an open file.
|
||||
//
|
||||
// CanLootUnit("unit"): whether that unit's corpse holds loot for us. Needs no new feed —
|
||||
// [`UnitState`] already carries `dynamic_flags` raw and `dead`, so this is the same two-part
|
||||
// test the loot sparkle makes on the engine side (`creature_anim/spell_visual.rs`'s
|
||||
// `shows_loot_sparkle`: `unit_is_dead() && unit_lootable()`), read through the token map
|
||||
// instead of the ECS. One source of truth: a corpse that sparkles answers 1 here.
|
||||
//
|
||||
// `UNIT_DYNFLAG_LOOTABLE` (bit `0x1` of `UNIT_DYNAMIC_FLAGS`) is **per viewer** on the server
|
||||
// — vmangos raises it only for players who may actually loot — so permission is already in
|
||||
// the bit, and "can" means can, not merely "has loot". A unit with no descriptor (an
|
||||
// out-of-range party member, a token naming nothing) answers nil rather than 0.
|
||||
//
|
||||
// 1 or nil, the shape every 1.12 predicate answers in, which an addon tests with `== 1`.
|
||||
g.set(
|
||||
"CanLootUnit",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let u = token.as_deref().and_then(|t| model.unit(t));
|
||||
Ok(crate::script::binding_abi::flag(
|
||||
u.is_some_and(|u| u.dead && u.dynamic_flags & 0x1 != 0),
|
||||
))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// UnitNameplate("unit"): that unit's nameplate frame, nil when it has none up. The same
|
||||
// lookup ClassicAPI's `C_NamePlate.GetNamePlateForUnit` makes, through the same
|
||||
// `plate_frame` wrapper, so the two names cannot hand back different objects for one plate —
|
||||
// which matters here, since an addon may well feature-detect one and fall back to the other.
|
||||
//
|
||||
// benilla's plates are engine frames with no FrameXML container, so this is the plate itself
|
||||
// (see `compat/classic_api/nameplate.rs`'s header for the same note about modern's wrapper).
|
||||
g.set(
|
||||
"UnitNameplate",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
let frame = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
token
|
||||
.as_deref()
|
||||
.and_then(|t| model.unit(t))
|
||||
.map(|u| u.guid)
|
||||
.filter(|g| *g != 0)
|
||||
.and_then(|g| model.nameplates.frame_for_guid(g))
|
||||
};
|
||||
match frame {
|
||||
Some(frame) => Ok(Some(super::classic_api::nameplate::plate_frame(
|
||||
lua, frame,
|
||||
)?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// GetSpeed(): the player's own current movement speed in yards a second, from the per-tick
|
||||
// posture snapshot — the engine side computes it with `net.rs`'s `current_speed`, which is
|
||||
// the reference's own `CMovement::GetCurrentSpeed 0x7c4c90` walk (swimming first, then walk
|
||||
// mode as `min(walk, run)`, then run, each backward leg a `min` with its forward field).
|
||||
//
|
||||
// **What could not be settled**: upstream publishes no usage string for this one (it takes no
|
||||
// argument) and no addon in grom's corpus calls it, so whether its number is the *current*
|
||||
// speed or the unit's *run* speed is not knowable from here. Current is the reading taken,
|
||||
// because it is the one function in the client named after speed that a hook would return,
|
||||
// and because it is the strictly more informative of the two — a snare or a mount shows in
|
||||
// it. The consequence to know: standing still, this is 0, exactly as the reference's own
|
||||
// getter is (`flags & ANY_MOVE == 0`).
|
||||
g.set(
|
||||
"GetSpeed",
|
||||
lua.create_function(|lua, ()| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(f64::from(model.self_movement.speed))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// The second argument is the DLL's own: its usage string reads `CombatLogAdd("text" [,
|
||||
// addToRawLog])`, printed beside the `Logs\WoWCombatLog.txt` literal it writes to. Truthy,
|
||||
// the line is ALSO fired as `RAW_COMBATLOG`, so an addon listening there sees it beside the
|
||||
@@ -693,6 +764,62 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// `CanLootUnit("unit")` is the loot sparkle's own two-part test read through the token map:
|
||||
/// dead, and `UNIT_DYNAMIC_FLAGS` bit `0x1`. A unit with no descriptor answers nil, not 0.
|
||||
#[test]
|
||||
fn can_loot_unit_is_dead_plus_the_lootable_bit() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
let unit = |dead: bool, flags: u32| {
|
||||
Some(UnitState {
|
||||
exists: true,
|
||||
has_object: true,
|
||||
dead,
|
||||
dynamic_flags: flags,
|
||||
..Default::default()
|
||||
})
|
||||
};
|
||||
s.set_unit("target", unit(true, 0x1));
|
||||
s.set_unit("mouseover", unit(true, 0x4)); // tapped by someone else, no loot for us
|
||||
s.set_unit("focus", unit(false, 0x1)); // alive and flagged: not a corpse yet
|
||||
|
||||
assert_eq!(s.eval::<i64>(r#"return CanLootUnit("target")"#).unwrap(), 1);
|
||||
assert_eq!(
|
||||
s.eval::<Option<i64>>(r#"return CanLootUnit("mouseover")"#)
|
||||
.unwrap(),
|
||||
None,
|
||||
"nil, not 0 — the 1.12 predicate shape"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<Option<i64>>(r#"return CanLootUnit("focus")"#)
|
||||
.unwrap(),
|
||||
None,
|
||||
"the bit alone is not enough: the sparkle tests dead as well"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<Option<i64>>(r#"return CanLootUnit("party1")"#)
|
||||
.unwrap(),
|
||||
None,
|
||||
"no snapshot at all"
|
||||
);
|
||||
assert_eq!(s.eval::<Option<i64>>("return CanLootUnit()").unwrap(), None);
|
||||
}
|
||||
|
||||
/// `GetSpeed()` reads the posture snapshot's own number, and is 0 before anything feeds one.
|
||||
#[test]
|
||||
fn get_speed_reads_the_fed_snapshot() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return GetSpeed()").unwrap(),
|
||||
0.0,
|
||||
"standing still — and before the first feed, which read the same"
|
||||
);
|
||||
s.set_self_movement(crate::script::SelfMovement {
|
||||
speed: 14.0, // epic mount
|
||||
..Default::default()
|
||||
});
|
||||
assert_eq!(s.eval::<f64>("return GetSpeed()").unwrap(), 14.0);
|
||||
}
|
||||
|
||||
/// `CombatLogAdd(text)` queues one line per call, in order, for the app to append to
|
||||
/// `WoWCombatLog.txt`; the drain empties it.
|
||||
///
|
||||
|
||||
@@ -370,7 +370,9 @@ pub struct PlayerCacheEntry {
|
||||
/// Lua has no path to any of these at all (no mounted/swim/indoor getter exists on the client),
|
||||
/// so there is no reference default to match — `Default` (everything `false`/`0`) is only ever
|
||||
/// observed for the one frame before the feed system's first run.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
// No `Eq`: [`SelfMovement::speed`] is an `f32`. Nothing compares two snapshots for equality —
|
||||
// the feed assigns a fresh one each tick — so the bound was never load-bearing.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
pub struct SelfMovement {
|
||||
/// `MOVEFLAG_SWIMMING`.
|
||||
pub swimming: bool,
|
||||
@@ -390,6 +392,10 @@ pub struct SelfMovement {
|
||||
pub moving: bool,
|
||||
/// `UNIT_FIELD_BYTES_2` byte 3, `0` not shapeshifted (`ShapeshiftForm.dbc` row otherwise).
|
||||
pub shapeshift_form: u8,
|
||||
/// SuperWoW's `GetSpeed()`: the player's current speed in yd/s, `benilla-app`'s
|
||||
/// `net::current_speed` over this unit's own speed block and move flags — the reference's
|
||||
/// `CMovement::GetCurrentSpeed 0x7c4c90`. 0 standing still, as that getter is.
|
||||
pub speed: f32,
|
||||
/// `IsMouselooking()`: the camera rig's own mouse-look flag, the reference's `[cam+0x90] & 1`
|
||||
/// (`CameraControl::mouselooking`) — a right-held turn or a both-button run, not a
|
||||
/// left-button orbit. This one *is* 1.12 data; it is here rather than behind its own feed
|
||||
|
||||
@@ -274,6 +274,16 @@ fn allowed_beyond_1_12() -> HashSet<&'static str> {
|
||||
"Clickthrough",
|
||||
"SetMouseoverUnit",
|
||||
"SpellInfo",
|
||||
// SuperWoW's `CanLootUnit`, `GetSpeed` and `UnitNameplate` (script/compat/superwow.rs),
|
||||
// three facts 1.12's Lua cannot reach: whether a corpse holds loot for us (the dyn-flag
|
||||
// bit the loot sparkle reads, which is per-viewer on the server so it means *may* loot),
|
||||
// the player's own speed in yd/s (the reference computes it C-side for the movement code
|
||||
// and publishes it nowhere), and a unit's nameplate frame (1.12's only route is scanning
|
||||
// `WorldFrame:GetChildren()` and reading regions positionally). `UnitNameplate` is the
|
||||
// same lookup as `C_NamePlate.GetNamePlateForUnit` through one shared wrapper.
|
||||
"CanLootUnit",
|
||||
"GetSpeed",
|
||||
"UnitNameplate",
|
||||
// SuperWoW's `CombatLogAdd(text)` (script/compat/superwow.rs): one line appended to
|
||||
// `WoWCombatLog.txt`, which 1.12 writes C-side off its own chat router and gives Lua no
|
||||
// way into. It is also a detection name in its own right — BigWigs' Spell Requests gates
|
||||
|
||||
@@ -80,10 +80,11 @@ The usage strings, verbatim, for what is left:
|
||||
| `TrackUnit("unit")` / `UntrackUnit("unit")` | todo — pfUI calls `TrackUnit` for every party and raid member under `C.unitframes.track_group` (`modules/superwow.lua:96-127`, inside `pcall`), to put the group on the minimap |
|
||||
| `UnitPosition("unit")` | done |
|
||||
| `GetPlayerBuffID(buffIndex)` | done |
|
||||
| `UnitNameplate("unit")` | todo — the plate frame for a unit; `NamePlates::guid_for_frame` already keys the other direction |
|
||||
| `CanlootUnit("unit")` | todo. **Note the lowercase `l`**: the *usage* string says `CanlootUnit` where the registered name is `CanLootUnit`. The registered name is what an addon calls; the typo is in the error message only |
|
||||
| `UnitNameplate("unit")` | **done** — `compat/superwow.rs`, over `NamePlates::frame_for_guid`, through the **same** `plate_frame` wrapper ClassicAPI's `C_NamePlate.GetNamePlateForUnit` uses, so the two names cannot hand back different objects for one plate. Welded by a test that asserts exactly that, because an addon may feature-detect either |
|
||||
| `CanlootUnit("unit")` | **done** — needed no new feed: `UnitState` already carries `dead` and raw `dynamic_flags`, so it is the loot sparkle's own two-part test (`shows_loot_sparkle`: `unit_is_dead() && unit_lootable()`) read through the token map instead of the ECS, and a corpse that sparkles answers 1. Bit `0x1` is **per viewer** on the server — vmangos raises it only for players who may loot — so "can" really means can. **Note the lowercase `l`**: the *usage* string says `CanlootUnit` where the registered name is `CanLootUnit`; the registered name is what an addon calls, and the typo is in the error message only |
|
||||
| `GetMapPositionWorldLoc(continentIndex, zoneIndex, mapX, mapY)` | todo — map fraction to world XY, which `benilla-world`'s own map maths already does for the world map |
|
||||
| `GetMapBoundaries(continentIndex, zoneIndex)` | todo — the zone's world extent, the same table that conversion reads |
|
||||
| `GetSpeed()` (no usage string: it takes no argument) | **done** — one new field on the posture snapshot, filled by `ui_unit.rs`'s `feed_self_movement` from `net.rs`'s `current_speed`, the reference's own `CMovement::GetCurrentSpeed 0x7c4c90` walk over our speed block and the same move flags every other field of that snapshot reads. **What could not be settled**: upstream publishes no prose and no addon in the corpus calls it, so whether its number is the *current* speed or the unit's *run* speed is unknowable from here. Current is the reading taken — it is the one client function named after speed that a hook would return, and the strictly more informative of the two (a snare or a mount shows in it). The consequence to know: standing still it is 0, exactly as that getter is |
|
||||
| `GetInboxItemLink(messageIndex)` | done (built for ClassicAPI's `Mail`) |
|
||||
| `GetSoundDriverName(driverIndex)` | todo — with `GetSoundDevices`, `GetNumSoundDrivers`, `GetLoadedSoundDriver`: four getters over the audio backend. benilla has one backend and no enumeration, so these answer a one-row list honestly or not at all; no addon in the corpus calls any of them |
|
||||
| `GetQuestIDForLogIndex(questIndex)` / `GetQuestLinkForLogIndex(questIndex)` | done |
|
||||
|
||||
Reference in New Issue
Block a user