From bb84f1f1fb842aa104acba09b2cfbc3067c8b72c Mon Sep 17 00:00:00 2001 From: goran2711 Date: Mon, 5 Oct 2026 16:08:33 +0100 Subject: [PATCH] Unit tooltips title the unit with its PvP name, as 1.12.1 does (#802) A unit tooltip's title read the bare name where 1.12.1 decorates it. The tooltip calls the name builder `0x609370` itself (`0x52a1ab`, or `0x52a1db` when there is no second part) with the decoration flag a literal `1`, the same call `UnitPVPName` makes, so no CVar gates it. With the flag on, a ranked player reads `UNIT_PVP_NAME` of the gendered `PVP_RANK__` and the name, a city protector's `PVP_MEDAL` follows on a second line (`0x6093ef`), and a civilian kill (`0x612550`) reads the `PVP_RANK_CIVILIAN` prefix (`0x609461`). The title now runs the builder `UnitPVPName` already uses. The test covers rank, gender, medal, civilian, an unranked player and a creature; an on-demand `ui-tooltip-rank` capture shows a ranked player's tooltip. Fixes #794 Co-Authored-By: Claude Opus 5.5 Co-Authored-By: Sam --- crates/benilla-app/src/capture/fixtures.rs | 26 +++++ crates/benilla-app/src/capture/scenarios.rs | 12 +++ crates/benilla-ui/src/script/pvp/mod.rs | 6 +- .../src/script/tests/tooltip_unit.rs | 99 +++++++++++++++++++ crates/benilla-ui/src/script/tooltip_unit.rs | 15 +-- 5 files changed, 149 insertions(+), 9 deletions(-) diff --git a/crates/benilla-app/src/capture/fixtures.rs b/crates/benilla-app/src/capture/fixtures.rs index dfd42c07..758659c5 100644 --- a/crates/benilla-app/src/capture/fixtures.rs +++ b/crates/benilla-app/src/capture/fixtures.rs @@ -783,6 +783,32 @@ pub(super) fn seed_ui_fixture( warn!("capture: ui-tooltip-world seed failed to open the tooltip"); } } + UiFixture::TooltipRank => { + let Some(mut script) = script else { + return; + }; + // A ranked player under the cursor, pushed as the mouseover feed does, then the call + // `drive_mouseover_tooltip` makes: the title is `0x609370`'s rank leg over the name, + // "Sergeant Bob". + script.set_unit( + "mouseover", + Some(benilla_ui::script::UnitState { + exists: true, + is_player: true, + name: Some("Bob".into()), + level: 60, + reaction: 5, + pvp: true, + pvp_rank: 7, + pvp_team: 1, + sex: 2, + ..Default::default() + }), + ); + if !script.world_tooltip_unit("mouseover") { + warn!("capture: ui-tooltip-rank seed failed to open the tooltip"); + } + } UiFixture::Character => { // A synthetic self player carrying the full stat block, which the `ui_char` feed turns // into snapshots and events as live. A level-12 warrior; positive (stamina, fire) and diff --git a/crates/benilla-app/src/capture/scenarios.rs b/crates/benilla-app/src/capture/scenarios.rs index 3e566771..db3258f8 100644 --- a/crates/benilla-app/src/capture/scenarios.rs +++ b/crates/benilla-app/src/capture/scenarios.rs @@ -53,6 +53,9 @@ pub(super) enum UiFixture { /// The world-mouseover tooltip over a seeded unit: the default anchor puts it at the screen's /// bottom-right (`GameTooltip.lua:73-77`), never on the hovered model. TooltipWorld, + /// The world-mouseover tooltip over a seeded ranked player: the same anchor, with the title's + /// PvP rank leg ("Sergeant Bob"). + TooltipRank, /// The character window fed through a synthetic self player's stat block and equipped item /// guids, with the items in [`crate::items::Items`]. Character, @@ -715,6 +718,15 @@ pub(super) const ON_DEMAND: &[Scenario] = &[ minute: 720, ui: Some(UiFixture::TooltipWorld), }, + // The same anchor over a ranked player: the tooltip title carries the rank, "Sergeant Bob". + Scenario { + name: "ui-tooltip-rank", + map: Some(MAP_AZEROTH), + eye: GROUND_EYE, + look: GROUND_LOOK, + minute: 720, + ui: Some(UiFixture::TooltipRank), + }, Scenario { name: "ui-char", map: Some(MAP_AZEROTH), diff --git a/crates/benilla-ui/src/script/pvp/mod.rs b/crates/benilla-ui/src/script/pvp/mod.rs index 6bb8d4d3..35876903 100644 --- a/crates/benilla-ui/src/script/pvp/mod.rs +++ b/crates/benilla-ui/src/script/pvp/mod.rs @@ -317,9 +317,9 @@ pub fn decorated_name( Some(format_two_strings(&template, &title, name)) } -/// `0x609370`, the name builder behind `UnitPVPName` (its legs are at the binding); `name` is the -/// plain `UnitName`. -fn pvp_name(lua: &Lua, u: &super::UnitState, name: &str, player_level: u32) -> String { +/// `0x609370`, the name builder behind `UnitPVPName` (its legs are at the binding) and the unit +/// tooltip's title (`0x52a1ab`), both with the decoration flag `1`; `name` is the plain `UnitName`. +pub(super) fn pvp_name(lua: &Lua, u: &super::UnitState, name: &str, player_level: u32) -> String { // Leg A: a ranked player; the title is gendered by this unit and range-unchecked. if u.is_player && u.pvp_rank != 0 { let decorated = decorated_name( diff --git a/crates/benilla-ui/src/script/tests/tooltip_unit.rs b/crates/benilla-ui/src/script/tests/tooltip_unit.rs index cfa1789d..867ba62a 100644 --- a/crates/benilla-ui/src/script/tests/tooltip_unit.rs +++ b/crates/benilla-ui/src/script/tests/tooltip_unit.rs @@ -182,6 +182,105 @@ fn a_pending_name_titles_unknownobject_and_the_answer_replaces_it() { assert!(s.take_errors().is_empty()); } +/// The title is decorated by `0x609370` with the flag on (`0x52a1ab`), the builder `UnitPVPName` +/// runs: a ranked player's rank first through `UNIT_PVP_NAME`, gendered by the unit's sex, and a +/// city protector's medal on a second line; a civilian kill's `PVP_RANK_CIVILIAN` prefix; anyone +/// else the plain name. +#[test] +fn the_tooltip_title_is_the_pvp_name() { + let mut s = script(); + seed_level_strings(&mut s); + s.set_screen_size(800.0, 600.0); + s.set_player_req_state(PlayerReqState { + level: 30, + ..Default::default() + }); + let player = |sex: u8, pvp_rank: u8| UnitState { + exists: true, + is_player: true, + guid: 0x0000_0000_0000_002A, + name: Some("Pfedh".into()), + level: 1, + race: Some("Human".into()), + class: Some("Warrior".into()), + sex, + pvp_rank, + pvp_team: 1, + ..Default::default() + }; + s.run( + r#" + UNIT_PVP_NAME = "%s %s" + PVP_RANK_7_1 = "Sergeant" + PVP_RANK_7_1_FEMALE = "Sergeant (f)" + PVP_MEDAL1 = "Guardian of Stormwind" + PVP_RANK_CIVILIAN = "Civilian" + local a = CreateFrame("Button", "UF1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10) + local tt = CreateFrame("GameTooltip", "TT") + tt:SetOwner(a, "ANCHOR_RIGHT") + "#, + ) + .unwrap(); + let title = |s: &UiScript| -> String { s.eval("return TTTextLeft1:GetText()").unwrap() }; + + s.set_unit("target", Some(player(2, 7))); + s.run(r#"assert(TT:SetUnit("target") == 1)"#).unwrap(); + assert_eq!(title(&s), "Sergeant Pfedh", "the rank rides first"); + + s.set_unit("target", Some(player(3, 7))); + s.run(r#"assert(TT:SetUnit("target") == 1)"#).unwrap(); + assert_eq!( + title(&s), + "Sergeant (f) Pfedh", + "the unit's sex picks the twin" + ); + + s.set_unit("target", Some(player(2, 0))); + s.run(r#"assert(TT:SetUnit("target") == 1)"#).unwrap(); + assert_eq!( + title(&s), + "Pfedh", + "an unranked player keeps the plain name" + ); + + let mut protector = player(2, 7); + protector.pvp_medal = 1; + s.set_unit("target", Some(protector)); + s.run(r#"assert(TT:SetUnit("target") == 1)"#).unwrap(); + assert_eq!( + title(&s), + "Sergeant Pfedh\nGuardian of Stormwind", + "the medal rides a second line" + ); + + let mut beast = wolf(); + beast.pvp_rank = 7; // a creature carries no rank byte, and must not read one anyway + s.set_unit("target", Some(beast)); + s.run(r#"assert(TT:SetUnit("target") == 1)"#).unwrap(); + assert_eq!(title(&s), "Timber Wolf", "a creature takes no rank"); + + // Hostile, PvP-flagged, a civilian and grey (`0x612550`): the dishonorable-kill prefix. + s.set_unit( + "target", + Some(UnitState { + exists: true, + name: Some("Defias Civilian".into()), + level: 20, + reaction: 2, + pvp: true, + civilian: true, + ..Default::default() + }), + ); + s.run(r#"assert(TT:SetUnit("target") == 1)"#).unwrap(); + assert_eq!( + title(&s), + "Civilian Defias Civilian", + "a civilian kill warns" + ); + assert!(s.take_errors().is_empty()); +} + /// The faction line sits between the level line and "PvP" (`0x529fe0`). Civilian needs the PvP /// bit, the flag, hostility and a grey level (`0x612550`); Leader only the PvP bit and the flag /// (`0x6125c0`). diff --git a/crates/benilla-ui/src/script/tooltip_unit.rs b/crates/benilla-ui/src/script/tooltip_unit.rs index 4786c9b2..fe74f1ae 100644 --- a/crates/benilla-ui/src/script/tooltip_unit.rs +++ b/crates/benilla-ui/src/script/tooltip_unit.rs @@ -1,7 +1,8 @@ -//! The engine unit tooltip, in `0x529fe0`'s line order: the name (gold; FrameXML recolours it by -//! reaction), the creature subtitle, the level line, the faction name, then "PvP", "Skinnable", -//! "Civilian" and "Leader", with health on the `StatusBar` child. The world mouseover drives -//! it through [`super::UiScript::world_tooltip_unit`]; unit-frame hovers call `SetUnit` from Lua. +//! The engine unit tooltip, in `0x529fe0`'s line order: the name (gold, decorated by `0x609370` +//! as `UnitPVPName`'s is; FrameXML recolours it by reaction), the creature subtitle, the level +//! line, the faction name, then "PvP", "Skinnable", "Civilian" and "Leader", with health on the +//! `StatusBar` child. The world mouseover drives it through +//! [`super::UiScript::world_tooltip_unit`]; unit-frame hovers call `SetUnit` from Lua. use mlua::{Lua, Table}; @@ -118,10 +119,12 @@ fn render_unit(lua: &Lua, this: &Table, token: &str) -> mlua::Result { return Ok(false); }; // The name: `CGUnit_C::GetUnitName` `0x609210` (`0x52a187`), whose every miss falls to - // `UNKNOWNOBJECT`, as `UnitName`'s does. A token with no object never reaches the builder + // `UNKNOWNOBJECT`, as `UnitName`'s does, then decorated by `0x609370` with the flag a literal + // `1` (`0x52a19b`), as `UnitPVPName` does, so no CVar gates it: a ranked player's rank and + // medal line, a civilian kill's prefix. A token with no object never reaches the builder // (`0x468460`): no plate, and `SetUnit` answers nil. let title = match &u.name { - Some(n) => n.clone(), + Some(name) => super::pvp::pvp_name(lua, &u, name, player_level), None => unknownobject(lua)?.to_str()?.to_string(), }; append_line(lua, this, (title, GOLD), None, false)?;