mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-11 02:21:38 -10:00
Merge main into native/render-hand-pose
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
e8afde61ab
44 files changed
+645
-84
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -933,10 +933,7 @@ impl Graphics {
|
||||
// and render threads are raised above four cores, where vanilla
|
||||
// raises its server thread). Leaving two idled a core of a 4-thread
|
||||
// laptop while a world loaded: a teleport there took ~15% longer.
|
||||
let auto = std::thread::available_parallelism()
|
||||
.map_or(2, |n| n.get())
|
||||
.saturating_sub(1)
|
||||
.max(1);
|
||||
let auto = horizon_threads::worker_count().saturating_sub(1).max(1);
|
||||
if self.battery { auto.div_ceil(2) } else { auto }
|
||||
}
|
||||
/// The frame cap battery mode adds: 60 in a world, 30 in menus without a
|
||||
@@ -1294,7 +1291,7 @@ mod tests {
|
||||
fn automatic_workers_leave_a_core_as_vanilla_does() {
|
||||
let g = Graphics::default();
|
||||
assert_eq!(g.workers, 0);
|
||||
let cores = std::thread::available_parallelism().map_or(2, |n| n.get());
|
||||
let cores = horizon_threads::worker_count();
|
||||
assert_eq!(g.worker_threads(), cores.saturating_sub(1).max(1));
|
||||
// A manual count is the player's own choice, kept as asked.
|
||||
let manual = Graphics { workers: 3, ..g };
|
||||
|
||||
@@ -1114,7 +1114,7 @@ impl Lod {
|
||||
let keeps_observed = source
|
||||
.as_ref()
|
||||
.is_some_and(|source| source.keeps_observed());
|
||||
let cores = thread::available_parallelism().map_or(1, |n| n.get());
|
||||
let cores = horizon_threads::worker_count();
|
||||
let shared = Arc::new(Shared {
|
||||
jobs: Mutex::new(VecDeque::new()),
|
||||
mesh_jobs: Mutex::new(std::collections::BinaryHeap::new()),
|
||||
|
||||
@@ -7,9 +7,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
/// Threads take the next item as they finish one, so a run of slow items
|
||||
/// doesn't hold back the others.
|
||||
pub fn par_map<T: Sync, R: Send>(items: &[T], f: impl Fn(&T) -> R + Sync) -> Vec<R> {
|
||||
let threads = std::thread::available_parallelism()
|
||||
.map_or(1, |n| n.get())
|
||||
.min(items.len());
|
||||
let threads = horizon_threads::worker_count().min(items.len());
|
||||
if threads <= 1 {
|
||||
return items.iter().map(f).collect();
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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.
|
||||
@@ -6014,9 +6015,8 @@ impl Game<'_> {
|
||||
if entity && hand == Hand::Off && self.with_hand(hand, |g| g.interact_entity(None))? {
|
||||
return Ok(Some(hand));
|
||||
}
|
||||
// A client of a server never hits a fence knot with its crosshair:
|
||||
// `LeadItem.useOn` and `LeashFenceKnotEntity.interact` reach the
|
||||
// server as a click on the fence block.
|
||||
// A click on the fence block: `LeadItem.useOn`. A client whose crosshair
|
||||
// met the knot first sends Interact with the knot's id instead.
|
||||
if on_block && self.with_hand(hand, Game::interact_knot) {
|
||||
return Ok(Some(hand));
|
||||
}
|
||||
@@ -6062,8 +6062,7 @@ impl Game<'_> {
|
||||
|| self.interact_boat(target)
|
||||
|| self.interact_cart(target)
|
||||
|| self.interact_mob(target)?
|
||||
// Knots are reached only through the crosshair pick for now: a
|
||||
// guest's Interact naming a knot's id is a known gap.
|
||||
// A guest's Interact naming a knot's id goes to `use_knot` before this.
|
||||
|| target.is_none() && self.interact_knot())
|
||||
}
|
||||
/// `ItemStack.applyAfterUseComponentSideEffects`: a used stack with a
|
||||
@@ -15048,6 +15047,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 {
|
||||
@@ -18349,6 +18349,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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -2174,6 +2174,17 @@ impl Game<'_> {
|
||||
{
|
||||
return;
|
||||
}
|
||||
// LeashFenceKnotEntity.interact: a knot has no entity here, only its fence.
|
||||
if let Some(knot) = self.knot_at_id(p.id) {
|
||||
let hand = if p.hand == 1 { Hand::Off } else { Hand::Main };
|
||||
let swung = self.as_guest(host, conn, |g| {
|
||||
let swings = g.swings_started;
|
||||
g.with_hand(hand, |g| g.use_knot(knot, true));
|
||||
g.swings_started != swings
|
||||
});
|
||||
self.lan_swing_back(host, conn, swung);
|
||||
return;
|
||||
}
|
||||
let before = host.hub.guests[gi].vehicle;
|
||||
if self
|
||||
.look_at_entity(host, gi, p.id, Some(p.location))
|
||||
@@ -2958,6 +2969,15 @@ impl Game<'_> {
|
||||
if let Some(b) = self.tracked_special_box(id) {
|
||||
return Some(b);
|
||||
}
|
||||
if let Some(knot) = self.knot_at_id(id) {
|
||||
let knot = horizon_client::mobs::leash::knot_state(knot);
|
||||
let p = knot.position;
|
||||
let half = knot.width / 2.0;
|
||||
return Some(Aabb::new(
|
||||
Vec3::new(p.x - half, p.y, p.z - half),
|
||||
Vec3::new(p.x + half, p.y + knot.height, p.z + half),
|
||||
));
|
||||
}
|
||||
self.creatures
|
||||
.projectiles
|
||||
.iter()
|
||||
@@ -2965,6 +2985,14 @@ impl Game<'_> {
|
||||
.map(|p| p.bounds())
|
||||
}
|
||||
|
||||
/// The fence of the knot whose entity id is `id` (`lan_assign_ids`).
|
||||
fn knot_at_id(&self, id: i32) -> Option<[i32; 3]> {
|
||||
self.knot_ids
|
||||
.iter()
|
||||
.find(|&(_, &knot)| knot as i32 == id)
|
||||
.map(|(&fence, _)| fence)
|
||||
}
|
||||
|
||||
fn guest_attack(&mut self, host: &mut LanHost, conn: u32, id: i32) {
|
||||
let Some(gi) = host.hub.guests.iter().position(|g| g.conn == conn) else {
|
||||
return;
|
||||
|
||||
@@ -337,7 +337,7 @@ impl Game<'_> {
|
||||
}
|
||||
false
|
||||
}
|
||||
/// `LeadItem.useOn` and `LeashFenceKnotEntity.interact` on a fence.
|
||||
/// `LeadItem.useOn` on the fence the crosshair hits.
|
||||
pub(super) fn interact_knot(&mut self) -> bool {
|
||||
let Some(hit) = self.pick() else {
|
||||
return false;
|
||||
@@ -355,7 +355,15 @@ impl Game<'_> {
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let knot_pos = hit.pos;
|
||||
// `FenceBlock.useWithoutItem` / `LeadItem.useOn` only tie the player's own mobs
|
||||
// (`LeadItem.bindPlayerMobs`). A client whose crosshair met the knot sends Interact with
|
||||
// the knot's id instead. Taking a mob back here would undo a tie: the client sends the
|
||||
// click once for each hand.
|
||||
self.use_knot(hit.pos, false)
|
||||
}
|
||||
/// A click at the fence at `knot_pos`: `knot_hit` for `LeashFenceKnotEntity.interact`,
|
||||
/// otherwise `LeadItem.useOn`. Returns whether it was used.
|
||||
pub(super) fn use_knot(&mut self, knot_pos: [i32; 3], knot_hit: bool) -> bool {
|
||||
let knot = leash::knot_state(knot_pos).center;
|
||||
let center = glam::DVec3::new(knot.x, knot.y, knot.z);
|
||||
let at = [center.x, center.y, center.z];
|
||||
@@ -363,27 +371,6 @@ impl Game<'_> {
|
||||
.filter(|&i| self.mobs[i].extra.leash == Some(Holder::Knot(knot_pos)))
|
||||
.collect();
|
||||
let existing_boats = self.boats_on(Holder::Knot(knot_pos));
|
||||
// `LeashFenceKnotEntity.interact` runs only when the ray meets the knot
|
||||
// before the post: the knot's box (`recalculateBoundingBox`, 0.375 wide,
|
||||
// y + 0.375 to y + 0.875) is wider than the post, so a hit point on the
|
||||
// post inside that box went through the knot first. Any other click on
|
||||
// the fence is `FenceBlock.useWithoutItem` / `LeadItem.useOn`, which only
|
||||
// ties the player's own mobs (`LeadItem.bindPlayerMobs`).
|
||||
let knot_hit = !(existing.is_empty() && existing_boats.is_empty()) && {
|
||||
let box_ = leash::knot_state(knot_pos);
|
||||
let half = box_.width / 2.0 + 1.0e-6;
|
||||
(hit.point[0] - box_.position.x).abs() <= half
|
||||
&& (hit.point[2] - box_.position.z).abs() <= half
|
||||
&& hit.point[1] >= box_.position.y - 1.0e-6
|
||||
&& hit.point[1] <= box_.position.y + box_.height + 1.0e-6
|
||||
// The knot is an entity: a hit on it counts only within the
|
||||
// entity interaction range (`filterHitResult`), shorter than
|
||||
// the block range the fence was hit at.
|
||||
&& self.eye_position().is_some_and(|eye| {
|
||||
let d = [0, 1, 2].map(|i| hit.point[i] - eye[i]);
|
||||
d[0] * d[0] + d[1] * d[1] + d[2] * d[2] <= self.entity_reach * self.entity_reach
|
||||
})
|
||||
};
|
||||
let held_item = self.held().map(|s| s.item.to_string());
|
||||
let sneaking = self.keys.contains(&winit::keyboard::KeyCode::ShiftLeft);
|
||||
// Shears snip every lead on the knot.
|
||||
|
||||
@@ -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, ¤t) {
|
||||
(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) = ¤t {
|
||||
self.native_screen_opening(&new.view());
|
||||
self.native_screen_init_post(&new.view());
|
||||
}
|
||||
} else if let (Some(old), Some(new)) = (&self.observed_screen, ¤t)
|
||||
&& 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
|
||||
@@ -2925,6 +3025,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
|
||||
@@ -9364,6 +9478,22 @@ mod tests {
|
||||
.transform_point3(glam::Vec3::X);
|
||||
assert_eq!(point, glam::Vec3::new(3.0, 2.0, 3.0));
|
||||
}
|
||||
#[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]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -7153,7 +7153,7 @@ impl Game<'_> {
|
||||
}
|
||||
// Resolve the crosshair once. A passing entity interaction keeps
|
||||
// that target and may only fall back to using the item itself.
|
||||
let entity = self.entity_targeted();
|
||||
let entity = self.remote_use_targets_entity();
|
||||
horizon_client::interaction_input::use_on_target(&mut RemoteUseWorld(self), entity);
|
||||
}
|
||||
|
||||
@@ -8191,7 +8191,7 @@ impl Game<'_> {
|
||||
}
|
||||
return;
|
||||
}
|
||||
let off_target = if self.entity_targeted() {
|
||||
let off_target = if self.remote_use_targets_entity() {
|
||||
self.remote_use_entity(Hand::Off)
|
||||
} else {
|
||||
self.remote_use_block(Hand::Off)
|
||||
|
||||
@@ -26,8 +26,39 @@ pub(super) struct ObjectHit {
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// BlockAttachedEntity.isPickable: the nearest fence knot's box the view meets, with its id,
|
||||
/// distance and position. Its box is wider than the post inside it, so the crosshair meets it
|
||||
/// first and LeashFenceKnotEntity.interact takes the click. The fence block behind only ties
|
||||
/// the player's own mobs (LeadItem.useOn), it never takes one back.
|
||||
fn remote_knot_hit(&self) -> Option<(i32, f64, [f64; 3])> {
|
||||
let (eye, dir) = self.aim()?;
|
||||
self.remote_knots
|
||||
.iter()
|
||||
.filter_map(|(&id, &block)| {
|
||||
let knot = horizon_client::mobs::leash::knot_state(block);
|
||||
let p = knot.position;
|
||||
let half = knot.width / 2.0;
|
||||
let bounds = Aabb::new(
|
||||
Vec3::new(p.x - half, p.y, p.z - half),
|
||||
Vec3::new(p.x + half, p.y + knot.height, p.z + half),
|
||||
);
|
||||
ray_box(eye, dir, self.entity_reach, bounds)
|
||||
.map(|d| (id as i32, d, [p.x, p.y, p.z]))
|
||||
})
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1))
|
||||
}
|
||||
|
||||
/// Whether a use starts on an entity: the crosshair's `EntityHitResult`, or a fence knot.
|
||||
/// Only a use: Horizon's attacks still go to the fence behind a knot.
|
||||
pub(super) fn remote_use_targets_entity(&self) -> bool {
|
||||
self.entity_targeted()
|
||||
|| self
|
||||
.remote_knot_hit()
|
||||
.is_some_and(|(_, d, _)| self.before_block(d))
|
||||
}
|
||||
|
||||
/// The nearest boat, minecart, item frame, armor stand or painting the
|
||||
/// view hits within reach; also End crystals when attacking.
|
||||
/// view hits within reach; also End crystals when attacking, and fence knots when using.
|
||||
pub(super) fn remote_object_hit(&self, attacking: bool) -> Option<ObjectHit> {
|
||||
let (eye, dir) = self.aim()?;
|
||||
let at = |d: f64| {
|
||||
@@ -80,6 +111,9 @@ impl Game<'_> {
|
||||
hits.push((id, d, [p.x, p.y, p.z]));
|
||||
}
|
||||
}
|
||||
if !attacking && let Some((id, d, p)) = self.remote_knot_hit() {
|
||||
hits.push((id, d, p));
|
||||
}
|
||||
if attacking {
|
||||
// EndCrystal.isPickable: its 2 x 2 box, with the server's entity
|
||||
// id, not special_targets' local projectile target id.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -374,7 +374,7 @@ struct WarmPool {
|
||||
fn warm_pool() -> Option<&'static WarmPool> {
|
||||
static POOL: OnceLock<Option<Arc<WarmPool>>> = OnceLock::new();
|
||||
POOL.get_or_init(|| {
|
||||
let threads = std::thread::available_parallelism().map_or(1, |n| n.get()) - 1;
|
||||
let threads = horizon_threads::worker_count() - 1;
|
||||
let pool = Arc::new(WarmPool {
|
||||
job: Default::default(),
|
||||
ready: Default::default(),
|
||||
|
||||
@@ -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
@@ -4542,6 +4542,21 @@ pub trait Hooks: Send + Sync {
|
||||
let _ = event;
|
||||
Flow::Continue
|
||||
}
|
||||
/// 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]
|
||||
@@ -13973,3 +13988,48 @@ pub fn dispatch_render_item_in_frame(
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
@@ -2247,7 +2247,7 @@ fn tables(base: &Path, plan: &Plan, overlay: &Overlay<'_>) -> Result<()> {
|
||||
overlay.write(name, &bytes)
|
||||
};
|
||||
let next = std::sync::atomic::AtomicUsize::new(0);
|
||||
let threads = std::thread::available_parallelism().map_or(1, |n| n.get());
|
||||
let threads = horizon_threads::worker_count();
|
||||
std::thread::scope(|scope| {
|
||||
let workers: Vec<_> = (0..threads.min(TABLE_THREADS))
|
||||
.map(|_| {
|
||||
|
||||
@@ -2100,8 +2100,8 @@ fn compress_records(
|
||||
.map(|tag| ChunkRecord::from_nbt(&tag, Compression::Zlib, timestamp, Limits::saved()))
|
||||
.transpose()
|
||||
};
|
||||
let helpers = std::thread::available_parallelism()
|
||||
.map_or(1, |n| n.get().div_ceil(2))
|
||||
let helpers = horizon_threads::worker_count()
|
||||
.div_ceil(2)
|
||||
.min(records.len() / CHUNKS_PER_HELPER);
|
||||
if helpers <= 1 {
|
||||
return records.iter().map(|(_, r)| compress(r)).collect();
|
||||
|
||||
@@ -72,9 +72,7 @@ impl Builder {
|
||||
// ticks and the frame leave, so more cores can share it: a third of
|
||||
// the threads, rounded up (a 4-thread machine's one thread left idle
|
||||
// time while the far trees and structures came in).
|
||||
let count = thread::available_parallelism()
|
||||
.map_or(1, |n| n.get().div_ceil(3))
|
||||
.clamp(1, 12);
|
||||
let count = horizon_threads::worker_count().div_ceil(3).clamp(1, 12);
|
||||
let threads = (0..count)
|
||||
.map(|_| Self::spawn(terrain.clone(), shared.clone(), tx.clone()))
|
||||
.collect();
|
||||
|
||||
@@ -165,10 +165,7 @@ static POOL: OnceLock<Pool> = OnceLock::new();
|
||||
/// but one, which the render and server threads share with them (they run
|
||||
/// ahead of the workers: [`crate::favour_current`], [`crate::behind_server`]).
|
||||
pub fn default_threads() -> usize {
|
||||
std::thread::available_parallelism()
|
||||
.map_or(2, |n| n.get())
|
||||
.saturating_sub(1)
|
||||
.max(1)
|
||||
crate::worker_count().saturating_sub(1).max(1)
|
||||
}
|
||||
|
||||
/// Size the process's pool before its first use (later calls change
|
||||
|
||||
@@ -8,7 +8,32 @@ pub mod heap_census;
|
||||
pub mod jobs;
|
||||
pub mod memory_limit;
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{
|
||||
OnceLock,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
/// The worker threads the machine offers this process: `available_parallelism`'s
|
||||
/// answer (its share of the cores — CPU affinity, a container's quota), resolved
|
||||
/// once, since it doesn't change under a run and asking again on a later path
|
||||
/// can only disagree with the pools already started. `HORIZON_WORKER_THREADS`
|
||||
/// sets it instead, to cap every pool on a machine whose OS reports more cores
|
||||
/// than a run should take; a value that isn't a positive number is ignored.
|
||||
/// Each pool keeps its own share of the count (the streaming pool all but one,
|
||||
/// far decoration a third of it).
|
||||
pub fn worker_count() -> usize {
|
||||
static COUNT: OnceLock<usize> = OnceLock::new();
|
||||
*COUNT.get_or_init(|| {
|
||||
parse_worker_count(std::env::var("HORIZON_WORKER_THREADS").ok())
|
||||
.unwrap_or_else(|| std::thread::available_parallelism().map_or(1, |n| n.get()))
|
||||
})
|
||||
}
|
||||
|
||||
/// `Some(n)` when `value` names a positive count (`" 4 "` counts; `"0"`,
|
||||
/// `"lots"`, a missing variable don't).
|
||||
fn parse_worker_count(value: Option<String>) -> Option<usize> {
|
||||
value?.trim().parse().ok().filter(|&n| n > 0)
|
||||
}
|
||||
|
||||
/// Ask the OS to run the calling thread ahead of the process's other
|
||||
/// threads, as vanilla's `MinecraftServer.spin` does for its server thread
|
||||
@@ -403,6 +428,32 @@ pub fn keep_stdio_from_children() {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// Without `HORIZON_WORKER_THREADS`, the count is the machine's.
|
||||
#[test]
|
||||
fn worker_count_defaults_to_the_machine() {
|
||||
if std::env::var_os("HORIZON_WORKER_THREADS").is_some() {
|
||||
return;
|
||||
}
|
||||
let machine = std::thread::available_parallelism().map_or(1, |n| n.get());
|
||||
assert_eq!(super::worker_count(), machine);
|
||||
}
|
||||
|
||||
/// Only a positive number overrides the machine's count.
|
||||
#[test]
|
||||
fn worker_count_override_parses() {
|
||||
for (value, parsed) in [
|
||||
(None, None),
|
||||
(Some("4"), Some(4)),
|
||||
(Some(" 4 "), Some(4)),
|
||||
(Some("0"), None),
|
||||
(Some("-2"), None),
|
||||
(Some("lots"), None),
|
||||
(Some(""), None),
|
||||
] {
|
||||
assert_eq!(super::parse_worker_count(value.map(String::from)), parsed);
|
||||
}
|
||||
}
|
||||
|
||||
/// After it, this process's standard output is no longer handed to the processes it starts.
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
|
||||
@@ -787,6 +787,8 @@ API 87 adds `native_render_hand(hand, stack, partial, swing, equip, canceled)` r
|
||||
|
||||
`native_render_hand_pose` runs after cancellation for each visible first-person hand, including maps and the empty main-hand arm (the same path as `RenderArmEvent`). Add to `x`, `y`, `z` in blocks and `pitch`, `yaw`, `roll` in degrees, and multiply `scale` to compose with other mods. The pose is applied in hand view space before the ordinary hand and item poses, in translation, pitch, yaw, roll, scale order. Non-finite or out-of-range components are ignored individually: offsets must be within -16..=16 blocks, angles within -3600..=3600 degrees and scale within 0.01..=100. Bundles need "hooks": @API:render_hand_pose@.
|
||||
|
||||
`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@."
|
||||
}
|
||||
@@ -50,7 +50,7 @@ MerchantScreen.extractButtonArrows inline crates/horizon-client/src/game/trading
|
||||
MerchantScreen.extractAndDecorateCostA inline crates/horizon-client/src/game/trading.rs::Game::draw_merchant discount: base count struck through, new count drawn (merchant.rs cost_a)
|
||||
MerchantScreen.mouseScrolled port crates/horizon-client/src/game/trading.rs::Game::merchant_scroll gap: minor: one offer per wheel step; vanilla uses the float scroll amount
|
||||
MerchantScreen.mouseDragged port crates/horizon-client/src/game/trading.rs::Game::merchant_drag the scroller takes presses only past seven offers and picks the nearest one
|
||||
MerchantScreen.mouseClicked port crates/horizon-client/src/game/trading.rs::Game::merchant_click gap: player: clicking the scroller does not start a drag (see mouseDragged); the offer buttons work
|
||||
MerchantScreen.mouseClicked port crates/horizon-client/src/game/trading.rs::Game::merchant_click a press inside the scroller's 139-px track starts the drag (dragging set, merchant_drag follows the pointer), only past seven offers; otherwise the offer buttons are hit-tested
|
||||
MerchantScreen.TradeOfferButton.extractToolTip inline crates/horizon-client/src/game/trading.rs::Game::draw_merchant item tooltip for cost A, cost B or the result under the hovered offer
|
||||
MinecartCommandBlockEditScreen.populateAndSendPacket port crates/horizon-client/src/game/command_minecarts.rs::Game::open_cart_command_block a game master's interact opens the editor on the cart's BaseCommandBlock, and the edit is written back to the cart
|
||||
PageButton.extractContents inline crates/horizon-client/src/game/books.rs::Game::draw_book page arrows with their hover sprites (page_buttons)
|
||||
@@ -76,4 +76,4 @@ TestBlockEditScreen home crates/horizon-client/src/game/block_editor.rs
|
||||
TestInstanceBlockEditScreen home crates/horizon-client/src/game/block_editor.rs
|
||||
BelowOrAboveWidgetTooltipPositioner.positionTooltip port crates/horizon-client-render/src/ui.rs::menu_tooltip_position the tooltip under the widget, over it when it does not fit, with vanilla's margins and overlaps
|
||||
ClientActivePlayersTooltip.getWidth port crates/horizon-client/src/multiplayer.rs::player_list the sample's names are the tooltip's lines; its width comes from them (the status tooltip's own sizing)
|
||||
ClientActivePlayersTooltip.extractImage port crates/horizon-client/src/multiplayer.rs::player_list each sample name styled by its § codes, then and_more for the players the sample leaves out
|
||||
ClientActivePlayersTooltip.extractImage inline crates/horizon-client/src/multiplayer.rs::player_list the sample names, styled by their § codes, become the tooltip's text lines, then and_more for the players the sample leaves out; gap: player: no player face is drawn beside each name (vanilla draws a 10x10 face per entry through PlayerFaceExtractor), so the tooltip is text only
|
||||
|
@@ -1,4 +1,4 @@
|
||||
GhostSlots.extractTooltip port crates/horizon-client/src/game/recipe_book.rs::Game::draw_ghost the ghost stack in a menu slot shows its tooltip at the slot, after the ghosts themselves are drawn (GhostSlots.extractRenderState)
|
||||
GhostSlots.extractTooltip port crates/horizon-client/src/game/recipe_book.rs::Game::ghost_stack the stack a ghost slot is showing, cycling like the ghost's own draw, becomes that slot's tooltip: draw_slot_tooltip asks ghost_stack first, so a ghost's tooltip wins over the slot's own
|
||||
GhostSlots.GhostSlot.getItem port crates/horizon-client/src/recipe_book_screen.rs::ghost the stack cycles with index % items; empty ghost slots are dropped in Book::ghost
|
||||
OverlayRecipeComponent.mouseClicked port crates/horizon-client/src/game/recipe_book.rs::Game::recipe_book_click left click places the overlay's recipe; right click and outside clicks close the overlay
|
||||
OverlayRecipeComponent.OverlayCraftingRecipeButton.calculateIngredientsPositions port crates/horizon-client/src/recipe_book_screen.rs::overlay_cells shaped recipes use the 3x3 grid cells, shapeless ones i % 3 and i / 3; empty ingredients are dropped
|
||||
@@ -14,7 +14,7 @@ RecipeBookComponent.selectMatchingRecipes inline crates/horizon-client/src/game/
|
||||
RecipeBookComponent.updateCollections port crates/horizon-client/src/game/recipe_book.rs::Game::build_view collections filtered by the search (Book::collection_matches) and by the craftable filter; callers reset the page on tab and filter changes
|
||||
RecipeBookComponent.updateTabs inline crates/horizon-client/src/game/recipe_book.rs::Game::build_view tab visibility; positions are in Game::tab_pos and the squeeze in Game::start_new_recipe_animations
|
||||
RecipeBookComponent.tick port crates/horizon-client/src/game/recipe_book.rs::Game::draw_recipe_book advances the swap time and the squeezes; the inventory-changed refresh is the hash in refresh_book_view
|
||||
RecipeBookComponent.extractTooltip port crates/horizon-client/src/game/recipe_book.rs::Game::draw_book_panel gap: player: no tooltip for ghost slots (GhostSlots.extractTooltip); the page's recipe tooltip is the ItemTooltip call
|
||||
RecipeBookComponent.extractTooltip port crates/horizon-client/src/game/recipe_book.rs::Game::draw_book_panel the page's recipe tooltip is the ItemTooltip call; a ghost slot's tooltip is GhostSlots.extractTooltip, whose stack Game::ghost_stack answers for draw_slot_tooltip
|
||||
RecipeBookComponent.mouseClicked port crates/horizon-client/src/game/recipe_book.rs::Game::recipe_book_click gap: player: in the narrow layout vanilla closes the book after placing a recipe (isOffsetNextToMainGUI is false); Horizon keeps it open
|
||||
RecipeBookComponent.mouseDragged port crates/horizon-client/src/game/recipe_book.rs::Game::recipe_book_drag the drag that started in the search box moves the selection end to the pointer (EditBox::drag_to); letting go ends it
|
||||
RecipeBookComponent.onTabButtonPress port crates/horizon-client/src/game/recipe_book.rs::Game::recipe_book_click a tab click selects the tab and resets the page to 0
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -15,6 +15,7 @@ LeashFenceKnotEntity.recalculateBoundingBox port crates/horizon-client/src/mobs/
|
||||
LeashFenceKnotEntity.getOrCreateKnot inline crates/horizon-client/src/game/leads.rs::Game::interact_knot knots are not entities in game; the knot is used when a lead is tied
|
||||
LeashFenceKnotEntity.getKnot inline crates/horizon-client/src/game/leads.rs::Game::interact_knot the existing knot is the leashed-holder check at the fence position
|
||||
LeashFenceKnotEntity.createKnot inline crates/horizon-client/src/game/leads.rs::Game::interact_knot no in-game knot entity is created; LAN knot ids are assigned in lan_assign_ids
|
||||
LeashFenceKnotEntity.interact port crates/horizon-client/src/game/leads.rs::Game::use_knot a client sends it for the knot its crosshair meets (remote_knot_hit); Horizon attacks still go to the fence behind
|
||||
Painting.defineSynchedData skip:other entity data default variant; Horizon picks the variant in place_painting
|
||||
Painting.get port crates/horizon-client/src/game/paintings.rs::PaintingVariants::get lookup of the variant by id; there is no component getter
|
||||
Painting.applyImplicitComponent inline crates/horizon-client/src/game/paintings.rs::Game::place_painting the stack's painting/variant replaces the random pick (PaintingVariants::of_stack), and that variant has to survive where it goes
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -74,7 +74,7 @@ ResourceMetadata.fromJsonStream port crates/horizon-client-render/src/packs.rs::
|
||||
ResourceMetadata.of skip:java map-backed metadata constructor
|
||||
ResourceMetadata.getTypedSections skip:java typed view over sections; Horizon reads sections by key
|
||||
ResourceProvider.getResourceOrThrow skip:java Option instead of a thrown FileNotFound
|
||||
SimpleJsonResourceReloadListener.prepare port crates/horizon-client-render/src/datapack.rs::DataStore::load gap: player: only function, item_modifier, predicate and function-tag files are read; recipes, loot tables and advancements from datapacks are not
|
||||
SimpleJsonResourceReloadListener.prepare port crates/horizon-client-render/src/datapack.rs::DataStore::load functions and function tags are parsed, and the content tables' files (recipe, recipe_filter, loot_table, advancement, trade_set, villager_trade, dialog, data_maps, tags) are kept by id in `files` for those tables to merge over the resource root
|
||||
SimplePreparableReloadListener.reload port crates/horizon-client/src/game/packs.rs::stage_pack gap: minor: the prepare half; apply_staged_pack does the apply
|
||||
SimpleReloadInstance.of port crates/horizon-client/src/game/packs.rs::PackReload::spawn stage then apply, in one call: the worker stages, drive_pack_reload applies
|
||||
SimpleReloadInstance.startTasks inline crates/horizon-client/src/game/packs.rs::Game::reload_packs defers while a world or rest loads; errors go to Options
|
||||
|
||||
|
@@ -11,7 +11,7 @@ StringUtil.truncateStringIfNecessary port crates/horizon-client-render/src/scree
|
||||
StringUtil.lineCount skip:other no caller in 26.3 (dead helper)
|
||||
StringUtil.isBlank port crates/horizon-client/src/title.rs::Title::buttons Save of the rename screen is active only for a non-blank name; trim() uses Unicode whitespace, a tiny difference from Java's isWhitespace
|
||||
ThreadingDetector skip:debug debug check that two threads enter the same structure; Rust's ownership rules rule it out
|
||||
TickThrottler.isUnderThreshold port crates/horizon-client/src/game/lan_actions.rs::Game::lan_guest_packet gap: player: chat and command spam kicks (disconnect.spam) are missing; the throttle only guards creative drops (limit 1480, decays in tick_guests)
|
||||
TickThrottler.isUnderThreshold port crates/horizon-client/src/game/lan_chat.rs::detect_rate_spam a chat message or command adds 20 to the guest's counter, and a guest over the threshold who is not an operator is kicked with disconnect.spam; a threshold of 0 or less never kicks
|
||||
Util.makeDescriptionId inline crates/horizon-client/src/game/commands.rs::Game::stack_display_name gap: minor: each caller builds its own item, block or entity key (creative.rs, brewing.rs, stats_screen.rs, combat.rs); no shared key builder
|
||||
Util.makeExecutor skip:java background ForkJoin pool with thread names
|
||||
Util.getMaxThreads skip:java max.bg.threads pool size; Horizon sizes its own pools
|
||||
|
||||
|
@@ -114,14 +114,14 @@ TrapezoidFloat.sample port crates/horizon-world/src/worldgen/value_provider.rs::
|
||||
TrapezoidInt.sample port crates/horizon-world/src/worldgen/value_provider.rs::IntProvider::sample Trapezoid arm
|
||||
UniformFloat.of port crates/horizon-world/src/worldgen/value_provider.rs::FloatProvider::from_json Uniform arm rejects max <= min
|
||||
VeryBiasedToBottomInt.sample port crates/horizon-world/src/worldgen/value_provider.rs::IntProvider::sample BiasedToBottom arm with the extra draw
|
||||
RegionStorageUpgrader.init inline crates/horizon-world/src/datafix/files.rs::upgrade_regions gap: minor: region folder and storage are set up per call; no progress object
|
||||
RegionStorageUpgrader.upgrade port crates/horizon-world/src/datafix/files.rs::upgrade_regions gap: minor: no progress, cancel or recreate mode
|
||||
RegionStorageUpgrader.createStorage inline crates/horizon-world/src/datafix/files.rs::upgrade_regions gap: minor: in-place storage only; the recreate-region-files option is not ported
|
||||
RegionStorageUpgrader.getFilesToProcess inline crates/horizon-world/src/datafix/files.rs::upgrade_regions lists the .mca files; progress totals are not kept
|
||||
RegionStorageUpgrader.getAllChunkPositions inline crates/horizon-world/src/datafix/files.rs::upgrade_regions walks region files and their chunks
|
||||
RegionStorageUpgrader.processOnePosition inline crates/horizon-world/src/datafix/files.rs::upgrade_regions per-chunk fix through the closure; counts are not kept
|
||||
RegionStorageUpgrader.tryProcessOnePosition inline crates/horizon-world/src/datafix/files.rs::upgrade_regions gap: minor: every chunk is rewritten, not only the changed ones
|
||||
RegionStorageUpgrader.onFileFinished inline crates/horizon-world/src/datafix/files.rs::upgrade_regions rename of the staged file over the old one; recreate mode absent
|
||||
RegionStorageUpgrader.init inline crates/horizon-world/src/datafix/world_upgrader.rs::RegionStorageUpgrader::init the dimension's region folder is resolved and its chunk positions counted into the progress totals
|
||||
RegionStorageUpgrader.upgrade port crates/horizon-world/src/datafix/world_upgrader.rs::RegionStorageUpgrader::upgrade the per-file, per-chunk loop runs until the progress is canceled, counting converted and skipped chunks and setting dimension and total progress
|
||||
RegionStorageUpgrader.createStorage inline crates/horizon-world/src/datafix/world_upgrader.rs::RegionStorageUpgrader::upgrade the reader and writer region files are opened lazily per file; recreate_region_files writes into the new folder
|
||||
RegionStorageUpgrader.getFilesToProcess inline crates/horizon-world/src/datafix/world_upgrader.rs::RegionStorageUpgrader::init all_chunk_positions lists the .mca files and their chunks, and init adds them to the progress totals
|
||||
RegionStorageUpgrader.getAllChunkPositions inline crates/horizon-world/src/datafix/world_upgrader.rs::all_chunk_positions walks each region file's header for the chunk positions it holds
|
||||
RegionStorageUpgrader.processOnePosition inline crates/horizon-world/src/datafix/world_upgrader.rs::RegionStorageUpgrader::process_one_position the per-chunk fix; a failed chunk is logged and counted as skipped
|
||||
RegionStorageUpgrader.tryProcessOnePosition port crates/horizon-world/src/datafix/world_upgrader.rs::RegionStorageUpgrader::try_process_one_position only a chunk whose tag changed is written back, or every chunk with recreate_region_files
|
||||
RegionStorageUpgrader.onFileFinished inline crates/horizon-world/src/datafix/world_upgrader.rs::RegionStorageUpgrader::on_file_finished the rewritten file replaces the old one, and close drops the recreate folder
|
||||
RegionStorageUpgrader.Builder.addTagModifier skip:java builder
|
||||
RegionStorageUpgrader.Builder.copy skip:java builder
|
||||
RegionStorageUpgrader.Builder.build skip:java builder
|
||||
|
||||
|
Reference in new issue
Block a user