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_<rank>_<team>` and the name, a city protector's `PVP_MEDAL<n>` 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 <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
This commit is contained in:
goran2711
2026-10-05 22:08:33 +07:00
committed by GitHub
co-authored by Claude Opus 5.5 Sam
parent ab909df6d9
commit bb84f1f1fb
5 changed files with 149 additions and 9 deletions
@@ -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
@@ -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),
+3 -3
View File
@@ -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(
@@ -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`).
+9 -6
View File
@@ -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 `<name>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
//! `<name>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<bool> {
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)?;