Merge pull request #7354 from Xmilon/native/render-name-tag

🤖 Agent: queued at b7ca31b03a. a8cdcce9d keeps the no-mod path free (no player-id set, no dispatch without a native mod). Its head merged with today's main passes xtask mod-api --check and the stamp tests, and the native lib tests passed on that merge.
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-11-02 20:47:05 +01:00
commit 2105feb78d
5 files changed
+159 -21

No files matched your search

+98 -21
View File
@@ -8,8 +8,8 @@
//! and the player's own in third person with the `ownNameTag` extra.
use super::{Game, view_vector};
use horizon_client::{
block_entity_render::vertex_light, hud::HudBatch, mesher::Vertex, mobs::Mob, save::uuid_text,
scoreboard as sb, text::Text,
block_entity_render::vertex_light, hud::HudBatch, mesher::Vertex, mobs::Mob, mod_api,
save::uuid_text, scoreboard as sb, text::Text,
};
/// The world size of one text pixel in a name tag: `Matrix4f.scale(0.025F, -0.025F, 0.025F)`.
@@ -103,15 +103,27 @@ impl Game<'_> {
}
/// Each name tag this frame.
fn name_tags(&self, camera: glam::DVec3, alpha: f64) -> Vec<NameTag> {
let hooks = !mod_api::mods().is_empty();
let target = self.mob_hit().map(|(i, _)| self.mobs[i].id);
let viewer = self.player_team();
let mut tags = Vec::new();
let players = self.other_players();
let player_ids = hooks.then(|| {
players
.iter()
.map(|(player, _)| player.id)
.collect::<std::collections::HashSet<u32>>()
});
let named = self.mobs.iter().filter_map(|m| {
if player_ids.as_ref().is_some_and(|ids| ids.contains(&m.id)) {
return None;
}
// A mannequin with no name still shows its description.
let name = match &m.custom_name {
Some(name) => name.clone(),
None if m.species() == horizon_client::mobs::Species::Mannequin => String::new(),
None if hooks || m.species() == horizon_client::mobs::Species::Mannequin => {
String::new()
}
None => return None,
};
// The tag's 64 block distance is measured to the entity: copy the
@@ -180,7 +192,22 @@ impl Game<'_> {
let name_shown = sb::name_shown(team, viewer, view)
&& (player || m.name_visible || Some(m.id) == target)
&& (player || m.custom_name.is_some());
if !name_shown {
let mut visible = name_shown;
let mut text = None;
if hooks {
self.native_render_name_tag(
m.id,
if player {
"minecraft:player"
} else {
&m.kind.id
},
name_shown,
&mut visible,
&mut text,
);
}
if !visible {
if let Some(text) = description {
let l = self.world.light(
m.position.x.floor() as i32,
@@ -211,6 +238,9 @@ impl Game<'_> {
None => member,
}
.resolve_legacy();
let shown = text
.map(|text| Text::literal(text).resolve_legacy())
.unwrap_or(shown);
let mut lines = Vec::new();
// EntityRenderer.extractNameTags: the score shows within the
// below-name distance (10), under the name.
@@ -239,14 +269,17 @@ impl Game<'_> {
discrete,
});
}
drop(player_ids);
// Armor stands name themselves: an invisible one with a visible name
// is a hologram (its model is skipped, its name tag is not).
for a in &self.armor_stands {
let Some(name) = &a.custom_name else {
continue;
};
if !a.name_visible {
continue;
if !hooks {
let Some(_) = &a.custom_name else {
continue;
};
if !a.name_visible {
continue;
}
}
let height = match (a.marker, a.small) {
(true, _) => 0.0,
@@ -268,14 +301,31 @@ impl Game<'_> {
is_camera: false,
is_vehicle: false,
};
if !sb::name_shown(team, viewer, view) {
let name_shown =
a.custom_name.is_some() && a.name_visible && sb::name_shown(team, viewer, view);
let mut visible = name_shown;
let mut text = None;
if hooks {
self.native_render_name_tag(
a.id,
"minecraft:armor_stand",
name_shown,
&mut visible,
&mut text,
);
}
if !visible {
continue;
}
let name = a.custom_name.clone().unwrap_or_else(Text::empty);
let shown = match team {
Some(t) => t.formatted_name(name.clone()),
None => name.clone(),
}
.resolve_legacy();
let shown = text
.map(|text| Text::literal(text).resolve_legacy())
.unwrap_or(shown);
let mut lines = Vec::new();
let mut lift = 0.0;
let holder = self.persist.uuids.get(&a.id).map(|u| uuid_text(*u));
@@ -302,19 +352,43 @@ impl Game<'_> {
}
// ItemFrameRenderer.shouldShowName/getNameTag: only the targeted
// frame's custom-named item, with the HUD visible (also glow frames).
if !self.ui.hide_gui
&& let Some((i, distance)) = self.frame_hit()
&& self.before_block(distance)
&& self.mob_hit().is_none_or(|(_, d)| d > distance)
&& let Some(name) = self.item_frames[i]
.item
.as_ref()
.and_then(|item| item.custom_name())
{
let frame = &self.item_frames[i];
let targeted_frame = (hooks || !self.ui.hide_gui)
.then(|| self.frame_hit())
.flatten()
.filter(|(_, distance)| {
self.before_block(*distance) && self.mob_hit().is_none_or(|(_, d)| d > *distance)
})
.map(|(index, _)| index);
let frames = if hooks {
0..self.item_frames.len()
} else {
targeted_frame.map_or(0..0, |index| index..index + 1)
};
for index in frames {
let frame = &self.item_frames[index];
let name = frame.item.as_ref().and_then(|item| item.custom_name());
if !hooks && name.is_none() {
continue;
}
let (lo, hi) = frame.bounds();
let center = glam::DVec3::from_array(std::array::from_fn(|a| (lo[a] + hi[a]) / 2.0));
if center.distance_squared(camera) < 64.0 * 64.0 {
let name_shown =
!self.ui.hide_gui && targeted_frame == Some(index) && name.is_some();
let mut visible = name_shown;
let mut text = None;
if hooks {
self.native_render_name_tag(
frame.id,
frame.kind(),
name_shown,
&mut visible,
&mut text,
);
}
if !visible {
continue;
}
// NAME_TAG is at the entity height (0.5), then the name
// submitter lifts it 0.5. ItemFrameRenderer offsets rendering
// 0.3 toward its facing and -0.25 vertically.
@@ -326,7 +400,10 @@ impl Game<'_> {
0.75,
f64::from(step[2] as f32 * 0.3),
),
lines: vec![(Text::literal(name).resolve_legacy(), 0.0)],
lines: vec![(
Text::literal(text.or(name).unwrap_or_default()).resolve_legacy(),
0.0,
)],
light: self.world.light(
center.x.floor() as i32,
center.y.floor() as i32,
@@ -2493,6 +2493,25 @@ impl Game<'_> {
pub(super) fn native_camera_shake(&self, partial: f32, shake: &mut NativeShake) {
mod_api::dispatch_camera_shake(self, mod_api::mods(), partial, shake);
}
/// Name tag visibility and optional replacement text. API @API:render_name_tag@.
pub(super) fn native_render_name_tag(
&self,
entity_id: u32,
kind: &str,
vanilla_visible: bool,
visible: &mut bool,
text: &mut Option<String>,
) {
mod_api::dispatch_render_name_tag(
self,
mod_api::mods(),
entity_id,
kind,
vanilla_visible,
visible,
text,
);
}
/// Before one first-person hand and its displayed stack are drawn (RenderHandEvent).
/// Equip progress is the inverse arm height, including the item's swap animation scale.
/// Cancel skips that hand's default geometry, including its map and arm geometry.
+35
View File
@@ -4558,6 +4558,11 @@ pub trait Hooks: Send + Sync {
let _ = event;
Flow::Continue
}
/// Name tag visibility and optional replacement text. API @API:render_name_tag@.
fn render_name_tag(&self, host: &dyn Host, event: &mut RenderNameTagEvent<'_>) {
let _ = host;
let _ = event;
}
/// A screen was observed closing, at most once per frame.
fn screen_closing(&self, host: &dyn Host, event: &mut ScreenClosingEvent<'_>) {
let _ = host;
@@ -14032,6 +14037,36 @@ pub fn dispatch_render_item_in_frame(
}
}
}
/// What [`Hooks::render_name_tag`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
pub struct RenderNameTagEvent<'a> {
pub entity_id: u32,
pub kind: &'a str,
pub vanilla_visible: bool,
pub visible: bool,
pub text: &'a mut Option<String>,
}
pub fn dispatch_render_name_tag(
host: &dyn Host,
mods: &[Box<dyn Hooks>],
entity_id: u32,
kind: &str,
vanilla_visible: bool,
visible: &mut bool,
text: &mut Option<String>,
) {
let mut event = RenderNameTagEvent {
entity_id,
kind,
vanilla_visible,
visible: *visible,
text,
};
for mod_hook in super::entered(mods, "render_name_tag") {
mod_hook.render_name_tag(host, &mut event);
}
*visible = event.visible;
}
/// What [`Hooks::screen_closing`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
pub struct ScreenClosingEvent<'a> {
+2
View File
@@ -2,6 +2,8 @@
API @API:compute_fov_modifier@ adds `native_compute_fov_modifier(player, base, modifier)` on the client each tick for the local player's FOV scale, after vanilla effects and before zoom and camera easing. `player` contains the calculation inputs and `base` is the original scale. Non-finite modifiers or values outside 0.01..=10 fall back to `base`. It does not affect a spectator camera on a mob. Bundles need "hooks": @API:compute_fov_modifier@.
The client hook `native_render_name_tag(entity_id, kind, vanilla_visible, visible, text)` runs at player and mob, armor stand, and item frame name decisions before shared geometry. Set `visible` to override vanilla and `text` to `Some` literal replacement text (NeoForge `RenderNameTagEvent`). Distance and entity availability still apply; the local player's tag is considered only when its own camera and settings gates allow it. Bundles need "hooks": @API:render_name_tag@.
Native mods are **mixin bundles** that change the game's own Rust source before it is compiled, with
[horizon-mixins](https://github.com/horizon-rust/horizon-mixins) (MIT, by levzzz5154, made for Horizon). A bundle
is a `horizon-mixin.toml` and a few `.rs` helpers that hook into named functions of the game (HEAD, BEFORE_CALL,
@@ -0,0 +1,5 @@
{
"hooks": [{"name":"render_name_tag", "target":"horizon_client::game::native_hooks::Game<'_>::native_render_name_tag", "neoforge":"RenderNameTagEvent", "side":"client", "cancellable":false, "mod_api":{"contract":"listen"}}],
"rust_summary":"a name tag hook (native_render_name_tag).",
"doc_summary":"a name tag hook (native_render_name_tag), API @API:render_name_tag@."
}