Merge main into native/render-name-tag

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
levzzzandClaude Opus 5.5 committed 2026-11-02 21:51:22 +05:00
commit b7ca31b03a
25 files changed
+490 -18

No files matched your search

+28 -1
View File
@@ -6234,8 +6234,9 @@ impl Renderer {
self.config.height.max(1) as f32,
frame.depth_far,
));
let hand_view_proj = hand_projection * frame.bob * view;
let hand_globals = Globals {
view_proj: (hand_projection * frame.bob * view).to_cols_array_2d(),
view_proj: hand_view_proj.to_cols_array_2d(),
..globals
};
self.uploads.buffer(
@@ -8757,6 +8758,8 @@ impl Renderer {
first_crosshair: debug_depth + debug_top,
world_depth: false,
real: false,
hand_drawn: !pack_draws_hand,
hand_view_proj,
};
self.draw_overlay(&mut encoder, frame, &generated_view, generated, &overlay);
}
@@ -8799,6 +8802,8 @@ impl Renderer {
first_crosshair: debug_depth + debug_top,
world_depth: scaled.is_none(),
real: true,
hand_drawn: !pack_draws_hand,
hand_view_proj,
};
let readback =
self.draw_overlay(&mut encoder, frame, &view_target, &target_texture, &overlay);
@@ -9086,6 +9091,23 @@ impl Renderer {
}
}
}
// A native mod's turn after the hand, on the real frame only.
if overlay.real && !frame.title {
native_render::native_hand_drawn(&mut native_render::HandDrawn {
device: &self.device,
queue: &self.queue,
encoder: &mut *encoder,
color: target_texture,
color_view: view_target,
color_format: self.config.format.remove_srgb_suffix(),
size: [self.config.width, self.config.height],
hand: if overlay.hand_drawn { frame.hand } else { &[] },
view_proj: overlay.hand_view_proj,
camera: frame.camera,
atlas: self.hud.atlas_texture.as_ref(),
quad_indices: &self.indices,
});
}
// GameRenderer.render: LevelRenderer.blitEntityOutline after the level and the hand.
self.entity_outline.draw(
encoder,
@@ -9623,6 +9645,11 @@ struct Overlay {
world_depth: bool,
/// The real frame, not frame generation's made one: the world icon grabs it.
real: bool,
/// The game drew the first-person hand, in its own pass or at the end of the world's
/// (`hand_in_world`), not a shader pack.
hand_drawn: bool,
/// Camera-relative world to clip space for the hand: its own field of view and the bob.
hand_view_proj: Mat4,
}
fn world_projection(fov_degrees: f32, aspect: f32, far: f32) -> Mat4 {
@@ -12,7 +12,7 @@ use std::sync::{
};
mod generated;
pub use generated::{RenderHooks, RenderNeedsEvent, WorldDrawnEvent};
pub use generated::{HandDrawnEvent, RenderHooks, RenderNeedsEvent, WorldDrawnEvent};
pub use wgpu;
static RENDERERS: OnceLock<Box<[&'static dyn RenderHooks]>> = OnceLock::new();
@@ -40,8 +40,8 @@ pub fn world_drawn_hooked() -> bool {
}
/// What a mod can draw with once the world is drawn: after the sky, terrain, entities,
/// particles, weather and the world post pass, before the first-person hand, the HUD and the
/// screens. `color` holds the world's picture (copy it out before reading it: it is the target
/// particles, weather and the world post pass, before the first-person hand ([`HandDrawn`]),
/// the HUD and the screens. `color` holds the world's picture (copy it out before reading it: it is the target
/// drawn into), `depth` the world's depth, reversed (0 is the sky).
#[non_exhaustive]
pub struct WorldDrawn<'a> {
@@ -76,6 +76,35 @@ pub struct WorldDrawn<'a> {
pub quad_indices: &'a wgpu::Buffer,
}
/// What a mod can draw with once the first-person hand is drawn ([`native_hand_drawn`]): after
/// the world and the hand, before entity outlines, post effects, screen effects and the HUD.
/// `color` holds the frame's picture with the hand in it (copy it out before reading it).
#[non_exhaustive]
pub struct HandDrawn<'a> {
pub device: &'a wgpu::Device,
pub queue: &'a wgpu::Queue,
pub encoder: &'a mut wgpu::CommandEncoder,
/// The frame's target and a view of it in `color_format`.
pub color: &'a wgpu::Texture,
pub color_view: &'a wgpu::TextureView,
pub color_format: wgpu::TextureFormat,
/// The size of `color`.
pub size: [u32; 2],
/// The hand and held item just drawn, camera-relative quads of 4 vertices: empty when no
/// hand drew (third person, F1, a sleeping or spectating player) or a shader pack drew it.
pub hand: &'a [Vertex],
/// Camera-relative world to clip space for the hand: its own field of view
/// (`hand_fov_degrees`) and the view bob, without TAA's jitter (the hand has none). Its
/// depth is the hand's own: the hand's pass clears depth and keeps none.
pub view_proj: Mat4,
/// The camera's position in the world.
pub camera: DVec3,
/// The shared atlas the hand's UVs point into.
pub atlas: Option<&'a wgpu::Texture>,
/// Quad indices (0 1 2 0 2 3 for each 4 vertices).
pub quad_indices: &'a wgpu::Buffer,
}
/// What the mods need from a frame that the game doesn't make for itself. Each mod sets what it
/// needs and leaves the rest, so several can ask.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
@@ -94,6 +123,13 @@ pub fn native_world_drawn(world: &mut WorldDrawn<'_>) {
generated::dispatch_world_drawn(RENDERERS.get().map_or(&[], |renderers| renderers), world);
}
/// The first-person hand is drawn (see [`HandDrawn`]), on every frame [`native_world_drawn`]
/// runs, with no hand too. API @API:hand_drawn@.
#[inline(never)]
pub fn native_hand_drawn(hand: &mut HandDrawn<'_>) {
generated::dispatch_hand_drawn(RENDERERS.get().map_or(&[], |renderers| renderers), hand);
}
/// Asked as a frame is built and drawn: set what your mod needs.
#[inline(never)]
pub fn native_render_needs(needs: &mut RenderNeeds) {
@@ -11,6 +11,11 @@ pub trait RenderHooks: Send + Sync {
fn render_needs(&self, event: &mut RenderNeedsEvent<'_>) {
let _ = event;
}
/// The first-person hand is drawn (see [`HandDrawn`]), on every frame [`native_world_drawn`]
/// runs, with no hand too. API @API:hand_drawn@.
fn hand_drawn(&self, event: &mut HandDrawnEvent<'_, '_>) {
let _ = event;
}
}
/// What [`RenderHooks::world_drawn`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
@@ -34,3 +39,14 @@ pub fn dispatch_render_needs(mods: &[&dyn RenderHooks], needs: &mut RenderNeeds)
mod_hook.render_needs(&mut event);
}
}
/// What [`RenderHooks::hand_drawn`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
pub struct HandDrawnEvent<'a, 'b> {
pub hand: &'a mut HandDrawn<'b>,
}
pub fn dispatch_hand_drawn(mods: &[&dyn RenderHooks], hand: &mut HandDrawn<'_>) {
let mut event = HandDrawnEvent { hand };
for mod_hook in mods {
mod_hook.hand_drawn(&mut event);
}
}
+13
View File
@@ -2703,6 +2703,7 @@ pub struct TridentSave<'a> {
pub shake: u32,
pub dealt_damage: bool,
pub creative_only: bool,
pub no_gravity: bool,
pub item: &'a crate::items::ItemStack,
}
pub fn trident_to_nbt(t: &TridentSave, uuid: [i32; 4], raw: Option<&Tag>) -> Tag {
@@ -2732,6 +2733,12 @@ pub fn trident_to_nbt(t: &TridentSave, uuid: [i32; 4], raw: Option<&Tag>) -> Tag
] {
put(&mut tag, key, value);
}
// Entity.saveWithoutId writes NoGravity only when set.
if t.no_gravity {
put(&mut tag, "NoGravity", Tag::Byte(1));
} else if let Tag::Compound(map) = &mut tag {
map.remove(&"NoGravity".into());
}
tag
}
/// (position, motion, yaw, pitch, in ground, life, shake, dealt damage,
@@ -2746,6 +2753,7 @@ pub struct TridentNbt {
pub shake: u32,
pub dealt_damage: bool,
pub creative_only: bool,
pub no_gravity: bool,
pub item: crate::items::ItemStack,
}
pub fn trident_from_nbt(tag: &Tag) -> Option<TridentNbt> {
@@ -2760,6 +2768,7 @@ pub fn trident_from_nbt(tag: &Tag) -> Option<TridentNbt> {
shake: number(tag.get("shake")).unwrap_or(0).max(0) as u32,
dealt_damage: flag(tag, "DealtDamage"),
creative_only: number(tag.get("pickup")) == Some(2),
no_gravity: flag(tag, "NoGravity"),
item: tag
.get("item")
.and_then(item_from_nbt)
@@ -3445,12 +3454,16 @@ mod tests {
shake: 3,
dealt_damage: true,
creative_only: true,
no_gravity: true,
item: &trident,
},
[1, 2, 3, 4],
None,
);
let t = trident_from_nbt(&tag).unwrap();
// `Entity.saveWithoutId` writes NoGravity only when set.
assert!(t.no_gravity);
assert!(matches!(tag.get("NoGravity"), Some(Tag::Byte(1))));
assert_eq!((t.in_ground, t.life, t.shake), (true, 9, 3));
assert_eq!((t.dealt_damage, t.creative_only), (true, true));
assert_eq!((t.yaw, t.pitch), (10.0, 20.0));
+3
View File
@@ -2399,6 +2399,7 @@ struct Game<'a> {
/// `horizon-ctl gpu-passes`: time Horizon's own passes on the GPU with F3 shut.
gpu_passes: bool,
screen: Screen,
observed_screen: Option<native_hooks::ObservedScreen>,
window: Option<Arc<Window>>,
renderer: Option<Renderer>,
/// Avoid an endless rebuild loop if the new device is lost as well.
@@ -15023,6 +15024,7 @@ impl Game<'_> {
if self.renderer.as_mut().is_some_and(|r| r.display_changed()) {
self.apply_ui_settings();
}
self.observe_screen();
// Minecraft.doWorldLoad: the LevelLoadingScreen while single player's
// server starts, with the server's chunk map and progress.
if let Some(starting) = &self.integrated_starting {
@@ -18324,6 +18326,7 @@ impl<'a> Game<'a> {
debug: false,
gpu_passes: false,
screen: Screen::Playing,
observed_screen: None,
window: None,
renderer: None,
last_device_recovery: None,
@@ -69,6 +69,8 @@ pub(super) enum Target {
Falling(u32),
/// A firework rocket, by its entity id (`minecraft:firework_rocket`).
Rocket(u32),
/// A thrown trident, by its entity id (`minecraft:trident`).
Trident(u32),
/// An ominous item spawner, by its entity id (`minecraft:ominous_item_spawner`).
Spawner(u32),
}
@@ -152,6 +154,7 @@ fn non_player_entity_id(t: Target) -> Option<i32> {
| Target::Tnt(id)
| Target::Falling(id)
| Target::Rocket(id)
| Target::Trident(id)
| Target::Spawner(id)) = t
else {
return None;
@@ -549,6 +552,11 @@ impl Game<'_> {
.iter()
.find(|r| r.id == id)
.map(|r| [r.position.x, r.position.y, r.position.z]),
Target::Trident(id) => self
.tridents
.iter()
.find(|t| t.id == id)
.map(|t| [t.position.x, t.position.y, t.position.z]),
Target::Spawner(id) => self
.ominous_spawners
.iter()
@@ -603,6 +611,7 @@ impl Game<'_> {
Target::Tnt(_) => "minecraft:tnt".into(),
Target::Falling(_) => "minecraft:falling_block".into(),
Target::Rocket(_) => "minecraft:firework_rocket".into(),
Target::Trident(_) => "minecraft:trident".into(),
Target::Spawner(_) => "minecraft:ominous_item_spawner".into(),
}
}
@@ -905,6 +914,7 @@ impl Game<'_> {
Target::Tnt(id),
Target::Falling(id),
Target::Rocket(id),
Target::Trident(id),
Target::Spawner(id),
]
.into_iter()
@@ -930,6 +940,7 @@ impl Game<'_> {
Target::Tnt(id) => self.tnts.iter().any(|t| t.id == id),
Target::Falling(id) => self.falling.iter().any(|f| f.id == id),
Target::Rocket(id) => self.rockets.iter().any(|r| r.id == id),
Target::Trident(id) => self.tridents.iter().any(|t| t.id == id),
Target::Spawner(id) => self.ominous_spawners.iter().any(|s| s.id == id),
}
}
@@ -1600,6 +1611,12 @@ impl Game<'_> {
r.previous = v;
}
}
Target::Trident(id) => {
if let Some(t) = self.tridents.iter_mut().find(|t| t.id == id) {
t.position = v;
t.previous = v;
}
}
Target::Spawner(id) => {
if let Some(s) = self.ominous_spawners.iter_mut().find(|s| s.id == id) {
s.position = [v.x, v.y, v.z];
@@ -2102,6 +2119,7 @@ impl Game<'_> {
Target::Tnt(id) => self.tnts.retain(|t| t.id != id),
Target::Falling(id) => self.falling.retain(|f| f.id != id),
Target::Rocket(id) => self.rockets.retain(|r| r.id != id),
Target::Trident(id) => self.tridents.retain(|t| t.id != id),
Target::Spawner(id) => self.ominous_spawners.retain(|s| s.id != id),
}
// The orbs marked for removal are gone (Entity.kill removes them).
@@ -2242,6 +2260,10 @@ impl Game<'_> {
beam: None,
invulnerable: false,
});
// tryAddFreshEntityWithPassengers refused: ERROR_DUPLICATE_UUID.
if !self.native_joins_flush(false, self.next_entity) {
return Err(err("commands.summon.failed.uuid", &[]));
}
name = Text::translate("entity.minecraft.end_crystal", &[]);
}
k if super::minecarts::CartKind::from_id(&kind).is_some() => {
@@ -3841,6 +3863,7 @@ mod look_at_tests {
Target::Tnt(7),
Target::Falling(7),
Target::Rocket(7),
Target::Trident(7),
Target::Spawner(7),
] {
assert_eq!(non_player_entity_id(t), Some(7), "{t:?}");
@@ -224,6 +224,11 @@ impl Game<'_> {
let r = self.rockets.iter().find(|r| r.id == id)?;
r.to_nbt(uuid, player_uuid, self.persist.raw.get(&id))
}
Target::Trident(id) => {
let uuid = self.uuid_for(id);
let t = self.tridents.iter().find(|t| t.id == id)?;
t.to_nbt(uuid, self.persist.raw.get(&id))
}
Target::Spawner(id) => {
let s = self.ominous_spawners.iter().find(|s| s.id == id)?;
let [x, y, z] = s.position;
@@ -856,6 +861,8 @@ impl Game<'_> {
Some(Target::Falling(id))
} else if self.rockets.iter().any(|r| r.id == id) {
Some(Target::Rocket(id))
} else if self.tridents.iter().any(|t| t.id == id) {
Some(Target::Trident(id))
} else if self.ominous_spawners.iter().any(|s| s.id == id) {
Some(Target::Spawner(id))
} else {
+3
View File
@@ -513,6 +513,9 @@ impl Game<'_> {
// `/summon ender_dragon` outside the End: no fight, but the dragon flies.
self.tick_fight();
}
// The spikes' crystals (placed above) join before they light a fire:
// a mod that cancels one keeps its fire out too.
self.native_joins_flush(false, 0);
self.tick_crystals();
self.tick_eyes();
if self.dim == Dim::End {
+16 -2
View File
@@ -996,8 +996,7 @@ impl Entity for ThrownTrident {
!self.in_ground || self.previous != self.position
}
fn selector_target(&self) -> Option<Target> {
// No `Target` variant for it yet: `@e` does not find tridents.
None
Some(Target::Trident(self.id))
}
/// Like any arrow (`AbstractArrow.push`) not pushed while in the ground.
fn pushed_by_explosion(&self) -> bool {
@@ -1078,3 +1077,18 @@ mod no_gravity_stand_tests {
assert!(!stand.triggers_blocks());
}
}
#[cfg(test)]
mod trident_selector_tests {
use super::Entity as _;
use crate::game::{commands::Target, trident::ThrownTrident};
use horizon_core::geometry::Vec3;
/// `@e`, `/kill` and `/data` reach a thrown trident like any other entity
/// (`EntityTypeTest.forClass(Entity.class)`: every entity in the level).
#[test]
fn a_thrown_trident_is_a_selector_target() {
let t = ThrownTrident::remote(9, Vec3::new(0.5, 100.0, 0.5), 0.0, 0.0);
assert_eq!(t.selector_target(), Some(Target::Trident(9)));
}
}
@@ -117,6 +117,7 @@ fn target_id(t: Target) -> Option<u32> {
| Target::Tnt(id)
| Target::Falling(id)
| Target::Rocket(id)
| Target::Trident(id)
| Target::Spawner(id) => Some(id),
Target::Player | Target::Guest(_) => None,
}
@@ -343,6 +344,9 @@ impl Game<'_> {
self.persist.live.push((id, kind.into_owned(), true));
}
}
// Ids taken and never announced (the dragon's parts, another level's
// entities) must not keep the next flush walking.
self.persist.joined_mark = self.persist.joined_mark.max(self.next_entity);
kept
}
/// The entity with the lowest id above the last one announced.
@@ -936,6 +936,106 @@ use std::{
};
use winit::{event::MouseButton, keyboard::KeyCode};
pub(super) struct ObservedScreen {
id: &'static str,
menu: Option<String>,
title: Option<Text>,
rect: [f32; 4],
slots: usize,
mouse: [f32; 2],
partial_tick: f32,
}
impl ObservedScreen {
fn view(&self) -> NativeScreenView<'_> {
NativeScreenView {
id: self.id,
menu: self.menu.as_deref(),
title: self.title.as_ref(),
rect: self.rect,
slots: self.slots,
mouse: self.mouse,
partial_tick: self.partial_tick,
}
}
}
fn observed_overlay_id(overlays: &[(bool, &'static str)]) -> Option<&'static str> {
overlays.iter().find_map(|(open, id)| open.then_some(*id))
}
impl Game<'_> {
fn observed_screen_view(&self) -> Option<NativeScreenView<'_>> {
let book = match &self.book {
Some(super::books::BookScreen::Edit { signing: true, .. }) => Some("book_sign"),
Some(super::books::BookScreen::Edit { .. }) => Some("book_edit"),
Some(super::books::BookScreen::View {
lectern: Some(_), ..
}) => Some("lectern"),
Some(super::books::BookScreen::View { .. }) => Some("book_view"),
None => None,
};
let overlay = observed_overlay_id(&[
(self.command_prompt.is_some(), "command_prompt"),
(self.link_prompt.is_some(), "link_prompt"),
(self.code_of_conduct.is_some(), "code_of_conduct"),
(self.server_packs.prompt.is_some(), "server_pack_prompt"),
(self.dialog_open(), "dialog"),
(book.is_some(), book.unwrap_or("book_view")),
(self.sign_edit.is_some(), "sign_editor"),
]);
let Some(id) = overlay else {
return self.screen_view_now();
};
let renderer = self.renderer.as_ref()?;
let batch = HudBatch::new([renderer.config.width, renderer.config.height]);
Some(NativeScreenView {
id,
menu: None,
title: None,
rect: [0.0, 0.0, batch.width, batch.height],
slots: 0,
mouse: self.native_cursor(),
partial_tick: self.clock.fraction() as f32,
})
}
pub(super) fn observe_screen(&mut self) {
// Free with no native mod: the snapshot clones the title and menu.
if mod_api::mods().is_empty() {
return;
}
let current = self.observed_screen_view().map(|view| ObservedScreen {
id: view.id,
menu: view.menu.map(str::to_owned),
title: view.title.cloned(),
rect: view.rect,
slots: view.slots,
mouse: view.mouse,
partial_tick: view.partial_tick,
});
let changed = match (&self.observed_screen, &current) {
(Some(old), Some(new)) => old.id != new.id || old.menu != new.menu,
(None, None) => false,
_ => true,
};
if changed {
if let Some(old) = &self.observed_screen {
self.native_screen_closing(&old.view());
}
if let Some(new) = &current {
self.native_screen_opening(&new.view());
self.native_screen_init_post(&new.view());
}
} else if let (Some(old), Some(new)) = (&self.observed_screen, &current)
&& old.rect != new.rect
{
self.native_screen_init_post(&new.view());
}
self.observed_screen = current;
}
}
/// `Level.setBlock`'s flags, for the `native_set_block_property` helpers:
/// `flags::UPDATE_CLIENTS` as a vanilla crop grows, `flags::UPDATE_ALL` as a
/// furnace sets its `lit`. Horizon sends every write to players, so
@@ -2887,6 +2987,20 @@ impl Game<'_> {
) {
mod_api::dispatch_screen_draw(self, mod_api::mods(), block, data, batch, origin);
}
/// A screen was observed opening, at most once per frame.
pub(super) fn native_screen_opening(&self, screen: &NativeScreenView<'_>) {
mod_api::dispatch_screen_opening(self, mod_api::mods(), screen);
}
/// A screen was observed opening or changing rectangle, at most once per frame.
pub(super) fn native_screen_init_post(&self, screen: &NativeScreenView<'_>) {
mod_api::dispatch_screen_init_post(self, mod_api::mods(), screen);
}
/// A screen was observed closing, at most once per frame.
pub(super) fn native_screen_closing(&self, screen: &NativeScreenView<'_>) {
mod_api::dispatch_screen_closing(self, mod_api::mods(), screen);
}
/// The open screen has drawn itself and its tooltips (`Screen.render`,
/// the point of `ScreenEvent.Render.Post`): draw over it with
/// `native_fill`, `native_sprite` and `native_text`, `screen.rect` being
@@ -9306,6 +9420,22 @@ fn mod_screen_opens(
#[cfg(test)]
mod tests {
#[test]
fn observed_overlay_prefers_first_open_screen() {
assert_eq!(
super::observed_overlay_id(&[(false, "sign"), (false, "book")]),
None
);
assert_eq!(
super::observed_overlay_id(&[(false, "sign"), (true, "book")]),
Some("book")
);
assert_eq!(
super::observed_overlay_id(&[(true, "sign"), (true, "book")]),
Some("sign")
);
}
/// Exercise the gameplay constructor, where name-based entities compete
/// with a mod block's sign template.
#[test]
+7 -5
View File
@@ -504,8 +504,9 @@ impl Game<'_> {
let mut t = super::trident::ThrownTrident::from_nbt(n);
t.id = self.take_entity_id();
// One that came through a portal keeps its cooldown.
self.restore_portal_cooldown(t.id, &tag);
let id = t.id;
self.tridents.push(t);
self.remember(id, uuid, tag);
}
}
"minecraft:firework_rocket" => {
@@ -1581,10 +1582,11 @@ impl Game<'_> {
i: usize,
keys: &HashSet<ChunkKey>,
) -> Option<(ChunkKey, Tag)> {
let t = &self.tridents[i];
keys.contains(&chunk_of(t.position)).then(|| {
let (uuid, raw) = anon_uuid_raw(&self.persist, "minecraft:trident", t.position);
(chunk_of(t.position), t.to_nbt(uuid, raw.as_ref()))
let (id, at) = (self.tridents[i].id, self.tridents[i].position);
keys.contains(&chunk_of(at)).then(|| {
let uuid = self.uuid_for(id);
let tag = self.tridents[i].to_nbt(uuid, self.persist.raw.get(&id));
(chunk_of(at), tag)
})
}
pub(super) fn save_tnt(
@@ -182,6 +182,7 @@ impl Game<'_> {
| Target::Tnt(id)
| Target::Falling(id)
| Target::Rocket(id)
| Target::Trident(id)
| Target::Other(id) => Some(uuid_text(self.uuid_for(id))),
// It keeps its UUID with itself.
Target::Spawner(id) => self
@@ -226,6 +227,7 @@ impl Game<'_> {
| Target::Crystal(id)
| Target::Dragon(id)
| Target::Rocket(id)
| Target::Trident(id)
| Target::Other(id) => self.persist.raw.get(&id),
Target::Player => self.level.player_extra.as_ref(),
// A server's player: the saved compound the guest's file is written from.
+9 -2
View File
@@ -37,6 +37,8 @@ pub(super) struct ThrownTrident {
pub(super) no_physics: bool,
returning: u32,
creative_only: bool,
/// `Entity.isNoGravity`: `AbstractArrow.applyGravity` then skips the pull.
pub(super) no_gravity: bool,
/// The entity id a server sent as its owner (the one Loyalty returns it to).
pub(super) owner: u32,
/// The guest that threw it (`Projectile.getOwner`); `None` for the host.
@@ -66,6 +68,7 @@ impl ThrownTrident {
t.shake = n.shake;
t.dealt_damage = n.dealt_damage;
t.creative_only = n.creative_only;
t.no_gravity = n.no_gravity;
t
}
/// `AbstractArrow.addAdditionalSaveData` + `DealtDamage`.
@@ -85,6 +88,7 @@ impl ThrownTrident {
shake: self.shake,
dealt_damage: self.dealt_damage,
creative_only: self.creative_only,
no_gravity: self.no_gravity,
item: &self.stack,
},
uuid,
@@ -109,6 +113,7 @@ impl ThrownTrident {
no_physics: false,
returning: 0,
creative_only: false,
no_gravity: false,
owner: 0,
guest: None,
}
@@ -175,7 +180,7 @@ impl ThrownTrident {
self.position = to;
// A trident's water inertia (0.99) equals the air drag.
self.velocity = self.velocity.scale(0.99);
if !self.no_physics {
if !self.no_physics && !self.no_gravity {
self.velocity.y -= 0.05;
}
}
@@ -426,6 +431,7 @@ impl Game<'_> {
no_physics: false,
returning: 0,
creative_only: self.creative,
no_gravity: false,
owner: 0,
guest: self.lan_acting,
});
@@ -733,9 +739,10 @@ impl Game<'_> {
if !t.in_ground {
// A trident's water inertia (0.99) equals the air drag; gravity 0.05.
let drag = 0.99;
let gravity = if t.no_gravity { 0.0 } else { 0.05 };
t.velocity = Vec3::new(
t.velocity.x * drag,
t.velocity.y * drag - 0.05,
t.velocity.y * drag - gravity,
t.velocity.z * drag,
);
let h = (t.velocity.x * t.velocity.x + t.velocity.z * t.velocity.z).sqrt();
+52
View File
@@ -492,6 +492,23 @@ pub fn leash_offset(m: &Mob) -> [f64; 3] {
f64::from(0.55_f32 * m.eye_height() as f32),
f64::from(m.width() * 0.4),
]
} else if let Some(factor) = match m.species() {
// Pig.getLeashOffset, Rabbit.getLeashOffset and
// Strider.getLeashOffset: 0.6 of the eye height; Parrot.getLeashOffset
// and Ocelot.getLeashOffset: half of it.
super::Species::Pig | super::Species::Rabbit | super::Species::Strider => Some(0.6_f32),
super::Species::Parrot | super::Species::Ocelot => Some(0.5_f32),
_ => None,
} {
[
0.0,
f64::from(factor * m.eye_height() as f32),
f64::from(m.width() * 0.4),
]
} else if m.species() == super::Species::HappyGhast {
// HappyGhast.getLeashOffset: the origin, so the rope hangs at the
// ghast's feet rather than an eye height up.
[0.0, 0.0, 0.0]
} else {
[0.0, m.eye_height(), f64::from(m.width() * 0.4)]
}
@@ -631,6 +648,41 @@ mod tests {
}
});
}
/// `Pig`, `Rabbit`, `Strider`, `Parrot`, `Ocelot` and
/// `HappyGhast.getLeashOffset`: 0.6 or half of the eye height, and the
/// happy ghast's rope at the origin, against the default eye-height
/// height the fallthrough gave them.
#[test]
fn a_pig_a_rabbit_a_parrot_an_ocelot_and_a_ghast_hang_their_lead_where_vanilla_does() {
let _ = crate::mobs::world::with_test_world(&[], |_, types| {
for (kind, factor) in [
("minecraft:pig", 0.6),
("minecraft:rabbit", 0.6),
("minecraft:strider", 0.6),
("minecraft:parrot", 0.5),
("minecraft:ocelot", 0.5),
// `Vec3.ZERO`: no eye-height part and no forward shift.
("minecraft:happy_ghast", 0.0),
] {
let m = Mob::new(
1,
std::sync::Arc::new(types.types[kind].clone()),
Vec3::ZERO,
0.0,
);
let o = leash_offset(&m);
let h = f64::from(factor * m.eye_height() as f32);
let z = f64::from(m.width() * 0.4);
assert!((o[1] - h).abs() < 1e-9, "{kind}: height {} vs {h}", o[1]);
assert!(
(o[2] - if factor == 0.0 { 0.0 } else { z }).abs() < 1e-9,
"{kind}: forward {} vs {z}",
o[2]
);
assert_eq!(o[0], 0.0, "{kind}: centred");
}
});
}
/// `Nautilus.canBeLeashed` and `ZombieNautilus.canBeLeashed`: an
/// aggravated nautilus, one holding an `ANGRY_AT` or an `ATTACK_TARGET`
/// memory, cannot be put on a lead.
+60
View File
@@ -4540,6 +4540,21 @@ pub trait Hooks: Send + Sync {
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;
let _ = event;
}
/// A screen was observed opening or changing rectangle, at most once per frame.
fn screen_init_post(&self, host: &dyn Host, event: &mut ScreenInitPostEvent<'_>) {
let _ = host;
let _ = event;
}
/// A screen was observed opening, at most once per frame.
fn screen_opening(&self, host: &dyn Host, event: &mut ScreenOpeningEvent<'_>) {
let _ = host;
let _ = event;
}
}
/// What [`Hooks::frame`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
@@ -13944,3 +13959,48 @@ pub fn dispatch_render_name_tag(
}
*visible = event.visible;
}
/// What [`Hooks::screen_closing`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
pub struct ScreenClosingEvent<'a> {
pub screen: &'a NativeScreenView<'a>,
}
pub fn dispatch_screen_closing(
host: &dyn Host,
mods: &[Box<dyn Hooks>],
screen: &NativeScreenView<'_>,
) {
let mut event = ScreenClosingEvent { screen };
for mod_hook in super::entered(mods, "screen_closing") {
mod_hook.screen_closing(host, &mut event);
}
}
/// What [`Hooks::screen_init_post`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
pub struct ScreenInitPostEvent<'a> {
pub screen: &'a NativeScreenView<'a>,
}
pub fn dispatch_screen_init_post(
host: &dyn Host,
mods: &[Box<dyn Hooks>],
screen: &NativeScreenView<'_>,
) {
let mut event = ScreenInitPostEvent { screen };
for mod_hook in super::entered(mods, "screen_init_post") {
mod_hook.screen_init_post(host, &mut event);
}
}
/// What [`Hooks::screen_opening`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
pub struct ScreenOpeningEvent<'a> {
pub screen: &'a NativeScreenView<'a>,
}
pub fn dispatch_screen_opening(
host: &dyn Host,
mods: &[Box<dyn Hooks>],
screen: &NativeScreenView<'_>,
) {
let mut event = ScreenOpeningEvent { screen };
for mod_hook in super::entered(mods, "screen_opening") {
mod_hook.screen_opening(host, &mut event);
}
}
+9
View File
@@ -787,6 +787,8 @@ API 86 adds `native_block_highlight(pos, state, partial, canceled)` runs before
API 87 adds `native_render_hand(hand, stack, partial, swing, equip, canceled)` runs before each visible first-person hand: `Hand::Main` or `Hand::Off`, the displayed stack (None for an empty arm), partial tick, swing progress and inverse equip height including swap animation scale. Set `canceled` to skip the default geometry for that hand (arms and maps included). It runs after the existing spectator, hidden-HUD, sleep and camera gates. It only hides: the hook has no way to draw a replacement. Bundles need "hooks": 87.
`native_screen_opening(screen)`, `native_screen_init_post(screen)` and `native_screen_closing(screen)` observe screens once per frame with `NativeScreenView`, including sign editors, book edit/sign/view and lectern screens, dialogs and prompts. Overlay views use the full window as their rectangle and have no menu or title; anvil rename remains part of its inventory screen. Dialog states share the `dialog` id. Screens opened and closed within one frame are missed; cancelling an opening is not possible because there is no single transition point. Bundles need "hooks": @API:screen_opening@.
API 88 adds `native_client_resources_reloaded()` fires on the render thread after successful client resource-pack application: the atlas, models, renderer and client caches already use the new resources. It covers selection/F3+T and server-pack reloads. Failed staging or application and initial startup do not fire it. This is a completion notification, not a reload-listener registration or staging API. Bundles need "hooks": 88.
API 89 adds `native_item_decorations(stack, x, y, batch)` appends custom GUI drawing after the default bar, cooldown shade and count have been painted for an item icon. The stack is the displayed stack, x/y are its icon's top-left in GUI units, the units the batch takes (no item-local translation remains active). Existing GUI helpers can draw badges, text and overlays. The shared path covers the hotbar, carried stacks, menu slots, recipe book and item tooltip icons, and the item icons of the advancement, beacon, stats and trading screens. `native_item_bar` still controls the bar separately; this hook does not suppress vanilla decorations. Bundles need "hooks": 89.
@@ -2536,6 +2538,13 @@ render thread with the frame's wgpu objects; what a mod draws there shows in tha
colour target (copy it out before sampling it), the world's reversed depth and its size, the camera's position and
its matrices with and without TAA's jitter, the frame's camera-relative entity quads with their runs by entity, the
entity atlas and the shared quad index buffer. Not called on the title screen's panorama.
- `native_hand_drawn(hand)`: the first-person hand and held item are drawn, after `native_world_drawn` and before
entity outlines, post effects, screen effects and the HUD, on every frame `native_world_drawn` runs. `HandDrawn`
holds the device, queue and encoder, the frame's colour target with the hand in it, the hand's camera-relative
quads (empty when no hand drew: third person, F1, a shader pack's own hand), the hand's clip matrix (its own field
of view and the view bob, no jitter), the camera, the entity atlas and the quad index buffer. The hand's depth isn't
kept: draw its quads into a depth of your own. A motion blur can match the hand's quads with last frame's for the
swing, attack, equip and bob motion, and blur the picture with the hand in it.
- `native_render_needs(needs)`: set `entity_spans` to have the entity geometry split into runs by entity without a
shader pack (each run's id is stable from frame to frame: a hash of the entity type's name, a block entity's state),
and `world_depth` to keep the world pass's depth when nothing else needs it. Set what you need and leave the rest:
@@ -0,0 +1,15 @@
{
"hooks": [
{
"name": "hand_drawn",
"target": "horizon_client_render::gfx::native_render::native_hand_drawn",
"side": "client",
"mod_api": {"contract": "listen"}
}
],
"rust_summary": "the render hook point `native_hand_drawn(hand)`: the first-person hand is drawn (`HandDrawn`).",
"doc_summary": "the render hook point `native_hand_drawn(hand)`, after the first-person hand and held item are drawn, with the\nframe's target, the hand's quads and its clip matrix (`HandDrawn`). Bundles need `\"hooks\": @API:hand_drawn@`.",
"comments": [
"crates/horizon-client-render/src/gfx/native_render.rs"
]
}
@@ -0,0 +1,16 @@
{
"hooks": [
{
"name": "screen_closing",
"target": "horizon_client::game::native_hooks::Game<'_>::native_screen_closing",
"neoforge": "ScreenEvent.Closing",
"side": "client",
"cancellable": false,
"mod_api": {
"contract": "listen"
}
}
],
"rust_summary": "the screen_closing screen lifecycle observation (`native_screen_closing`) at most once per frame.",
"doc_summary": "`native_screen_closing` receives a NativeScreenView, observed at most once per frame, including sign, book, dialog and prompt overlays; cancelling an opening is not possible because transitions do not share a single choke point. Bundles need hooks version @API:screen_closing@."
}
@@ -0,0 +1,16 @@
{
"hooks": [
{
"name": "screen_init_post",
"target": "horizon_client::game::native_hooks::Game<'_>::native_screen_init_post",
"neoforge": "ScreenEvent.Init.Post",
"side": "client",
"cancellable": false,
"mod_api": {
"contract": "listen"
}
}
],
"rust_summary": "the screen_init_post screen lifecycle observation (`native_screen_init_post`) at most once per frame.",
"doc_summary": "`native_screen_init_post` receives a NativeScreenView, observed at most once per frame, including sign, book, dialog and prompt overlays; cancelling an opening is not possible because transitions do not share a single choke point. Bundles need hooks version @API:screen_init_post@."
}
@@ -0,0 +1,16 @@
{
"hooks": [
{
"name": "screen_opening",
"target": "horizon_client::game::native_hooks::Game<'_>::native_screen_opening",
"neoforge": "ScreenEvent.Opening",
"side": "client",
"cancellable": false,
"mod_api": {
"contract": "listen"
}
}
],
"rust_summary": "the screen_opening screen lifecycle observation (`native_screen_opening`) at most once per frame.",
"doc_summary": "`native_screen_opening` receives a NativeScreenView, observed at most once per frame, including sign, book, dialog and prompt overlays; cancelling an opening is not possible because transitions do not share a single choke point. Bundles need hooks version @API:screen_opening@."
}
+1 -1
View File
@@ -112,7 +112,7 @@ Ocelot.handleEntityEvent port crates/horizon-client-render/src/particles.rs::ent
Ocelot.reassessTrustingGoals port crates/horizon-client/src/mobs/tame.rs::register_cat gap: player: the player-avoidance and tempt goals are always registered; trust is not modelled, so a trusting ocelot keeps them
Ocelot.checkSpawnObstruction port crates/horizon-client/src/mobs/spawn_selection.rs::spawn_obstruction unobstructed, no liquid, at or above sea level on grass or leaves
Ocelot.finalizeSpawn port crates/horizon-client/src/mobs/finalize.rs::Ctx::finalize baby chance 1.0 for a group
Ocelot.getLeashOffset missing minor: the lead ties at full eye height instead of half of it (leash::leash_offset only special-cases llamas)
Ocelot.getLeashOffset port crates/horizon-client/src/mobs/leash.rs::leash_offset half of the eye height, the default width/2 forwards
Ocelot.OcelotAvoidEntityGoal.canUse port crates/horizon-client/src/mobs/tame.rs::OcelotAvoid::can_use gap: player: no isTrusting check (trust is not modelled), so a trusting ocelot still flees the player; 16 blocks, 0.8 and 1.33 match
Ocelot.OcelotAvoidEntityGoal.canContinueToUse port crates/horizon-client/src/mobs/tame.rs::OcelotAvoid::can_continue `!isTrusting() && super`, the same guard as its canUse
Ocelot.OcelotTemptGoal.canScare port crates/horizon-client/src/mobs/tame.rs::OcelotTempt::scared_unless_trusting the tempt goal's canScare with a trusting ocelot's scare turned off
1 AbstractHorse.registerGoals port crates/horizon-client/src/mobs/equine.rs::register same goals and priorities; llamas and camels have their own register
112 Ocelot.reassessTrustingGoals port crates/horizon-client/src/mobs/tame.rs::register_cat gap: player: the player-avoidance and tempt goals are always registered; trust is not modelled, so a trusting ocelot keeps them
113 Ocelot.checkSpawnObstruction port crates/horizon-client/src/mobs/spawn_selection.rs::spawn_obstruction unobstructed, no liquid, at or above sea level on grass or leaves
114 Ocelot.finalizeSpawn port crates/horizon-client/src/mobs/finalize.rs::Ctx::finalize baby chance 1.0 for a group
115 Ocelot.getLeashOffset missing port crates/horizon-client/src/mobs/leash.rs::leash_offset minor: the lead ties at full eye height instead of half of it (leash::leash_offset only special-cases llamas) half of the eye height, the default width/2 forwards
116 Ocelot.OcelotAvoidEntityGoal.canUse port crates/horizon-client/src/mobs/tame.rs::OcelotAvoid::can_use gap: player: no isTrusting check (trust is not modelled), so a trusting ocelot still flees the player; 16 blocks, 0.8 and 1.33 match
117 Ocelot.OcelotAvoidEntityGoal.canContinueToUse port crates/horizon-client/src/mobs/tame.rs::OcelotAvoid::can_continue `!isTrusting() && super`, the same guard as its canUse
118 Ocelot.OcelotTemptGoal.canScare port crates/horizon-client/src/mobs/tame.rs::OcelotTempt::scared_unless_trusting the tempt goal's canScare with a trusting ocelot's scare turned off
+1
View File
@@ -127,5 +127,6 @@ HappyGhast.checkRestriction inline crates/horizon-client/src/mobs/happy_ghast.rs
HappyGhast.continuousHeal inline crates/horizon-client/src/mobs/happy_ghast.rs::ai_step gap: minor: one point every 600 ticks, every 20 in precipitation; the in-clouds fast heal is missing
HappyGhast.defineSynchedData port crates/horizon-client/src/game/lan.rs::mob_meta gap: minor: STAYS_STILL is synced; IS_LEASH_HOLDER is not tracked (no leashHolderTime), so the harness rope layer is never drawn
HappyGhast.syncStayStillFlag port crates/horizon-client/src/game/lan.rs::mob_meta STAYS_STILL shown while the timeout is above zero (on_still_timeout)
HappyGhast.getLeashOffset port crates/horizon-client/src/mobs/leash.rs::leash_offset Vec3.ZERO, the rope at the ghast's feet rather than an eye height up
HappyGhastAi.initCoreActivity port crates/horizon-client/src/mobs/animal_brain.rs::ghastling_package swim, flying panic 2.0, look sink, move sink and the tempt cooldown countdown
HappyGhastAi.initIdleActivity port crates/horizon-client/src/mobs/animal_brain.rs::ghastling_package tempt with look in the eyes, follow a player, follow an adult, then fly stroll or walk to the look target
1 Fox.defineSynchedData port crates/horizon-client/src/game/lan.rs::mob_meta gap: minor: the type and the flags byte are synced; the two trusted-entity slots are not
127 HappyGhast.continuousHeal inline crates/horizon-client/src/mobs/happy_ghast.rs::ai_step gap: minor: one point every 600 ticks, every 20 in precipitation; the in-clouds fast heal is missing
128 HappyGhast.defineSynchedData port crates/horizon-client/src/game/lan.rs::mob_meta gap: minor: STAYS_STILL is synced; IS_LEASH_HOLDER is not tracked (no leashHolderTime), so the harness rope layer is never drawn
129 HappyGhast.syncStayStillFlag port crates/horizon-client/src/game/lan.rs::mob_meta STAYS_STILL shown while the timeout is above zero (on_still_timeout)
130 HappyGhast.getLeashOffset port crates/horizon-client/src/mobs/leash.rs::leash_offset Vec3.ZERO, the rope at the ghast's feet rather than an eye height up
131 HappyGhastAi.initCoreActivity port crates/horizon-client/src/mobs/animal_brain.rs::ghastling_package swim, flying panic 2.0, look sink, move sink and the tempt cooldown countdown
132 HappyGhastAi.initIdleActivity port crates/horizon-client/src/mobs/animal_brain.rs::ghastling_package tempt with look in the eyes, follow a player, follow an adult, then fly stroll or walk to the look target
+3 -3
View File
@@ -64,7 +64,7 @@ Parrot.get skip:other reads the variant as a data component for predicates; Hor
Parrot.applyImplicitComponent missing minor: a spawn egg's parrot_variant component is not applied (only bucketed mobs read item components)
Parrot.defineSynchedData skip:other synched variant id; Horizon keeps it in extra.variant (remote_mob_data.rs reads index 20)
Parrot.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::read_variant Variant clamped to 0 to 4, red and blue when absent
Parrot.getLeashOffset port crates/horizon-client/src/mobs/leash.rs::leash_offset gap: minor: no parrot arm, the lead attaches at the full eye height instead of 0.5 times it
Parrot.getLeashOffset port crates/horizon-client/src/mobs/leash.rs::leash_offset half of the eye height, the default width/2 forwards
Parrot.ParrotWanderGoal.getPosition port crates/horizon-client/src/mobs/parrot.rs::wander_pos
Parrot.ParrotWanderGoal.getTreePos port crates/horizon-client/src/mobs/parrot.rs::tree_pos
ShoulderRidingEntity.setEntityOnShoulder port crates/horizon-client/src/game/shoulders.rs::Game::parrot_to_shoulder saved into the first free ShoulderEntityLeft or ShoulderEntityRight slot, the parrot removed from the world
@@ -82,7 +82,7 @@ Pig.thunderHit port crates/horizon-client/src/game/lightning.rs::Game::thunder_h
Pig.tickRidden inline crates/horizon-client/src/mobs/equine.rs::ridden_step turns to the rider's yaw, pitch halved, and ticks the boost
Pig.getRiddenSpeed inline crates/horizon-client/src/mobs/equine.rs::ridden_step movement speed times 0.225 times the boost factor
Pig.getBreedOffspring port crates/horizon-client/src/game/creatures.rs::Game::breed gap: minor: the baby always takes the first parent's variant, not either parent's by a coin
Pig.getLeashOffset port crates/horizon-client/src/mobs/leash.rs::leash_offset gap: minor: no pig arm, the lead attaches at the full eye height instead of 0.6 times it
Pig.getLeashOffset port crates/horizon-client/src/mobs/leash.rs::leash_offset 0.6 of the eye height, the default width/2 forwards
Pig.getSoundSet inline crates/horizon-client/src/mobs/ai.rs::voice_sounds gap: player: only the baby or adult classic sounds are used, so mini and big sound variants (chosen and saved) never sound different
Pig.finalizeSpawn port crates/horizon-client/src/mobs/finalize.rs::Ctx::finalize variant by biome tag and a random sound variant
PigSoundVariants.bootstrap skip:data sound variant registry; names come from finalize::sound_variants
@@ -110,7 +110,7 @@ Rabbit.applyImplicitComponent missing minor: a spawn egg's rabbit variant compo
Rabbit.finalizeSpawn port crates/horizon-client/src/mobs/finalize.rs::Ctx::finalize variant by biome, shared across a group
Rabbit.getRandomRabbitVariant inline crates/horizon-client/src/mobs/finalize.rs::Ctx::finalize white and white splotched in snowy biomes, gold in deserts, else brown, salt or black
Rabbit.checkRabbitSpawnRules port crates/horizon-client/src/mobs/spawn_rules.rs::check_spawn_rules S::Rabbit arm: rabbits_spawnable_on below and bright enough
Rabbit.getLeashOffset port crates/horizon-client/src/mobs/leash.rs::leash_offset gap: minor: no rabbit arm, the lead attaches at the full eye height instead of 0.6 times it
Rabbit.getLeashOffset port crates/horizon-client/src/mobs/leash.rs::leash_offset 0.6 of the eye height, the default width/2 forwards
Rabbit.shouldPlayIdleAnimation inline crates/horizon-client/src/mobs/rabbit.rs::base_tick idle timeout over, not leashed, not no-AI
Rabbit.RabbitAvoidEntityGoal.canUse port crates/horizon-client/src/mobs/rabbit.rs::register flees players (8), wolves (10) and monsters (4) unless it is the killer rabbit
Rabbit.RaidGardenGoal.canUse port crates/horizon-client/src/mobs/rabbit.rs::RaidGarden::can_use a MoveToBlockGoal (0.7, 16) over ripe carrots, delayed 200 to 400 ticks; no raid while mob griefing is off or while moreCarrotTicks is still positive
1 AbstractNautilus.createAttributes skip:data attributes come from the entity type table (MobType in mobs.rs)
64 Parrot.applyImplicitComponent missing minor: a spawn egg's parrot_variant component is not applied (only bucketed mobs read item components)
65 Parrot.defineSynchedData skip:other synched variant id; Horizon keeps it in extra.variant (remote_mob_data.rs reads index 20)
66 Parrot.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::read_variant Variant clamped to 0 to 4, red and blue when absent
67 Parrot.getLeashOffset port crates/horizon-client/src/mobs/leash.rs::leash_offset gap: minor: no parrot arm, the lead attaches at the full eye height instead of 0.5 times it half of the eye height, the default width/2 forwards
68 Parrot.ParrotWanderGoal.getPosition port crates/horizon-client/src/mobs/parrot.rs::wander_pos
69 Parrot.ParrotWanderGoal.getTreePos port crates/horizon-client/src/mobs/parrot.rs::tree_pos
70 ShoulderRidingEntity.setEntityOnShoulder port crates/horizon-client/src/game/shoulders.rs::Game::parrot_to_shoulder saved into the first free ShoulderEntityLeft or ShoulderEntityRight slot, the parrot removed from the world
82 Pig.tickRidden inline crates/horizon-client/src/mobs/equine.rs::ridden_step turns to the rider's yaw, pitch halved, and ticks the boost
83 Pig.getRiddenSpeed inline crates/horizon-client/src/mobs/equine.rs::ridden_step movement speed times 0.225 times the boost factor
84 Pig.getBreedOffspring port crates/horizon-client/src/game/creatures.rs::Game::breed gap: minor: the baby always takes the first parent's variant, not either parent's by a coin
85 Pig.getLeashOffset port crates/horizon-client/src/mobs/leash.rs::leash_offset gap: minor: no pig arm, the lead attaches at the full eye height instead of 0.6 times it 0.6 of the eye height, the default width/2 forwards
86 Pig.getSoundSet inline crates/horizon-client/src/mobs/ai.rs::voice_sounds gap: player: only the baby or adult classic sounds are used, so mini and big sound variants (chosen and saved) never sound different
87 Pig.finalizeSpawn port crates/horizon-client/src/mobs/finalize.rs::Ctx::finalize variant by biome tag and a random sound variant
88 PigSoundVariants.bootstrap skip:data sound variant registry; names come from finalize::sound_variants
110 Rabbit.finalizeSpawn port crates/horizon-client/src/mobs/finalize.rs::Ctx::finalize variant by biome, shared across a group
111 Rabbit.getRandomRabbitVariant inline crates/horizon-client/src/mobs/finalize.rs::Ctx::finalize white and white splotched in snowy biomes, gold in deserts, else brown, salt or black
112 Rabbit.checkRabbitSpawnRules port crates/horizon-client/src/mobs/spawn_rules.rs::check_spawn_rules S::Rabbit arm: rabbits_spawnable_on below and bright enough
113 Rabbit.getLeashOffset port crates/horizon-client/src/mobs/leash.rs::leash_offset gap: minor: no rabbit arm, the lead attaches at the full eye height instead of 0.6 times it 0.6 of the eye height, the default width/2 forwards
114 Rabbit.shouldPlayIdleAnimation inline crates/horizon-client/src/mobs/rabbit.rs::base_tick idle timeout over, not leashed, not no-AI
115 Rabbit.RabbitAvoidEntityGoal.canUse port crates/horizon-client/src/mobs/rabbit.rs::register flees players (8), wolves (10) and monsters (4) unless it is the killer rabbit
116 Rabbit.RaidGardenGoal.canUse port crates/horizon-client/src/mobs/rabbit.rs::RaidGarden::can_use a MoveToBlockGoal (0.7, 16) over ripe carrots, delayed 200 to 400 ticks; no raid while mob griefing is off or while moreCarrotTicks is still positive
+1 -1
View File
@@ -12,7 +12,7 @@ Strider.tickRidden port crates/horizon-client/src/mobs/equine.rs::ridden_step st
Strider.playStepSound port crates/horizon-client/src/mobs/ai.rs::step_sound lava or normal step sound
Strider.isBeingTempted inline crates/horizon-client/src/mobs/ai.rs::play_ambient the Tempt goal running check is folded into the ambient sound guard
Strider.canAddPassenger missing no boarding rule of its own: `mobs/riders.rs::start_riding` tests the mount and the cooldown only, so a player can still board a saddled strider whose head is in lava where vanilla refuses (`isEyeInFluid` LAVA)
Strider.getLeashOffset port crates/horizon-client/src/mobs/leash.rs::leash_offset gap: minor: the strider 0.6 x eye height override is not applied; the default eye-height offset is used
Strider.getLeashOffset port crates/horizon-client/src/mobs/leash.rs::leash_offset 0.6 of the eye height, the default width/2 forwards
Strider.StriderPathNavigation.createPathFinder port crates/horizon-client/src/mobs/nav.rs::Navigation::evaluator ground nav kind gets the walk evaluator, as WalkNodeEvaluator
Strider.StriderPathNavigation.isStableDestination port crates/horizon-client/src/mobs/nav.rs::Mob::stable_destination lava counts as stable for lava walkers
Vex.registerGoals port crates/horizon-client/src/mobs/vex.rs::register float, charge, random move, look goals, owner and player targets
1 Strider.checkStriderSpawnRules port crates/horizon-client/src/mobs/spawn_rules.rs::check_spawn_rules strider arm: climbs lava upward, then needs air above
12 Strider.playStepSound port crates/horizon-client/src/mobs/ai.rs::step_sound lava or normal step sound
13 Strider.isBeingTempted inline crates/horizon-client/src/mobs/ai.rs::play_ambient the Tempt goal running check is folded into the ambient sound guard
14 Strider.canAddPassenger missing no boarding rule of its own: `mobs/riders.rs::start_riding` tests the mount and the cooldown only, so a player can still board a saddled strider whose head is in lava where vanilla refuses (`isEyeInFluid` LAVA)
15 Strider.getLeashOffset port crates/horizon-client/src/mobs/leash.rs::leash_offset gap: minor: the strider 0.6 x eye height override is not applied; the default eye-height offset is used 0.6 of the eye height, the default width/2 forwards
16 Strider.StriderPathNavigation.createPathFinder port crates/horizon-client/src/mobs/nav.rs::Navigation::evaluator ground nav kind gets the walk evaluator, as WalkNodeEvaluator
17 Strider.StriderPathNavigation.isStableDestination port crates/horizon-client/src/mobs/nav.rs::Mob::stable_destination lava counts as stable for lava walkers
18 Vex.registerGoals port crates/horizon-client/src/mobs/vex.rs::register float, charge, random move, look goals, owner and player targets