"mouseover" names nobody once no unit wins the hover: empty ground, a corpse or a nearer GameObject clear it, and the fading tooltip's health bar follows the unit's guid, not the token (#518)
`"mouseover"` now names nobody once the mouse leaves a unit, as in 1.12.1, where the publisher `0x492890` zeroes the mouseover guid pair on teardown (`0x4928e8`, `0x4928f2`) and a corpse or GameObject hover leaves it naming no unit. Before, `UnitExists("mouseover")`, `UnitName("mouseover")` and `UnitIsUnit("mouseover", …)` kept answering for the last unit hovered.
- The world tooltip's health bar follows its subject's guid, as the builder `0x529fe0` registers its health watcher on the guid (`0x52a0a8`), so clearing the token leaves the fading plate and its bar as they were; the clear fires no `UPDATE_MOUSEOVER_UNIT`, whose one fire site is the unit arm (`0x4929b3`).
- One definition of the hovered unit (none when a nearer GameObject wins the pick) serves the tooltip, the unit tokens and the aura feed, so they clear together.
Fixes #500
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
829194ac1a
commit
0e39273f13
@@ -113,6 +113,16 @@ impl Hovered {
|
||||
pub(crate) fn any(&self) -> Option<Entity> {
|
||||
self.target.or(self.corpse)
|
||||
}
|
||||
|
||||
/// The unit `"mouseover"` names: the hovered unit, unless a nearer GameObject won the pick.
|
||||
/// The publisher `0x492890` writes whatever won to `0xb4e2c8`/`0xb4e2cc` (`0x492927`,
|
||||
/// `0x492938`), and the token resolver re-resolves it as a unit (`0x515bca mov ecx,8`), so a
|
||||
/// GameObject, a corpse or nothing names nobody (`0x515bd9 je`).
|
||||
pub(crate) fn mouseover(&self, go: &HoveredObject) -> Option<(Entity, u64)> {
|
||||
self.target
|
||||
.zip(self.guid)
|
||||
.filter(|_| !go_is_nearest(self, go))
|
||||
}
|
||||
}
|
||||
|
||||
/// The GameObject under the cursor this frame, by [`hover::update_hovered_object`]: usable, never
|
||||
|
||||
@@ -393,6 +393,7 @@ struct AuraUnits<'w> {
|
||||
// anyone.
|
||||
group: Option<Res<'w, crate::ui_party::GroupState>>,
|
||||
hovered: Option<Res<'w, crate::target::Hovered>>,
|
||||
hovered_go: Option<Res<'w, crate::target::HoveredObject>>,
|
||||
interact: Option<Res<'w, crate::ui_session::InteractNpc>>,
|
||||
}
|
||||
|
||||
@@ -548,8 +549,10 @@ fn feed_auras(
|
||||
mouseover: units
|
||||
.hovered
|
||||
.as_deref()
|
||||
.and_then(|h| h.target.and(h.guid))
|
||||
.unwrap_or(0),
|
||||
.and_then(|h| {
|
||||
h.mouseover(&units.hovered_go.as_deref().copied().unwrap_or_default())
|
||||
})
|
||||
.map_or(0, |(_, guid)| guid),
|
||||
npc: units.interact.as_deref().and_then(|i| i.1).unwrap_or(0),
|
||||
held,
|
||||
buffs_visible,
|
||||
|
||||
@@ -963,6 +963,26 @@ mod tests {
|
||||
assert_eq!(listed(&mut app, "party1target", true), [770, 589]);
|
||||
}
|
||||
|
||||
/// A nearer GameObject wins the pick, and the publisher writes its guid, which the resolver
|
||||
/// rejects as a unit (`0x515bd9 je`): the unit behind it is no longer the mouseover.
|
||||
#[test]
|
||||
fn a_nearer_gameobject_takes_the_mouseover_from_the_unit_behind_it() {
|
||||
let mut app = chain_app();
|
||||
assert_eq!(listed(&mut app, "mouseover", true), [770]);
|
||||
app.world_mut()
|
||||
.resource_mut::<crate::target::Hovered>()
|
||||
.distance = 10.0;
|
||||
let chest = app.world_mut().spawn_empty().id();
|
||||
app.insert_resource(crate::target::HoveredObject {
|
||||
target: Some(chest),
|
||||
guid: Some(0xF110_0000_0000_0004),
|
||||
distance: 5.0,
|
||||
});
|
||||
app.update();
|
||||
assert!(listed(&mut app, "mouseover", true).is_empty());
|
||||
assert!(listed(&mut app, "mouseovertarget", true).is_empty());
|
||||
}
|
||||
|
||||
/// A raid member's target that is out of view but on the roster reads its record, as
|
||||
/// `UnitDebuff`'s roster walk finds it (`0x519b25`); a hop off a unit not held is nobody.
|
||||
#[test]
|
||||
|
||||
@@ -565,6 +565,8 @@ pub(crate) struct HoverMemo<'s> {
|
||||
last_lines: Local<'s, crate::ui_script::VmMemo<Option<UnitState>>>,
|
||||
/// The hover probe's last trace line, so a stationary probe logs it once.
|
||||
trace: Local<'s, String>,
|
||||
/// Whether the VM holds a `"mouseover"` unit, so the clear is pushed once.
|
||||
published: Local<'s, crate::ui_script::VmMemo<bool>>,
|
||||
}
|
||||
|
||||
fn lines_view(s: &UnitState) -> UnitState {
|
||||
@@ -619,10 +621,11 @@ fn drive_mouseover_tooltip(
|
||||
let (last, last_lines, trace) = (&mut memo.last, &mut memo.last_lines, &mut *memo.trace);
|
||||
let last = last.get(&script);
|
||||
let last_lines = last_lines.get(&script);
|
||||
let published = memo.published.get(&script);
|
||||
let self_store = self_q.iter().next();
|
||||
let chr = classes.as_deref().map(|t| &t.0);
|
||||
|
||||
let unit = hovered.target.zip(hovered.guid).and_then(|(entity, guid)| {
|
||||
let unit = hovered.mouseover(&hovered_go).and_then(|(entity, guid)| {
|
||||
let store = stores.get(entity).ok()?;
|
||||
let name = names
|
||||
.resolve_unit(guid, Some(store), &commands)
|
||||
@@ -648,11 +651,12 @@ fn drive_mouseover_tooltip(
|
||||
(unit.is_none() && hovered.corpse.is_none()) || go_is_nearest(&hovered, &hovered_go)
|
||||
});
|
||||
|
||||
if let Some((guid, state)) = unit.filter(|_| go.is_none()) {
|
||||
if let Some((guid, state)) = unit {
|
||||
// Push first: the builder reads the token. Rebuild on a new target or a late name or
|
||||
// creature answer; health and power stay out of the key, since the watcher drives the bar.
|
||||
let key = lines_view(&state);
|
||||
script.set_unit("mouseover", Some(state));
|
||||
*published = true;
|
||||
if *last != LastHover::Unit(guid) || last_lines.as_ref() != Some(&key) {
|
||||
script.world_tooltip_unit("mouseover");
|
||||
*last = LastHover::Unit(guid);
|
||||
@@ -660,6 +664,14 @@ fn drive_mouseover_tooltip(
|
||||
}
|
||||
return;
|
||||
}
|
||||
// No unit won the pick: the publisher's teardown zeroes the mouseover pair (`0x4928e8`,
|
||||
// `0x4928f2`) and writes a null, corpse or GameObject guid, none of which resolves as a unit,
|
||||
// so `"mouseover"` names nobody from this frame. A fading plate keeps its lines and its bar,
|
||||
// which follows the unit's guid, not the token; the null publish fires no event (the sole
|
||||
// `UPDATE_MOUSEOVER_UNIT` is the unit arm's, `0x4929b3`).
|
||||
if std::mem::take(published) {
|
||||
script.set_unit("mouseover", None);
|
||||
}
|
||||
// The corpse plate, "Corpse of <owner>" (`0x52aef0`): a name and nothing else, corner-seated.
|
||||
// The publisher fires no event for a corpse, so `UnitName("mouseover")` on one stays nil.
|
||||
if let Some((entity, guid)) = hovered
|
||||
@@ -830,7 +842,7 @@ fn drive_mouseover_tooltip(
|
||||
return;
|
||||
}
|
||||
if !matches!(*last, LastHover::None) {
|
||||
// Hover lost: arm the fade. The `mouseover` state stays, so the fading plate keeps it.
|
||||
// Hover lost: arm the fade (`0x492909` → `0x530ae0`).
|
||||
script.world_tooltip_fade();
|
||||
*last = LastHover::None;
|
||||
}
|
||||
|
||||
@@ -518,17 +518,14 @@ fn the_spell_feed_runs_after_the_trainer_feed() {
|
||||
));
|
||||
}
|
||||
|
||||
/// The hover pushes `"mouseover"` with the hovered guid, the pair `0x492890` writes to
|
||||
/// `0xb4e2c8`/`0xb4e2cc`: `UnitIsUnit` (`0x516070`) resolves both tokens through `0x515970` and
|
||||
/// compares guids, so hovering the target answers 1 and hovering anyone else nil.
|
||||
#[test]
|
||||
fn the_mouseover_token_carries_the_hovered_guid() {
|
||||
use benilla_protocol::ObjectFields;
|
||||
use bevy::ecs::system::RunSystemOnce;
|
||||
const ME: u64 = 0x77;
|
||||
const WOLF: u64 = 0xF130_0000_4500_0001;
|
||||
const BOAR: u64 = 0xF130_0000_4600_0002;
|
||||
|
||||
const ME: u64 = 0x77;
|
||||
const WOLF: u64 = 0xF130_0000_4500_0001;
|
||||
const BOAR: u64 = 0xF130_0000_4600_0002;
|
||||
/// The world-hover driver over a bare VM holding `"player"` (us) and `"target"` (the wolf), each
|
||||
/// with its guid, as the unit feed pushes them; returns the app, the wolf and the boar.
|
||||
fn mouseover_app() -> (App, Entity, Entity) {
|
||||
use benilla_protocol::ObjectFields;
|
||||
|
||||
let mut app = App::new();
|
||||
let (tx, _rx) = crossbeam_channel::unbounded();
|
||||
@@ -541,19 +538,17 @@ fn the_mouseover_token_carries_the_hovered_guid() {
|
||||
.init_resource::<crate::net::GuidIndex>()
|
||||
.init_resource::<crate::go_templates::GameObjectTemplates>()
|
||||
.init_resource::<Items>()
|
||||
.init_resource::<PlayerActions>();
|
||||
.init_resource::<PlayerActions>()
|
||||
.add_systems(Update, drive_mouseover_tooltip);
|
||||
app.world_mut()
|
||||
.spawn((SelfPlayer, ObjectStore(ObjectFields::default())));
|
||||
let wolf = app
|
||||
.world_mut()
|
||||
.spawn(ObjectStore(ObjectFields::default()))
|
||||
.id();
|
||||
let boar = app
|
||||
.world_mut()
|
||||
.spawn(ObjectStore(ObjectFields::default()))
|
||||
.id();
|
||||
let mut unit = || {
|
||||
app.world_mut()
|
||||
.spawn(ObjectStore(ObjectFields::default()))
|
||||
.id()
|
||||
};
|
||||
let (wolf, boar) = (unit(), unit());
|
||||
|
||||
// `"player"` and `"target"` as the unit feed pushes them, each with its guid.
|
||||
let mut script = UiScript::new().unwrap();
|
||||
for (token, guid) in [("player", ME), ("target", WOLF)] {
|
||||
script.set_unit(
|
||||
@@ -567,29 +562,51 @@ fn the_mouseover_token_carries_the_hovered_guid() {
|
||||
);
|
||||
}
|
||||
app.insert_non_send_resource(script);
|
||||
(app, wolf, boar)
|
||||
}
|
||||
|
||||
let hover = |app: &mut App, entity: Entity, guid: u64| {
|
||||
*app.world_mut().resource_mut::<Hovered>() = Hovered {
|
||||
target: Some(entity),
|
||||
guid: Some(guid),
|
||||
..Default::default()
|
||||
};
|
||||
app.world_mut()
|
||||
.run_system_once(drive_mouseover_tooltip)
|
||||
.unwrap();
|
||||
/// Set this frame's pick and run the driver.
|
||||
fn hover(app: &mut App, pick: Hovered, object: HoveredObject) {
|
||||
*app.world_mut().resource_mut::<Hovered>() = pick;
|
||||
*app.world_mut().resource_mut::<HoveredObject>() = object;
|
||||
app.update();
|
||||
}
|
||||
|
||||
fn hover_unit(app: &mut App, entity: Entity, guid: u64) {
|
||||
let pick = Hovered {
|
||||
target: Some(entity),
|
||||
guid: Some(guid),
|
||||
distance: 10.0,
|
||||
..Default::default()
|
||||
};
|
||||
let is_unit = |app: &mut App, other: &str| -> Option<i64> {
|
||||
app.world_mut()
|
||||
.non_send_resource_mut::<UiScript>()
|
||||
.eval::<Option<i64>>(&format!(r#"return UnitIsUnit("mouseover", "{other}")"#))
|
||||
.unwrap()
|
||||
hover(app, pick, HoveredObject::default());
|
||||
}
|
||||
|
||||
fn eval(app: &mut App, lua: &str) -> Option<i64> {
|
||||
app.world_mut()
|
||||
.non_send_resource_mut::<UiScript>()
|
||||
.eval::<Option<i64>>(lua)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The hover pushes `"mouseover"` with the hovered guid, the pair `0x492890` writes to
|
||||
/// `0xb4e2c8`/`0xb4e2cc`: `UnitIsUnit` (`0x516070`) resolves both tokens through `0x515970` and
|
||||
/// compares guids, so hovering the target answers 1 and hovering anyone else nil.
|
||||
#[test]
|
||||
fn the_mouseover_token_carries_the_hovered_guid() {
|
||||
let (mut app, wolf, boar) = mouseover_app();
|
||||
let is_unit = |app: &mut App, other: &str| {
|
||||
eval(
|
||||
app,
|
||||
&format!(r#"return UnitIsUnit("mouseover", "{other}")"#),
|
||||
)
|
||||
};
|
||||
|
||||
hover(&mut app, wolf, WOLF);
|
||||
hover_unit(&mut app, wolf, WOLF);
|
||||
assert_eq!(is_unit(&mut app, "target"), Some(1), "hovering the target");
|
||||
assert_eq!(is_unit(&mut app, "player"), None, "the target is not us");
|
||||
|
||||
hover(&mut app, boar, BOAR);
|
||||
hover_unit(&mut app, boar, BOAR);
|
||||
assert_eq!(
|
||||
is_unit(&mut app, "target"),
|
||||
None,
|
||||
@@ -601,3 +618,60 @@ fn the_mouseover_token_carries_the_hovered_guid() {
|
||||
"the token is itself"
|
||||
);
|
||||
}
|
||||
|
||||
/// Once no unit wins the pick, `"mouseover"` names nobody: the publisher `0x492890` zeroes the pair
|
||||
/// (`0x4928e8`, `0x4928f2`) and writes a null, corpse or GameObject guid, which the resolver
|
||||
/// rejects as a unit (`0x515bca mov ecx,8`, `0x515bd9 je`). Empty ground, a corpse and a nearer
|
||||
/// GameObject each clear it, in the frame the hover leaves the unit.
|
||||
#[test]
|
||||
fn the_mouseover_token_clears_when_no_unit_is_hovered() {
|
||||
use benilla_protocol::ObjectFields;
|
||||
|
||||
let (mut app, wolf, _) = mouseover_app();
|
||||
let corpse = app
|
||||
.world_mut()
|
||||
.spawn(ObjectStore(ObjectFields::default()))
|
||||
.id();
|
||||
let chest = app
|
||||
.world_mut()
|
||||
.spawn(ObjectStore(ObjectFields::default()))
|
||||
.id();
|
||||
let named = |app: &mut App| {
|
||||
(
|
||||
eval(app, r#"return UnitExists("mouseover")"#),
|
||||
eval(app, r#"return UnitIsUnit("mouseover", "target")"#),
|
||||
)
|
||||
};
|
||||
let empty = Hovered::default();
|
||||
let on_corpse = Hovered {
|
||||
corpse: Some(corpse),
|
||||
corpse_guid: Some(0xF100_0000_0000_0003),
|
||||
distance: 10.0,
|
||||
..Default::default()
|
||||
};
|
||||
let under_chest = Hovered {
|
||||
target: Some(wolf),
|
||||
guid: Some(WOLF),
|
||||
distance: 10.0,
|
||||
..Default::default()
|
||||
};
|
||||
let chest_nearer = HoveredObject {
|
||||
target: Some(chest),
|
||||
guid: Some(0xF110_0000_0000_0004),
|
||||
distance: 5.0,
|
||||
};
|
||||
for (leave, object, what) in [
|
||||
(empty, HoveredObject::default(), "empty ground"),
|
||||
(on_corpse, HoveredObject::default(), "a corpse"),
|
||||
(under_chest, chest_nearer, "a nearer GameObject"),
|
||||
] {
|
||||
hover_unit(&mut app, wolf, WOLF);
|
||||
assert_eq!(
|
||||
named(&mut app),
|
||||
(Some(1), Some(1)),
|
||||
"over the wolf, before {what}"
|
||||
);
|
||||
hover(&mut app, leave, object);
|
||||
assert_eq!(named(&mut app), (None, None), "after {what}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -582,6 +582,7 @@ pub(crate) struct UnitTokens<'w, 's> {
|
||||
index: Option<Res<'w, crate::net::GuidIndex>>,
|
||||
pet: Option<Res<'w, crate::ui_pet::PetBar>>,
|
||||
hovered: Option<Res<'w, crate::target::Hovered>>,
|
||||
hovered_go: Option<Res<'w, crate::target::HoveredObject>>,
|
||||
group: Res<'w, crate::ui_party::GroupState>,
|
||||
pub(crate) stores: Query<'w, 's, &'static ObjectStore>,
|
||||
me: Query<'w, 's, (Entity, &'static Guid), With<SelfPlayer>>,
|
||||
@@ -610,11 +611,11 @@ impl UnitTokens<'_, '_> {
|
||||
.and_then(|s| s.0.unit_target())
|
||||
.filter(|g| *g != 0)
|
||||
.and_then(|g| self.held(g)),
|
||||
// The same `Hovered` pick `ui_tooltip` pushes `"mouseover"` from.
|
||||
"mouseover" => {
|
||||
let h = self.hovered.as_ref()?;
|
||||
h.target.zip(h.guid)
|
||||
}
|
||||
// The same pick `ui_tooltip` pushes `"mouseover"` from.
|
||||
"mouseover" => self
|
||||
.hovered
|
||||
.as_ref()?
|
||||
.mouseover(&self.hovered_go.as_deref().copied().unwrap_or_default()),
|
||||
// Off the pet bar's cached guid, as the `"pet"` snapshot reads it.
|
||||
"pet" => {
|
||||
let guid = self.pet.as_ref()?.spells.pet_guid;
|
||||
@@ -1629,6 +1630,41 @@ fn combo_edge(last: Option<(u8, u64)>, now: (u8, u64)) -> Option<bool> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// `"mouseover"` resolves through the pick the tooltip publishes: the hovered unit, until a
|
||||
/// nearer GameObject wins it and the resolver rejects the GameObject's guid (`0x515bd9 je`).
|
||||
#[test]
|
||||
fn the_mouseover_token_resolves_nobody_behind_a_nearer_gameobject() {
|
||||
use crate::target::{Hovered, HoveredObject};
|
||||
use bevy::ecs::system::RunSystemOnce;
|
||||
const WOLF: u64 = 0xF130_0000_4500_0001;
|
||||
|
||||
let mut app = App::new();
|
||||
app.init_resource::<crate::ui_party::GroupState>()
|
||||
.init_resource::<HoveredObject>();
|
||||
let wolf = app.world_mut().spawn_empty().id();
|
||||
app.insert_resource(Hovered {
|
||||
target: Some(wolf),
|
||||
guid: Some(WOLF),
|
||||
distance: 10.0,
|
||||
..Default::default()
|
||||
});
|
||||
let resolve = |app: &mut App| {
|
||||
app.world_mut()
|
||||
.run_system_once(|tokens: UnitTokens| {
|
||||
tokens.resolve("mouseover", &Selection::default())
|
||||
})
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(resolve(&mut app), Some((wolf, WOLF)), "the hovered unit");
|
||||
let chest = app.world_mut().spawn_empty().id();
|
||||
app.insert_resource(HoveredObject {
|
||||
target: Some(chest),
|
||||
guid: Some(0xF110_0000_0000_0004),
|
||||
distance: 5.0,
|
||||
});
|
||||
assert_eq!(resolve(&mut app), None, "a nearer GameObject names nobody");
|
||||
}
|
||||
|
||||
/// The team digit comes off the race byte, whatever template sits beside it (a GM's 35).
|
||||
#[test]
|
||||
fn the_team_digit_comes_off_the_race_byte_not_the_faction_template() {
|
||||
|
||||
@@ -25,6 +25,7 @@ fn seed_level_strings(s: &mut UiScript) {
|
||||
fn wolf() -> UnitState {
|
||||
UnitState {
|
||||
exists: true,
|
||||
guid: 0xF130_0000_4500_0001,
|
||||
name: Some("Timber Wolf".into()),
|
||||
health: 30,
|
||||
max_health: 50,
|
||||
@@ -454,8 +455,28 @@ fn world_hover_drive_and_health_watcher() {
|
||||
r#"assert(GameTooltipStatusBar:GetValue() == 12, "the health watcher tracked the push")"#,
|
||||
)
|
||||
.unwrap();
|
||||
// Hover lost: the fade arms and `"mouseover"` names nobody (`0x492890` zeroes the pair), yet
|
||||
// the fading plate keeps its bar, whose watcher follows the unit's guid, not the token.
|
||||
s.world_tooltip_fade();
|
||||
s.tick(0.6);
|
||||
s.set_unit("mouseover", None);
|
||||
s.tick(0.1);
|
||||
s.run(
|
||||
r#"
|
||||
assert(UnitExists("mouseover") == nil, "the token is cleared")
|
||||
assert(GameTooltip:IsShown(), "still fading")
|
||||
assert(GameTooltipStatusBar:IsShown(), "the fading plate keeps its bar")
|
||||
assert(GameTooltipStatusBar:GetValue() == 12, "and its value")
|
||||
assert(recolored == 1, "the clear fires no UPDATE_MOUSEOVER_UNIT")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
// The same unit pushed under another token still drives the bar.
|
||||
let mut as_target = wolf();
|
||||
as_target.health = 8;
|
||||
s.set_unit("target", Some(as_target));
|
||||
s.run(r#"assert(GameTooltipStatusBar:GetValue() == 8, "the watcher follows the guid")"#)
|
||||
.unwrap();
|
||||
s.tick(0.5);
|
||||
s.run(r#"assert(not GameTooltip:IsShown(), "faded out after the ramp")"#)
|
||||
.unwrap();
|
||||
assert!(s.take_errors().is_empty());
|
||||
|
||||
@@ -271,7 +271,8 @@ pub(super) fn clear_content(model: &mut Model, h: FrameHandle) {
|
||||
if let Ok(t) = tip_mut(model, h) {
|
||||
t.num_lines = 0;
|
||||
t.min_width = 0.0;
|
||||
t.unit_token = None;
|
||||
// Clear drops the subject and unregisters its health watcher (`0x5300ad`, `0x53007c`).
|
||||
t.unit_guid = None;
|
||||
t.world_owned = false;
|
||||
}
|
||||
// The `<name>StatusBar` health bar is unit content: it hides with the lines, and the next
|
||||
|
||||
@@ -113,7 +113,7 @@ fn render_unit(lua: &Lua, this: &Table, token: &str) -> mlua::Result<bool> {
|
||||
}
|
||||
fire_cleared(lua, h);
|
||||
let Some(u) = unit else {
|
||||
set_live_token(lua, h, None);
|
||||
set_subject(lua, h, None);
|
||||
show_or_hide_empty(lua, h);
|
||||
return Ok(false);
|
||||
};
|
||||
@@ -149,7 +149,8 @@ fn render_unit(lua: &Lua, this: &Table, token: &str) -> mlua::Result<bool> {
|
||||
if u.racial_leader && u.pvp {
|
||||
append_line(lua, this, ("Leader".into(), WHITE), None, false)?;
|
||||
}
|
||||
set_live_token(lua, h, Some(token.to_string()));
|
||||
// An unresolved guid (0) is no subject: nothing can push for it.
|
||||
set_subject(lua, h, Some(u.guid).filter(|&g| g != 0));
|
||||
update_bar(lua, h, Some(&u));
|
||||
show_or_hide_empty(lua, h);
|
||||
Ok(true)
|
||||
@@ -165,12 +166,12 @@ fn set_world_owned(lua: &Lua, h: FrameHandle) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Remember (or drop) the tooltip's live unit token, the health watcher's key.
|
||||
fn set_live_token(lua: &Lua, h: FrameHandle, token: Option<String>) {
|
||||
/// Remember (or drop) the guid of the unit the tooltip shows, the health watcher's key.
|
||||
fn set_subject(lua: &Lua, h: FrameHandle, guid: Option<u64>) {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
if let Some(f) = model.arena.frame_mut(h) {
|
||||
if let KindState::Tooltip(t) = &mut f.kind_state {
|
||||
t.unit_token = token;
|
||||
t.unit_guid = guid;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,13 +228,18 @@ fn update_bar(lua: &Lua, h: FrameHandle, unit: Option<&UnitState>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// A `set_unit` push for a tooltip's live token updates its health bar without rebuilding the
|
||||
/// lines, as the reference's HEALTH field watcher does.
|
||||
/// A `set_unit` push of the unit a tooltip shows updates its health bar without rebuilding the
|
||||
/// lines, as the reference's HEALTH field watcher does: it is keyed on the unit's guid (`0x52a0a8`
|
||||
/// registers `0x529560` on `+0x368`/`+0x36c`), so a token that stops naming the unit, as
|
||||
/// `"mouseover"` does when the hover ends, leaves a fading plate's bar standing.
|
||||
pub(super) fn on_unit_push(lua: &Lua, token: &str) {
|
||||
let hits: Vec<FrameHandle> = {
|
||||
let (unit, hits): (UnitState, Vec<FrameHandle>) = {
|
||||
let model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let Some(unit) = model.unit(token).filter(|u| u.exists && u.guid != 0) else {
|
||||
return;
|
||||
};
|
||||
// Registered frames only, as `update_bar` writes layout.
|
||||
model
|
||||
let hits: Vec<FrameHandle> = model
|
||||
.arena
|
||||
.tooltip_kinds()
|
||||
.iter()
|
||||
@@ -242,17 +248,17 @@ pub(super) fn on_unit_push(lua: &Lua, token: &str) {
|
||||
model.frame_to_id.contains_key(&h)
|
||||
&& matches!(
|
||||
model.arena.frame(h).map(|f| &f.kind_state),
|
||||
Some(KindState::Tooltip(t)) if t.unit_token.as_deref() == Some(token)
|
||||
Some(KindState::Tooltip(t)) if t.unit_guid == Some(unit.guid)
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
.collect();
|
||||
if hits.is_empty() {
|
||||
return;
|
||||
}
|
||||
(unit.clone(), hits)
|
||||
};
|
||||
for h in hits {
|
||||
let unit = {
|
||||
let model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
model.unit(token).cloned().filter(|u| u.exists)
|
||||
};
|
||||
update_bar(lua, h, unit.as_ref());
|
||||
update_bar(lua, h, Some(&unit));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -308,7 +308,7 @@ impl super::UiScript {
|
||||
}
|
||||
}
|
||||
}
|
||||
// A push for a tooltip's live unit token re-drives its health bar, without a line rebuild.
|
||||
// A push of the unit a tooltip shows re-drives its health bar, without a line rebuild.
|
||||
super::tooltip_unit::on_unit_push(&self.lua, token);
|
||||
}
|
||||
|
||||
|
||||
@@ -189,8 +189,10 @@ pub struct TooltipState {
|
||||
pub min_width: f32,
|
||||
/// `FadeOut()`'s start on the `GetTime` clock; any fresh content or `Show` cancels the fade.
|
||||
pub fade_start: Option<f64>,
|
||||
/// The unit this tooltip shows; a `set_unit` push for it re-drives the health bar.
|
||||
pub unit_token: Option<String>,
|
||||
/// The guid of the unit this tooltip shows (`+0x368`/`+0x36c`, stored at `0x52a070`), whose
|
||||
/// health watcher (`0x467e70`, registered at `0x52a0a8`) drives the bar; a `set_unit` push of
|
||||
/// that unit, under any token, re-drives it.
|
||||
pub unit_guid: Option<u64>,
|
||||
/// Shows world-hover content, which fades when the hover is lost; a window hover never fades.
|
||||
pub world_owned: bool,
|
||||
/// Armed for a compare render: the next `SetInventoryItem` prepends "Currently Equipped"
|
||||
|
||||
Reference in New Issue
Block a user