Merge pull request #6291 from levzzz5154/mods/native-render-player

Native mods: drawing a player and adding layers to entities, RenderPlayerEvent and AddLayers
This commit is contained in:
levzzz authored and GitHub committed 2026-11-02 01:11:14 +05:00
commit dd69edfbca
6 files changed
+191 -12

No files matched your search

+20 -5
View File
@@ -2,7 +2,10 @@
//! chunk-generation spawning (`NaturalSpawner`), mob events (attacks,
//! breeding, conversions, drops), projectiles, player interactions
//! (feeding, shearing, milking, spawn eggs, throwing) and rendering.
use super::{Dim, Game, InteractTarget, dispenser, native_projectiles::Impact, view_vector};
use super::{
Dim, Game, InteractTarget, dispenser, native_mob_draw::NativePlayerRender,
native_projectiles::Impact, view_vector,
};
use horizon_assets::item_model::DisplayContext;
use horizon_client::{
Result,
@@ -6200,10 +6203,20 @@ impl Game<'_> {
// A native mod's own model in place of the vanilla one
// (`native_mob_submit`), before any of the vanilla model's parts
// (a sulfur cube's swallowed block) are drawn.
if !is_player
&& !layers_only
&& self.mod_mob_geometry(mob, alpha as f32, camera, packed, out)
{
// A player's own hook (`native_player_submit`) is asked for the
// other players in view.
let who = is_player
.then(|| self.remote_player_name(mob.id))
.flatten()
.map(|name| NativePlayerRender { name, local: false });
let modded = if layers_only {
false
} else if let Some(who) = who {
self.mod_player_geometry(mob, who, alpha as f32, camera, packed, out)
} else {
!is_player && self.mod_mob_geometry(mob, alpha as f32, camera, packed, out)
};
if modded {
if let Some(rgb) = outline {
self.mark_outline(&out[entity_start..], rgb);
if mob.invisible {
@@ -6282,6 +6295,8 @@ impl Game<'_> {
}
// CustomHeadLayer (a pumpkin, skull or head item) included.
self.mob_block_geometry(mob, camera, alpha, packed, out);
// `native_mob_layer`: a native mod's layer, over the vanilla ones.
self.mod_mob_layer_geometry(mob, who, alpha as f32, camera, packed, out);
if let Some(rgb) = outline {
self.mark_outline(&out[entity_start..], rgb);
if mob.invisible {
+42 -1
View File
@@ -279,7 +279,7 @@ use super::{
execution_role::ExecutionRole,
maps::{NativeMapDecoration, NativeMapInfo},
native_block_entity_draw::BlockEntityDraw,
native_mob_draw::NativeMobRender,
native_mob_draw::{NativeMobRender, NativePlayerRender},
native_panel_recipes::RecipePage,
native_server::{NativeCommand, NativeServerPlayer},
};
@@ -1864,6 +1864,47 @@ impl Game<'_> {
) {
let _ = (mob, draw, drawn);
}
/// Whether a mod listens to `native_player_submit` or `native_mob_layer`: a
/// mod that hooks either hooks this one too and sets `listened` (HEAD,
/// `*listened = true`). Without it the game poses no player or mob for
/// them, each frame. Client-side. API @API:player_render@.
pub(super) fn native_player_render_listened(&self, listened: &mut bool) {
let _ = listened;
}
/// Draw the player `player` (`who`: its profile name, and whether it is
/// this window's own player) with a model of your own, once a frame while
/// it is in view (`RenderPlayerEvent.Pre`), and set `drawn` to leave out its
/// vanilla model, armour, held items, cape and the other layers. This
/// window's player is drawn only in third person (and F5 front view); an
/// invisible player is not asked. `draw` is the frame of `native_mob_submit`:
/// at the player's feet, turned with the body. The name tag, shadow,
/// flames and glowing outline draw as before. API @API:player_render@.
pub(super) fn native_player_submit(
&self,
player: &NativeMobRender<'_>,
who: &NativePlayerRender<'_>,
draw: &mut BlockEntityDraw,
drawn: &mut bool,
) {
let _ = (player, who, draw, drawn);
}
/// A layer on every mob and player in view, once a frame after its
/// vanilla model and layers (`EntityRenderersEvent.AddLayers`, with
/// `RenderLivingEvent.Post` and `RenderPlayerEvent.Post`): add geometry
/// in the entity's frame, which starts at its feet turned with the body
/// (`mob.head_yaw`, `mob.pitch` and the walk cycle place a part, the eyes
/// being at the entity type's eye height). `who` is set for a player.
/// Not asked for what a mod drew itself (`native_mob_submit`,
/// `native_player_submit`) nor for a mob that isn't drawn at all.
/// API @API:player_render@.
pub(super) fn native_mob_layer(
&self,
mob: &NativeMobRender<'_>,
who: Option<&NativePlayerRender<'_>>,
draw: &mut BlockEntityDraw,
) {
let _ = (mob, who, draw);
}
/// Icons a mod puts on map `map` each time it is drawn: held, in an
/// item frame or on the cartography table's screen (a map's pixels are
/// its own; nothing is saved in the map). Push `NativeMapDecoration`s
@@ -3,7 +3,10 @@
//! it cancels) gets the mob's render state and a [`BlockEntityDraw`] posed at
//! its feet and turned with its body, so a mod draws a model of its own with
//! the helpers block entities draw with (`native_draw_cube`,
//! `native_draw_quad`, `native_draw_block`, `native_draw_item`).
//! `native_draw_quad`, `native_draw_block`, `native_draw_item`). A player
//! has `native_player_submit` (`RenderPlayerEvent.Pre`), and every mob and
//! player in view gets `native_mob_layer` after its vanilla model
//! (`EntityRenderersEvent.AddLayers`), in the same frame.
use super::{Dim, Game, native_block_entity_draw::BlockEntityDraw};
use glam::{DVec3, Mat4};
@@ -77,6 +80,18 @@ impl<'a> NativeMobRender<'a> {
}
}
/// Which player a [`NativeMobRender`] is, for `native_player_submit` and
/// `native_mob_layer`.
// Native mods read it; nothing in Horizon does.
#[allow(dead_code)]
#[derive(Clone, Copy, Debug)]
pub struct NativePlayerRender<'a> {
/// The player's profile name.
pub name: &'a str,
/// Whether it is the player of this window, drawn in third person.
pub local: bool,
}
/// The pose a mod's mob model starts from: at the feet, `at` (relative to the
/// camera), turned with the body so +z is the way it faces, +x its left and +y
/// up, in blocks.
@@ -96,6 +111,69 @@ impl Game<'_> {
camera: DVec3,
light: u32,
out: &mut Vec<Vertex>,
) -> bool {
self.mod_entity_draw(mob, partial, camera, light, out, |render, draw| {
let mut drawn = false;
self.native_mob_submit(render, draw, &mut drawn);
drawn
})
}
/// `native_player_submit` for the player `mob` (`who` says which): true
/// when a mod drew it and the vanilla model, armour, held items, cape and
/// other layers are left out.
pub(super) fn mod_player_geometry(
&self,
mob: &Mob,
who: NativePlayerRender<'_>,
partial: f32,
camera: DVec3,
light: u32,
out: &mut Vec<Vertex>,
) -> bool {
// Posing a player for the mods costs, each frame: only when one listens.
let mut listened = false;
self.native_player_render_listened(&mut listened);
if !listened {
return false;
}
self.mod_entity_draw(mob, partial, camera, light, out, |render, draw| {
let mut drawn = false;
self.native_player_submit(render, &who, draw, &mut drawn);
drawn
})
}
/// `native_mob_layer` for `mob` (`who` set for a player): what a mod adds
/// is kept, drawn after the vanilla model and its layers.
pub(super) fn mod_mob_layer_geometry(
&self,
mob: &Mob,
who: Option<NativePlayerRender<'_>>,
partial: f32,
camera: DVec3,
light: u32,
out: &mut Vec<Vertex>,
) {
// Every mob in view each frame: only when a mod listens.
let mut listened = false;
self.native_player_render_listened(&mut listened);
if !listened {
return;
}
self.mod_entity_draw(mob, partial, camera, light, out, |render, draw| {
self.native_mob_layer(render, who.as_ref(), draw);
true
});
}
/// Gives `hook` the frame of `mob` and a draw posed at its feet; what it
/// added to `out` stays when it answers true.
fn mod_entity_draw(
&self,
mob: &Mob,
partial: f32,
camera: DVec3,
light: u32,
out: &mut Vec<Vertex>,
hook: impl FnOnce(&NativeMobRender<'_>, &mut BlockEntityDraw) -> bool,
) -> bool {
let render = NativeMobRender::of(mob, partial);
let a = f64::from(partial);
@@ -111,13 +189,12 @@ impl Game<'_> {
diffuse_lights(self.dim == Dim::Nether),
std::mem::take(out),
);
let mut drawn = false;
self.native_mob_submit(&render, &mut draw, &mut drawn);
let keep = hook(&render, &mut draw);
*out = draw.into_vertices();
if !drawn {
if !keep {
out.truncate(start);
}
drawn
keep
}
}
+14 -1
View File
@@ -2,7 +2,7 @@
//! in front, cycled by F5), the detached camera (`Camera.setup` with
//! `getMaxZoom`), and the local player drawn as a model: in third person
//! and in the inventory (`InventoryScreen.extractEntityInInventoryFollowsMouse`).
use super::{Game, Screen, shoulders::shoulder_variants};
use super::{Game, Screen, native_mob_draw::NativePlayerRender, shoulders::shoulder_variants};
use horizon_client::{
blocks::BlockFacts,
camera::{CameraEntity, LivingCamera},
@@ -625,6 +625,17 @@ impl Game<'_> {
let lookup = |name: &str| atlas.sprites.get(name).map(|r| [r.u0, r.v0, r.u1, r.v1]);
let start = out.len();
let body = self.own_body_visible();
// `native_player_submit`: a native mod's own model for this player,
// in place of the vanilla one and its layers (an invisible player is
// not asked, as for the others).
let name = self.own_profile_name();
let who = NativePlayerRender {
name: &name,
local: true,
};
if body && self.mod_player_geometry(m, who, alpha as f32, camera, packed, out) {
return;
}
// A spectator still draws its own model in third person: the poses'
// visibility (set by player_model_visibility) hides body parts, and
// translucent_body marks the body OIT for the ghostly look. Only first
@@ -664,6 +675,8 @@ impl Game<'_> {
self.held_item_geometry(m, camera, alpha, packed, true, out);
// CustomHeadLayer: a pumpkin, skull or other head item.
self.head_item_geometry(m, camera, alpha, packed, true, out);
// `native_mob_layer`: a native mod's layer, over the vanilla ones.
self.mod_mob_layer_geometry(m, Some(who), alpha as f32, camera, packed, out);
// LivingEntity.isCurrentlyGlowing: the Glowing effect.
if self.potions.effects.get("glowing").is_some() {
let color = super::glowing::team_color(self.own_team());
+7
View File
@@ -1126,6 +1126,13 @@ items are left out (NeoForge's `RenderLivingEvent.Pre`, cancelled); its name tag
- A mod's own creature is an ordinary mob (`native_spawn_mob`, API 37) drawn this way: a quiet villager, an allay or a bat
gives it movement, sounds and saving, and the mod gives it a model and animations of its own. Tell yours apart by the
ids the server half sends with mod messages (API 40), or by its custom name.
- Players have their own hook, `native_player_submit(player, who, draw, drawn)` (`RenderPlayerEvent.Pre`): once a frame for
each other player in view and for this window's player in third person, with the same frame and `who` (the profile name,
and `local`). `drawn` leaves out the vanilla model, armour, held items, cape and other layers.
- `native_mob_layer(mob, who, draw)` (`EntityRenderersEvent.AddLayers`, `RenderLivingEvent.Post`) adds to every mob and player
after its vanilla model and layers, in the same frame (not for one a mod drew itself); `who` is set for a player. The
geometry hangs from the entity's frame, not from a model bone: place a hat from `head_yaw`, `pitch` and the entity type's
eye height.
### Advancement triggers
@@ -0,0 +1,26 @@
{
"hooks": [
{
"name": "player_submit",
"target": "horizon_client::game::native_hooks::Game<'_>::native_player_submit",
"neoforge": "RenderPlayerEvent.Pre",
"side": "client",
"cancellable": true
},
{
"name": "mob_layer",
"target": "horizon_client::game::native_hooks::Game<'_>::native_mob_layer",
"neoforge": "EntityRenderersEvent.AddLayers",
"side": "client",
"cancellable": false
},
{
"name": "player_render_listened",
"target": "horizon_client::game::native_hooks::Game<'_>::native_player_render_listened",
"side": "client",
"cancellable": false
}
],
"rust_summary": "a player drawn with a mod's own model, and layers added to mobs and players (`native_player_submit`, `native_mob_layer`, told while a mod sets `native_player_render_listened`).",
"doc_summary": "`native_player_submit(player, who, draw, drawn)` fires once a frame for each other player in view and for this window's player in third person (not while invisible), with the `native_mob_submit` frame (`NativeMobRender`, a `BlockEntityDraw` at the feet turned with the body) and `who` (profile name, whether it is the local player); set `drawn` to leave out the vanilla model, armour, held items, cape and other layers (`RenderPlayerEvent.Pre`). `native_mob_layer(mob, who, draw)` fires once a frame after the vanilla model and layers of every mob and player in view that a mod did not draw itself, and what the mod adds in that frame is drawn with it (`EntityRenderersEvent.AddLayers`, `RenderLivingEvent.Post`); `who` is set for a player. Geometry attaches to the entity's frame, not to the model's bones: head yaw, pitch and the walk cycle are in `NativeMobRender`. A mod that hooks either also hooks `native_player_render_listened(listened)` and sets `*listened = true`: otherwise neither is asked. Bundles need \"hooks\": @API:player_render@."
}