nameplates: draw overhead names in the UNIT_NAME_FONT face (#3013)
Overhead unit names were laid out in Friz Quadrata whatever the install, so on a zhCN install, whose glyphs that face lacks, NPC names over heads drew nothing (#3005). In 1.12.1 the name font is not a locale table: `PlayerNameInitialize` (`0x6c7470`) reads the Lua global `UNIT_NAME_FONT` at world entry through `FrameScript::GetVariable` (`0x704350`) and builds the name font from it (`0x5c1ae0`), so the install's own `Fonts.xml`, or an addon that reassigns the global, picks the face. `s_loadingFontPaths` (`0x82f018`) is the loading screen's tip font. A `UnitNameFont` resource is read at the same world-entry seat as `DAMAGE_TEXT_FONT`, the name mesh lays out its baseline and glyphs in that face, and the mesh cache drops when the face changes. Tests: `the_name_mesh_lays_out_in_the_unit_name_font`, `world_entry_reads_unit_name_font`, `a_new_name_face_drops_the_meshes_and_the_live_plates`. Fixes #3005 Co-authored-by: hyrw <47274436+hyrw@users.noreply.github.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -311,10 +311,19 @@ const ROCK_SNAP: f32 = 1.0;
|
||||
/// larger source is crisper up close.
|
||||
const BAKE_PX: f32 = 36.0;
|
||||
|
||||
/// The face of the overhead names: the Lua global `UNIT_NAME_FONT`, resolved once per world
|
||||
/// session. `PlayerNameInitialize` (`0x6c7470`) reads it through `FrameScript::GetVariable`
|
||||
/// (`0x704350`) at world entry and creates the name font from it (`GxuFontCreateFont`, `0x5c1ae0`,
|
||||
/// size 0.99, flags 4) into `s_nameFont` (`0xce8738`); each locale's `Fonts.xml` assigns it, so a
|
||||
/// zhCN install names a CJK face. `None` draws in the default face, as `DamageTextFont` does.
|
||||
#[derive(Resource, Default)]
|
||||
pub(crate) struct UnitNameFont(pub(crate) Option<String>);
|
||||
|
||||
/// Build one name-block mesh in pitch units: x centered, y up, line `i`'s baseline at
|
||||
/// `lineCount - i`, one pitch being the font size. The baseline sits `TextEngine::ascent_ratio`
|
||||
/// into the layout cell (the `[0x17c]` load param), as in the 2-D path.
|
||||
fn build_name_mesh(atlas: &mut UiFontAtlas, lines: &[String]) -> Mesh {
|
||||
/// into the layout cell (the `[0x17c]` load param), as in the 2-D path. `font` is the
|
||||
/// `UNIT_NAME_FONT` face, for the baseline and the glyphs both.
|
||||
fn build_name_mesh(atlas: &mut UiFontAtlas, lines: &[String], font: Option<&str>) -> Mesh {
|
||||
use bevy::asset::RenderAssetUsages;
|
||||
use bevy::mesh::{Indices, PrimitiveTopology};
|
||||
|
||||
@@ -329,7 +338,7 @@ fn build_name_mesh(atlas: &mut UiFontAtlas, lines: &[String]) -> Mesh {
|
||||
// The size the glyphs are laid out at: `BAKE_PX` rounded to whole device pixels and back.
|
||||
let src = e.drawn_size(BAKE_PX);
|
||||
let baseline_frac =
|
||||
((f64::from(src) * f64::from(e.ascent_ratio(None)) + 0.5).floor() / f64::from(src)) as f32;
|
||||
((f64::from(src) * f64::from(e.ascent_ratio(font)) + 0.5).floor() / f64::from(src)) as f32;
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
let glyphs = layout_text_quads(
|
||||
&mut e,
|
||||
@@ -342,7 +351,7 @@ fn build_name_mesh(atlas: &mut UiFontAtlas, lines: &[String]) -> Mesh {
|
||||
},
|
||||
0,
|
||||
FontSpec {
|
||||
path: None, // UNIT_NAME_FONT = Friz Quadrata, the default face
|
||||
path: font, // UNIT_NAME_FONT
|
||||
height: Some(BAKE_PX),
|
||||
outline: Outline::None,
|
||||
monochrome: false,
|
||||
@@ -448,6 +457,7 @@ pub(crate) fn drive_nameplates(
|
||||
mirror: Res<crate::ui_chat::AfkMirror>,
|
||||
// The show-gate inputs, one tuple param for Bevy's 16-param ceiling.
|
||||
gates: (
|
||||
Res<UnitNameFont>,
|
||||
Res<Selection>,
|
||||
Res<CombatFlash>,
|
||||
Option<Res<crate::player::CameraControl>>,
|
||||
@@ -480,12 +490,17 @@ pub(crate) fn drive_nameplates(
|
||||
Query<(), With<crate::entities::mount::MountChild>>,
|
||||
),
|
||||
) {
|
||||
let (selection, flash, rig, vplates, bubbles, name_cfg, pvp_strings, self_guid) = gates;
|
||||
let (unit_font, selection, flash, rig, vplates, bubbles, name_cfg, pvp_strings, self_guid) =
|
||||
gates;
|
||||
let (Ok(cam_tf), Some(atlas)) = (camera.single(), atlas.as_mut()) else {
|
||||
return;
|
||||
};
|
||||
// A glyph-sheet reset moves every cached UV.
|
||||
drop_stale_glyph_caches(&mut plates, atlas.generation, &mut commands);
|
||||
// The cached meshes were laid out in the old name face.
|
||||
if unit_font.is_changed() {
|
||||
drop_name_meshes(&mut plates, &mut commands);
|
||||
}
|
||||
// The palette materials, once (they need the atlas image).
|
||||
if plates.materials.is_empty() {
|
||||
let image = atlas.image();
|
||||
@@ -633,7 +648,9 @@ pub(crate) fn drive_nameplates(
|
||||
let mesh = plates
|
||||
.meshes
|
||||
.entry(lines.clone())
|
||||
.or_insert_with(|| meshes.add(build_name_mesh(atlas, &lines)))
|
||||
.or_insert_with(|| {
|
||||
meshes.add(build_name_mesh(atlas, &lines, unit_font.0.as_deref()))
|
||||
})
|
||||
.clone();
|
||||
let material = plates.materials[&color].clone();
|
||||
// The spawn-frame seat: placement can miss a plate whose unit despawns later
|
||||
@@ -798,6 +815,7 @@ impl Plugin for NameplatesPlugin {
|
||||
.init_resource::<Nameplates>()
|
||||
.init_resource::<NameConfig>()
|
||||
.init_resource::<NameLineStrings>()
|
||||
.init_resource::<UnitNameFont>()
|
||||
// After targeting, V-key plates and chat bubbles: the gate reads their verdicts. After the
|
||||
// VM pass too, whose script calls change the selection a plate marks.
|
||||
.add_systems(
|
||||
@@ -831,17 +849,22 @@ fn drop_stale_glyph_caches(plates: &mut Nameplates, generation: u64, commands: &
|
||||
plates.meshes.len(),
|
||||
plates.live.len(),
|
||||
);
|
||||
for live in plates.live.values() {
|
||||
if let Ok(mut e) = commands.get_entity(live.plate) {
|
||||
e.despawn();
|
||||
}
|
||||
}
|
||||
plates.live.clear();
|
||||
plates.meshes.clear();
|
||||
drop_name_meshes(plates, commands);
|
||||
}
|
||||
plates.baked_from = Some(generation);
|
||||
}
|
||||
|
||||
/// Empty the mesh cache and despawn the live plates, which hold the old `Mesh3d`.
|
||||
fn drop_name_meshes(plates: &mut Nameplates, commands: &mut Commands) {
|
||||
for live in plates.live.values() {
|
||||
if let Ok(mut e) = commands.get_entity(live.plate) {
|
||||
e.despawn();
|
||||
}
|
||||
}
|
||||
plates.live.clear();
|
||||
plates.meshes.clear();
|
||||
}
|
||||
|
||||
/// Drop the line-stack mesh dedup on a cross-map transition: the old map's names never return.
|
||||
fn evict_name_meshes(
|
||||
mut changes: MessageReader<benilla_world::world_map::MapChange>,
|
||||
@@ -1391,6 +1414,56 @@ mod tests {
|
||||
assert!(world.get_entity(plate).is_ok());
|
||||
}
|
||||
|
||||
/// A changed `UNIT_NAME_FONT` drops every mesh laid out in the old face, and the live plates.
|
||||
#[test]
|
||||
fn a_new_name_face_drops_the_meshes_and_the_live_plates() {
|
||||
let mut world = World::new();
|
||||
let unit = world.spawn_empty().id();
|
||||
let plate = world.spawn_empty().id();
|
||||
let mut plates = primed(Some(7), unit, plate);
|
||||
|
||||
let mut queue = bevy::ecs::world::CommandQueue::default();
|
||||
{
|
||||
let mut commands = Commands::new(&mut queue, &world);
|
||||
drop_name_meshes(&mut plates, &mut commands);
|
||||
}
|
||||
queue.apply(&mut world);
|
||||
|
||||
assert!(plates.meshes.is_empty());
|
||||
assert!(plates.live.is_empty());
|
||||
assert_eq!(plates.materials.len(), 1, "materials hold no glyphs");
|
||||
assert!(world.get_entity(plate).is_err());
|
||||
}
|
||||
|
||||
/// The face reaches the layout: the same line is wider in Friz Quadrata than in Arial Narrow.
|
||||
#[test]
|
||||
fn the_name_mesh_lays_out_in_the_unit_name_font() {
|
||||
use bevy::mesh::VertexAttributeValues;
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
let mut atlas = UiFontAtlas::for_test(1.0).expect("the client faces open");
|
||||
let lines = vec!["Magister Kalendris".to_string()];
|
||||
let span = |mesh: &Mesh| {
|
||||
let Some(VertexAttributeValues::Float32x3(p)) =
|
||||
mesh.attribute(Mesh::ATTRIBUTE_POSITION)
|
||||
else {
|
||||
panic!("positions");
|
||||
};
|
||||
let xs = p.iter().map(|v| v[0]);
|
||||
xs.clone().fold(f32::MIN, f32::max) - xs.fold(f32::MAX, f32::min)
|
||||
};
|
||||
let friz = span(&build_name_mesh(&mut atlas, &lines, None));
|
||||
let arial = span(&build_name_mesh(
|
||||
&mut atlas,
|
||||
&lines,
|
||||
Some("Fonts\\ARIALN.TTF"),
|
||||
));
|
||||
assert!(friz > 0.0 && arial > 0.0);
|
||||
assert!(
|
||||
(friz - arial).abs() > 0.05,
|
||||
"the face is honoured: {friz} vs {arial}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The block goes through `0x7716f0` whole: a plain block is left as it is, and a `|2` rewrites
|
||||
/// its word on its own line.
|
||||
#[test]
|
||||
|
||||
@@ -747,6 +747,9 @@ fn load_ingame_ui_seated(world: &mut World, seat: LoadSeat) {
|
||||
// `0x495669`), so the face the login read stays until the next login.
|
||||
if seat == LoadSeat::Roster {
|
||||
world.insert_resource(crate::combat_text::read_damage_text_font(&script));
|
||||
// `UNIT_NAME_FONT` binds at the same point: `0x6c7470` runs at world entry beside
|
||||
// `0x6c8470`, so a `/reload` keeps the face the login read.
|
||||
world.insert_resource(read_unit_name_font(&script));
|
||||
}
|
||||
// The load is over: disarm its instruction bound, so no handler is killed for being slow.
|
||||
script.clear_instruction_budget();
|
||||
@@ -1387,6 +1390,14 @@ pub(crate) fn is_emote_token_line(line: &str) -> bool {
|
||||
.all(|b| b.is_ascii_uppercase() || b.is_ascii_digit() || b == b'_')
|
||||
}
|
||||
|
||||
/// Read `UNIT_NAME_FONT` at the tail of the world-entry UI load (`0x6c7470`, reached with
|
||||
/// `0x6c8470` at world entry). `0x704350` takes a string or number and leaves `""` otherwise,
|
||||
/// which the font factory rejects; empty is `None`.
|
||||
fn read_unit_name_font(script: &benilla_ui::script::UiScript) -> crate::nameplates::UnitNameFont {
|
||||
let value: Option<String> = script.lua().globals().get("UNIT_NAME_FONT").ok();
|
||||
crate::nameplates::UnitNameFont(value.filter(|v| !v.is_empty()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -3246,6 +3246,43 @@ fn a_reload_keeps_the_damage_text_font_the_login_read() {
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
}
|
||||
|
||||
/// The overhead names' face is read at world entry (`0x6c7470` reads `UNIT_NAME_FONT` through
|
||||
/// `0x704350`): an addon's assignment reaches the resource; an unset global leaves `None`.
|
||||
#[test]
|
||||
fn world_entry_reads_unit_name_font() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
let _l = ENV_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let face = |world: &World| {
|
||||
world
|
||||
.get_resource::<crate::nameplates::UnitNameFont>()
|
||||
.and_then(|f| f.0.clone())
|
||||
};
|
||||
let (tmp, _c, _h) = hermetic_addon(
|
||||
"unitnamefont",
|
||||
"FontProbe",
|
||||
"## Interface: 11200\nFontProbe.lua\n",
|
||||
"UNIT_NAME_FONT = \"Fonts\\\\ARIALN.TTF\"\n",
|
||||
);
|
||||
let mut world = booted_world();
|
||||
log_in_as(&mut world, "Onehunter", 1);
|
||||
assert_eq!(face(&world).as_deref(), Some("Fonts\\ARIALN.TTF"));
|
||||
|
||||
let file = tmp
|
||||
.join("benilla-config")
|
||||
.join("AddOns")
|
||||
.join("FontProbe")
|
||||
.join("FontProbe.lua");
|
||||
std::fs::write(&file, "UNIT_NAME_FONT = \"\"\n").expect("the addon's empty face");
|
||||
super::end_ui_session(&mut world);
|
||||
log_in_as(&mut world, "Onehunter", 1);
|
||||
assert_eq!(face(&world), None, "an empty face is None");
|
||||
|
||||
drop(world);
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
}
|
||||
|
||||
/// A `/reload` releases every command: its shutdown's leave clears the once-per-world latch
|
||||
/// (`0x490c20`, `0x490a8d`), so the re-init's world-enter cascade runs its release of every
|
||||
/// command (`0x490168` → `0x4908c0` → `0x49093c` → `0x5144c0`). A right held and a mouselook
|
||||
|
||||
+1
-1
@@ -174,7 +174,7 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
|
||||
- `AlphaMode::Blend` writes no depth where the reference does, because overlapping names resolving by sort order look the same. ([`src/nameplates.rs:4`](../crates/benilla-app/src/nameplates.rs#L4))
|
||||
- A mounted plate keeps only this fraction of its seat's oscillation about a slow mean, because the raw seat, which the reference also rides, read more intense than the reference side by side. `1.0` is the raw seat; on-foot plates are untouched. ([`src/nameplates.rs:300`](../crates/benilla-app/src/nameplates.rs#L300))
|
||||
- Larger than the reference's 32 px `UNIT_NAME_FONT` raster, which it magnifies (`0x6c749b`), because a larger source is crisper up close. ([`src/nameplates.rs:309`](../crates/benilla-app/src/nameplates.rs#L309))
|
||||
- `ShouldShowName`'s first leg is `IsSelectable` (`0x60be60`), which passes our own unit; the avatar's fade stands in because what hides the own name in first person in 1.12.1 is untraced. ([`src/nameplates.rs:541`](../crates/benilla-app/src/nameplates.rs#L541))
|
||||
- `ShouldShowName`'s first leg is `IsSelectable` (`0x60be60`), which passes our own unit; the avatar's fade stands in because what hides the own name in first person in 1.12.1 is untraced. ([`src/nameplates.rs:556`](../crates/benilla-app/src/nameplates.rs#L556))
|
||||
|
||||
#### names
|
||||
|
||||
|
||||
Reference in New Issue
Block a user