mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-09 02:29:50 -10:00
Merge main into #7316
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
a3740ffa49
118 files changed
+2337
-663
No files matched your search
@@ -7011,7 +7011,11 @@ impl Renderer {
|
||||
let additive_quads = (frame.additive.len() / 4) as u32 - self.entity_batch.moved_additive;
|
||||
let see_through_quads = (frame.see_through.len() / 4) as u32;
|
||||
let shadow_only_quads = (frame.shadow_only.len() / 4) as u32;
|
||||
let entity_vertices = u64::from(posed) + all_entities.len() as u64;
|
||||
// Glowing entities' silhouettes posed on the GPU go last, past every
|
||||
// material (the outline pass draws them from there).
|
||||
let outline_base = posed + all_entities.len() as u32;
|
||||
let outlined = skin.map_or(0, |s| s.outline_vertices);
|
||||
let entity_vertices = u64::from(outline_base) + u64::from(outlined);
|
||||
if entity_vertices > self.entity_capacity {
|
||||
self.entity_capacity = entity_vertices.next_power_of_two();
|
||||
self.entity_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
@@ -7037,6 +7041,7 @@ impl Renderer {
|
||||
skin,
|
||||
&self.entity_buffer,
|
||||
0,
|
||||
outline_base,
|
||||
);
|
||||
}
|
||||
let static_sections = frame.static_block_entities;
|
||||
@@ -7163,6 +7168,7 @@ impl Renderer {
|
||||
&mut self.uploads,
|
||||
&mut encoder,
|
||||
frame.glowing,
|
||||
(outline_base, outlined / 4),
|
||||
[self.config.width, self.config.height],
|
||||
);
|
||||
if let Some((rotation, brightness)) = frame.stars {
|
||||
@@ -7194,7 +7200,8 @@ impl Renderer {
|
||||
lightning::QUADS
|
||||
})
|
||||
.max(weather_quads)
|
||||
.max(frame.glowing.len() as u32 / 4);
|
||||
.max(frame.glowing.len() as u32 / 4)
|
||||
.max(outlined / 4);
|
||||
if max_quads > self.index_quads {
|
||||
self.index_quads = max_quads.next_power_of_two();
|
||||
self.indices = Self::quad_indices(&self.device, self.index_quads);
|
||||
@@ -9083,6 +9090,7 @@ impl Renderer {
|
||||
view_target,
|
||||
&self.entity_group,
|
||||
&self.indices,
|
||||
&self.entity_buffer,
|
||||
timing.as_mut(),
|
||||
);
|
||||
// The world icon grabs here: after the level and the hand, before
|
||||
|
||||
@@ -29,6 +29,8 @@ pub(super) struct EntityOutline {
|
||||
vertices: wgpu::Buffer,
|
||||
capacity: u64,
|
||||
quads: u32,
|
||||
/// The silhouettes posed on the GPU: first vertex in the entity buffer, quads.
|
||||
posed: (u32, u32),
|
||||
targets: Option<Targets>,
|
||||
}
|
||||
|
||||
@@ -159,6 +161,7 @@ impl EntityOutline {
|
||||
vertices: Self::vertex_buffer(device, 256),
|
||||
capacity: 256,
|
||||
quads: 0,
|
||||
posed: (0, 0),
|
||||
targets: None,
|
||||
}
|
||||
}
|
||||
@@ -175,16 +178,20 @@ impl EntityOutline {
|
||||
/// Uploads this frame's silhouettes (`LevelRenderer.currentFrameRendersEntityOutline`
|
||||
/// when there are any) and sizes the targets like the window. A frame with
|
||||
/// none gives the targets' video memory back until an entity glows again.
|
||||
/// `posed`: where the silhouettes posed on the GPU start in the entity
|
||||
/// buffer (`entity_skin`'s outline jobs), and how many quads they are.
|
||||
pub(super) fn prepare(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
uploads: &mut super::upload::Uploads,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
silhouettes: &[Vertex],
|
||||
posed: (u32, u32),
|
||||
size: [u32; 2],
|
||||
) {
|
||||
self.quads = (silhouettes.len() / 4) as u32;
|
||||
if self.quads == 0 {
|
||||
self.posed = posed;
|
||||
if self.quads == 0 && posed.1 == 0 {
|
||||
self.targets = None;
|
||||
return;
|
||||
}
|
||||
@@ -193,12 +200,14 @@ impl EntityOutline {
|
||||
self.capacity = vertices.next_power_of_two();
|
||||
self.vertices = Self::vertex_buffer(device, self.capacity);
|
||||
}
|
||||
uploads.buffer(
|
||||
encoder,
|
||||
&self.vertices,
|
||||
0,
|
||||
bytemuck::cast_slice(silhouettes),
|
||||
);
|
||||
if !silhouettes.is_empty() {
|
||||
uploads.buffer(
|
||||
encoder,
|
||||
&self.vertices,
|
||||
0,
|
||||
bytemuck::cast_slice(silhouettes),
|
||||
);
|
||||
}
|
||||
if self.targets.as_ref().is_some_and(|t| t.size == size) {
|
||||
return;
|
||||
}
|
||||
@@ -260,9 +269,14 @@ impl EntityOutline {
|
||||
frame: &wgpu::TextureView,
|
||||
entity_group: &wgpu::BindGroup,
|
||||
indices: &wgpu::Buffer,
|
||||
entities: &wgpu::Buffer,
|
||||
mut timing: Option<&mut super::pack_timing::PassTiming>,
|
||||
) {
|
||||
let Some(t) = self.targets.as_ref().filter(|_| self.quads > 0) else {
|
||||
let Some(t) = self
|
||||
.targets
|
||||
.as_ref()
|
||||
.filter(|_| self.quads > 0 || self.posed.1 > 0)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let clear = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
|
||||
@@ -277,8 +291,15 @@ impl EntityOutline {
|
||||
pass.set_pipeline(&self.silhouette);
|
||||
pass.set_bind_group(0, entity_group, &[]);
|
||||
pass.set_index_buffer(indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
pass.set_vertex_buffer(0, self.vertices.slice(..));
|
||||
pass.draw_indexed(0..self.quads * 6, 0, 0..1);
|
||||
if self.quads > 0 {
|
||||
pass.set_vertex_buffer(0, self.vertices.slice(..));
|
||||
pass.draw_indexed(0..self.quads * 6, 0, 0..1);
|
||||
}
|
||||
let (first, quads) = self.posed;
|
||||
if quads > 0 {
|
||||
pass.set_vertex_buffer(0, entities.slice(..));
|
||||
pass.draw_indexed(0..quads * 6, first as i32, 0..1);
|
||||
}
|
||||
}
|
||||
// post_effect/entity_outline.json; its last pass only copies swap back to the
|
||||
// outline target, so the blit reads swap.
|
||||
|
||||
@@ -107,6 +107,11 @@ pub struct SkinFrame {
|
||||
pub jobs: Vec<SkinJob>,
|
||||
/// Vertices the jobs write: a whole number of quads.
|
||||
pub vertices: u32,
|
||||
/// Glowing entities' silhouettes posed here (`OutlineBufferSource`'s copy of
|
||||
/// their draws, [`SkinFrame::outline_since`]): `dst` counts from the first.
|
||||
pub outline_jobs: Vec<SkinJob>,
|
||||
/// Vertices the outline jobs write.
|
||||
pub outline_vertices: u32,
|
||||
}
|
||||
|
||||
impl SkinFrame {
|
||||
@@ -115,6 +120,24 @@ impl SkinFrame {
|
||||
self.parts.clear();
|
||||
self.jobs.clear();
|
||||
self.vertices = 0;
|
||||
self.outline_jobs.clear();
|
||||
self.outline_vertices = 0;
|
||||
}
|
||||
|
||||
/// Copies the draws since job `first` into the glowing entities' outline
|
||||
/// (`Frame::glowing`'s, on the GPU), as `glowing.rs`'s `mark_outline` copies
|
||||
/// vertices: the same ones, in `color` (`Vertex::color`) with no normal.
|
||||
pub fn outline_since(&mut self, first: usize, color: u32) {
|
||||
for i in first..self.jobs.len() {
|
||||
let job = self.jobs[i];
|
||||
self.outline_jobs.push(SkinJob {
|
||||
dst: self.outline_vertices,
|
||||
color,
|
||||
mode: SkinJob::FLAT,
|
||||
..job
|
||||
});
|
||||
self.outline_vertices += job.count;
|
||||
}
|
||||
}
|
||||
|
||||
/// The renderer took the new models.
|
||||
@@ -138,7 +161,10 @@ impl SkinFrame {
|
||||
|
||||
/// Whether the renderer has anything to take.
|
||||
pub fn pending(&self) -> bool {
|
||||
self.reset || !self.new_model.is_empty() || !self.jobs.is_empty()
|
||||
self.reset
|
||||
|| !self.new_model.is_empty()
|
||||
|| !self.jobs.is_empty()
|
||||
|| !self.outline_jobs.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +319,8 @@ impl EntitySkin {
|
||||
}
|
||||
|
||||
/// Records the frame's jobs: they write `frame.vertices` vertices into
|
||||
/// `out` from vertex `base` on. Call after the CPU vertices' upload.
|
||||
/// `out` from vertex `base` on, and the outline jobs `frame.outline_vertices`
|
||||
/// from vertex `outline_base` on. Call after the CPU vertices' upload.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn dispatch(
|
||||
&mut self,
|
||||
@@ -303,26 +330,32 @@ impl EntitySkin {
|
||||
frame: &SkinFrame,
|
||||
out: &wgpu::Buffer,
|
||||
base: u32,
|
||||
outline_base: u32,
|
||||
) {
|
||||
if frame.jobs.is_empty() {
|
||||
if frame.jobs.is_empty() && frame.outline_jobs.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Some(model) = &self.model_buffer else {
|
||||
return;
|
||||
};
|
||||
// Whole rows of workgroups past `ROW` jobs: the padding jobs write nothing.
|
||||
let count = frame.jobs.len() as u32;
|
||||
let count = (frame.jobs.len() + frame.outline_jobs.len()) as u32;
|
||||
let rows = count.div_ceil(ROW);
|
||||
let padded = if rows > 1 {
|
||||
(rows * ROW) as usize
|
||||
} else {
|
||||
frame.jobs.len()
|
||||
count as usize
|
||||
};
|
||||
self.staged.clear();
|
||||
self.staged.extend(frame.jobs.iter().map(|job| SkinJob {
|
||||
dst: job.dst + base,
|
||||
..*job
|
||||
}));
|
||||
let moved = |at: u32| {
|
||||
move |job: &SkinJob| SkinJob {
|
||||
dst: job.dst + at,
|
||||
..*job
|
||||
}
|
||||
};
|
||||
self.staged.extend(frame.jobs.iter().map(moved(base)));
|
||||
self.staged
|
||||
.extend(frame.outline_jobs.iter().map(moved(outline_base)));
|
||||
self.staged.resize(padded, SkinJob::default());
|
||||
self.parts.ensure(
|
||||
device,
|
||||
@@ -369,8 +402,22 @@ impl EntitySkin {
|
||||
/// the same way: the reference its tests hold the shader and the game's walk
|
||||
/// (`mobs::skin`) to. `model` is the whole store.
|
||||
pub fn expand(model: &[SkinVertex], frame: &SkinFrame) -> Vec<Vertex> {
|
||||
let mut out = vec![bytemuck::Zeroable::zeroed(); frame.vertices as usize];
|
||||
for job in &frame.jobs {
|
||||
expand_jobs(model, frame, &frame.jobs, frame.vertices)
|
||||
}
|
||||
|
||||
/// [`expand`] of the frame's outline jobs: the silhouettes they write.
|
||||
pub fn expand_outline(model: &[SkinVertex], frame: &SkinFrame) -> Vec<Vertex> {
|
||||
expand_jobs(model, frame, &frame.outline_jobs, frame.outline_vertices)
|
||||
}
|
||||
|
||||
fn expand_jobs(
|
||||
model: &[SkinVertex],
|
||||
frame: &SkinFrame,
|
||||
jobs: &[SkinJob],
|
||||
vertices: u32,
|
||||
) -> Vec<Vertex> {
|
||||
let mut out = vec![bytemuck::Zeroable::zeroed(); vertices as usize];
|
||||
for job in jobs {
|
||||
for k in 0..job.count {
|
||||
let v = model[(job.first + k) as usize];
|
||||
let part = &frame.parts[(job.part + v.part) as usize];
|
||||
@@ -557,13 +604,30 @@ mod tests {
|
||||
},
|
||||
],
|
||||
vertices: 120,
|
||||
outline_jobs: Vec::new(),
|
||||
outline_vertices: 0,
|
||||
};
|
||||
// A glowing entity's silhouette: the item job and the first job again,
|
||||
// in its team colour with no normal.
|
||||
let mut frame = frame;
|
||||
frame.jobs.push(frame.jobs[2]);
|
||||
frame.jobs.last_mut().expect("pushed").dst = 120;
|
||||
frame.jobs.push(SkinJob {
|
||||
dst: 144,
|
||||
..frame.jobs[0]
|
||||
});
|
||||
frame.vertices = 192;
|
||||
frame.outline_since(4, 0xff20_c0e0);
|
||||
assert_eq!(frame.outline_vertices, 72);
|
||||
let expected = expand(&model, &frame);
|
||||
// The posed vertices land after 5 others, which they leave alone.
|
||||
let outline = expand_outline(&model, &frame);
|
||||
// The posed vertices land after 5 others, which they leave alone, and the
|
||||
// silhouettes 3 past them.
|
||||
let before = 5u32;
|
||||
let outline_base = before + frame.vertices + 3;
|
||||
let out = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: None,
|
||||
size: u64::from(before + frame.vertices) * 44,
|
||||
size: u64::from(outline_base + frame.outline_vertices) * 44,
|
||||
usage: EntitySkin::usage(true) | wgpu::BufferUsages::COPY_SRC,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
@@ -572,7 +636,15 @@ mod tests {
|
||||
let mut uploads = Uploads::new(&device);
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
assert!(skin.register(&device, &mut uploads, &mut encoder, &frame));
|
||||
skin.dispatch(&device, &mut uploads, &mut encoder, &frame, &out, before);
|
||||
skin.dispatch(
|
||||
&device,
|
||||
&mut uploads,
|
||||
&mut encoder,
|
||||
&frame,
|
||||
&out,
|
||||
before,
|
||||
outline_base,
|
||||
);
|
||||
let read = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: None,
|
||||
size: out.size(),
|
||||
@@ -595,5 +667,24 @@ mod tests {
|
||||
"vertex {i}: {got:?} != {want:?}"
|
||||
);
|
||||
}
|
||||
let silhouettes = &got[(outline_base - before) as usize..];
|
||||
assert_eq!(silhouettes.len(), outline.len());
|
||||
for (i, (got, want)) in silhouettes.iter().zip(&outline).enumerate() {
|
||||
assert_eq!(
|
||||
bytemuck::bytes_of(got),
|
||||
bytemuck::bytes_of(want),
|
||||
"silhouette {i}: {got:?} != {want:?}"
|
||||
);
|
||||
}
|
||||
// A silhouette is its draw's vertex with the colour and no normal
|
||||
// (`mark_outline`).
|
||||
for (copy, of) in outline.iter().zip(&expected[120..]) {
|
||||
let marked = Vertex {
|
||||
color: 0xff20_c0e0,
|
||||
normal: [0; 4],
|
||||
..*of
|
||||
};
|
||||
assert_eq!(bytemuck::bytes_of(copy), bytemuck::bytes_of(&marked));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ fn cpu_if_enabled() -> Option<Duration> {
|
||||
/// Run `f`, recording its duration under `name` when spans are enabled.
|
||||
#[inline]
|
||||
pub fn time<T>(name: &'static str, f: impl FnOnce() -> T) -> T {
|
||||
let _heap = horizon_threads::heap_tag(name);
|
||||
if !recording_here() {
|
||||
return f();
|
||||
}
|
||||
@@ -291,9 +292,15 @@ pub mod startup {
|
||||
pub struct Step {
|
||||
name: &'static str,
|
||||
started: Instant,
|
||||
traced: bool,
|
||||
/// A heap-census build's tag for the step's allocations.
|
||||
_heap: Option<horizon_threads::heap_census::Tag>,
|
||||
}
|
||||
impl Drop for Step {
|
||||
fn drop(&mut self) {
|
||||
if !self.traced {
|
||||
return;
|
||||
}
|
||||
let end = Instant::now();
|
||||
write(format_args!(
|
||||
r#""ph":"X","name":"{}","ts":{},"dur":{}"#,
|
||||
@@ -307,9 +314,12 @@ pub mod startup {
|
||||
/// Starts a step named `name` (a literal: no quotes or backslashes).
|
||||
#[inline]
|
||||
pub fn step(name: &'static str) -> Option<Step> {
|
||||
on().then(|| Step {
|
||||
let traced = on();
|
||||
(traced || horizon_threads::HEAP_CENSUS).then(|| Step {
|
||||
name,
|
||||
started: Instant::now(),
|
||||
traced,
|
||||
_heap: horizon_threads::heap_tag(name),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -774,12 +774,23 @@ impl<G: ChunkGenerator> Source for Jobs<G> {
|
||||
};
|
||||
(kind, crate::spans::thread_cpu().unwrap_or_default())
|
||||
});
|
||||
let _tag = horizon_threads::heap_tag(match &job.job {
|
||||
Job::Generate(..) => "stream.generate",
|
||||
Job::Decorate(..) => "stream.decorate",
|
||||
Job::Finish(..) => "stream.finish",
|
||||
Job::Light(..) => "stream.light",
|
||||
Job::Mesh(..) => "stream.mesh",
|
||||
_ => "stream.other",
|
||||
});
|
||||
let done = self.locals.with(
|
||||
worker,
|
||||
|| WorkerLocal {
|
||||
worker: self.generator.worker(),
|
||||
faces: None,
|
||||
mesh_scratch: MeshScratch::default(),
|
||||
|| {
|
||||
let _tag = horizon_threads::heap_tag("stream.worker_state");
|
||||
WorkerLocal {
|
||||
worker: self.generator.worker(),
|
||||
faces: None,
|
||||
mesh_scratch: MeshScratch::default(),
|
||||
}
|
||||
},
|
||||
|local| self.run_job(job.job, local),
|
||||
);
|
||||
|
||||
@@ -117,6 +117,9 @@ rng-trace = ["horizon-core/rng-trace", "horizon-client-lib/rng-trace"]
|
||||
# Rust's allocations go to the system heap instead of mimalloc, where heap profilers that replace
|
||||
# malloc (heaptrack, valgrind's massif) see them. For profiling only: mimalloc is faster.
|
||||
system-allocator = []
|
||||
# mimalloc with a tag on each block: `horizon-ctl memory` lists the live heap by startup step and
|
||||
# thread (`heap_by_tag_mb`). For measuring only: each block costs 16 bytes more.
|
||||
alloc-census = ["horizon-threads/alloc-census"]
|
||||
# Compatibility with earlier experiment commands: main already uses mimalloc.
|
||||
windows-mimalloc = []
|
||||
# Opt-in Windows heap experiment; see docs/windows-allocators.md.
|
||||
|
||||
@@ -753,6 +753,9 @@ fn print(out: &mut String, t: i32, i: usize, m: &Mob, player_down: bool, now: i6
|
||||
if let Some((acts, mem)) = brain {
|
||||
let _ = write!(out, "\tact {acts}\tmem {mem}");
|
||||
}
|
||||
if horizon_client::mobs::finalize::ageable(m.species()) {
|
||||
let _ = write!(out, "\tage {}", m.extra.age);
|
||||
}
|
||||
if m.species() == Species::Villager {
|
||||
let _ = write!(out, "\tvx {}", villager(m));
|
||||
}
|
||||
@@ -1026,6 +1029,7 @@ pub fn main() -> horizon_client::Result<()> {
|
||||
m.rider = Some(horizon_client::mobs::equine::Rider::default());
|
||||
}
|
||||
"noai" => m.no_ai = true,
|
||||
"agelocked" => m.extra.age_locked = true,
|
||||
// Warden.finalizeSpawn's own part.
|
||||
"digcooldown" | "emerge" => horizon_client::mobs::warden::finalize(
|
||||
&mut m,
|
||||
|
||||
@@ -170,6 +170,8 @@ pub struct ArrowState {
|
||||
/// The block it stuck in (`lastState`).
|
||||
pub last_state: Option<BlockStateId>,
|
||||
pub pickup: Pickup,
|
||||
/// Its entity type is `SpectralArrow` (the type is its own, not the pickup item's).
|
||||
pub spectral: bool,
|
||||
/// `pickupItemStack`: what picking the arrow up gives.
|
||||
pub pickup_item: crate::items::ItemStack,
|
||||
/// `AbstractArrow.firedFromWeapon`: the crossbow or bow it was shot from.
|
||||
@@ -227,6 +229,7 @@ impl ArrowState {
|
||||
shake: 0,
|
||||
last_state: None,
|
||||
pickup: Pickup::Disallowed,
|
||||
spectral: false,
|
||||
pickup_item: crate::items::ItemStack::new("minecraft:arrow", 1),
|
||||
fired_from_weapon: None,
|
||||
no_physics: false,
|
||||
|
||||
@@ -221,7 +221,7 @@ pub const COMMANDS: &[Cmd] = &[
|
||||
"Replies protocol (MAJOR.MINOR.PATCH of the control protocol, docs/api/stability.md), release (the game's version, the string MCP serverInfo carries too) and features (the command groups the game answers, so a script can test for one before calling it).",
|
||||
["horizon-ctl hello"], &[], mcp: true, local: false),
|
||||
cmd!("memory", Session, "memory", "what holds the game's memory: resident set, allocator, client and server columns by part, renderer, GPU memory by resource",
|
||||
"Columns count blocks, biomes, light, heightmaps and records, each shared section once; server_columns are the single-player server thread's own levels. GPU memory by label needs Vulkan or DirectX 12. What the census doesn't reach is the resident set less its parts.", ["horizon-ctl memory"], &[], mcp: true, local: false),
|
||||
"Columns count blocks, biomes, light, heightmaps and records, each shared section once; server_columns are the single-player server thread's own levels. GPU memory by label needs Vulkan or DirectX 12. What the census doesn't reach is the resident set less its parts. A build with the alloc-census feature adds heap_by_tag_mb: the live heap by startup step, timed span, chunk job and thread.", ["horizon-ctl memory"], &[], mcp: true, local: false),
|
||||
cmd!("worlds", Session, "worlds", "the saved worlds, newest first: folder, name, mode, last played, version",
|
||||
"folder is what open, world-rename, world-backup and world-delete take.", ["horizon-ctl worlds"], &[], mcp: true, local: false),
|
||||
cmd!("world-rename", Session, "world-rename FOLDER NAME", "rename a world, as Edit World does (its folder stays)",
|
||||
|
||||
@@ -1483,6 +1483,7 @@ impl ArmorStand {
|
||||
facts: &BlockFacts,
|
||||
in_water: bool,
|
||||
push: &mut dyn FnMut(Aabb, &mut Vec3),
|
||||
border: Option<&BorderGeometry>,
|
||||
) {
|
||||
self.previous = self.position;
|
||||
if self.marker {
|
||||
@@ -1519,7 +1520,20 @@ impl ArmorStand {
|
||||
1.0
|
||||
};
|
||||
let mut velocity = wanted;
|
||||
if let Ok(moved) = scene.resolve(self.bounds(), wanted, 0.0, self.on_ground) {
|
||||
// Entity.collide: the world border's walls stop the stand too.
|
||||
let moved = SceneCollisions {
|
||||
world: scene.world,
|
||||
binding: scene.binding,
|
||||
subject: scene.subject,
|
||||
}
|
||||
.resolve_in_border(
|
||||
self.bounds(),
|
||||
wanted,
|
||||
0.0,
|
||||
self.on_ground,
|
||||
border.map(|b| (b, [self.position.x, self.position.z])),
|
||||
);
|
||||
if let Ok(moved) = moved {
|
||||
self.position = Vec3::new(
|
||||
self.position.x + moved.x,
|
||||
self.position.y + moved.y,
|
||||
@@ -2699,7 +2713,53 @@ mod armor_stand_motion_tests {
|
||||
binding: collision,
|
||||
subject: None,
|
||||
};
|
||||
stand.tick(&mut scene, facts, false, &mut |_, _| {});
|
||||
stand.tick(&mut scene, facts, false, &mut |_, _| {}, None);
|
||||
}
|
||||
|
||||
/// `Entity.collectColliders` (#7080): the world border's wall stops an
|
||||
/// armor stand as it stops the player. A border of size 10 around
|
||||
/// (20, 20) has its east wall at x = 25; the stand starts at x = 24 on the
|
||||
/// test scene's flat ground, moving 1.5 east.
|
||||
#[test]
|
||||
fn the_world_border_stops_an_armor_stand() {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../.local/horizon-client")
|
||||
.join(horizon_core::VANILLA_VERSION);
|
||||
if !root.join("movement-properties.nbt").exists() {
|
||||
return;
|
||||
}
|
||||
let assets = crate::assets::Assets::load(&root).unwrap();
|
||||
let render = assets.reload(&root).unwrap();
|
||||
let facts = BlockFacts::load(&root, assets.registry).unwrap();
|
||||
let collision = crate::collisions::load(&root, assets.registry).unwrap();
|
||||
let world = World::test_scene(&assets, &render, 3).unwrap();
|
||||
let border = BorderGeometry::new([20.0, 20.0], 10.0, 29_999_984).unwrap();
|
||||
for bordered in [false, true] {
|
||||
let mut stand = ArmorStand::new(1, Vec3::new(24.0, 4.0, 20.5), 0.0);
|
||||
stand.on_ground = true;
|
||||
stand.velocity = Vec3::new(1.5, 0.0, 0.0);
|
||||
let mut scene = SceneCollisions {
|
||||
world: &world,
|
||||
binding: &collision,
|
||||
subject: None,
|
||||
};
|
||||
stand.tick(
|
||||
&mut scene,
|
||||
&facts,
|
||||
false,
|
||||
&mut |_, _| {},
|
||||
bordered.then_some(&border),
|
||||
);
|
||||
let east = stand.bounds().max_position().x;
|
||||
if bordered {
|
||||
assert!(
|
||||
(east - 25.0).abs() < 1.0e-7,
|
||||
"stops on the wall, got {east}"
|
||||
);
|
||||
} else {
|
||||
assert!(east > 25.0, "no border: goes on, got {east}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -380,7 +380,7 @@ mod tests {
|
||||
binding: &collision,
|
||||
subject: None,
|
||||
};
|
||||
still.tick(&mut scene, &facts, in_water, &mut |_, _| {});
|
||||
still.tick(&mut scene, &facts, in_water, &mut |_, _| {}, None);
|
||||
assert_eq!(still.position.x, at.x);
|
||||
assert_eq!(still.velocity.x, 0.0);
|
||||
|
||||
@@ -394,7 +394,7 @@ mod tests {
|
||||
binding: &collision,
|
||||
subject: None,
|
||||
};
|
||||
marker.tick(&mut scene, &facts, in_water, &mut |_, _| {});
|
||||
marker.tick(&mut scene, &facts, in_water, &mut |_, _| {}, None);
|
||||
assert_eq!(marker.position, at);
|
||||
assert_eq!(marker.velocity, Vec3::ZERO);
|
||||
|
||||
@@ -406,7 +406,7 @@ mod tests {
|
||||
binding: &collision,
|
||||
subject: None,
|
||||
};
|
||||
carried.tick(&mut scene, &facts, in_water, &mut |_, _| {});
|
||||
carried.tick(&mut scene, &facts, in_water, &mut |_, _| {}, None);
|
||||
assert!((carried.position.x - (at.x + 0.014)).abs() < 1e-12);
|
||||
assert!((carried.velocity.x - 0.014 * f64::from(0.8f32)).abs() < 1e-12);
|
||||
for _ in 0..20 {
|
||||
@@ -416,7 +416,7 @@ mod tests {
|
||||
binding: &collision,
|
||||
subject: None,
|
||||
};
|
||||
carried.tick(&mut scene, &facts, in_water, &mut |_, _| {});
|
||||
carried.tick(&mut scene, &facts, in_water, &mut |_, _| {}, None);
|
||||
}
|
||||
assert!(carried.position.x > at.x + 0.5);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ fn string(s: &str) -> Tag {
|
||||
}
|
||||
/// Entity type ID (`id`).
|
||||
pub fn kind(tag: &Tag) -> Option<String> {
|
||||
text(tag.get("id"))
|
||||
horizon_client_core::identifier::normalize_id(&text(tag.get("id"))?)
|
||||
}
|
||||
pub fn position(tag: &Tag) -> Option<Vec3> {
|
||||
read_doubles::<3>(tag.get("Pos")).map(|p| Vec3::new(p[0], p[1], p[2]))
|
||||
@@ -2238,7 +2238,12 @@ pub fn arrow_to_nbt(
|
||||
return None;
|
||||
};
|
||||
let base = Base {
|
||||
id: "minecraft:arrow",
|
||||
// A spectral arrow is its own entity type.
|
||||
id: if a.spectral || &*a.pickup_item.item == "minecraft:spectral_arrow" {
|
||||
"minecraft:spectral_arrow"
|
||||
} else {
|
||||
"minecraft:arrow"
|
||||
},
|
||||
uuid,
|
||||
position: p.position,
|
||||
motion: p.velocity,
|
||||
@@ -2298,10 +2303,17 @@ pub fn arrow_from_nbt(
|
||||
let position = position(tag)?;
|
||||
let owner = uuid_from_tag(tag.get("Owner"));
|
||||
let from_player = player.is_some() && owner == player;
|
||||
// `getDefaultPickupItem`: a `SpectralArrow` is picked up as its own item.
|
||||
let spectral = kind(tag).as_deref() == Some("minecraft:spectral_arrow");
|
||||
let default_item = if spectral {
|
||||
"minecraft:spectral_arrow"
|
||||
} else {
|
||||
"minecraft:arrow"
|
||||
};
|
||||
let item = tag
|
||||
.get("item")
|
||||
.and_then(item_from_nbt)
|
||||
.unwrap_or_else(|| crate::items::ItemStack::new("minecraft:arrow", 1));
|
||||
.unwrap_or_else(|| crate::items::ItemStack::new(default_item, 1));
|
||||
let kind = ProjectileKind::Arrow {
|
||||
base_damage: float(tag.get("damage")).unwrap_or(2.0),
|
||||
critical: flag(tag, "crit"),
|
||||
@@ -2322,6 +2334,7 @@ pub fn arrow_from_nbt(
|
||||
a.has_been_shot = flag(tag, "HasBeenShot");
|
||||
a.fire_ticks = number(tag.get("Fire")).unwrap_or(0) as i16 as i32;
|
||||
a.fall_distance = float(tag.get("fall_distance")).unwrap_or(0.0);
|
||||
a.spectral = spectral;
|
||||
a.pickup_item = item;
|
||||
a.fired_from_weapon = tag.get("weapon").and_then(item_from_nbt);
|
||||
p.stuck = a.in_ground;
|
||||
@@ -2403,12 +2416,13 @@ fn put_projectile(tag: &mut Tag, p: &Projectile, owner: Option<[i32; 4]>) {
|
||||
|
||||
/// The thrown item projectiles Horizon saves: each entity type's id is also
|
||||
/// the item it saves when none was loaded (`getDefaultItem`).
|
||||
const THROWABLE_ITEMS: [&str; 5] = [
|
||||
const THROWABLE_ITEMS: [&str; 6] = [
|
||||
"minecraft:snowball",
|
||||
"minecraft:egg",
|
||||
"minecraft:ender_pearl",
|
||||
"minecraft:splash_potion",
|
||||
"minecraft:lingering_potion",
|
||||
"minecraft:experience_bottle",
|
||||
];
|
||||
/// The item a throwable flies as: its stack (a potion's), the mod type's
|
||||
/// item, or the item its entity type is named after.
|
||||
@@ -2432,6 +2446,7 @@ pub fn throwable_kind(id: &str) -> Option<ProjectileKind> {
|
||||
"minecraft:egg" => Some(ProjectileKind::Egg),
|
||||
"minecraft:ender_pearl" => Some(ProjectileKind::EnderPearl),
|
||||
// The entity type is the id; the `Item` read later may be another potion.
|
||||
"minecraft:experience_bottle" => Some(ProjectileKind::ExperienceBottle),
|
||||
_ => Some(ProjectileKind::Potion(
|
||||
Box::new(crate::items::ItemStack::new(id, 1)),
|
||||
id == "minecraft:lingering_potion",
|
||||
@@ -2454,12 +2469,17 @@ pub fn saved_with_chunk(p: &Projectile) -> bool {
|
||||
| ProjectileKind::Egg
|
||||
| ProjectileKind::EnderPearl
|
||||
| ProjectileKind::Potion(..)
|
||||
| ProjectileKind::ExperienceBottle
|
||||
| ProjectileKind::LlamaSpit
|
||||
| ProjectileKind::ShulkerBullet { target: None, .. }
|
||||
| ProjectileKind::Thrown(_) => true,
|
||||
ProjectileKind::Arrow { .. } => p.arrow.is_some(),
|
||||
// Not written yet: tridents save from their own list.
|
||||
// Not written yet: tridents save from their own list, and a bullet
|
||||
// with a target would lose it.
|
||||
ProjectileKind::Trident
|
||||
| ProjectileKind::LlamaSpit
|
||||
| ProjectileKind::ShulkerBullet { .. }
|
||||
| ProjectileKind::ShulkerBullet {
|
||||
target: Some(_), ..
|
||||
}
|
||||
| ProjectileKind::WitherSkull { .. } => false,
|
||||
}
|
||||
}
|
||||
@@ -2522,6 +2542,59 @@ pub fn throwable_from_nbt(tag: &Tag, player: Option<[i32; 4]>) -> Option<Project
|
||||
Some(p)
|
||||
}
|
||||
|
||||
/// `LlamaSpit` and `ShulkerBullet` as `/summon` or a chunk loads them:
|
||||
/// `Projectile`'s `Owner`, `LeftOwner`, `NoGravity` and the motion. A bullet
|
||||
/// loaded without a `Target` has none, so it falls (`ShulkerBullet.tick`).
|
||||
pub fn spit_or_bullet_from_nbt(tag: &Tag, player: Option<[i32; 4]>) -> Option<Projectile> {
|
||||
let kind = match kind(tag)?.as_str() {
|
||||
"minecraft:llama_spit" => ProjectileKind::LlamaSpit,
|
||||
"minecraft:shulker_bullet" => ProjectileKind::ShulkerBullet {
|
||||
target: None,
|
||||
dir: None,
|
||||
steps: 0,
|
||||
delta: [0.0; 3],
|
||||
seed: 0,
|
||||
},
|
||||
_ => return None,
|
||||
};
|
||||
let owner = owner_uuid(tag);
|
||||
let from_player = player.is_some() && owner == player;
|
||||
let mut p = Projectile::new(kind, position(tag)?, from_player.then_some(Who::Player));
|
||||
p.velocity = motion(tag);
|
||||
[p.yaw, p.pitch] = rotation(tag);
|
||||
p.left_owner = flag(tag, "LeftOwner");
|
||||
p.owner_uuid = owner;
|
||||
p.no_gravity = flag(tag, "NoGravity");
|
||||
Some(p)
|
||||
}
|
||||
|
||||
/// The save of [`spit_or_bullet_from_nbt`]'s kinds: `Projectile`'s keys.
|
||||
pub fn spit_or_bullet_to_nbt(
|
||||
p: &Projectile,
|
||||
uuid: [i32; 4],
|
||||
owner: Option<[i32; 4]>,
|
||||
raw: Option<&Tag>,
|
||||
) -> Option<Tag> {
|
||||
if !matches!(
|
||||
p.kind,
|
||||
ProjectileKind::LlamaSpit | ProjectileKind::ShulkerBullet { target: None, .. }
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
let base = Base {
|
||||
id: p.kind.id(),
|
||||
uuid,
|
||||
position: p.position,
|
||||
motion: p.velocity,
|
||||
rotation: [p.yaw, p.pitch],
|
||||
on_ground: false,
|
||||
fall_distance: 0.0,
|
||||
};
|
||||
let mut tag = base.tag(raw);
|
||||
put_projectile(&mut tag, p, owner);
|
||||
Some(tag)
|
||||
}
|
||||
|
||||
/// `Projectile`'s `Owner`.
|
||||
pub fn owner_uuid(tag: &Tag) -> Option<[i32; 4]> {
|
||||
uuid_from_tag(tag.get("Owner"))
|
||||
@@ -3311,6 +3384,31 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::items::ItemStack;
|
||||
|
||||
#[test]
|
||||
fn mob_loading_accepts_default_namespace_and_preserves_custom_namespaces() {
|
||||
let cow = crate::mobs::sensor_scan_tests::kind();
|
||||
let types = [
|
||||
("minecraft:cow".into(), cow.clone()),
|
||||
("test:cow".into(), cow),
|
||||
]
|
||||
.into();
|
||||
let pos = || {
|
||||
Tag::List(
|
||||
[4.5_f64, 220.0, 6.5]
|
||||
.map(|v| Tag::Double(v.to_bits()))
|
||||
.to_vec(),
|
||||
)
|
||||
};
|
||||
for id in ["cow", ":cow", "minecraft:cow", "test:cow"] {
|
||||
let tag = Tag::compound([("id", string(id)), ("Pos", pos()), ("NoAI", Tag::Byte(1))]);
|
||||
let mob = mob_from_nbt(&tag, &types, 7, None).expect(id);
|
||||
assert_eq!(mob.position, Vec3::new(4.5, 220.0, 6.5));
|
||||
assert!(mob.no_ai);
|
||||
}
|
||||
let unknown = Tag::compound([("id", string("other:cow")), ("Pos", pos())]);
|
||||
assert!(mob_from_nbt(&unknown, &types, 8, None).is_none());
|
||||
}
|
||||
|
||||
/// A saved rocket and trident come back with their own Life, LifeTime,
|
||||
/// flags and position (the keys vanilla's addAdditionalSaveData writes).
|
||||
#[test]
|
||||
@@ -5287,6 +5385,72 @@ mod mod_thrown_nbt_tests {
|
||||
// a type no mod declares now is not a thrown kind.
|
||||
assert!(throwable_kind("gone:bolt").is_none());
|
||||
}
|
||||
fn summoned(id: &str, extra: &[(&str, Tag)]) -> Tag {
|
||||
let mut c = vec![
|
||||
("id", Tag::String(id.into())),
|
||||
("Pos", doubles(&[0.5, 110.0, 0.5])),
|
||||
("Motion", doubles(&[0.0, 0.0, 0.0])),
|
||||
];
|
||||
c.extend(extra.iter().cloned());
|
||||
Tag::compound(c)
|
||||
}
|
||||
|
||||
/// Four summoned kinds had no loader and stayed inert (a spectral arrow,
|
||||
/// a shulker bullet, a llama's spit and an experience bottle never fell).
|
||||
#[test]
|
||||
fn summoned_projectiles_of_every_kind_load_and_honour_no_gravity() {
|
||||
use crate::entity_rules::EntityRules;
|
||||
let flag = ("NoGravity", Tag::Byte(1));
|
||||
let arrow = arrow_from_nbt(&summoned("minecraft:spectral_arrow", &[]), None, |_| None)
|
||||
.unwrap()
|
||||
.0;
|
||||
assert!(arrow.is_spectral_arrow() && !arrow.no_gravity);
|
||||
let held = arrow_from_nbt(
|
||||
&summoned("minecraft:spectral_arrow", std::slice::from_ref(&flag)),
|
||||
None,
|
||||
|_| None,
|
||||
)
|
||||
.unwrap()
|
||||
.0;
|
||||
assert!(held.no_gravity);
|
||||
// ...and it is saved as the entity type it is.
|
||||
let saved = arrow_to_nbt(&arrow, [1; 4], None, None, None).unwrap();
|
||||
assert_eq!(kind(&saved).as_deref(), Some("minecraft:spectral_arrow"));
|
||||
let plain = arrow_from_nbt(&summoned("minecraft:arrow", &[]), None, |_| None)
|
||||
.unwrap()
|
||||
.0;
|
||||
let saved = arrow_to_nbt(&plain, [1; 4], None, None, None).unwrap();
|
||||
assert_eq!(kind(&saved).as_deref(), Some("minecraft:arrow"));
|
||||
|
||||
for id in ["minecraft:llama_spit", "minecraft:shulker_bullet"] {
|
||||
let p = spit_or_bullet_from_nbt(&summoned(id, &[]), None).unwrap();
|
||||
assert_eq!(p.kind.id(), id);
|
||||
assert!(!p.no_gravity && p.position == Vec3::new(0.5, 110.0, 0.5));
|
||||
let held =
|
||||
spit_or_bullet_from_nbt(&summoned(id, std::slice::from_ref(&flag)), None).unwrap();
|
||||
assert!(held.no_gravity);
|
||||
}
|
||||
assert!(spit_or_bullet_from_nbt(&summoned("minecraft:arrow", &[]), None).is_none());
|
||||
// a spit and a target-less bullet are saved and selectable (`/data`, `@e`).
|
||||
let spit = spit_or_bullet_from_nbt(&summoned("minecraft:llama_spit", &[]), None).unwrap();
|
||||
assert!(saved_with_chunk(&spit));
|
||||
let saved = spit_or_bullet_to_nbt(&spit, [1; 4], None, None).unwrap();
|
||||
assert_eq!(kind(&saved).as_deref(), Some("minecraft:llama_spit"));
|
||||
|
||||
// A summoned experience bottle flies as the bottle item, with a bottle's gravity.
|
||||
let bottle =
|
||||
throwable_from_nbt(&summoned("minecraft:experience_bottle", &[]), None).unwrap();
|
||||
assert_eq!(bottle.kind.id(), "minecraft:experience_bottle");
|
||||
assert!(!bottle.no_gravity && (bottle.get_gravity() - 0.07).abs() < 1e-12);
|
||||
let held =
|
||||
throwable_from_nbt(&summoned("minecraft:experience_bottle", &[flag]), None).unwrap();
|
||||
assert!(held.no_gravity && held.get_gravity() == 0.0);
|
||||
let saved = throwable_to_nbt(&bottle, [1; 4], None, None).unwrap();
|
||||
assert_eq!(kind(&saved).as_deref(), Some("minecraft:experience_bottle"));
|
||||
let item = saved.get("Item").and_then(item_from_nbt).unwrap();
|
||||
assert_eq!(&*item.item, "minecraft:experience_bottle");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_gravity_survives_save_and_load_for_orbs() {
|
||||
// Entity.saveWithoutId / load: NoGravity round-trips on a kind that
|
||||
|
||||
@@ -3782,13 +3782,13 @@ impl Game<'_> {
|
||||
/// Item entity `i` leaves the level (`Entity.discard`); mods hear it.
|
||||
pub(super) fn remove_dropped(&mut self, i: usize) -> ItemEntity {
|
||||
let e = self.dropped.remove(i);
|
||||
self.native_entities_left("minecraft:item", &[e.id]);
|
||||
self.native_entity_left(e.id);
|
||||
e
|
||||
}
|
||||
/// Experience orb `i` leaves the level (`Entity.discard`); mods hear it.
|
||||
pub(super) fn remove_orb(&mut self, i: usize) -> ExperienceOrb {
|
||||
let o = self.orbs.remove(i);
|
||||
self.native_entities_left("minecraft:experience_orb", &[o.id]);
|
||||
self.native_entity_left(o.id);
|
||||
o
|
||||
}
|
||||
/// The item entity `spawn_item` just added, unless a native mod kept it
|
||||
@@ -7325,6 +7325,9 @@ impl Game<'_> {
|
||||
if running {
|
||||
horizon_client::spans::time("tick_lan_packets", || self.tick_lan_packets());
|
||||
horizon_client::spans::time("tick_blocks", || self.tick_blocks())?;
|
||||
// What the blocks spawned (a dispensed minecart, a lit TNT, a
|
||||
// placed boat) joins before the entities tick.
|
||||
self.native_joins_flush(false, 0);
|
||||
}
|
||||
if running {
|
||||
horizon_client::spans::time("tick_paintings", || self.tick_paintings());
|
||||
@@ -7356,34 +7359,35 @@ impl Game<'_> {
|
||||
self.handle_entity_portals();
|
||||
self.handle_end_gateway_portals();
|
||||
});
|
||||
let before = self.dropped_ids();
|
||||
horizon_client::spans::time("tick_items", || self.tick_items());
|
||||
if self.remote.is_none() {
|
||||
self.hoppers_catch_items();
|
||||
}
|
||||
// Merged, burnt, despawned or picked up.
|
||||
self.native_items_left_since(&before);
|
||||
self.native_leaves_flush();
|
||||
self.native_joins_flush(false, 0);
|
||||
horizon_client::spans::time("touch_arrows", || self.touch_arrows());
|
||||
}
|
||||
horizon_client::spans::time("spawn_loaded_entities", || self.spawn_loaded_entities());
|
||||
// The ride the player's file names, once the vehicle is in the world.
|
||||
self.restore_root_vehicle();
|
||||
if running {
|
||||
let mobs_before = self.mob_ids();
|
||||
horizon_client::spans::time("tick_mobs", || self.tick_mobs())?;
|
||||
// Dead, despawned or gone into a hive.
|
||||
self.native_mobs_left_since(&mobs_before);
|
||||
self.native_leaves_flush();
|
||||
horizon_client::spans::time("recheck_golem_openers", || self.recheck_golem_openers());
|
||||
self.tick_mod_container_viewers();
|
||||
let before = self.orb_ids();
|
||||
horizon_client::spans::time("tick_experience", || self.tick_experience());
|
||||
// Merged, despawned or picked up.
|
||||
self.native_orbs_left_since(&before);
|
||||
self.native_leaves_flush();
|
||||
horizon_client::spans::time("entity_portals", || {
|
||||
self.find_entity_portals();
|
||||
self.resolve_arrivals();
|
||||
});
|
||||
}
|
||||
// The rest of the tick's spawns join, and what left is told of.
|
||||
self.native_joins_flush(false, 0);
|
||||
self.native_leaves_flush();
|
||||
horizon_client::spans::time("tick_lan", || self.tick_lan(false));
|
||||
self.tick_friends();
|
||||
horizon_client::spans::time("tick_hand", || self.tick_hand());
|
||||
|
||||
@@ -810,17 +810,23 @@ impl Game<'_> {
|
||||
subject: None,
|
||||
};
|
||||
let mut tops: Vec<BlockPos> = Vec::new();
|
||||
a.tick(&mut scene, facts, in_water, &mut |bounds, velocity| {
|
||||
horizon_client::motion::bubble_column_push_over(
|
||||
world,
|
||||
registry,
|
||||
facts,
|
||||
collision,
|
||||
bounds,
|
||||
velocity,
|
||||
|pos| tops.push(pos),
|
||||
);
|
||||
});
|
||||
a.tick(
|
||||
&mut scene,
|
||||
facts,
|
||||
in_water,
|
||||
&mut |bounds, velocity| {
|
||||
horizon_client::motion::bubble_column_push_over(
|
||||
world,
|
||||
registry,
|
||||
facts,
|
||||
collision,
|
||||
bounds,
|
||||
velocity,
|
||||
|pos| tops.push(pos),
|
||||
);
|
||||
},
|
||||
Some(self.border.geometry()),
|
||||
);
|
||||
for pos in tops {
|
||||
self.bubble_column_particles(pos);
|
||||
}
|
||||
|
||||
@@ -2105,9 +2105,8 @@ impl Game<'_> {
|
||||
Target::Spawner(id) => self.ominous_spawners.retain(|s| s.id != id),
|
||||
}
|
||||
// The orbs marked for removal are gone (Entity.kill removes them).
|
||||
let before = self.orb_ids();
|
||||
self.orbs.retain(|o| o.age < u32::MAX / 2);
|
||||
self.native_orbs_left_since(&before);
|
||||
self.native_leaves_flush();
|
||||
}
|
||||
fn cmd_kill(&mut self, ctx: &Context) -> R<i32> {
|
||||
let targets = if ctx.get("targets").is_some() {
|
||||
|
||||
@@ -173,7 +173,8 @@ impl Game<'_> {
|
||||
let raw = self.persist.raw.get(&id);
|
||||
entity_nbt::arrow_to_nbt(p, random_uuid(), owner, in_block, raw)
|
||||
.or_else(|| entity_nbt::fireball_to_nbt(p, random_uuid(), owner, raw))
|
||||
.or_else(|| entity_nbt::throwable_to_nbt(p, random_uuid(), owner, raw))?
|
||||
.or_else(|| entity_nbt::throwable_to_nbt(p, random_uuid(), owner, raw))
|
||||
.or_else(|| entity_nbt::spit_or_bullet_to_nbt(p, random_uuid(), owner, raw))?
|
||||
}
|
||||
Target::Crystal(id) => {
|
||||
let c = self.crystals.iter().find(|c| c.id == id)?;
|
||||
@@ -879,7 +880,9 @@ impl Game<'_> {
|
||||
let target = self.spawn_entity_from_nbt(tag);
|
||||
self.native_mobs_joined(from, false);
|
||||
// A mod kept the entity out: tryAddFreshEntityWithPassengers is false.
|
||||
if target.is_none() && self.persist.joins_canceled != canceled {
|
||||
if self.persist.joins_canceled != canceled
|
||||
&& target.is_none_or(|t| self.target_position(t).is_none())
|
||||
{
|
||||
return Err(err("commands.summon.failed.uuid", &[]));
|
||||
}
|
||||
Ok(target)
|
||||
|
||||
@@ -1053,9 +1053,9 @@ impl Game<'_> {
|
||||
for e in &mut events[from..] {
|
||||
if let MobEvent::Projectile(p) = e
|
||||
&& let ProjectileKind::ShulkerBullet { target, .. } = &mut p.kind
|
||||
&& *target == Who::Player
|
||||
&& *target == Some(Who::Player)
|
||||
{
|
||||
*target = Who::Mob(id);
|
||||
*target = Some(Who::Mob(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4777,7 +4777,16 @@ impl Game<'_> {
|
||||
// flies as: the splash happens where the hit was, a lingering
|
||||
// potion's cloud sits at the mob it hit (ThrownLingeringPotion),
|
||||
// and the potion that lands names the hurt it deals.
|
||||
(ProjectileKind::Potion(stack, _), hit) => {
|
||||
(kind @ (ProjectileKind::Potion(..) | ProjectileKind::ExperienceBottle), hit) => {
|
||||
// A bottle flies as the bottle item (`getDefaultItem`).
|
||||
let bottle;
|
||||
let stack = match kind {
|
||||
ProjectileKind::Potion(stack, _) => stack,
|
||||
_ => {
|
||||
bottle = Box::new(ItemStack::new("minecraft:experience_bottle", 1));
|
||||
&bottle
|
||||
}
|
||||
};
|
||||
let block = match hit {
|
||||
Hit::Block { pos, face, .. } => Some((
|
||||
pos,
|
||||
@@ -6492,12 +6501,15 @@ impl Game<'_> {
|
||||
Vec::new()
|
||||
};
|
||||
let body = out.len();
|
||||
// The GPU path: an ordinary mob, no outline, no layers the CPU path changes after
|
||||
// drawing them (a player's, the HDR tiers below, the sulfur cube's shell).
|
||||
// The GPU path: an ordinary mob, no layers the CPU path changes after drawing them
|
||||
// (a player's, the HDR tiers below, the sulfur cube's shell). A glowing mob's
|
||||
// silhouette is posed too (`SkinFrame::outline_since`), unless it is invisible:
|
||||
// then only the outline draws.
|
||||
let silhouette_on_gpu = outline.is_none() || !mob.invisible;
|
||||
let posable = skinning
|
||||
&& !is_player
|
||||
&& !layers_only
|
||||
&& outline.is_none()
|
||||
&& silhouette_on_gpu
|
||||
&& armor.is_empty()
|
||||
&& !matches!(
|
||||
mob.species(),
|
||||
@@ -6576,14 +6588,18 @@ impl Game<'_> {
|
||||
}
|
||||
// CustomHeadLayer (a pumpkin, skull or head item) included.
|
||||
if layers {
|
||||
// Held items join the GPU path's draws unless an outline reads their vertices.
|
||||
let skin = (skinning && outline.is_none()).then_some(&mut skinned);
|
||||
// Held items join the GPU path's draws, their silhouette too.
|
||||
let skin = (skinning && silhouette_on_gpu).then_some(&mut skinned);
|
||||
self.mob_block_geometry(mob, camera, alpha, packed, skin, out);
|
||||
// `native_mob_layer`: a native mod's layer, over the vanilla ones.
|
||||
self.mod_mob_layer_geometry(mob, who, alpha as f32, camera, packed, out);
|
||||
}
|
||||
if let Some(rgb) = outline {
|
||||
self.mark_outline(&out[entity_start..], rgb);
|
||||
if let Some(((first, _, _), _)) = skinned {
|
||||
let color = horizon_client::mobs::render::abgr(rgb);
|
||||
self.skin_frame.borrow_mut().outline_since(first, color);
|
||||
}
|
||||
if mob.invisible {
|
||||
out.truncate(entity_start);
|
||||
continue;
|
||||
|
||||
@@ -1100,6 +1100,7 @@ impl Game<'_> {
|
||||
| K::EnderPearl
|
||||
| K::LlamaSpit
|
||||
| K::Potion { .. }
|
||||
| K::ExperienceBottle
|
||||
| K::Thrown(_) => (0.25, 0.2125),
|
||||
K::SmallFireball | K::ShulkerBullet { .. } | K::WitherSkull { .. } => {
|
||||
(0.3125, 0.265_625)
|
||||
|
||||
@@ -241,7 +241,7 @@ mod tests {
|
||||
// A shulker bullet takes a blow, with no pick radius round its box.
|
||||
let bullet = Projectile::new(
|
||||
ProjectileKind::ShulkerBullet {
|
||||
target: Who::Player,
|
||||
target: Some(Who::Player),
|
||||
dir: None,
|
||||
steps: 0,
|
||||
delta: [0.0; 3],
|
||||
|
||||
@@ -1456,6 +1456,7 @@ impl Game<'_> {
|
||||
// A gateway keeps the item in its level (teleportSameDimension
|
||||
// moves it): it doesn't leave, so no native_entity_leave_level.
|
||||
self.dropped.remove(i);
|
||||
self.native_leave_skip(id);
|
||||
self.detach_entity(id);
|
||||
tag
|
||||
} else if let Some(i) = self.mobs.iter().position(|m| m.id == id) {
|
||||
@@ -1479,11 +1480,13 @@ impl Game<'_> {
|
||||
}
|
||||
self.mobs.retain(|m| !gone.contains(&m.id));
|
||||
for g in gone {
|
||||
self.native_leave_skip(g);
|
||||
self.detach_entity(g);
|
||||
}
|
||||
tag
|
||||
} else if let Some(i) = self.creatures.projectiles.iter().position(|p| p.id == id) {
|
||||
let pearl = Box::new(self.creatures.projectiles.remove(i));
|
||||
self.native_leave_skip(id);
|
||||
let (job, target) = self.gateway_job([gateway.x, gateway.y, gateway.z]);
|
||||
let arrival = GatewayArrival {
|
||||
traveller: Arriving::Pearl(pearl),
|
||||
|
||||
@@ -623,7 +623,6 @@ impl Game<'_> {
|
||||
items_ignore: items_ignore_explosion(self.ui.rules.bool("mob_griefing"), interaction),
|
||||
spared: exploding.filter(|_| self.combat.explosion_source_named),
|
||||
};
|
||||
let (items_before, orbs_before) = (self.dropped_ids(), self.orb_ids());
|
||||
let (items_len, orbs_len) = (self.dropped.len(), self.orbs.len());
|
||||
let (mut items_kept, mut orbs_kept) = (0, 0);
|
||||
let (mut boats_gone, mut carts_gone) = (0, 0);
|
||||
@@ -723,8 +722,7 @@ impl Game<'_> {
|
||||
}
|
||||
self.dropped.drain(items_kept..items_len);
|
||||
self.orbs.drain(orbs_kept..orbs_len);
|
||||
self.native_items_left_since(&items_before);
|
||||
self.native_orbs_left_since(&orbs_before);
|
||||
self.native_leaves_flush();
|
||||
// Frames, paintings, cushions and armour stands last: vanilla lists
|
||||
// the entities in reach before hurting any, so the items they drop
|
||||
// here are neither destroyed nor pushed by the same blast.
|
||||
|
||||
@@ -618,8 +618,8 @@ impl Game<'_> {
|
||||
seed,
|
||||
);
|
||||
let glowing = self.entity_glowing(item.id);
|
||||
// Posed on the GPU when every layer can be (the outline reads the CPU's vertices).
|
||||
let skinned = (skinning && !glowing)
|
||||
// Posed on the GPU when every layer can be, its silhouette too when it glows.
|
||||
let skinned = skinning
|
||||
.then(|| self.submit_item_skinned(&state, &copies, vertex_light(packed), lights))
|
||||
.flatten();
|
||||
if skinned.is_none() {
|
||||
@@ -630,6 +630,10 @@ impl Game<'_> {
|
||||
if glowing {
|
||||
let rgb = super::glowing::team_color(self.mob_team(item.id));
|
||||
self.mark_outline(&out[entity_start..], rgb);
|
||||
if let Some(((first, _, _), _)) = skinned {
|
||||
let color = horizon_client::mobs::render::abgr(rgb);
|
||||
self.skin_frame.borrow_mut().outline_since(first, color);
|
||||
}
|
||||
} else {
|
||||
self.cull_skinned(out, entity_start, skinned);
|
||||
}
|
||||
|
||||
@@ -635,7 +635,6 @@ impl Game<'_> {
|
||||
// over the 40 a vehicle takes) destroys them. Lightning is no fire
|
||||
// source, so a TNT cart does not prime. `ExperienceOrb.hurtServer`:
|
||||
// 5 damage off 5 health. Last first, since each removes its entry.
|
||||
let orbs_before = self.orb_ids();
|
||||
let mut left = self.thunder_count(strike, ThunderHit::Vehicle)
|
||||
+ self.thunder_count(strike, ThunderHit::Orb);
|
||||
for h in self.entity_handles_rev() {
|
||||
@@ -659,7 +658,7 @@ impl Game<'_> {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self.native_orbs_left_since(&orbs_before);
|
||||
self.native_leaves_flush();
|
||||
// Every player in the box, the server's players too (0.6 x 1.8).
|
||||
let strike_players = move |g: &mut Self| {
|
||||
if let Some(p) = g.player.as_ref().map(|p| p.position)
|
||||
@@ -679,7 +678,6 @@ impl Game<'_> {
|
||||
strike_players(self);
|
||||
self.each_guest_hit(strike_players);
|
||||
// Dropped items burn up (ItemEntity health 5), save fire-resistant ones.
|
||||
let before = self.dropped_ids();
|
||||
let mut left = self.thunder_count(strike, ThunderHit::Item);
|
||||
for h in self.entity_handles_rev() {
|
||||
if left == 0 {
|
||||
@@ -696,7 +694,7 @@ impl Game<'_> {
|
||||
self.dropped.remove(i);
|
||||
}
|
||||
}
|
||||
self.native_items_left_since(&before);
|
||||
self.native_leaves_flush();
|
||||
struck
|
||||
}
|
||||
/// `LightningBolt.shouldRenderAtSqrDistance`: a bolt is drawn within 64
|
||||
|
||||
@@ -141,6 +141,15 @@ impl Game<'_> {
|
||||
json!({ "active": active, "cached": cached })
|
||||
}),
|
||||
"renderer": renderer,
|
||||
// A build with the `alloc-census` feature: the live heap by the
|
||||
// startup step, span or pool job that allocated it, else its thread.
|
||||
"heap_by_tag_mb": horizon_threads::HEAP_CENSUS.then(|| {
|
||||
horizon_threads::heap_census::census()
|
||||
.into_iter()
|
||||
.take(60)
|
||||
.map(|(tag, bytes)| json!([tag, mb(bytes.max(0) as u64)]))
|
||||
.collect::<Vec<_>>()
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -258,9 +258,38 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Entities joining the level (`native_entity_join_level`), for every kind in
|
||||
/// the entity walk. Kept out of `native_hooks.rs`, whose lines may not hold macros.
|
||||
/// The entities the walk does not hold: Horizon keeps these in lists of their own.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Apart {
|
||||
Crystal(usize),
|
||||
Cushion(usize),
|
||||
Painting(usize),
|
||||
Frame(usize),
|
||||
Dragon,
|
||||
}
|
||||
|
||||
/// One entity a mod is told of: in the walk, or apart from it.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Slot {
|
||||
Walk(super::entity::EntityHandle),
|
||||
Apart(Apart),
|
||||
}
|
||||
|
||||
/// Entities joining and leaving the level (`native_entity_join_level`,
|
||||
/// `native_entity_leave_level`), for every kind in the entity walk and the
|
||||
/// five kept apart from it. Kept out of `native_hooks.rs`, whose lines may not hold macros.
|
||||
impl Game<'_> {
|
||||
/// Whether an entity may be waiting to be told of: one with an id above
|
||||
/// the last announced, or one of the kinds that take their id late and
|
||||
/// have none. Reads a counter and five lists' ids; no walk.
|
||||
fn joins_pending(&self) -> bool {
|
||||
self.next_entity > self.persist.joined_mark
|
||||
|| self.tnts.iter().any(|t| t.id == 0)
|
||||
|| self.falling.iter().any(|f| f.id == 0)
|
||||
|| self.creatures.projectiles.iter().any(|p| p.id == 0)
|
||||
|| self.tridents.iter().any(|t| t.id == 0)
|
||||
|| self.rockets.iter().any(|r| r.id == 0)
|
||||
}
|
||||
/// Tells `native_entity_join_level` once for every entity added since the
|
||||
/// last call, of any kind, in the order they were created (ids count up
|
||||
/// from one counter). A mod that cancels takes the entity back out here,
|
||||
@@ -268,11 +297,10 @@ impl Game<'_> {
|
||||
/// `loaded_from_disk` is for the entities that came with a chunk. Returns
|
||||
/// whether `watch` (an id, or 0) is still in the level. Walks the lists
|
||||
/// with static dispatch and allocates nothing but the kind name of an
|
||||
/// entity it announces; nothing runs when no mod is loaded or on a client
|
||||
/// of a server (which tells nothing).
|
||||
/// entity it announces; nothing runs when no mod is loaded, on a client
|
||||
/// of a server (which tells nothing), or when nothing is new.
|
||||
pub(super) fn native_joins_flush(&mut self, loaded_from_disk: bool, watch: u32) -> bool {
|
||||
use super::entity::Entity;
|
||||
if self.remote.is_some() || crate::mod_api::mods().is_empty() {
|
||||
if self.remote.is_some() || crate::mod_api::mods().is_empty() || !self.joins_pending() {
|
||||
return true;
|
||||
}
|
||||
for t in &mut self.tnts {
|
||||
@@ -291,11 +319,9 @@ impl Game<'_> {
|
||||
super::lan_entities::assign_entity_id(&mut self.next_entity, &mut r.id);
|
||||
}
|
||||
let mut kept = true;
|
||||
while let Some((handle, id)) = self.next_unannounced() {
|
||||
while let Some((slot, id)) = self.next_unannounced() {
|
||||
self.persist.joined_mark = id;
|
||||
let (kind, at) = super::entity::with_entity!(self, handle, e => {
|
||||
(e.native_type(), e.position())
|
||||
});
|
||||
let (kind, at) = self.slot_kind_and_position(slot);
|
||||
let mut canceled = false;
|
||||
let dimension = self.dim.id();
|
||||
self.native_entity_join_level(
|
||||
@@ -311,32 +337,162 @@ impl Game<'_> {
|
||||
self.forget_entity(id);
|
||||
// Handles are indexes: the entity is taken out before any
|
||||
// other change to its list.
|
||||
self.discard_entity(handle);
|
||||
self.discard_slot(slot);
|
||||
kept &= id != watch;
|
||||
} else {
|
||||
self.persist.live.push((id, kind.into_owned(), true));
|
||||
}
|
||||
}
|
||||
kept
|
||||
}
|
||||
/// The entity with the lowest id above the last one announced, by handle.
|
||||
fn next_unannounced(&self) -> Option<(super::entity::EntityHandle, u32)> {
|
||||
/// The entity with the lowest id above the last one announced.
|
||||
fn next_unannounced(&self) -> Option<(Slot, u32)> {
|
||||
use super::entity::Entity;
|
||||
let mark = self.persist.joined_mark;
|
||||
let mut best: Option<(super::entity::EntityHandle, u32)> = None;
|
||||
for handle in self.entity_handles() {
|
||||
let id = {
|
||||
use super::entity::Entity;
|
||||
super::entity::with_entity!(self, handle, e => e.id())
|
||||
};
|
||||
let mut best: Option<(Slot, u32)> = None;
|
||||
let mut offer = |slot: Slot, id: u32| {
|
||||
if id > mark && best.is_none_or(|(_, b)| id < b) {
|
||||
best = Some((handle, id));
|
||||
best = Some((slot, id));
|
||||
}
|
||||
};
|
||||
for handle in self.entity_handles() {
|
||||
offer(
|
||||
Slot::Walk(handle),
|
||||
super::entity::with_entity!(self, handle, e => e.id()),
|
||||
);
|
||||
}
|
||||
for (i, c) in self.crystals.iter().enumerate() {
|
||||
offer(Slot::Apart(Apart::Crystal(i)), c.id);
|
||||
}
|
||||
for (i, c) in self.cushions.iter().enumerate() {
|
||||
offer(Slot::Apart(Apart::Cushion(i)), c.id);
|
||||
}
|
||||
for (i, p) in self.paintings.iter().enumerate() {
|
||||
offer(Slot::Apart(Apart::Painting(i)), p.id);
|
||||
}
|
||||
for (i, f) in self.item_frames.iter().enumerate() {
|
||||
offer(Slot::Apart(Apart::Frame(i)), f.id);
|
||||
}
|
||||
if let Some(d) = &self.dragon {
|
||||
offer(Slot::Apart(Apart::Dragon), d.id);
|
||||
}
|
||||
best
|
||||
}
|
||||
/// The type id and position a mod is told of.
|
||||
fn slot_kind_and_position(&self, slot: Slot) -> (std::borrow::Cow<'static, str>, Vec3) {
|
||||
use super::entity::Entity;
|
||||
let block = |p: [i32; 3]| {
|
||||
Vec3::new(
|
||||
f64::from(p[0]) + 0.5,
|
||||
f64::from(p[1]) + 0.5,
|
||||
f64::from(p[2]) + 0.5,
|
||||
)
|
||||
};
|
||||
match slot {
|
||||
Slot::Walk(h) => {
|
||||
super::entity::with_entity!(self, h, e => (e.native_type(), e.position()))
|
||||
}
|
||||
Slot::Apart(Apart::Crystal(i)) => {
|
||||
("minecraft:end_crystal".into(), self.crystals[i].position)
|
||||
}
|
||||
Slot::Apart(Apart::Cushion(i)) => {
|
||||
("minecraft:cushion".into(), self.cushions[i].position)
|
||||
}
|
||||
Slot::Apart(Apart::Painting(i)) => {
|
||||
("minecraft:painting".into(), block(self.paintings[i].pos))
|
||||
}
|
||||
Slot::Apart(Apart::Frame(i)) => {
|
||||
let f = &self.item_frames[i];
|
||||
let kind = if f.glow {
|
||||
"minecraft:glow_item_frame"
|
||||
} else {
|
||||
"minecraft:item_frame"
|
||||
};
|
||||
(kind.into(), block(f.pos))
|
||||
}
|
||||
Slot::Apart(Apart::Dragon) => (
|
||||
"minecraft:ender_dragon".into(),
|
||||
self.dragon.as_ref().map_or(Vec3::ZERO, |d| d.position),
|
||||
),
|
||||
}
|
||||
}
|
||||
/// Takes the entity out of the level, before it ever ticked.
|
||||
fn discard_slot(&mut self, slot: Slot) {
|
||||
match slot {
|
||||
Slot::Walk(h) => self.discard_entity(h),
|
||||
Slot::Apart(Apart::Crystal(i)) => drop(self.crystals.remove(i)),
|
||||
Slot::Apart(Apart::Cushion(i)) => drop(self.cushions.remove(i)),
|
||||
Slot::Apart(Apart::Painting(i)) => drop(self.paintings.remove(i)),
|
||||
Slot::Apart(Apart::Frame(i)) => drop(self.item_frames.remove(i)),
|
||||
Slot::Apart(Apart::Dragon) => self.dragon = None,
|
||||
}
|
||||
}
|
||||
/// Entities that were moved, not added (a portal's arrival keeps being
|
||||
/// silent): everything now in the level counts as announced.
|
||||
/// silent): everything now in the level counts as announced, and will
|
||||
/// be told when it leaves.
|
||||
pub(super) fn native_joins_skip(&mut self) {
|
||||
if self.remote.is_none() && !crate::mod_api::mods().is_empty() {
|
||||
while let Some((slot, id)) = self.next_unannounced() {
|
||||
self.persist.joined_mark = id;
|
||||
let kind = self.slot_kind_and_position(slot).0.into_owned();
|
||||
self.persist.live.push((id, kind, true));
|
||||
}
|
||||
}
|
||||
self.persist.joined_mark = self.persist.joined_mark.max(self.next_entity);
|
||||
}
|
||||
/// An entity taken out of the level without leaving it (an item through
|
||||
/// an End gateway): forgotten, not told of.
|
||||
pub(super) fn native_leave_skip(&mut self, id: u32) {
|
||||
if let Ok(i) = self.persist.live.binary_search_by_key(&id, |l| l.0) {
|
||||
self.persist.live.remove(i);
|
||||
}
|
||||
}
|
||||
/// Entity `id` has left the level: told once, if mods were told it joined.
|
||||
pub(super) fn native_entity_left(&mut self, id: u32) {
|
||||
if self.remote.is_some() {
|
||||
return;
|
||||
}
|
||||
if let Ok(i) = self.persist.live.binary_search_by_key(&id, |l| l.0) {
|
||||
let (id, kind, _) = self.persist.live.remove(i);
|
||||
let dimension = self.dim.id();
|
||||
self.native_entity_leave_level(dimension, id, &kind);
|
||||
}
|
||||
}
|
||||
/// Tells `native_entity_leave_level` for every announced entity that is
|
||||
/// no longer in any list (died, despawned, picked up, burnt, blown up, gone
|
||||
/// through a portal, unloaded), lowest id first. Marks the entities still
|
||||
/// there in the registry and tells the rest: no allocation, and nothing
|
||||
/// runs when no mod is loaded or none was announced.
|
||||
pub(super) fn native_leaves_flush(&mut self) {
|
||||
use super::entity::Entity;
|
||||
if self.persist.live.is_empty() || self.remote.is_some() {
|
||||
return;
|
||||
}
|
||||
for l in &mut self.persist.live {
|
||||
l.2 = false;
|
||||
}
|
||||
let live = &mut self.persist.live;
|
||||
let mut seen = |id: u32| {
|
||||
if let Ok(i) = live.binary_search_by_key(&id, |l| l.0) {
|
||||
live[i].2 = true;
|
||||
}
|
||||
};
|
||||
super::entity::for_each_entity!(super::entity::entity_lists_of!(self), e => {
|
||||
seen(e.id());
|
||||
});
|
||||
self.crystals.iter().for_each(|c| seen(c.id));
|
||||
self.cushions.iter().for_each(|c| seen(c.id));
|
||||
self.paintings.iter().for_each(|p| seen(p.id));
|
||||
self.item_frames.iter().for_each(|f| seen(f.id));
|
||||
if let Some(d) = &self.dragon {
|
||||
seen(d.id);
|
||||
}
|
||||
while let Some(i) = self.persist.live.iter().position(|l| !l.2) {
|
||||
let (id, kind, _) = self.persist.live.remove(i);
|
||||
let dimension = self.dim.id();
|
||||
self.native_entity_leave_level(dimension, id, &kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -697,7 +697,7 @@
|
||||
//! change a cauldron.
|
||||
//!
|
||||
//! API 53 adds entities leaving a level (`native_entity_leave_level`), for
|
||||
//! item entities, experience orbs and mobs so far.
|
||||
//! every kind the join hook tells (see API 47).
|
||||
//!
|
||||
//! API 52 adds `native_block_entity_sync`: a mod block entity's block and update tag
|
||||
//! sent to clients again (`Level.sendBlockUpdated`).
|
||||
@@ -718,7 +718,7 @@
|
||||
//! API 47 adds entities joining a level (`native_entity_join_level`); it tells
|
||||
//! every kind the game keeps in its entity lists (items, orbs, mobs, boats,
|
||||
//! minecarts, armor stands, TNT, falling blocks, projectiles, rockets,
|
||||
//! tridents).
|
||||
//! tridents, end crystals, the dragon, cushions, paintings, item frames).
|
||||
//!
|
||||
//! API 46 adds bounded world-space rim and colored-light requests
|
||||
//! (`native_visuals`) and opt-in texture-matched emissive material
|
||||
@@ -2309,6 +2309,16 @@ impl Game<'_> {
|
||||
pub(super) fn native_commands(&mut self, commands: &mut Vec<&'static str>) {
|
||||
mod_api::dispatch_commands(self, mod_api::mods(), commands);
|
||||
}
|
||||
/// Local player movement input, after reading it and before applying it.
|
||||
pub(super) fn native_movement_input(
|
||||
&self,
|
||||
forward: &mut f32,
|
||||
strafe: &mut f32,
|
||||
jump: &mut bool,
|
||||
sneak: &mut bool,
|
||||
) {
|
||||
mod_api::dispatch_movement_input(self, mod_api::mods(), forward, strafe, jump, sneak);
|
||||
}
|
||||
/// World field of view, after normal calculation and zoom and before projection/Nausea.
|
||||
/// Called for world rendering and near-plane overlays; hand/HUD FOV uses its own path.
|
||||
/// Only finite values in 1..=179 degrees replace the computed FOV.
|
||||
@@ -6317,8 +6327,10 @@ impl Game<'_> {
|
||||
/// a level closing with nobody in it included. Not for the entities still
|
||||
/// loaded when the server stops, as in vanilla. `entity` is its ID and
|
||||
/// `kind` its type (`minecraft:item`); it is no longer in the level's lists.
|
||||
/// It only hears. 53 tells item entities, experience orbs and mobs (`kind`
|
||||
/// the mob type); later APIs add other kinds, so check `kind`. Server-side.
|
||||
/// It only hears. 53 told item entities, experience orbs and mobs; it now
|
||||
/// tells every kind `native_entity_join_level` does (`kind` is the type), each
|
||||
/// once, when the game next notices it gone (the tick or action that removed
|
||||
/// it). Check `kind`. Server-side.
|
||||
pub(super) fn native_entity_leave_level(&mut self, dimension: &str, entity: u32, kind: &str) {
|
||||
mod_api::dispatch_entity_leave_level(self, mod_api::mods(), dimension, entity, kind);
|
||||
}
|
||||
@@ -6332,12 +6344,14 @@ impl Game<'_> {
|
||||
/// back out as if never added, and a `/summon` of it fails. Tells every
|
||||
/// kind in the game's entity lists (items, orbs, mobs, boats, minecarts,
|
||||
/// armor stands, primed TNT, falling blocks, projectiles, rockets,
|
||||
/// tridents), once each, in the order they were created; check `kind`. A
|
||||
/// gameplay spawn is told when the next tick starts (a drop, an orb, a mob, a
|
||||
/// `/summon` and a chunk's entities are told at once). A mod that
|
||||
/// cancels one of the others sees it removed before it ticks again.
|
||||
/// Not told: end crystals, the dragon, cushions, paintings, item frames and
|
||||
/// the entities Horizon keeps without simulating. Server-side.
|
||||
/// tridents, end crystals, the dragon, cushions, paintings, item frames),
|
||||
/// once each, in the order they were created; check `kind`. A drop, an
|
||||
/// orb, a mob, a `/summon` and a chunk's entities are told at once, any
|
||||
/// other spawn between the tick's phases (it is checked after the blocks,
|
||||
/// the vehicles, the projectiles, the items and the mobs, and at the
|
||||
/// tick's end). A mod that cancels one of the others sees it removed
|
||||
/// before it ticks again. Not told: the entities Horizon keeps without
|
||||
/// simulating (displays, markers, interactions). Server-side.
|
||||
pub(super) fn native_entity_join_level(
|
||||
&mut self,
|
||||
dimension: &str,
|
||||
@@ -8258,22 +8272,6 @@ pub(super) fn mob_kind(mob: &horizon_client::mobs::Mob) -> String {
|
||||
["minecraft:", mob.kind.id.trim_start_matches("minecraft:")].concat()
|
||||
}
|
||||
|
||||
/// The ids of `before` (id, kind) that are not in `now`, grouped by kind in
|
||||
/// first-seen order: the entities that left the level.
|
||||
fn vanished_by_kind(
|
||||
before: &[(u32, String)],
|
||||
now: &std::collections::HashSet<u32>,
|
||||
) -> Vec<(String, Vec<u32>)> {
|
||||
let mut out: Vec<(String, Vec<u32>)> = Vec::new();
|
||||
for (id, kind) in before.iter().filter(|(id, _)| !now.contains(id)) {
|
||||
match out.iter_mut().find(|(k, _)| k == kind) {
|
||||
Some((_, ids)) => ids.push(*id),
|
||||
None => out.push((kind.clone(), Vec::from([*id]))),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Where the game calls the hook points.
|
||||
impl Game<'_> {
|
||||
/// The acting player as the hook points name one: the guest whose turn
|
||||
@@ -8395,80 +8393,6 @@ impl Game<'_> {
|
||||
self.native_mob_spawned(self.dim.id(), &kind, pos, natural);
|
||||
}
|
||||
}
|
||||
/// `native_entity_leave_level` for each of `entities`, of `kind`, gone
|
||||
/// from the level in place. A client of a server tells nothing.
|
||||
pub(super) fn native_entities_left(&mut self, kind: &str, entities: &[u32]) {
|
||||
if self.remote.is_some() {
|
||||
return;
|
||||
}
|
||||
let dimension = self.dim.id();
|
||||
for &entity in entities {
|
||||
self.native_entity_leave_level(dimension, entity, kind);
|
||||
}
|
||||
}
|
||||
/// The IDs of the level's item entities, for `native_items_left_since`
|
||||
/// (none on a client of a server, which tells nothing).
|
||||
pub(super) fn dropped_ids(&self) -> Vec<u32> {
|
||||
if self.remote.is_some() || mod_api::mods().is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
self.dropped.iter().map(|e| e.id).collect()
|
||||
}
|
||||
/// The IDs of the level's experience orbs, for `native_orbs_left_since`.
|
||||
pub(super) fn orb_ids(&self) -> Vec<u32> {
|
||||
if self.remote.is_some() || mod_api::mods().is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
self.orbs.iter().map(|o| o.id).collect()
|
||||
}
|
||||
/// The (id, kind) of the level's mobs, for `native_mobs_left_since` (none
|
||||
/// on a client of a server, which tells nothing).
|
||||
pub(super) fn mob_ids(&self) -> Vec<(u32, String)> {
|
||||
if self.remote.is_some() || mod_api::mods().is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
self.mobs.iter().map(|m| (m.id, mob_kind(m))).collect()
|
||||
}
|
||||
/// The mobs of `before` gone since have left the level (died, despawned,
|
||||
/// entered a hive, unloaded). Mobs spawned meanwhile are not counted.
|
||||
pub(super) fn native_mobs_left_since(&mut self, before: &[(u32, String)]) {
|
||||
// Nothing was taken: no mod listens, or a client of a server.
|
||||
if before.is_empty() {
|
||||
return;
|
||||
}
|
||||
let now: std::collections::HashSet<u32> = self.mobs.iter().map(|m| m.id).collect();
|
||||
for (kind, ids) in vanished_by_kind(before, &now) {
|
||||
self.native_entities_left(&kind, &ids);
|
||||
}
|
||||
}
|
||||
/// The item entities of `before` gone since have left the level.
|
||||
pub(super) fn native_items_left_since(&mut self, before: &[u32]) {
|
||||
// Nothing was taken: no mod listens, or a client of a server.
|
||||
if before.is_empty() {
|
||||
return;
|
||||
}
|
||||
let now: std::collections::HashSet<u32> = self.dropped.iter().map(|e| e.id).collect();
|
||||
let gone: Vec<u32> = before
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|id| !now.contains(id))
|
||||
.collect();
|
||||
self.native_entities_left("minecraft:item", &gone);
|
||||
}
|
||||
/// The experience orbs of `before` gone since have left the level.
|
||||
pub(super) fn native_orbs_left_since(&mut self, before: &[u32]) {
|
||||
// Nothing was taken: no mod listens, or a client of a server.
|
||||
if before.is_empty() {
|
||||
return;
|
||||
}
|
||||
let now: std::collections::HashSet<u32> = self.orbs.iter().map(|o| o.id).collect();
|
||||
let gone: Vec<u32> = before
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|id| !now.contains(id))
|
||||
.collect();
|
||||
self.native_entities_left("minecraft:experience_orb", &gone);
|
||||
}
|
||||
/// The acting player's blow killed mob `index`.
|
||||
pub(super) fn native_kill_event(&mut self, index: usize) {
|
||||
let (entity, kind) = (self.mobs[index].id, mob_kind(&self.mobs[index]));
|
||||
@@ -9319,26 +9243,6 @@ fn mod_screen_opens(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn vanished_mobs_are_reported_by_kind() {
|
||||
use std::collections::HashSet;
|
||||
let z = || "minecraft:zombie".to_string();
|
||||
let b = || "minecraft:bee".to_string();
|
||||
let before = vec![(1, z()), (2, b()), (3, z()), (4, b())];
|
||||
let now: HashSet<u32> = [1, 4, 99].into_iter().collect(); // 99 spawned meanwhile
|
||||
assert_eq!(
|
||||
super::vanished_by_kind(&before, &now),
|
||||
vec![(b(), vec![2]), (z(), vec![3])]
|
||||
);
|
||||
let all: HashSet<u32> = [1, 2, 3, 4, 5].into_iter().collect();
|
||||
assert!(super::vanished_by_kind(&before, &all).is_empty());
|
||||
assert_eq!(
|
||||
super::vanished_by_kind(&before, &HashSet::new()),
|
||||
vec![(z(), vec![1, 3]), (b(), vec![2, 4])]
|
||||
);
|
||||
assert!(super::vanished_by_kind(&[], &now).is_empty());
|
||||
}
|
||||
|
||||
/// Exercise the gameplay constructor, where name-based entities compete
|
||||
/// with a mod block's sign template.
|
||||
#[test]
|
||||
|
||||
@@ -45,6 +45,10 @@ pub struct EntityMemory {
|
||||
pub(super) joins_canceled: u32,
|
||||
/// The highest entity id `native_entity_join_level` has been told of.
|
||||
pub(super) joined_mark: u32,
|
||||
/// The entities mods were told of and that have not left since: id (in
|
||||
/// increasing order), type, and whether the last leave sweep saw it.
|
||||
/// Only filled while a mod is loaded.
|
||||
pub(super) live: Vec<(u32, String, bool)>,
|
||||
}
|
||||
|
||||
/// The key that marks a compound queued with the chunk loads as a fresh add
|
||||
@@ -355,6 +359,10 @@ impl Game<'_> {
|
||||
kind.as_str(),
|
||||
"minecraft:experience_orb"
|
||||
| "minecraft:arrow"
|
||||
| "minecraft:spectral_arrow"
|
||||
| "minecraft:llama_spit"
|
||||
| "minecraft:shulker_bullet"
|
||||
| "minecraft:experience_bottle"
|
||||
| "minecraft:fireball"
|
||||
| "minecraft:small_fireball"
|
||||
| "minecraft:wind_charge"
|
||||
@@ -398,7 +406,7 @@ impl Game<'_> {
|
||||
self.remember_tags(id, &tag);
|
||||
}
|
||||
}
|
||||
"minecraft:arrow" => {
|
||||
"minecraft:arrow" | "minecraft:spectral_arrow" => {
|
||||
if let Some((mut p, in_block)) = entity_nbt::arrow_from_nbt(
|
||||
&tag,
|
||||
player_uuid,
|
||||
@@ -431,6 +439,17 @@ impl Game<'_> {
|
||||
self.remember_last_projectile_tags(&tag);
|
||||
}
|
||||
}
|
||||
"minecraft:llama_spit" | "minecraft:shulker_bullet" => {
|
||||
if let Some(mut p) = entity_nbt::spit_or_bullet_from_nbt(&tag, player_uuid)
|
||||
{
|
||||
self.claim_owner(&mut p);
|
||||
self.next_entity += 1;
|
||||
p.id = self.next_entity;
|
||||
self.restore_portal_cooldown(p.id, &tag);
|
||||
self.add_projectile(p);
|
||||
self.remember_last_projectile_tags(&tag);
|
||||
}
|
||||
}
|
||||
id if entity_nbt::throwable_kind(id).is_some() => {
|
||||
if let Some(mut p) = entity_nbt::throwable_from_nbt(&tag, player_uuid) {
|
||||
self.claim_owner(&mut p);
|
||||
@@ -1326,7 +1345,6 @@ impl Game<'_> {
|
||||
})
|
||||
.map(|i| self.mobs[i].id)
|
||||
.collect();
|
||||
let mobs_before = self.mob_ids();
|
||||
self.mobs.retain(|m| {
|
||||
let keep = !leaving.contains(&m.id);
|
||||
if !keep {
|
||||
@@ -1334,7 +1352,6 @@ impl Game<'_> {
|
||||
}
|
||||
keep
|
||||
});
|
||||
let (items_before, orbs_before) = (self.dropped_ids(), self.orb_ids());
|
||||
self.dropped.retain(|e| {
|
||||
let keep = !gone(e.position);
|
||||
if !keep {
|
||||
@@ -1344,9 +1361,7 @@ impl Game<'_> {
|
||||
});
|
||||
self.orbs.retain(|o| !gone(o.position));
|
||||
// Unloaded with their chunk: PersistentEntitySectionManager.stopTracking.
|
||||
self.native_mobs_left_since(&mobs_before);
|
||||
self.native_items_left_since(&items_before);
|
||||
self.native_orbs_left_since(&orbs_before);
|
||||
self.native_leaves_flush();
|
||||
// the ones saved with the chunk go with it (a trident too, unsaved).
|
||||
self.creatures.projectiles.retain(|p| {
|
||||
let leaves = entity_nbt::saved_with_chunk(p) || p.arrow.is_some();
|
||||
@@ -1512,7 +1527,8 @@ impl Game<'_> {
|
||||
.map(|s| self.state_to_nbt(s));
|
||||
let tag = entity_nbt::arrow_to_nbt(p, uuid, owner, in_block, raw.as_ref())
|
||||
.or_else(|| entity_nbt::fireball_to_nbt(p, uuid, owner, raw.as_ref()))
|
||||
.or_else(|| entity_nbt::throwable_to_nbt(p, uuid, owner, raw.as_ref()))?;
|
||||
.or_else(|| entity_nbt::throwable_to_nbt(p, uuid, owner, raw.as_ref()))
|
||||
.or_else(|| entity_nbt::spit_or_bullet_to_nbt(p, uuid, owner, raw.as_ref()))?;
|
||||
Some((
|
||||
chunk_of(p.position),
|
||||
with_raw(tag, self.persist.raw.get(&p.id)),
|
||||
|
||||
@@ -5781,6 +5781,20 @@ impl Game<'_> {
|
||||
if self.integrated.is_none() && self.server_address().is_some() {
|
||||
keys.analog = None;
|
||||
}
|
||||
let [mut strafe, mut forward] = keys.direction();
|
||||
let original = [strafe, forward];
|
||||
self.native_movement_input(&mut forward, &mut strafe, &mut keys.jump, &mut keys.shift);
|
||||
if [strafe, forward] != original {
|
||||
if forward != original[1] {
|
||||
keys.forward = forward > 0.0;
|
||||
keys.backward = forward < 0.0;
|
||||
}
|
||||
if strafe != original[0] {
|
||||
keys.left = strafe > 0.0;
|
||||
keys.right = strafe < 0.0;
|
||||
}
|
||||
keys.analog = Some([strafe, forward]);
|
||||
}
|
||||
let input = u8::from(keys.forward)
|
||||
| u8::from(keys.backward) << 1
|
||||
| u8::from(keys.left) << 2
|
||||
|
||||
@@ -98,7 +98,7 @@ fn projectile_kind(kind: &str) -> Option<ProjectileKind> {
|
||||
"minecraft:breeze_wind_charge" => ProjectileKind::BreezeWindCharge,
|
||||
// Homing state stays on the server; the client interpolates its positions.
|
||||
"minecraft:shulker_bullet" => ProjectileKind::ShulkerBullet {
|
||||
target: horizon_client::mobs::Who::Player,
|
||||
target: Some(horizon_client::mobs::Who::Player),
|
||||
dir: None,
|
||||
steps: 0,
|
||||
delta: [0.0; 3],
|
||||
|
||||
@@ -222,7 +222,10 @@ impl Game<'_> {
|
||||
if r.trial {
|
||||
tag.insert("PersistenceRequired".into(), Tag::Byte(1));
|
||||
}
|
||||
let tag = Tag::Compound(tag);
|
||||
let mut tag = Tag::Compound(tag);
|
||||
// EntityType.loadEntityRecursive applies the spawner's placement
|
||||
// callback to every passenger as well as the root.
|
||||
super::commands::place_summoned(&mut tag, r.position);
|
||||
let mob = horizon_client::entity_nbt::mob_from_nbt(
|
||||
&tag,
|
||||
self.res.mob_types(),
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
//! Horizon: native Rust client for Minecraft Java Edition 26.3 gameplay.
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
#[cfg(not(feature = "system-allocator"))]
|
||||
#[cfg(not(any(feature = "system-allocator", feature = "alloc-census")))]
|
||||
#[global_allocator]
|
||||
static ALLOCATOR: horizon_threads::Allocator = horizon_threads::Allocator;
|
||||
#[cfg(all(feature = "alloc-census", not(feature = "system-allocator")))]
|
||||
#[global_allocator]
|
||||
static ALLOCATOR: horizon_threads::CensusAllocator = horizon_threads::CensusAllocator;
|
||||
|
||||
/// The mod API level this game serves, kept in the program for the launcher
|
||||
/// to read without running it (`horizon_mods::api_level::MARK`). `main`
|
||||
|
||||
@@ -636,6 +636,7 @@ pub fn tick(
|
||||
if m.species() == Species::Endermite {
|
||||
endermite_lifetime(m);
|
||||
}
|
||||
ageable_step(m, w, level_random, events);
|
||||
// AbstractHorse.tick runs the stand counter down with or without AI.
|
||||
if super::equine::is_equine(m.species())
|
||||
&& !super::camel::is_camel(m.species())
|
||||
@@ -1263,6 +1264,68 @@ fn play_ambient(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
fn turtle_ambient_silent(m: &Mob) -> bool {
|
||||
m.fluids.in_water || !m.on_ground || m.is_baby()
|
||||
}
|
||||
/// `AgeableMob.aiStep` runs even when `Mob.serverAiStep` is disabled.
|
||||
fn ageable_step(
|
||||
m: &mut Mob,
|
||||
w: &MobWorld,
|
||||
level_random: &mut LegacyRandom,
|
||||
events: &mut Vec<MobEvent>,
|
||||
) {
|
||||
// AgeableMob.aiStep: a forced age-up's particles (`sendParticles`, so the
|
||||
// players are sent them), then the age.
|
||||
if m.extra.forced_age_timer > 0 {
|
||||
if m.extra.forced_age_timer % 4 == 0 {
|
||||
let (w, h) = (f64::from(m.width()), f64::from(m.height()));
|
||||
let r = &mut m.random;
|
||||
let x = m.position.x + w * (2.0 * r.next_double() - 1.0);
|
||||
let y = m.position.y + h * r.next_double() + 0.5;
|
||||
let z = m.position.z + w * (2.0 * r.next_double() - 1.0);
|
||||
// sendParticles(HAPPY_VILLAGER, x, y, z, 1, 0, 0, 0, 0): exactly there, at rest.
|
||||
events.push(MobEvent::LevelParticles {
|
||||
kind: "happy_villager",
|
||||
at: [x, y, z],
|
||||
spread: [0.0; 3],
|
||||
speed: 0.0,
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
m.extra.forced_age_timer -= 1;
|
||||
}
|
||||
// AgeableMob.aiStep: an age counts only for its subclasses (a tadpole
|
||||
// counts its own age up, a zombie's baby age never moves). `canAgeUp`:
|
||||
// a baby grows unless an age lock holds it; a breeding pause counts down.
|
||||
if m.alive() && super::finalize::ageable(m.species()) {
|
||||
if can_age_up(m) {
|
||||
let (bw, bh, _) = m.dims();
|
||||
// AgeableMob.setAge(getAge() + 1): the crossing is where a species'
|
||||
// ageBoundaryReached runs (a goat's and a hoglin's attack damage, a
|
||||
// sulfur cube's size), which the age write used to skip.
|
||||
let next = m.extra.age + 1;
|
||||
super::finalize::set_age(m, next);
|
||||
// setAge at the age boundary: refreshDimensions.
|
||||
if m.extra.age >= 0 {
|
||||
m.fudge_position_after_size_change(w, bw, bh);
|
||||
if m.species() == Species::Villager {
|
||||
super::villager_brain::age_boundary_reached(m, w, level_random, events);
|
||||
}
|
||||
// Turtle.ageBoundaryReached: a grown turtle drops a scute.
|
||||
if m.species() == Species::Turtle {
|
||||
events.push(MobEvent::GiftLoot {
|
||||
mob: m.id,
|
||||
table: "minecraft:gameplay/turtle_grow",
|
||||
at: [m.position.x, m.position.y, m.position.z],
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if m.extra.age > 0 {
|
||||
m.extra.age -= 1;
|
||||
}
|
||||
}
|
||||
if super::finalize::ageable(m.species()) {
|
||||
make_age_locked_particle(m, events);
|
||||
}
|
||||
}
|
||||
|
||||
/// LivingEntity.aiStep (and Mob/AgeableMob/Animal additions).
|
||||
fn ai_step(m: &mut Mob, w: &MobWorld, level_random: &mut LegacyRandom, events: &mut Vec<MobEvent>) {
|
||||
if m.species() == Species::Parrot {
|
||||
@@ -1337,59 +1400,7 @@ fn ai_step(m: &mut Mob, w: &MobWorld, level_random: &mut LegacyRandom, events: &
|
||||
if m.kind.flags.climber {
|
||||
m.climbing = m.horizontal_collision;
|
||||
}
|
||||
// AgeableMob.aiStep: a forced age-up's particles (`sendParticles`, so the
|
||||
// players are sent them), then the age.
|
||||
if m.extra.forced_age_timer > 0 {
|
||||
if m.extra.forced_age_timer % 4 == 0 {
|
||||
let (w, h) = (f64::from(m.width()), f64::from(m.height()));
|
||||
let r = &mut m.random;
|
||||
let x = m.position.x + w * (2.0 * r.next_double() - 1.0);
|
||||
let y = m.position.y + h * r.next_double() + 0.5;
|
||||
let z = m.position.z + w * (2.0 * r.next_double() - 1.0);
|
||||
// sendParticles(HAPPY_VILLAGER, x, y, z, 1, 0, 0, 0, 0): exactly there, at rest.
|
||||
events.push(MobEvent::LevelParticles {
|
||||
kind: "happy_villager",
|
||||
at: [x, y, z],
|
||||
spread: [0.0; 3],
|
||||
speed: 0.0,
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
m.extra.forced_age_timer -= 1;
|
||||
}
|
||||
// AgeableMob.aiStep: an age counts only for its subclasses (a tadpole
|
||||
// counts its own age up, a zombie's baby age never moves). `canAgeUp`:
|
||||
// a baby grows unless an age lock holds it; a breeding pause counts down.
|
||||
if m.alive() && super::finalize::ageable(m.species()) {
|
||||
if can_age_up(m) {
|
||||
let (bw, bh, _) = m.dims();
|
||||
// AgeableMob.setAge(getAge() + 1): the crossing is where a species'
|
||||
// ageBoundaryReached runs (a goat's and a hoglin's attack damage, a
|
||||
// sulfur cube's size), which the age write used to skip.
|
||||
let next = m.extra.age + 1;
|
||||
super::finalize::set_age(m, next);
|
||||
// setAge at the age boundary: refreshDimensions.
|
||||
if m.extra.age >= 0 {
|
||||
m.fudge_position_after_size_change(w, bw, bh);
|
||||
if m.species() == Species::Villager {
|
||||
super::villager_brain::age_boundary_reached(m, w, level_random, events);
|
||||
}
|
||||
// Turtle.ageBoundaryReached: a grown turtle drops a scute.
|
||||
if m.species() == Species::Turtle {
|
||||
events.push(MobEvent::GiftLoot {
|
||||
mob: m.id,
|
||||
table: "minecraft:gameplay/turtle_grow",
|
||||
at: [m.position.x, m.position.y, m.position.z],
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if m.extra.age > 0 {
|
||||
m.extra.age -= 1;
|
||||
}
|
||||
}
|
||||
if super::finalize::ageable(m.species()) {
|
||||
make_age_locked_particle(m, events);
|
||||
}
|
||||
ageable_step(m, w, level_random, events);
|
||||
if m.alive() {
|
||||
// Animal.aiStep
|
||||
if m.extra.age != 0 {
|
||||
@@ -3431,6 +3442,75 @@ mod age_up_tests {
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Official Java 26.3: NoAI leaves both age clocks running. AgeLocked
|
||||
/// holds a baby's age, but a positive breeding cooldown still expires.
|
||||
#[test]
|
||||
fn age_clocks_tick_with_no_ai_and_respect_baby_age_lock() {
|
||||
let ran = crate::mobs::world::with_test_world(&[], |w, types| {
|
||||
for species in ["pig", "cow", "horse", "turtle"] {
|
||||
for no_ai in [true, false] {
|
||||
for locked in [false, true] {
|
||||
for age in [-2, 2] {
|
||||
let kind =
|
||||
Arc::new(types.types[&format!("minecraft:{species}")].clone());
|
||||
let mut m = Mob::new(1, kind, Vec3::new(4.0, 220.0, 4.0), 0.0);
|
||||
m.no_ai = no_ai;
|
||||
m.no_gravity = true;
|
||||
m.persistent = true;
|
||||
m.extra.age_locked = locked;
|
||||
super::super::finalize::set_age(&mut m, age);
|
||||
let (mut random, mut events) = (LegacyRandom::new(0), Vec::new());
|
||||
let expected = if age > 0 {
|
||||
[1, 0, 0, 0]
|
||||
} else if locked {
|
||||
[-2, -2, -2, -2]
|
||||
} else {
|
||||
[-1, 0, 0, 0]
|
||||
};
|
||||
for (elapsed, expected_age) in expected.into_iter().enumerate() {
|
||||
let previous_age = m.extra.age;
|
||||
let previous_position = m.position;
|
||||
tick(&mut m, w, &mut random, &mut events);
|
||||
assert_eq!(
|
||||
m.extra.age,
|
||||
expected_age,
|
||||
"{species}: NoAI={no_ai}, AgeLocked={locked}, initial Age={age}, tick={}",
|
||||
elapsed + 1
|
||||
);
|
||||
if no_ai {
|
||||
assert_eq!(m.velocity, Vec3::ZERO);
|
||||
// Growing dimensions can reposition the mob at adulthood.
|
||||
if previous_age != -1 || expected_age != 0 {
|
||||
assert_eq!(m.position, previous_position);
|
||||
}
|
||||
}
|
||||
}
|
||||
if species == "turtle" && age < 0 && !locked {
|
||||
assert_eq!(
|
||||
events
|
||||
.iter()
|
||||
.filter(|event| matches!(
|
||||
event,
|
||||
MobEvent::GiftLoot {
|
||||
table: "minecraft:gameplay/turtle_grow",
|
||||
..
|
||||
}
|
||||
))
|
||||
.count(),
|
||||
1,
|
||||
"growing up drops one scute"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if !ran {
|
||||
eprintln!("age clock regression needs prepared vanilla resources");
|
||||
}
|
||||
}
|
||||
|
||||
/// A baby goat `age` ticks from adulthood, from no vanilla data.
|
||||
fn baby_goat(age: i32) -> Mob {
|
||||
let mut kind = crate::mobs::sensor_scan_tests::kind();
|
||||
|
||||
+47
-8
@@ -714,12 +714,14 @@ pub trait Hooks: Send + Sync {
|
||||
/// back out as if never added, and a `/summon` of it fails. Tells every
|
||||
/// kind in the game's entity lists (items, orbs, mobs, boats, minecarts,
|
||||
/// armor stands, primed TNT, falling blocks, projectiles, rockets,
|
||||
/// tridents), once each, in the order they were created; check `kind`. A
|
||||
/// gameplay spawn is told when the next tick starts (a drop, an orb, a mob, a
|
||||
/// `/summon` and a chunk's entities are told at once). A mod that
|
||||
/// cancels one of the others sees it removed before it ticks again.
|
||||
/// Not told: end crystals, the dragon, cushions, paintings, item frames and
|
||||
/// the entities Horizon keeps without simulating. Server-side.
|
||||
/// tridents, end crystals, the dragon, cushions, paintings, item frames),
|
||||
/// once each, in the order they were created; check `kind`. A drop, an
|
||||
/// orb, a mob, a `/summon` and a chunk's entities are told at once, any
|
||||
/// other spawn between the tick's phases (it is checked after the blocks,
|
||||
/// the vehicles, the projectiles, the items and the mobs, and at the
|
||||
/// tick's end). A mod that cancels one of the others sees it removed
|
||||
/// before it ticks again. Not told: the entities Horizon keeps without
|
||||
/// simulating (displays, markers, interactions). Server-side.
|
||||
///
|
||||
/// Return `Flow::Cancel` to take it: this sets `canceled`, and later mods are skipped.
|
||||
fn entity_join_level(&self, host: &mut dyn Host, event: &mut EntityJoinLevelEvent<'_>) -> Flow {
|
||||
@@ -763,8 +765,10 @@ pub trait Hooks: Send + Sync {
|
||||
/// a level closing with nobody in it included. Not for the entities still
|
||||
/// loaded when the server stops, as in vanilla. `entity` is its ID and
|
||||
/// `kind` its type (`minecraft:item`); it is no longer in the level's lists.
|
||||
/// It only hears. 53 tells item entities, experience orbs and mobs (`kind`
|
||||
/// the mob type); later APIs add other kinds, so check `kind`. Server-side.
|
||||
/// It only hears. 53 told item entities, experience orbs and mobs; it now
|
||||
/// tells every kind `native_entity_join_level` does (`kind` is the type), each
|
||||
/// once, when the game next notices it gone (the tick or action that removed
|
||||
/// it). Check `kind`. Server-side.
|
||||
fn entity_leave_level(&self, host: &mut dyn Host, event: &mut EntityLeaveLevelEvent<'_>) {
|
||||
let _ = host;
|
||||
let _ = event;
|
||||
@@ -4508,6 +4512,11 @@ pub trait Hooks: Send + Sync {
|
||||
fn living_jump(&self, event: &mut LivingJumpEvent<'_>) {
|
||||
let _ = event;
|
||||
}
|
||||
/// Local player movement input, after reading it and before applying it.
|
||||
fn movement_input(&self, host: &dyn Host, event: &mut MovementInputEvent) {
|
||||
let _ = host;
|
||||
let _ = event;
|
||||
}
|
||||
}
|
||||
/// What [`Hooks::frame`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
|
||||
#[non_exhaustive]
|
||||
@@ -13795,3 +13804,33 @@ pub fn dispatch_living_jump(mods: &[Box<dyn Hooks>], entity: u32, kind: &str) {
|
||||
mod_hook.living_jump(&mut event);
|
||||
}
|
||||
}
|
||||
/// What [`Hooks::movement_input`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
|
||||
#[non_exhaustive]
|
||||
pub struct MovementInputEvent {
|
||||
pub forward: f32,
|
||||
pub strafe: f32,
|
||||
pub jump: bool,
|
||||
pub sneak: bool,
|
||||
}
|
||||
pub fn dispatch_movement_input(
|
||||
host: &dyn Host,
|
||||
mods: &[Box<dyn Hooks>],
|
||||
forward: &mut f32,
|
||||
strafe: &mut f32,
|
||||
jump: &mut bool,
|
||||
sneak: &mut bool,
|
||||
) {
|
||||
let mut event = MovementInputEvent {
|
||||
forward: *forward,
|
||||
strafe: *strafe,
|
||||
jump: *jump,
|
||||
sneak: *sneak,
|
||||
};
|
||||
for mod_hook in super::entered(mods, "movement_input") {
|
||||
mod_hook.movement_input(host, &mut event);
|
||||
}
|
||||
*forward = event.forward;
|
||||
*strafe = event.strafe;
|
||||
*jump = event.jump;
|
||||
*sneak = event.sneak;
|
||||
}
|
||||
@@ -2033,9 +2033,26 @@ fn link(target: &Path, at: &Path) -> std::io::Result<()> {
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
fs::copy(target, at).map(|_| ())
|
||||
link_file(target, at, |target, at| fs::hard_link(target, at))
|
||||
}
|
||||
}
|
||||
/// A file of the overlay where symlinks need a privilege: a hard link to the base's file, which costs a name where
|
||||
/// a copy costs the file's bytes (the base is ~28,000 files, 0.6 GiB), or a copy when no link can be made (another
|
||||
/// volume, a filesystem without links, the NTFS limit on links to one file). A link shares the base file's data,
|
||||
/// which is safe because the overlay is only ever replaced name by name (`Overlay::write` removes the name before it
|
||||
/// writes), never written in place. A symlink is copied through, as before: linked, it would stay a symlink.
|
||||
#[cfg(any(not(unix), test))]
|
||||
fn link_file(
|
||||
target: &Path,
|
||||
at: &Path,
|
||||
hard_link: impl FnOnce(&Path, &Path) -> std::io::Result<()>,
|
||||
) -> std::io::Result<()> {
|
||||
let symlink = fs::symlink_metadata(target).is_ok_and(|m| m.file_type().is_symlink());
|
||||
if !symlink && hard_link(target, at).is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
fs::copy(target, at).map(|_| ())
|
||||
}
|
||||
|
||||
/// A tag file's JSON text with `more` appended to its values (no repeats).
|
||||
fn merge_tag(text: Option<&str>, more: &[J], what: &str) -> Result<String> {
|
||||
|
||||
@@ -3774,3 +3774,168 @@ fn a_feature_gives_a_mods_sign_its_block_entity() {
|
||||
assert_eq!(generate(true), (placed, placed));
|
||||
let _ = fs::remove_dir_all(&scratch);
|
||||
}
|
||||
|
||||
/// A small stand-in for the base root: nested folders, an empty file, an empty folder.
|
||||
fn small_base(base: &Path) {
|
||||
for (name, bytes) in [
|
||||
("items.json", &b"{}"[..]),
|
||||
("empty.log", &b""[..]),
|
||||
("assets/minecraft/lang/en_us.json", &b"{\"a\": 1}"[..]),
|
||||
(
|
||||
"data/minecraft/tags/block/logs.json",
|
||||
&b"{\"values\": []}"[..],
|
||||
),
|
||||
] {
|
||||
let path = base.join(name);
|
||||
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
fs::write(path, bytes).unwrap();
|
||||
}
|
||||
fs::create_dir_all(base.join("worldgen/empty-folder")).unwrap();
|
||||
}
|
||||
|
||||
/// Every folder (`None`) and file (`Some(bytes)`) under `root`, by relative path.
|
||||
fn tree(root: &Path) -> BTreeMap<String, Option<Vec<u8>>> {
|
||||
fn walk(root: &Path, dir: &Path, out: &mut BTreeMap<String, Option<Vec<u8>>>) {
|
||||
for entry in fs::read_dir(dir).unwrap().flatten() {
|
||||
let path = entry.path();
|
||||
let rel = path
|
||||
.strip_prefix(root)
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
if path.is_dir() {
|
||||
out.insert(rel, None);
|
||||
walk(root, &path, out);
|
||||
} else {
|
||||
out.insert(rel, Some(fs::read(&path).unwrap()));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = BTreeMap::new();
|
||||
walk(root, root, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
/// Writes into a file where it is (same data), not by replacing it: what a link shares and a copy does not.
|
||||
fn edit_in_place(path: &Path, bytes: &[u8]) {
|
||||
use std::io::Write;
|
||||
let mut file = fs::File::options()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(path)
|
||||
.unwrap();
|
||||
file.write_all(bytes).unwrap();
|
||||
}
|
||||
|
||||
/// The overlay looks like the base, and shares its files (a hard link on Windows, a symlink elsewhere) rather than
|
||||
/// holding copies of them.
|
||||
#[test]
|
||||
fn the_overlay_shares_the_base_files() {
|
||||
let scratch = std::env::temp_dir().join(format!("horizon-mod-share-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&scratch);
|
||||
let (base, out) = (scratch.join("base"), scratch.join("root"));
|
||||
small_base(&base);
|
||||
Overlay::new(&base, &out).unwrap();
|
||||
assert_eq!(tree(&out), tree(&base));
|
||||
edit_in_place(
|
||||
&base.join("assets/minecraft/lang/en_us.json"),
|
||||
b"{\"a\": 2}",
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(out.join("assets/minecraft/lang/en_us.json")).unwrap(),
|
||||
b"{\"a\": 2}",
|
||||
"the overlay copied the base file instead of linking it"
|
||||
);
|
||||
let _ = fs::remove_dir_all(&scratch);
|
||||
}
|
||||
|
||||
/// Writing a file of the overlay replaces the name, so the base file it was linked to keeps its bytes and the other
|
||||
/// files of the overlay still follow the base.
|
||||
#[test]
|
||||
fn writing_into_the_overlay_leaves_the_base_alone() {
|
||||
let scratch = std::env::temp_dir().join(format!("horizon-mod-write-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&scratch);
|
||||
let (base, out) = (scratch.join("base"), scratch.join("root"));
|
||||
small_base(&base);
|
||||
let overlay = Overlay::new(&base, &out).unwrap();
|
||||
let before = tree(&base);
|
||||
overlay
|
||||
.write(
|
||||
"data/minecraft/tags/block/logs.json",
|
||||
b"{\"values\": [\"mod:log\"]}",
|
||||
)
|
||||
.unwrap();
|
||||
overlay.write("items.json", b"{\"mod\": 1}").unwrap();
|
||||
overlay.write("assets/mod/new.json", b"{}").unwrap();
|
||||
assert_eq!(tree(&base), before, "a write reached the base");
|
||||
assert_eq!(
|
||||
fs::read(out.join("data/minecraft/tags/block/logs.json")).unwrap(),
|
||||
b"{\"values\": [\"mod:log\"]}"
|
||||
);
|
||||
assert_eq!(fs::read(out.join("items.json")).unwrap(), b"{\"mod\": 1}");
|
||||
edit_in_place(
|
||||
&base.join("assets/minecraft/lang/en_us.json"),
|
||||
b"{\"a\": 3}",
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(out.join("assets/minecraft/lang/en_us.json")).unwrap(),
|
||||
b"{\"a\": 3}"
|
||||
);
|
||||
// A rebuild starts by removing the overlay: that removes names, not the base's data.
|
||||
fs::remove_dir_all(&out).unwrap();
|
||||
assert_eq!(fs::read(base.join("items.json")).unwrap(), b"{}");
|
||||
let _ = fs::remove_dir_all(&scratch);
|
||||
}
|
||||
|
||||
/// Where a hard link cannot be made (another volume, a filesystem without links) a file is copied, as it always was
|
||||
/// on Windows; where it can, the file is linked.
|
||||
#[test]
|
||||
fn a_file_is_linked_or_else_copied() {
|
||||
let scratch = std::env::temp_dir().join(format!("horizon-mod-linkfile-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&scratch);
|
||||
fs::create_dir_all(&scratch).unwrap();
|
||||
let target = scratch.join("base.json");
|
||||
fs::write(&target, b"original").unwrap();
|
||||
|
||||
let linked = scratch.join("linked.json");
|
||||
link_file(&target, &linked, |from, to| fs::hard_link(from, to)).unwrap();
|
||||
edit_in_place(&target, b"edited");
|
||||
assert_eq!(fs::read(&linked).unwrap(), b"edited", "not a link");
|
||||
|
||||
let copied = scratch.join("copied.json");
|
||||
fs::write(&target, b"original").unwrap();
|
||||
link_file(&target, &copied, |_, _| {
|
||||
Err(std::io::Error::other("no links here"))
|
||||
})
|
||||
.unwrap();
|
||||
edit_in_place(&target, b"edited");
|
||||
assert_eq!(fs::read(&copied).unwrap(), b"original", "not a copy");
|
||||
|
||||
// A target that is not there is an error whichever way the file is made.
|
||||
let missing = link_file(
|
||||
&scratch.join("missing.json"),
|
||||
&scratch.join("x.json"),
|
||||
|from, to| fs::hard_link(from, to),
|
||||
);
|
||||
assert!(missing.is_err());
|
||||
let _ = fs::remove_dir_all(&scratch);
|
||||
}
|
||||
|
||||
/// A symlink in the base is copied through, as before; linked, the overlay's file would be a symlink too.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn a_symlink_file_is_copied_through() {
|
||||
let scratch = std::env::temp_dir().join(format!("horizon-mod-symfile-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&scratch);
|
||||
fs::create_dir_all(&scratch).unwrap();
|
||||
fs::write(scratch.join("real.json"), b"real").unwrap();
|
||||
std::os::unix::fs::symlink("real.json", scratch.join("alias.json")).unwrap();
|
||||
let at = scratch.join("copy.json");
|
||||
link_file(&scratch.join("alias.json"), &at, |from, to| {
|
||||
fs::hard_link(from, to)
|
||||
})
|
||||
.unwrap();
|
||||
assert!(!fs::symlink_metadata(&at).unwrap().file_type().is_symlink());
|
||||
assert_eq!(fs::read(&at).unwrap(), b"real");
|
||||
let _ = fs::remove_dir_all(&scratch);
|
||||
}
|
||||
@@ -94,9 +94,10 @@ pub enum ProjectileKind {
|
||||
/// flight that bursts on whatever it meets.
|
||||
WindCharge,
|
||||
BreezeWindCharge,
|
||||
/// `ShulkerBullet`: homing on `target` in axis-aligned steps.
|
||||
/// `ShulkerBullet`: homing on `target` in axis-aligned steps; one with
|
||||
/// no target (a summoned one) falls like any projectile.
|
||||
ShulkerBullet {
|
||||
target: Who,
|
||||
target: Option<Who>,
|
||||
/// The current step direction (3D data value), if any.
|
||||
dir: Option<u8>,
|
||||
steps: i32,
|
||||
@@ -116,6 +117,8 @@ pub enum ProjectileKind {
|
||||
/// type is the entity's own and `/summon` can pair either with any `Item`,
|
||||
/// so it is kept next to the stack (a thrown potion's follows its item).
|
||||
Potion(Box<crate::items::ItemStack>, bool),
|
||||
/// `ThrownExperienceBottle`: its own entity type, gravity 0.07.
|
||||
ExperienceBottle,
|
||||
/// a content mod's type.
|
||||
Thrown(&'static ThrownType),
|
||||
}
|
||||
@@ -148,6 +151,7 @@ impl ProjectileKind {
|
||||
| Self::EnderPearl
|
||||
| Self::LlamaSpit
|
||||
| Self::Potion(..)
|
||||
| Self::ExperienceBottle
|
||||
| Self::Thrown(_) => 0.25,
|
||||
Self::SmallFireball
|
||||
| Self::WindCharge
|
||||
@@ -175,6 +179,7 @@ impl ProjectileKind {
|
||||
// `ThrownSplashPotion`/`ThrownLingeringPotion`: the item names it.
|
||||
Self::Potion(_, true) => "minecraft:lingering_potion",
|
||||
Self::Potion(_, false) => "minecraft:splash_potion",
|
||||
Self::ExperienceBottle => "minecraft:experience_bottle",
|
||||
Self::Thrown(t) => t.id,
|
||||
}
|
||||
}
|
||||
@@ -314,13 +319,16 @@ impl crate::entity_rules::EntityRules for Projectile {
|
||||
}
|
||||
/// Each projectile class's `getDefaultGravity`.
|
||||
fn get_default_gravity(&self) -> f64 {
|
||||
match self.kind {
|
||||
match &self.kind {
|
||||
ProjectileKind::Arrow { .. } | ProjectileKind::Trident => 0.05,
|
||||
ProjectileKind::SmallFireball
|
||||
| ProjectileKind::LargeFireball { .. }
|
||||
| ProjectileKind::WitherSkull { .. } => 0.0,
|
||||
ProjectileKind::LlamaSpit => 0.06,
|
||||
// `ThrownExperienceBottle.getDefaultGravity` is 0.07.
|
||||
ProjectileKind::ExperienceBottle => 0.07,
|
||||
ProjectileKind::Potion { .. } => 0.05,
|
||||
ProjectileKind::ShulkerBullet { .. } => 0.04,
|
||||
_ => 0.03,
|
||||
}
|
||||
}
|
||||
@@ -465,7 +473,7 @@ impl Projectile {
|
||||
|| self
|
||||
.arrow
|
||||
.as_ref()
|
||||
.is_some_and(|a| &*a.pickup_item.item == "minecraft:spectral_arrow")
|
||||
.is_some_and(|a| a.spectral || &*a.pickup_item.item == "minecraft:spectral_arrow")
|
||||
}
|
||||
/// `Arrow.ID_EFFECT_COLOR` (`updateColor`): the opaque colour of the
|
||||
/// potion the arrow carries, from its tipped pickup item or the effect a
|
||||
@@ -598,7 +606,7 @@ impl Projectile {
|
||||
);
|
||||
let mut p = Self::new(
|
||||
ProjectileKind::ShulkerBullet {
|
||||
target,
|
||||
target: Some(target),
|
||||
dir: Some(1),
|
||||
steps: 0,
|
||||
delta: [0.0; 3],
|
||||
@@ -629,7 +637,11 @@ impl Projectile {
|
||||
]
|
||||
}
|
||||
fn target_box(&self, w: &impl ProjectileWorld) -> Option<Aabb> {
|
||||
let ProjectileKind::ShulkerBullet { target, .. } = self.kind else {
|
||||
let ProjectileKind::ShulkerBullet {
|
||||
target: Some(target),
|
||||
..
|
||||
} = self.kind
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
w.targets()
|
||||
@@ -743,7 +755,8 @@ impl Projectile {
|
||||
v.z + (dl[2] - v.z) * 0.2,
|
||||
);
|
||||
} else {
|
||||
self.velocity.y -= 0.04;
|
||||
// `Entity.applyGravity`: `getGravity` is 0 under NoGravity.
|
||||
self.velocity.y -= crate::entity_rules::EntityRules::get_gravity(self);
|
||||
}
|
||||
let from = self.position;
|
||||
let to = from.add_vector(self.velocity);
|
||||
@@ -1004,6 +1017,10 @@ impl Projectile {
|
||||
pub fn drawn_item(&self) -> Option<crate::items::ItemStack> {
|
||||
match &self.kind {
|
||||
ProjectileKind::Potion(stack, _) => Some((**stack).clone()),
|
||||
ProjectileKind::ExperienceBottle => Some(crate::items::ItemStack::new(
|
||||
"minecraft:experience_bottle",
|
||||
1,
|
||||
)),
|
||||
ProjectileKind::Thrown(t) => Some(crate::items::ItemStack::new(t.item, 1)),
|
||||
_ => None,
|
||||
}
|
||||
@@ -1018,6 +1035,7 @@ impl Projectile {
|
||||
"item/fire_charge"
|
||||
}
|
||||
ProjectileKind::Potion(stack, _) => potion_sprite(&stack.item),
|
||||
ProjectileKind::ExperienceBottle => "item/experience_bottle",
|
||||
ProjectileKind::LlamaSpit => "entity/llama/llama_spit",
|
||||
// Drawn as `WindChargeModel` and `ShulkerBulletModel` by the game.
|
||||
ProjectileKind::WindCharge
|
||||
@@ -1298,6 +1316,38 @@ mod tests {
|
||||
assert_eq!(settings("ruby:late"), (5, 3, true));
|
||||
}
|
||||
|
||||
/// `ShulkerBullet.tick` with no target: `applyGravity` (0.04, none under
|
||||
/// NoGravity), then the move, no drag. 20 ticks from y 110 fall 8.4.
|
||||
#[test]
|
||||
fn a_shulker_bullet_without_a_target_falls_unless_no_gravity() {
|
||||
let bullet = || {
|
||||
Projectile::new(
|
||||
ProjectileKind::ShulkerBullet {
|
||||
target: None,
|
||||
dir: None,
|
||||
steps: 0,
|
||||
delta: [0.0; 3],
|
||||
seed: 0,
|
||||
},
|
||||
Vec3::new(0.5, 110.0, 0.5),
|
||||
None,
|
||||
)
|
||||
};
|
||||
let w = Empty(vec![]);
|
||||
let mut falling = bullet();
|
||||
let mut held = bullet();
|
||||
held.no_gravity = true;
|
||||
for _ in 0..20 {
|
||||
falling.tick(&w);
|
||||
held.tick(&w);
|
||||
}
|
||||
assert!(
|
||||
(falling.position.y - 101.6).abs() < 1e-6,
|
||||
"{}",
|
||||
falling.position.y
|
||||
);
|
||||
assert_eq!(held.position, Vec3::new(0.5, 110.0, 0.5));
|
||||
}
|
||||
#[test]
|
||||
fn llama_spit_moves_first_then_drags_and_falls() {
|
||||
let mut p = Projectile::new(ProjectileKind::LlamaSpit, Vec3::new(0.0, 100.0, 0.0), None);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! A mod crate's view of the mod API: `Hooks` is implemented with the names `mod_api` exports.
|
||||
//! The test is that this file builds; #7044's mod could not name `key`'s event.
|
||||
use horizon_client::mod_api::{Hooks, Host, InputKeyEvent, KeyEvent, winit};
|
||||
use horizon_client::mod_api::{
|
||||
Hooks, Host, InputKeyEvent, KeyEvent, MovementInputEvent, dispatch_movement_input, winit,
|
||||
};
|
||||
|
||||
struct Keys;
|
||||
|
||||
@@ -18,3 +20,29 @@ impl Hooks for Keys {
|
||||
fn a_mod_implements_the_key_hooks() {
|
||||
let _: Box<dyn Hooks> = Box::new(Keys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn movement_input_writes_back_all_fields() {
|
||||
struct TestHost;
|
||||
impl Host for TestHost {}
|
||||
struct Movement;
|
||||
impl Hooks for Movement {
|
||||
fn movement_input(&self, _host: &dyn Host, event: &mut MovementInputEvent) {
|
||||
event.forward = -0.5;
|
||||
event.strafe = 0.25;
|
||||
event.jump = true;
|
||||
event.sneak = true;
|
||||
}
|
||||
}
|
||||
let mods: Vec<Box<dyn Hooks>> = vec![Box::new(Movement)];
|
||||
let (mut forward, mut strafe, mut jump, mut sneak) = (1.0, 0.0, false, false);
|
||||
dispatch_movement_input(
|
||||
&TestHost,
|
||||
&mods,
|
||||
&mut forward,
|
||||
&mut strafe,
|
||||
&mut jump,
|
||||
&mut sneak,
|
||||
);
|
||||
assert_eq!((forward, strafe, jump, sneak), (-0.5, 0.25, true, true));
|
||||
}
|
||||
@@ -467,6 +467,124 @@ fn upgrade_condition(c: &mut Value) {
|
||||
{
|
||||
*c = json!(named);
|
||||
}
|
||||
match c["type"].as_str() {
|
||||
Some("minecraft:entity_properties") => {
|
||||
if let Some(p) = c.get_mut("predicate") {
|
||||
upgrade_entity_predicate(p);
|
||||
}
|
||||
}
|
||||
Some("minecraft:damage_source_properties") => {
|
||||
for key in ["direct_entity", "source_entity"] {
|
||||
if let Some(p) = c["predicate"].get_mut(key) {
|
||||
upgrade_entity_predicate(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// The fields of a 1.21 entity predicate that are, in 26.3, sub-predicates
|
||||
/// keyed by their registry name (`minecraft:<field>`), as 26.3's own data
|
||||
/// writes them (`"minecraft:flags": {"is_on_fire": true}`).
|
||||
const ENTITY_FIELDS: [&str; 15] = [
|
||||
"distance",
|
||||
"movement",
|
||||
"location",
|
||||
"stepping_on",
|
||||
"movement_affected_by",
|
||||
"effects",
|
||||
"nbt",
|
||||
"flags",
|
||||
"equipment",
|
||||
"periodic_tick",
|
||||
"team",
|
||||
"slots",
|
||||
"components",
|
||||
"predicates",
|
||||
"type_specific",
|
||||
];
|
||||
|
||||
/// Entity predicates nested in an entity predicate (1.21 field names).
|
||||
const ENTITY_NESTED: [&str; 3] = ["vehicle", "passenger", "targeted_entity"];
|
||||
|
||||
/// Kinds of 1.21's `type_specific` whose `variant` 26.3 matches as a component
|
||||
/// (`"minecraft:components": {"minecraft:cat/variant": ...}`).
|
||||
const VARIANT_KINDS: [&str; 14] = [
|
||||
"axolotl",
|
||||
"cat",
|
||||
"chicken",
|
||||
"cow",
|
||||
"fox",
|
||||
"frog",
|
||||
"horse",
|
||||
"llama",
|
||||
"mooshroom",
|
||||
"painting",
|
||||
"parrot",
|
||||
"pig",
|
||||
"rabbit",
|
||||
"wolf",
|
||||
];
|
||||
|
||||
/// A 1.21 entity predicate in 26.3's shape, in place: `type` is
|
||||
/// `minecraft:entity_type`, every other field `minecraft:<field>`, `vehicle`,
|
||||
/// `passenger` and `targeted_entity` recursively; `type_specific {"type": k,
|
||||
/// ...}` is `minecraft:type_specific/<k>` (`slime` is `cube_mob`), a variant
|
||||
/// kind's `variant` and a sheep's `color` go to `minecraft:components`. The
|
||||
/// game's loot code reads only these keys, so an old-shaped predicate never
|
||||
/// matched. A predicate already in 26.3's shape stays as it is.
|
||||
pub(crate) fn upgrade_entity_predicate(p: &mut Value) {
|
||||
let Some(o) = p.as_object_mut() else {
|
||||
return;
|
||||
};
|
||||
let key = |f: &str| format!("minecraft:{f}");
|
||||
if let Some(t) = o.remove("type") {
|
||||
o.insert(key("entity_type"), t);
|
||||
}
|
||||
for f in ENTITY_NESTED {
|
||||
if let Some(mut inner) = o.remove(f) {
|
||||
upgrade_entity_predicate(&mut inner);
|
||||
o.insert(key(f), inner);
|
||||
}
|
||||
}
|
||||
for f in ENTITY_FIELDS {
|
||||
if let Some(v) = o.remove(f) {
|
||||
o.insert(key(f), v);
|
||||
}
|
||||
}
|
||||
let Some(Value::Object(mut ts)) = o.remove("minecraft:type_specific") else {
|
||||
return;
|
||||
};
|
||||
let kind = ts.remove("type");
|
||||
let kind = kind.as_ref().and_then(Value::as_str).unwrap_or("any");
|
||||
let kind = match kind.trim_start_matches("minecraft:") {
|
||||
"slime" => "cube_mob",
|
||||
k => k,
|
||||
};
|
||||
let component = match kind {
|
||||
k if VARIANT_KINDS.contains(&k) => {
|
||||
ts.remove("variant").map(|v| (format!("{k}/variant"), v))
|
||||
}
|
||||
"sheep" => ts.remove("color").map(|v| ("sheep/color".to_owned(), v)),
|
||||
_ => None,
|
||||
};
|
||||
if let Some((name, v)) = component {
|
||||
let components = o.entry(key("components")).or_insert_with(|| json!({}));
|
||||
if let Some(c) = components.as_object_mut() {
|
||||
c.insert(key(&name), v);
|
||||
}
|
||||
}
|
||||
// A player's `looking_at`, a lightning bolt's `entity_struck`.
|
||||
for f in ["looking_at", "entity_struck"] {
|
||||
if let Some(v) = ts.get_mut(f) {
|
||||
upgrade_entity_predicate(v);
|
||||
}
|
||||
}
|
||||
// A variant kind with nothing else to match is the component alone.
|
||||
if !ts.is_empty() || !(kind == "any" || VARIANT_KINDS.contains(&kind)) {
|
||||
o.insert(key(&format!("type_specific/{kind}")), Value::Object(ts));
|
||||
}
|
||||
}
|
||||
|
||||
/// 26.3's named tool conditions for the 1.21.1 predicates they replace.
|
||||
@@ -1424,6 +1542,59 @@ mod tests {
|
||||
assert_eq!(new, glass);
|
||||
}
|
||||
|
||||
/// Entity predicates in 26.3's keys, as its own tables write them; the
|
||||
/// 1.21 shape never matched in the game's loot code.
|
||||
#[test]
|
||||
fn entity_predicates_get_26_3_keys() {
|
||||
let mut t = json!({"pools": [{"rolls": 1, "entries": [{"type": "minecraft:item", "name": "m:hide",
|
||||
"functions": [{"function": "minecraft:furnace_smelt", "conditions": [{
|
||||
"condition": "minecraft:entity_properties", "entity": "this",
|
||||
"predicate": {"flags": {"is_on_fire": true}}}]}],
|
||||
"conditions": [
|
||||
{"condition": "minecraft:entity_properties", "entity": "this",
|
||||
"predicate": {"type": "minecraft:cow", "vehicle": {"type": "#minecraft:boats"},
|
||||
"type_specific": {"type": "minecraft:frog", "variant": "minecraft:warm"}}},
|
||||
{"condition": "minecraft:damage_source_properties", "predicate": {
|
||||
"source_entity": {"type": "minecraft:player", "equipment": {"mainhand": {"items": "minecraft:shears"}}},
|
||||
"is_direct": true}},
|
||||
{"condition": "minecraft:entity_properties", "entity": "attacker",
|
||||
"predicate": {"type_specific": {"type": "minecraft:slime", "size": {"min": 2}}}},
|
||||
{"condition": "minecraft:entity_properties", "entity": "this",
|
||||
"predicate": {"type_specific": {"type": "sheep", "sheared": false, "color": "black"}}}
|
||||
]}]}]});
|
||||
upgrade_loot(&mut t);
|
||||
let e = &t["pools"][0]["entries"][0];
|
||||
assert_eq!(
|
||||
e["modifier"][0]["condition"]["predicate"],
|
||||
json!({"minecraft:flags": {"is_on_fire": true}})
|
||||
);
|
||||
let c = &e["condition"]["terms"];
|
||||
assert_eq!(
|
||||
c[0]["predicate"],
|
||||
json!({"minecraft:entity_type": "minecraft:cow",
|
||||
"minecraft:vehicle": {"minecraft:entity_type": "#minecraft:boats"},
|
||||
"minecraft:components": {"minecraft:frog/variant": "minecraft:warm"}})
|
||||
);
|
||||
assert_eq!(
|
||||
c[1]["predicate"],
|
||||
json!({"source_entity": {"minecraft:entity_type": "minecraft:player",
|
||||
"minecraft:equipment": {"mainhand": {"items": "minecraft:shears"}}}, "is_direct": true})
|
||||
);
|
||||
assert_eq!(
|
||||
c[2]["predicate"],
|
||||
json!({"minecraft:type_specific/cube_mob": {"size": {"min": 2}}})
|
||||
);
|
||||
assert_eq!(
|
||||
c[3]["predicate"],
|
||||
json!({"minecraft:type_specific/sheep": {"sheared": false},
|
||||
"minecraft:components": {"minecraft:sheep/color": "black"}})
|
||||
);
|
||||
// Already 26.3's shape: unchanged.
|
||||
let again = t.clone();
|
||||
upgrade_loot(&mut t);
|
||||
assert_eq!(t, again);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn types_bare_number_ranges() {
|
||||
// When Dungeons Arise's and Structory's chests: rolls and counts as
|
||||
|
||||
@@ -42,10 +42,12 @@ pub fn read(path: &Path) -> Result<JarInfo, String> {
|
||||
zip.by_name(name).ok()?.read_to_string(&mut s).ok()?;
|
||||
Some(s)
|
||||
};
|
||||
let mut info = if let Some(t) = text(&mut zip, "META-INF/neoforge.mods.toml")
|
||||
.or_else(|| text(&mut zip, "META-INF/mods.toml"))
|
||||
{
|
||||
mods_toml(&t)?
|
||||
let tomls: Vec<String> = ["META-INF/neoforge.mods.toml", "META-INF/mods.toml"]
|
||||
.iter()
|
||||
.filter_map(|n| text(&mut zip, n))
|
||||
.collect();
|
||||
let mut info = if !tomls.is_empty() {
|
||||
metadata(&tomls)?
|
||||
} else if let Some(t) = text(&mut zip, "fabric.mod.json") {
|
||||
fabric(&t)?
|
||||
} else if let Some(t) = text(&mut zip, "mcmod.info") {
|
||||
@@ -84,6 +86,71 @@ fn manifest_value(manifest: &str, key: &str) -> Option<String> {
|
||||
.filter(|v| !v.is_empty())
|
||||
}
|
||||
|
||||
/// The jar's mod from its metadata files (`neoforge.mods.toml` first, then
|
||||
/// `mods.toml`). A jar built for one loader may carry the other loader's file
|
||||
/// as an unfilled template (`modId = "${id}"`, `[[dependencies.${id}]]`), which
|
||||
/// its own loader never reads: the first file that parses and names a real mod
|
||||
/// id is the one. When none parses, the first file is read line by line
|
||||
/// ([`mods_toml_lines`]), as a loader that reads past what it does not need.
|
||||
pub fn metadata(tomls: &[String]) -> Result<JarInfo, String> {
|
||||
let real = |i: &JarInfo| !i.mod_id.is_empty() && !i.mod_id.contains("${");
|
||||
let mut first_error = None;
|
||||
for t in tomls {
|
||||
match mods_toml(t) {
|
||||
Ok(i) if real(&i) => return Ok(i),
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
first_error.get_or_insert(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
tomls
|
||||
.iter()
|
||||
.map(|t| mods_toml_lines(t))
|
||||
.find(real)
|
||||
.ok_or_else(|| first_error.unwrap_or_else(|| "mods.toml: no [[mods]] modId".into()))
|
||||
}
|
||||
|
||||
/// The first `[[mods]]` table's `modId`, `displayName` and `version` and the
|
||||
/// top-level `license`, read line by line (`key = "value"`, `#` comments), for
|
||||
/// a metadata file a TOML parser refuses. Dependencies are not read.
|
||||
pub fn mods_toml_lines(text: &str) -> JarInfo {
|
||||
let mut info = JarInfo::default();
|
||||
// 0: top level, 1: the first [[mods]], 2: past it.
|
||||
let mut section = 0;
|
||||
for line in text.lines() {
|
||||
let line = line.trim();
|
||||
if line.starts_with('[') {
|
||||
let name = line.trim_matches(['[', ']']).trim();
|
||||
section = match (section, name) {
|
||||
(0, "mods") => 1,
|
||||
_ => 2,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
let Some((key, value)) = line.split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
let value = value.trim();
|
||||
let Some(value) = value
|
||||
.strip_prefix('"')
|
||||
.and_then(|v| v.split_once('"'))
|
||||
.or_else(|| value.strip_prefix('\'').and_then(|v| v.split_once('\'')))
|
||||
.map(|(v, _)| v.to_owned())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
match (section, key.trim()) {
|
||||
(0, "license") => info.license = value,
|
||||
(1, "modId") if info.mod_id.is_empty() => info.mod_id = value,
|
||||
(1, "displayName") => info.name = value,
|
||||
(1, "version") => info.version = value,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
info
|
||||
}
|
||||
|
||||
/// `[[mods]]` (the first one is the jar's mod), the top-level `license`, and
|
||||
/// `[[dependencies.<id>]]` with `type = "required"` (or the older
|
||||
/// `mandatory = true`).
|
||||
@@ -295,6 +362,34 @@ mandatory = true
|
||||
assert_eq!(info.depends, ["geckolib", "curios"]);
|
||||
}
|
||||
|
||||
/// Drip Sounds' Forge jar carries an unfilled NeoForge template beside its
|
||||
/// `mods.toml`; a file no parser reads still gives its id line by line.
|
||||
#[test]
|
||||
fn templates_and_broken_files_are_read_tolerantly() {
|
||||
let template = "license = \"LGPL\"\n[[mods]]\nmodId = \"${id}\"\nversion = \"${version}\"\n\n[[dependencies.${id}]]\nmodId = \"minecraft\"\n";
|
||||
let forge = "license = \"LGPL-3.0-only\"\n[[mods]]\nmodId = \"waterdripsound\"\ndisplayName = \"Drip Sounds\"\nversion = \"0.6.3\"\n[[dependencies.waterdripsound]]\nmodId = \"cloth_config\"\nmandatory = true\n";
|
||||
assert!(mods_toml(template).is_err());
|
||||
let info = metadata(&[template.to_owned(), forge.to_owned()]).unwrap();
|
||||
assert_eq!(
|
||||
(info.mod_id.as_str(), info.version.as_str()),
|
||||
("waterdripsound", "0.6.3")
|
||||
);
|
||||
assert_eq!(info.depends, ["cloth_config"]);
|
||||
// Nothing parses: the lines still name the mod (a duplicate key is a TOML error).
|
||||
let broken = "license = 'MIT' # x\n[[mods]]\nmodId = \"ruby\"\nmodId = \"ruby\"\nversion = \"2\"\ndisplayName = \"Ruby\"\n[[mods]]\nmodId = \"other\"\n";
|
||||
assert!(mods_toml(broken).is_err());
|
||||
let info = metadata(&[template.to_owned(), broken.to_owned()]).unwrap();
|
||||
assert_eq!(
|
||||
(
|
||||
info.mod_id.as_str(),
|
||||
info.name.as_str(),
|
||||
info.license.as_str()
|
||||
),
|
||||
("ruby", "Ruby", "MIT")
|
||||
);
|
||||
assert!(metadata(&[template.to_owned()]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_fabric_metadata() {
|
||||
let info = fabric(
|
||||
|
||||
@@ -16,5 +16,9 @@ libc = "0.2"
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Graphics_Gdi", "Win32_System_Console", "Win32_System_IO", "Win32_System_Memory", "Win32_System_ProcessStatus", "Win32_System_Threading", "Win32_System_WindowsProgramming", "Win32_UI_WindowsAndMessaging"] }
|
||||
|
||||
[features]
|
||||
# Counts live heap bytes by startup step and thread (`CensusAllocator`); for measuring only.
|
||||
alloc-census = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,269 @@
|
||||
//! Who holds the heap, for `horizon-ctl memory` (feature `alloc-census`, for
|
||||
//! measuring only): mimalloc with a tag in front of each block, and the live
|
||||
//! bytes of each tag. A block's tag is what ran on its thread when it was
|
||||
//! allocated: the innermost [`tag`] (a startup step, a timed span or a pool
|
||||
//! job: `startup::time`, `spans::time` and `jobs` set them), else the
|
||||
//! thread's name. A block keeps its tag when another thread frees or grows
|
||||
//! it, so a column generated by a pool job and kept by the server thread
|
||||
//! counts for the job.
|
||||
//!
|
||||
//! Each block costs a header of 16 bytes (its alignment when larger) and an
|
||||
//! atomic add: heaptrack's interpreter needed more than 7 GB for the game
|
||||
//! (18 GB with line tables), this needs nothing beside it.
|
||||
use std::{
|
||||
alloc::{GlobalAlloc, Layout},
|
||||
cell::Cell,
|
||||
sync::atomic::{AtomicIsize, AtomicPtr, AtomicUsize, Ordering::Relaxed},
|
||||
};
|
||||
|
||||
const TAGS: usize = 512;
|
||||
/// Blocks allocated while this thread names itself, and past the table's end.
|
||||
const UNTAGGED: u32 = 0;
|
||||
const NONE: u32 = u32::MAX;
|
||||
|
||||
static LIVE: [AtomicIsize; TAGS] = [const { AtomicIsize::new(0) }; TAGS];
|
||||
static NAME_PTR: [AtomicPtr<u8>; TAGS] = [const { AtomicPtr::new(std::ptr::null_mut()) }; TAGS];
|
||||
static NAME_LEN: [AtomicUsize; TAGS] = [const { AtomicUsize::new(0) }; TAGS];
|
||||
/// The next free slot; slot 0 is [`UNTAGGED`].
|
||||
static NEXT: AtomicUsize = AtomicUsize::new(1);
|
||||
|
||||
thread_local! {
|
||||
static STEP: Cell<u32> = const { Cell::new(NONE) };
|
||||
static THREAD: Cell<u32> = const { Cell::new(NONE) };
|
||||
/// Allocations this thread made before its tag settled: std names a
|
||||
/// thread after its first few, so the name is read again until then.
|
||||
static NAMING: Cell<u32> = const { Cell::new(0) };
|
||||
static BUSY: Cell<bool> = const { Cell::new(false) };
|
||||
}
|
||||
|
||||
/// The slot of `name`, registering it. A race may give one name two slots;
|
||||
/// [`census`] adds them up.
|
||||
fn slot(name: &'static str) -> u32 {
|
||||
let n = NEXT.load(Relaxed).min(TAGS);
|
||||
for i in 1..n {
|
||||
let ptr = NAME_PTR[i].load(Relaxed);
|
||||
if !ptr.is_null() && NAME_LEN[i].load(Relaxed) == name.len() {
|
||||
// SAFETY: the slot holds a leaked or static string of that length.
|
||||
let s = unsafe { std::slice::from_raw_parts(ptr, name.len()) };
|
||||
if s == name.as_bytes() {
|
||||
return i as u32;
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = NEXT.fetch_add(1, Relaxed);
|
||||
if i >= TAGS {
|
||||
return UNTAGGED;
|
||||
}
|
||||
NAME_LEN[i].store(name.len(), Relaxed);
|
||||
NAME_PTR[i].store(name.as_ptr().cast_mut(), Relaxed);
|
||||
i as u32
|
||||
}
|
||||
|
||||
/// The OS name of the calling thread, read without allocating.
|
||||
#[cfg(any(target_vendor = "apple", target_os = "linux"))]
|
||||
fn thread_name(buf: &mut [u8; 64]) -> &[u8] {
|
||||
// SAFETY: writes at most `buf.len()` bytes, NUL included, into `buf`.
|
||||
let ok = unsafe {
|
||||
libc::pthread_getname_np(libc::pthread_self(), buf.as_mut_ptr().cast(), buf.len())
|
||||
} == 0;
|
||||
let len = buf.iter().position(|&b| b == 0).unwrap_or(0);
|
||||
if ok { &buf[..len] } else { &[] }
|
||||
}
|
||||
#[cfg(not(any(target_vendor = "apple", target_os = "linux")))]
|
||||
fn thread_name(_: &mut [u8; 64]) -> &[u8] {
|
||||
&[]
|
||||
}
|
||||
|
||||
/// The calling thread's own tag: its name, looked up again for its first
|
||||
/// allocations. Allocations made while it registers a name are untagged.
|
||||
fn thread_tag() -> u32 {
|
||||
let known = THREAD.get();
|
||||
if known != NONE && NAMING.get() >= 256 {
|
||||
return known;
|
||||
}
|
||||
if BUSY.replace(true) {
|
||||
return UNTAGGED;
|
||||
}
|
||||
NAMING.set(NAMING.get().saturating_add(1));
|
||||
let mut buf = [0u8; 64];
|
||||
let name = match std::str::from_utf8(thread_name(&mut buf)) {
|
||||
Ok("") | Err(_) => "unnamed thread",
|
||||
Ok(name) => name,
|
||||
};
|
||||
let tag = if known != NONE && slot_name(known) == Some(name) {
|
||||
known
|
||||
} else {
|
||||
// Thread names are few: each is leaked once.
|
||||
let name: &'static str = match name {
|
||||
"unnamed thread" => "unnamed thread",
|
||||
name => Box::leak(name.to_owned().into_boxed_str()),
|
||||
};
|
||||
slot(name)
|
||||
};
|
||||
THREAD.set(tag);
|
||||
BUSY.set(false);
|
||||
tag
|
||||
}
|
||||
|
||||
fn slot_name(i: u32) -> Option<&'static str> {
|
||||
let i = i as usize;
|
||||
let ptr = NAME_PTR.get(i)?.load(Relaxed);
|
||||
if ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
// SAFETY: registered slots hold leaked or static UTF-8 of that length.
|
||||
Some(unsafe {
|
||||
std::str::from_utf8_unchecked(std::slice::from_raw_parts(ptr, NAME_LEN[i].load(Relaxed)))
|
||||
})
|
||||
}
|
||||
|
||||
fn current() -> u32 {
|
||||
match STEP.get() {
|
||||
NONE => thread_tag(),
|
||||
step => step,
|
||||
}
|
||||
}
|
||||
|
||||
/// While it lives, the calling thread's blocks are tagged `name`.
|
||||
pub struct Tag(u32);
|
||||
impl Drop for Tag {
|
||||
fn drop(&mut self) {
|
||||
STEP.set(self.0);
|
||||
}
|
||||
}
|
||||
/// Tag the calling thread's blocks `name` until the guard drops.
|
||||
pub fn tag(name: &'static str) -> Tag {
|
||||
let previous = STEP.get();
|
||||
let was = BUSY.replace(true);
|
||||
let tag = slot(name);
|
||||
BUSY.set(was);
|
||||
STEP.set(tag);
|
||||
Tag(previous)
|
||||
}
|
||||
|
||||
/// Live bytes by tag, largest first.
|
||||
pub fn census() -> Vec<(&'static str, isize)> {
|
||||
let mut by_name: Vec<(&'static str, isize)> = Vec::new();
|
||||
for i in 0..NEXT.load(Relaxed).min(TAGS) {
|
||||
let bytes = LIVE[i].load(Relaxed);
|
||||
let name = slot_name(i as u32).unwrap_or("untagged");
|
||||
match by_name.iter_mut().find(|(n, _)| *n == name) {
|
||||
Some(entry) => entry.1 += bytes,
|
||||
None => by_name.push((name, bytes)),
|
||||
}
|
||||
}
|
||||
by_name.sort_by_key(|(_, bytes)| std::cmp::Reverse(*bytes));
|
||||
by_name
|
||||
}
|
||||
|
||||
/// mimalloc with a tag in front of each block (see the module).
|
||||
pub struct CensusAllocator;
|
||||
|
||||
/// The header in front of a block of `layout`: 16 bytes, or its alignment.
|
||||
fn header(layout: Layout) -> usize {
|
||||
layout.align().max(16)
|
||||
}
|
||||
|
||||
// SAFETY: forwards every call to mimalloc with the header added; the tag sits
|
||||
// in the header's last four bytes, inside the block mimalloc returned.
|
||||
unsafe impl GlobalAlloc for CensusAllocator {
|
||||
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
|
||||
let h = header(layout);
|
||||
let Ok(outer) = Layout::from_size_align(layout.size() + h, layout.align().max(16)) else {
|
||||
return std::ptr::null_mut();
|
||||
};
|
||||
// SAFETY: `outer` has a nonzero size.
|
||||
let base = unsafe { mimalloc::MiMalloc.alloc(outer) };
|
||||
if base.is_null() {
|
||||
return base;
|
||||
}
|
||||
let tag = current();
|
||||
LIVE[tag as usize].fetch_add(layout.size() as isize, Relaxed);
|
||||
// SAFETY: `h` bytes precede the caller's block inside the allocation.
|
||||
unsafe {
|
||||
let block = base.add(h);
|
||||
block.sub(4).cast::<u32>().write_unaligned(tag);
|
||||
block
|
||||
}
|
||||
}
|
||||
unsafe fn dealloc(&self, block: *mut u8, layout: Layout) {
|
||||
let h = header(layout);
|
||||
// SAFETY: `block` came from `alloc` with this layout.
|
||||
unsafe {
|
||||
let tag = block.sub(4).cast::<u32>().read_unaligned();
|
||||
LIVE[tag as usize].fetch_sub(layout.size() as isize, Relaxed);
|
||||
let outer =
|
||||
Layout::from_size_align_unchecked(layout.size() + h, layout.align().max(16));
|
||||
mimalloc::MiMalloc.dealloc(block.sub(h), outer);
|
||||
}
|
||||
}
|
||||
unsafe fn realloc(&self, block: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
|
||||
let h = header(layout);
|
||||
// SAFETY: `block` came from `alloc` with this layout; the header moves with it.
|
||||
unsafe {
|
||||
let outer =
|
||||
Layout::from_size_align_unchecked(layout.size() + h, layout.align().max(16));
|
||||
let tag = block.sub(4).cast::<u32>().read_unaligned();
|
||||
let base = mimalloc::MiMalloc.realloc(block.sub(h), outer, new_size + h);
|
||||
if base.is_null() {
|
||||
return base;
|
||||
}
|
||||
LIVE[tag as usize].fetch_add(new_size as isize - layout.size() as isize, Relaxed);
|
||||
base.add(h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{CensusAllocator, census, tag};
|
||||
use std::alloc::{GlobalAlloc, Layout};
|
||||
|
||||
fn live(name: &str) -> isize {
|
||||
census().iter().find(|(n, _)| *n == name).map_or(0, |e| e.1)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_block_counts_for_its_step_until_another_thread_frees_it() {
|
||||
let layout = Layout::from_size_align(1 << 20, 64).unwrap();
|
||||
let block = {
|
||||
let _tag = tag("test.step");
|
||||
// SAFETY: a nonzero layout.
|
||||
unsafe { CensusAllocator.alloc(layout) }
|
||||
};
|
||||
assert_eq!(live("test.step"), 1 << 20);
|
||||
// SAFETY: `block` came from this allocator with `layout`.
|
||||
let block = unsafe { CensusAllocator.realloc(block, layout, 3 << 20) } as usize;
|
||||
assert_eq!(live("test.step"), 3 << 20, "growing keeps the step's tag");
|
||||
let grown = Layout::from_size_align(3 << 20, 64).unwrap();
|
||||
std::thread::spawn(move || {
|
||||
let _other = tag("test.other");
|
||||
// SAFETY: `block` came from this allocator with `grown`.
|
||||
unsafe { CensusAllocator.dealloc(block as *mut u8, grown) };
|
||||
})
|
||||
.join()
|
||||
.unwrap();
|
||||
assert_eq!(live("test.step"), 0);
|
||||
assert_eq!(live("test.other"), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_a_step_a_block_counts_for_its_thread() {
|
||||
let layout = Layout::from_size_align(4096, 8).unwrap();
|
||||
let (name, block) = std::thread::Builder::new()
|
||||
.name("census-test".into())
|
||||
.spawn(move || {
|
||||
// SAFETY: a nonzero layout.
|
||||
let block = unsafe { CensusAllocator.alloc(layout) } as usize;
|
||||
(std::thread::current().name().unwrap().to_owned(), block)
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
// Linux keeps 15 bytes of a thread's name; the test's is shorter.
|
||||
assert_eq!(live(&name), 4096);
|
||||
// SAFETY: `block` came from this allocator with `layout`.
|
||||
unsafe { CensusAllocator.dealloc(block as *mut u8, layout) };
|
||||
assert_eq!(live(&name), 0);
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,10 @@ pub trait Source: Send + Sync {
|
||||
fn idle(&self, _worker: usize) {}
|
||||
/// Every worker is idle: free what the workers share.
|
||||
fn all_idle(&self) {}
|
||||
/// What a heap-census build tags its jobs' allocations (`crate::heap_tag`).
|
||||
fn label(&self) -> &'static str {
|
||||
std::any::type_name::<Self>()
|
||||
}
|
||||
}
|
||||
|
||||
/// How long a worker waits with no job before it frees its buffers.
|
||||
@@ -384,7 +388,11 @@ fn work(inner: &Inner, worker: usize) {
|
||||
}
|
||||
// A panicking job loses its own work, not the worker: the panic
|
||||
// hook has already reported it.
|
||||
let _ = catch_unwind(AssertUnwindSafe(|| entry.source.run(worker)));
|
||||
let _ = catch_unwind(AssertUnwindSafe(|| {
|
||||
let _tag =
|
||||
crate::HEAP_CENSUS.then(|| crate::heap_census::tag(entry.source.label()));
|
||||
entry.source.run(worker)
|
||||
}));
|
||||
if far {
|
||||
inner.far.fetch_sub(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
/// The game window's boot screen, before wgpu's first frame paints over it.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod boot_paint;
|
||||
pub mod heap_census;
|
||||
pub mod jobs;
|
||||
pub mod memory_limit;
|
||||
|
||||
@@ -207,6 +208,18 @@ pub fn catch_stop_signals() {}
|
||||
/// 16% of a chunk's generation time), mimalloc keeps a heap per thread.
|
||||
pub use mimalloc::MiMalloc as Allocator;
|
||||
|
||||
/// The global allocator of a build with the `alloc-census` feature: live
|
||||
/// heap bytes by startup step and thread, for `horizon-ctl memory`.
|
||||
pub use heap_census::CensusAllocator;
|
||||
/// Whether this build counts the heap by tag (feature `alloc-census`).
|
||||
pub const HEAP_CENSUS: bool = cfg!(feature = "alloc-census");
|
||||
/// Tag the calling thread's allocations `name` while the guard lives, in a
|
||||
/// build that counts the heap ([`HEAP_CENSUS`]); nothing otherwise.
|
||||
#[inline]
|
||||
pub fn heap_tag(name: &'static str) -> Option<heap_census::Tag> {
|
||||
HEAP_CENSUS.then(|| heap_census::tag(name))
|
||||
}
|
||||
|
||||
/// `mi_option_purge_delay` in mimalloc.h (index 15 in v2 and v3); the bindings
|
||||
/// don't name it.
|
||||
const PURGE_DELAY: libmimalloc_sys::mi_option_t = 15;
|
||||
|
||||
@@ -14,7 +14,7 @@ use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
Mutex, OnceLock,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
atomic::{AtomicU32, Ordering},
|
||||
},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
@@ -24,7 +24,7 @@ const VERSION: u32 = 2;
|
||||
const FILE_NAME: &str = "behaviours-v2.bin";
|
||||
const MAX_FILE_BYTES: usize = 128 * 1024 * 1024;
|
||||
const MAX_DECODED_BYTES: usize = 256 * 1024 * 1024;
|
||||
static NEXT_TEMP: AtomicU64 = AtomicU64::new(0);
|
||||
static NEXT_TEMP: AtomicU32 = AtomicU32::new(0);
|
||||
static DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||
|
||||
/// Keep snapshots in `dir` (the client passes `<game folder>/.horizon-cache`).
|
||||
@@ -776,9 +776,9 @@ fn write(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
|
||||
static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0);
|
||||
static NEXT_FIXTURE: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
struct FixtureDir(PathBuf);
|
||||
impl Drop for FixtureDir {
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::{
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{self, Read, Seek, SeekFrom, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
sync::atomic::{AtomicU32, Ordering},
|
||||
};
|
||||
|
||||
const SECTOR: usize = 4096;
|
||||
@@ -387,7 +387,7 @@ impl RegionFile {
|
||||
}
|
||||
type BTreeChanges<'a> = std::collections::BTreeMap<usize, Option<&'a ChunkRecord>>;
|
||||
|
||||
static NEXT_TEMP: AtomicU64 = AtomicU64::new(0);
|
||||
static NEXT_TEMP: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
/// Writes a complete new region to a temporary sibling, then publishes it with
|
||||
/// an atomic, non-overwriting hard link. Drop rolls back unpublished files.
|
||||
|
||||
@@ -10,10 +10,10 @@ use std::{
|
||||
fs,
|
||||
io::{Seek, SeekFrom, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
sync::atomic::{AtomicU32, Ordering},
|
||||
};
|
||||
|
||||
static NEXT: AtomicU64 = AtomicU64::new(0);
|
||||
static NEXT: AtomicU32 = AtomicU32::new(0);
|
||||
struct Directory(PathBuf);
|
||||
impl Directory {
|
||||
fn new() -> Self {
|
||||
|
||||
@@ -841,6 +841,8 @@ API 108 adds `native_server_data_loaded(reload)` fires once the server's data ta
|
||||
|
||||
API 109 adds `native_compute_fov(partial, camera, degrees)` receives the computed world FOV after settings, ordinary FOV effects and zoom, before projection/Nausea. `camera` is its world position and `partial` the interpolation fraction. Mods can narrow or widen the angle; only finite values in 1..=179 degrees are accepted, otherwise the original computed angle is used. It runs for world rendering and near-plane overlays, not just once per frame. Hand/HUD FOV and the title panorama keep their existing paths. Bundles need "hooks": 109.
|
||||
|
||||
`native_movement_input(forward, strafe, jump, sneak)` runs on the client each local player tick after input is read and before it is applied. Mods may change all four values; positive strafe is left. The server receives direction flags, not scalar magnitudes, so remote servers may correct modified movement. Bundles need "hooks": @API:movement_input@.
|
||||
|
||||
API 110 adds mobs drawn by client mods: `native_mob_submit(mob, draw, drawn)` is asked each frame for every mob in view but players,
|
||||
with its id, kind, custom name and the frame's body and head turns and walk cycle; a mod draws a model of its own with the
|
||||
block entity drawing helpers under `draw`'s pose (at the feet, turned with the body) and sets `drawn` to leave out the vanilla
|
||||
|
||||
@@ -214,6 +214,9 @@
|
||||
"Starting": "Startet",
|
||||
"Show in folder": "Im Ordner anzeigen",
|
||||
"Settings": "Einstellungen",
|
||||
"Settings were not saved.": "Die Einstellungen wurden nicht gespeichert.",
|
||||
"Your changes are still in this window. Retry to save them. {error}": "Deine Änderungen sind noch in diesem Fenster vorhanden. Versuche erneut, sie zu speichern. {error}",
|
||||
"Retry": "Erneut versuchen",
|
||||
"Doesn't port": "Wird nicht portiert",
|
||||
"Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.": "Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.",
|
||||
"Could not read {name}: {error}": "{name} konnte nicht gelesen werden: {error}",
|
||||
|
||||
@@ -214,6 +214,9 @@
|
||||
"Starting": "Iniciando",
|
||||
"Show in folder": "Mostrar en la carpeta",
|
||||
"Settings": "Ajustes",
|
||||
"Settings were not saved.": "No se han guardado los ajustes.",
|
||||
"Your changes are still in this window. Retry to save them. {error}": "Tus cambios siguen en esta ventana. Vuelve a intentarlo para guardarlos. {error}",
|
||||
"Retry": "Reintentar",
|
||||
"Doesn't port": "No se porta",
|
||||
"Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.": "Nunca se envía: el nombre o UUID de tu cuenta, nombres de mundos, direcciones de servidores, chat ni tu dirección IP. Cada informe lleva solo la fecha del día, bajo un ID de instalación aleatorio que puedes eliminar.",
|
||||
"Could not read {name}: {error}": "No se pudo leer {name}: {error}",
|
||||
|
||||
@@ -214,6 +214,9 @@
|
||||
"Starting": "Démarrage",
|
||||
"Show in folder": "Afficher dans le dossier",
|
||||
"Settings": "Paramètres",
|
||||
"Settings were not saved.": "Les paramètres n’ont pas été enregistrés.",
|
||||
"Your changes are still in this window. Retry to save them. {error}": "Vos modifications sont toujours dans cette fenêtre. Réessayez pour les enregistrer. {error}",
|
||||
"Retry": "Réessayer",
|
||||
"Doesn't port": "Non porté",
|
||||
"Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.": "Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.",
|
||||
"Could not read {name}: {error}": "Impossible de lire {name} : {error}",
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"Versions": "バージョン",
|
||||
"Patch notes": "更新履歴",
|
||||
"Settings": "設定",
|
||||
"Settings were not saved.": "設定を保存できませんでした。",
|
||||
"Your changes are still in this window. Retry to save them. {error}": "変更内容はこのウィンドウに残っています。もう一度保存をお試しください。{error}",
|
||||
"Retry": "再試行",
|
||||
"Accounts, updates, and the launcher itself.": "アカウント、アップデート、ランチャーの設定",
|
||||
"Account": "アカウント",
|
||||
"Updates": "アップデート",
|
||||
|
||||
@@ -214,6 +214,9 @@
|
||||
"Starting": "Uruchamianie",
|
||||
"Show in folder": "Pokaż w folderze",
|
||||
"Settings": "Ustawienia",
|
||||
"Settings were not saved.": "Nie udało się zapisać ustawień.",
|
||||
"Your changes are still in this window. Retry to save them. {error}": "Twoje zmiany nadal są w tym oknie. Spróbuj ponownie je zapisać. {error}",
|
||||
"Retry": "Spróbuj ponownie",
|
||||
"Doesn't port": "Nieprzenoszone",
|
||||
"Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.": "Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.",
|
||||
"Could not read {name}: {error}": "Nie udało się odczytać {name}: {error}",
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"Versions": "Versões",
|
||||
"Patch notes": "Notas da atualização",
|
||||
"Settings": "Configurações",
|
||||
"Settings were not saved.": "As configurações não foram salvas.",
|
||||
"Your changes are still in this window. Retry to save them. {error}": "Suas alterações ainda estão nesta janela. Tente novamente para salvá-las. {error}",
|
||||
"Retry": "Tentar novamente",
|
||||
"Accounts, updates, and the launcher itself.": "Contas, atualizações e o próprio launcher.",
|
||||
"Account": "Conta",
|
||||
"Updates": "Atualizações",
|
||||
|
||||
@@ -214,6 +214,9 @@
|
||||
"Starting": "Запуск",
|
||||
"Show in folder": "Показать в папке",
|
||||
"Settings": "Настройки",
|
||||
"Settings were not saved.": "Не удалось сохранить настройки.",
|
||||
"Your changes are still in this window. Retry to save them. {error}": "Ваши изменения остаются в этом окне. Попробуйте сохранить их ещё раз. {error}",
|
||||
"Retry": "Повторить",
|
||||
"Doesn't port": "Не портируется",
|
||||
"Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.": "Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.",
|
||||
"Could not read {name}: {error}": "Не удалось прочитать {name}: {error}",
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"Versions": "Sürümler",
|
||||
"Patch notes": "Sürüm Notları",
|
||||
"Settings": "Ayarlar",
|
||||
"Settings were not saved.": "Ayarlar kaydedilemedi.",
|
||||
"Your changes are still in this window. Retry to save them. {error}": "Değişiklikleriniz hâlâ bu pencerede duruyor. Kaydetmek için tekrar deneyin. {error}",
|
||||
"Retry": "Tekrar dene",
|
||||
"Accounts, updates, and the launcher itself.": "Hesaplar, güncellemeler ve başlatıcının kendisi.",
|
||||
"Account": "Hesap",
|
||||
"Updates": "Güncellemeler",
|
||||
|
||||
@@ -214,6 +214,9 @@
|
||||
"Starting": "Запуск",
|
||||
"Show in folder": "Показати в папці",
|
||||
"Settings": "Налаштування",
|
||||
"Settings were not saved.": "Не вдалося зберегти налаштування.",
|
||||
"Your changes are still in this window. Retry to save them. {error}": "Ваші зміни залишаються в цьому вікні. Спробуйте зберегти їх ще раз. {error}",
|
||||
"Retry": "Повторити",
|
||||
"Doesn't port": "Не портується",
|
||||
"Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.": "Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.",
|
||||
"Could not read {name}: {error}": "Не вдалося прочитати {name}: {error}",
|
||||
|
||||
@@ -214,6 +214,9 @@
|
||||
"Starting": "正在启动",
|
||||
"Show in folder": "在文件夹中显示",
|
||||
"Settings": "设置",
|
||||
"Settings were not saved.": "设置未保存。",
|
||||
"Your changes are still in this window. Retry to save them. {error}": "你的更改仍保留在此窗口中。请重试以保存更改。{error}",
|
||||
"Retry": "重试",
|
||||
"Doesn't port": "未移植",
|
||||
"Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.": "Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.",
|
||||
"Could not read {name}: {error}": "无法读取 {name}:{error}",
|
||||
|
||||
+11
-5
@@ -2317,7 +2317,7 @@ pub fn remove_version(tag: &str) -> Result<(), String> {
|
||||
let mut settings = Settings::load();
|
||||
if settings.selected.as_deref() == Some(tag) {
|
||||
settings.selected = installed().first().cloned();
|
||||
settings.save();
|
||||
settings.save()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -2666,7 +2666,7 @@ pub fn install_tag(tag: &str, as_update: bool, progress: impl Fn(f32)) -> Result
|
||||
let mut settings = Settings::load();
|
||||
if !as_update || select_after_update(settings.selected.as_deref(), &before) {
|
||||
settings.selected = Some(release.tag.clone());
|
||||
settings.save();
|
||||
settings.save()?;
|
||||
}
|
||||
if as_update && release.nightly.is_some() {
|
||||
crate::nightly::after_update(&release.tag);
|
||||
@@ -3400,19 +3400,25 @@ impl Settings {
|
||||
pub fn keep_newest(&self) -> usize {
|
||||
self.keep_versions.unwrap_or(KEEP_NEWEST).max(1)
|
||||
}
|
||||
pub fn save(&self) {
|
||||
pub fn save(&self) -> Result<(), String> {
|
||||
// The window and background jobs share the process's temporary file.
|
||||
static SAVE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
let _guard = SAVE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
// A write that stops halfway leaves settings.json unreadable, and `load` answers
|
||||
// defaults for it: the file is replaced only once it is whole.
|
||||
let dir = data_dir();
|
||||
let path = dir.join("settings.json");
|
||||
let tmp = dir.join(format!("settings.{}.tmp", std::process::id()));
|
||||
let json =
|
||||
serde_json::to_vec_pretty(self).map_err(|e| format!("{}: {e}", path.display()))?;
|
||||
let written = fs::create_dir_all(&dir)
|
||||
.and_then(|()| fs::write(&tmp, serde_json::to_string_pretty(self).unwrap()))
|
||||
.and_then(|()| fs::write(&tmp, json))
|
||||
.and_then(|()| fs::rename(&tmp, &path));
|
||||
if let Err(e) = written {
|
||||
let _ = fs::remove_file(&tmp);
|
||||
eprintln!("Horizon: settings.json is not saved: {e}");
|
||||
return Err(format!("{}: {e}", path.display()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ pub fn import(path: &Path, requested: Option<&str>) -> Result<String, String> {
|
||||
// Read again: the window may have saved other settings during the copy.
|
||||
let mut settings = backend::Settings::load();
|
||||
settings.selected = Some(tag.clone());
|
||||
settings.save();
|
||||
settings.save()?;
|
||||
Ok(tag)
|
||||
}
|
||||
|
||||
|
||||
+20
-19
@@ -1599,7 +1599,7 @@ fn execute(command: Cmd, as_json: bool) -> Result<(), String> {
|
||||
if let Some(key) = key {
|
||||
let value = value.ok_or("settings KEY VALUE: the value is missing")?;
|
||||
set_setting(&mut settings, &key, &value)?;
|
||||
settings.save();
|
||||
settings.save()?;
|
||||
horizon_launcher::proxy::announce(settings.proxy.clone());
|
||||
}
|
||||
let all = settings_json(&settings);
|
||||
@@ -1615,7 +1615,7 @@ fn execute(command: Cmd, as_json: bool) -> Result<(), String> {
|
||||
let mut settings = backend::Settings::load();
|
||||
if let Some(line) = args {
|
||||
settings.game_args = line;
|
||||
settings.save();
|
||||
settings.save()?;
|
||||
}
|
||||
let line = settings.game_args;
|
||||
if as_json {
|
||||
@@ -1677,7 +1677,7 @@ fn execute(command: Cmd, as_json: bool) -> Result<(), String> {
|
||||
Cmd::DesktopEntry { remove } => {
|
||||
let mut settings = backend::Settings::load();
|
||||
settings.desktop_entry = Some(!remove);
|
||||
settings.save();
|
||||
settings.save()?;
|
||||
if remove {
|
||||
horizon_launcher::desktop::remove()?;
|
||||
println!("Removed Horizon from the applications menu.");
|
||||
@@ -2273,7 +2273,15 @@ fn github_native(
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
});
|
||||
github_mods::discard(found);
|
||||
for o in done? {
|
||||
let done = done?;
|
||||
// The window's trust notice, once. Nothing is turned on: native mods are always on.
|
||||
let mut settings = backend::Settings::load();
|
||||
if !settings.native_mods_noticed {
|
||||
eprintln!("{}", native_status::NOTICE);
|
||||
settings.native_mods_noticed = true;
|
||||
settings.save()?;
|
||||
}
|
||||
for o in done {
|
||||
println!(
|
||||
"Installed {} {} from GitHub ({}), sha256 {}.",
|
||||
o.id,
|
||||
@@ -2282,13 +2290,6 @@ fn github_native(
|
||||
o.sha256
|
||||
);
|
||||
}
|
||||
// The window's trust notice, once. Nothing is turned on: native mods are always on.
|
||||
let mut settings = backend::Settings::load();
|
||||
if !settings.native_mods_noticed {
|
||||
eprintln!("{}", native_status::NOTICE);
|
||||
settings.native_mods_noticed = true;
|
||||
settings.save();
|
||||
}
|
||||
println!("Build it next: horizon-launcher native-build --trust-native-code");
|
||||
println!(
|
||||
"Developing a mod? horizon-launcher settings native-profile dev (or native-build --profile dev): a mod edit rebuilds about 6× faster, while game logic runs up to about twice as slow. Use it while developing, not for playing."
|
||||
@@ -2575,6 +2576,14 @@ fn mods(action: ModsCmd, as_json: bool) -> Result<(), String> {
|
||||
Some(target) => java_instance::install(&*source, target, &p, &home, &line)?,
|
||||
None => installed::install(&*source, &p, everything, &home, &line)?,
|
||||
};
|
||||
// The first native mod: the window's trust notice, once.
|
||||
let mut settings = backend::Settings::load();
|
||||
let native = done.iter().any(|e| native_status::is_native_file(&e.file));
|
||||
if native && !settings.native_mods_noticed {
|
||||
eprintln!("{}", native_status::NOTICE);
|
||||
settings.native_mods_noticed = true;
|
||||
settings.save()?;
|
||||
}
|
||||
if as_json {
|
||||
json(done.iter().map(entry_json).collect());
|
||||
}
|
||||
@@ -2584,14 +2593,6 @@ fn mods(action: ModsCmd, as_json: bool) -> Result<(), String> {
|
||||
println!(" not ported: {s}");
|
||||
}
|
||||
}
|
||||
// The first native mod: the window's trust notice, once.
|
||||
let mut settings = backend::Settings::load();
|
||||
let native = done.iter().any(|e| native_status::is_native_file(&e.file));
|
||||
if native && !settings.native_mods_noticed {
|
||||
eprintln!("{}", native_status::NOTICE);
|
||||
settings.native_mods_noticed = true;
|
||||
settings.save();
|
||||
}
|
||||
}
|
||||
ModsCmd::Update { project, check, .. } => {
|
||||
let list = installed::Installed::load(&home).entries;
|
||||
|
||||
@@ -128,7 +128,7 @@ impl Launcher {
|
||||
// The question and Settings say what the "I'm playing" ping holds, beside the switches.
|
||||
if !self.settings.ping_notice_seen {
|
||||
self.settings.ping_notice_seen = true;
|
||||
self.settings.save();
|
||||
self.save_settings();
|
||||
}
|
||||
self.collection.view = None;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ impl Launcher {
|
||||
let toggle = switch("dev-offline", self.settings.dev_offline, &t).on_click(cx.listener(
|
||||
|this, _: &ClickEvent, _, cx| {
|
||||
this.settings.dev_offline = !this.settings.dev_offline;
|
||||
this.settings.save();
|
||||
this.save_settings();
|
||||
cx.notify();
|
||||
},
|
||||
));
|
||||
|
||||
@@ -125,7 +125,7 @@ impl Launcher {
|
||||
self.github.newer.clear();
|
||||
// The review above was the trust notice; the Mods page offers Build.
|
||||
self.settings.native_mods_noticed = true;
|
||||
self.settings.save();
|
||||
self.save_settings();
|
||||
}
|
||||
self.instance_mods_changed();
|
||||
self.github.message = Some(result);
|
||||
|
||||
@@ -183,7 +183,7 @@ impl Launcher {
|
||||
.child(tr("Got it"))
|
||||
.on_click(cx.listener(|this, _: &ClickEvent, _, cx| {
|
||||
this.settings.ping_notice_seen = true;
|
||||
this.settings.save();
|
||||
this.save_settings();
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
|
||||
@@ -380,6 +380,13 @@ fn find_in(all: &[Instance], query: &str) -> Result<Instance, String> {
|
||||
/// and forgets it: its choices, and the selection when Play started it. Horizon's own instance,
|
||||
/// the game's home, stays.
|
||||
pub fn delete(query: &str) -> Result<Instance, String> {
|
||||
let (gone, saved) = delete_with_selection(query)?;
|
||||
saved?;
|
||||
Ok(gone)
|
||||
}
|
||||
|
||||
/// A completed deletion stays completed even if saving the new selection fails.
|
||||
pub fn delete_with_selection(query: &str) -> Result<(Instance, Result<(), String>), String> {
|
||||
let gone = delete_in(
|
||||
&backend::game_home(),
|
||||
&backend::data_dir(),
|
||||
@@ -387,11 +394,13 @@ pub fn delete(query: &str) -> Result<Instance, String> {
|
||||
&crate::desktop_seams::to_trash,
|
||||
)?;
|
||||
let mut settings = backend::Settings::load();
|
||||
if settings.instance.as_ref() == Some(&gone.id) {
|
||||
let saved = if settings.instance.as_ref() == Some(&gone.id) {
|
||||
settings.instance = None;
|
||||
settings.save();
|
||||
}
|
||||
Ok(gone)
|
||||
settings.save()
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
Ok((gone, saved))
|
||||
}
|
||||
|
||||
fn delete_in(
|
||||
@@ -429,7 +438,7 @@ pub fn select(query: &str) -> Result<Instance, String> {
|
||||
let instance = find(query)?;
|
||||
let mut settings = backend::Settings::load();
|
||||
settings.instance = (instance.kind != Kind::Default).then(|| instance.id.clone());
|
||||
settings.save();
|
||||
settings.save()?;
|
||||
Ok(instance)
|
||||
}
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ impl Launcher {
|
||||
self.native_update = None;
|
||||
self.settings.instance =
|
||||
(instance.kind != InstanceKind::Default).then(|| instance.id.clone());
|
||||
self.settings.save();
|
||||
self.save_settings();
|
||||
}
|
||||
|
||||
/// A texture from the selected version's prepared files, its first frame, drawn as pixels.
|
||||
@@ -849,13 +849,16 @@ impl Launcher {
|
||||
if self.client.is_some() || self.starting_client.is_some() {
|
||||
return;
|
||||
}
|
||||
match instances::delete(id) {
|
||||
Ok(_) => {
|
||||
match instances::delete_with_selection(id) {
|
||||
Ok((_, saved)) => {
|
||||
self.instances.confirm_delete = None;
|
||||
self.instances.delete_error = None;
|
||||
// Play starts Horizon again; the window's copy of the settings says so too.
|
||||
if self.settings.instance.as_deref() == Some(id) {
|
||||
self.settings.instance = None;
|
||||
self.settings.save();
|
||||
self.save_settings();
|
||||
} else if let Err(error) = saved {
|
||||
self.settings_save_error = Some(error);
|
||||
}
|
||||
self.reload_instances();
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ impl Launcher {
|
||||
})
|
||||
.on_click(cx.listener(move |this, _: &ClickEvent, _, cx| {
|
||||
this.settings.language = choice;
|
||||
this.settings.save();
|
||||
this.save_settings();
|
||||
i18n::set(this.settings.language());
|
||||
this.language_menu = false;
|
||||
cx.notify();
|
||||
|
||||
+77
-20
@@ -243,6 +243,8 @@ struct Launcher {
|
||||
/// Why the last picture for Home's background was refused.
|
||||
background_error: Option<String>,
|
||||
settings: Settings,
|
||||
/// The last failed settings write, kept across page changes until a save succeeds.
|
||||
settings_save_error: Option<String>,
|
||||
/// The Discord, sponsor and website links: built in until links.json on main has been read.
|
||||
links: backend::Links,
|
||||
preparing: bool,
|
||||
@@ -413,7 +415,7 @@ impl Launcher {
|
||||
s.partner_seen_secs += 10;
|
||||
if s.partner_seen_secs % 60 == 0 || s.partner_seen_secs >= partner::SEEN_BEFORE_HIDE
|
||||
{
|
||||
s.save();
|
||||
this.save_settings();
|
||||
cx.notify();
|
||||
}
|
||||
true
|
||||
@@ -511,6 +513,7 @@ impl Launcher {
|
||||
proxy_focused: false,
|
||||
proxy_focus: cx.focus_handle(),
|
||||
settings,
|
||||
settings_save_error: None,
|
||||
links: backend::Links::default(),
|
||||
preparing: false,
|
||||
prepare_log: VecDeque::new(),
|
||||
@@ -636,6 +639,53 @@ impl Launcher {
|
||||
this
|
||||
}
|
||||
|
||||
/// Keeps the current choices in memory if writing fails; every page offers the same retry.
|
||||
fn save_settings(&mut self) -> bool {
|
||||
self.settings_save_error = self.settings.save().err();
|
||||
self.settings_save_error.is_none()
|
||||
}
|
||||
|
||||
fn settings_save_notice(&self, cx: &mut Context<Self>) -> Option<Div> {
|
||||
let error = self.settings_save_error.as_ref()?;
|
||||
let t = self.theme;
|
||||
Some(
|
||||
div()
|
||||
.flex_none()
|
||||
.px(px(theme::PAGE_GUTTER))
|
||||
.py(px(12.))
|
||||
.child(
|
||||
crate::plate::plate(&t.night)
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap(px(16.))
|
||||
.p(px(16.))
|
||||
.child(
|
||||
div()
|
||||
.relative()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.child(
|
||||
shell::text(theme::night::UI, t.danger)
|
||||
.child(tr("Settings were not saved.")),
|
||||
)
|
||||
.child(
|
||||
shell::text(theme::night::CAPTION, t.ink_soft).child(trf(
|
||||
"Your changes are still in this window. Retry to save them. {error}",
|
||||
&[("error", error)],
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
button("settings-save-retry", "Retry", Kind::Secondary, &t)
|
||||
.on_click(cx.listener(|this, _: &ClickEvent, _, cx| {
|
||||
this.save_settings();
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn go(&mut self, page: Page) {
|
||||
// The game may have made up a name since, and the CLI or another window may have
|
||||
// changed the accounts: read them again (small files, once a switch).
|
||||
@@ -1030,12 +1080,14 @@ impl Launcher {
|
||||
if as_update.is_none_or(|(_, select)| select) {
|
||||
self.settings.selected = Some(tag);
|
||||
}
|
||||
self.settings.save();
|
||||
// After the save: the selected version is never pruned.
|
||||
let tx = self.tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let _ = tx.send(Msg::Pruned(backend::prune_old_versions()));
|
||||
});
|
||||
// Pruning reads settings from disk: protect the selection only once
|
||||
// it has been saved there. A failed save leaves all versions in place.
|
||||
if self.save_settings() {
|
||||
let tx = self.tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let _ = tx.send(Msg::Pruned(backend::prune_old_versions()));
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(e) => self.install_error = Some((tag, e)),
|
||||
}
|
||||
@@ -1043,8 +1095,12 @@ impl Launcher {
|
||||
Msg::Removed(tag, result) => {
|
||||
self.removing.retain(|t| *t != tag);
|
||||
self.installed = backend::installed();
|
||||
self.settings.selected =
|
||||
selection_after_removal(&self.installed, Settings::load().selected);
|
||||
let selected = if self.settings_save_error.is_some() {
|
||||
self.settings.selected.take()
|
||||
} else {
|
||||
Settings::load().selected
|
||||
};
|
||||
self.settings.selected = selection_after_removal(&self.installed, selected);
|
||||
if let Err(e) = result {
|
||||
self.install_error = Some((tag, e));
|
||||
}
|
||||
@@ -1242,7 +1298,7 @@ impl Launcher {
|
||||
self.client_error = None;
|
||||
if !self.settings.owner_notice_seen {
|
||||
self.settings.owner_notice_seen = true;
|
||||
self.settings.save();
|
||||
self.save_settings();
|
||||
}
|
||||
}
|
||||
changed = true;
|
||||
@@ -1642,7 +1698,7 @@ impl Launcher {
|
||||
if let Some(path) = paths.pop() {
|
||||
let _ = this.update(cx, |this, cx| {
|
||||
this.settings.minecraft_dir = Some(path);
|
||||
this.settings.save();
|
||||
this.save_settings();
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
@@ -2615,7 +2671,7 @@ impl Launcher {
|
||||
let hide = partner::hide_button("partner-hide", &t).on_click(cx.listener(
|
||||
|this, _: &ClickEvent, _, cx| {
|
||||
this.settings.partner_hidden = true;
|
||||
this.settings.save();
|
||||
this.save_settings();
|
||||
cx.notify();
|
||||
},
|
||||
));
|
||||
@@ -3263,7 +3319,7 @@ impl Launcher {
|
||||
let toggle = switch("auto-install", self.settings.auto_install(), &t).on_click(
|
||||
cx.listener(|this, _: &ClickEvent, _, cx| {
|
||||
this.settings.install_new_versions = Some(!this.settings.auto_install());
|
||||
this.settings.save();
|
||||
this.save_settings();
|
||||
if this.settings.auto_install() {
|
||||
this.install_update();
|
||||
}
|
||||
@@ -3273,14 +3329,14 @@ impl Launcher {
|
||||
let prune = switch("auto-prune", self.settings.auto_prune(), &t).on_click(cx.listener(
|
||||
|this, _: &ClickEvent, _, cx| {
|
||||
this.settings.prune_old_versions = Some(!this.settings.auto_prune());
|
||||
this.settings.save();
|
||||
this.save_settings();
|
||||
cx.notify();
|
||||
},
|
||||
));
|
||||
let opening = switch("opening", !self.settings.skip_opening, &t).on_click(cx.listener(
|
||||
|this, _: &ClickEvent, _, cx| {
|
||||
this.settings.skip_opening = !this.settings.skip_opening;
|
||||
this.settings.save();
|
||||
this.save_settings();
|
||||
cx.notify();
|
||||
},
|
||||
));
|
||||
@@ -3289,7 +3345,7 @@ impl Launcher {
|
||||
cx.listener(|this, _: &ClickEvent, _, cx| {
|
||||
let on = !this.settings.desktop_entry();
|
||||
this.settings.desktop_entry = Some(on);
|
||||
this.settings.save();
|
||||
this.save_settings();
|
||||
let _ = if on {
|
||||
horizon_launcher::desktop::install(backend::exe_path()).map(|_| ())
|
||||
} else {
|
||||
@@ -3390,7 +3446,7 @@ impl Launcher {
|
||||
move |choice, window, cx| {
|
||||
let _ = this.update(cx, |this, cx| {
|
||||
this.settings.appearance = choice;
|
||||
this.settings.save();
|
||||
this.save_settings();
|
||||
titlebar::apply_appearance(window, choice);
|
||||
this.theme = this.settings_theme(window);
|
||||
cx.notify();
|
||||
@@ -3655,7 +3711,7 @@ impl Launcher {
|
||||
line_edit::Key::Escape => window.blur(cx),
|
||||
line_edit::Key::Edited => {
|
||||
this.settings.game_args = this.args_input.text().to_string();
|
||||
this.settings.save();
|
||||
this.save_settings();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -3747,7 +3803,7 @@ impl Launcher {
|
||||
fn save_proxy(&mut self) {
|
||||
let value = self.proxy_input.text().trim().to_owned();
|
||||
self.settings.proxy = (!value.is_empty()).then_some(value);
|
||||
self.settings.save();
|
||||
self.save_settings();
|
||||
horizon_launcher::proxy::announce(self.settings.proxy.clone());
|
||||
}
|
||||
|
||||
@@ -4054,7 +4110,7 @@ impl Launcher {
|
||||
button(format!("use-{tag}"), "Use", Kind::Ghost, &t).on_click(cx.listener(
|
||||
move |this, _: &ClickEvent, _, cx| {
|
||||
this.settings.selected = Some(t1.clone());
|
||||
this.settings.save();
|
||||
this.save_settings();
|
||||
cx.notify();
|
||||
},
|
||||
)),
|
||||
@@ -4831,6 +4887,7 @@ impl Render for Launcher {
|
||||
.bg(t.side)
|
||||
.child(sky)
|
||||
.children(titlebar::render(&t, window))
|
||||
.children(self.settings_save_notice(cx))
|
||||
.child(root.flex_1().min_h_0())
|
||||
.children(titlebar::resize_handles(window))
|
||||
}
|
||||
|
||||
@@ -1092,7 +1092,7 @@ impl Launcher {
|
||||
button("native-notice-install", "Install", Kind::Primary, &t).on_click(
|
||||
cx.listener(|this, _: &ClickEvent, _, cx| {
|
||||
this.settings.native_mods_noticed = true;
|
||||
this.settings.save();
|
||||
this.save_settings();
|
||||
if let Some((source, id, known)) =
|
||||
this.mods.native_notice.take()
|
||||
{
|
||||
@@ -1548,7 +1548,7 @@ impl Launcher {
|
||||
this.settings.native_profile = Some(choice);
|
||||
this.settings.native_release = false;
|
||||
this.native_done = None;
|
||||
this.settings.save();
|
||||
this.save_settings();
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ impl Launcher {
|
||||
self.owner_screen_open = false;
|
||||
if !self.settings.owner_notice_seen {
|
||||
self.settings.owner_notice_seen = true;
|
||||
self.settings.save();
|
||||
self.save_settings();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1182,7 +1182,7 @@ impl Launcher {
|
||||
super::partner::hide_button("partner-servers-hide", &t).on_click(cx.listener(
|
||||
|this, _: &ClickEvent, _, cx| {
|
||||
this.settings.partner_hidden = true;
|
||||
this.settings.save();
|
||||
this.save_settings();
|
||||
cx.notify();
|
||||
},
|
||||
)),
|
||||
|
||||
@@ -60,7 +60,7 @@ impl Launcher {
|
||||
})
|
||||
.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
|
||||
this.settings.accent = accent;
|
||||
this.settings.save();
|
||||
this.save_settings();
|
||||
this.theme = this.settings_theme(window);
|
||||
cx.notify();
|
||||
})),
|
||||
@@ -154,13 +154,19 @@ impl Launcher {
|
||||
}))
|
||||
.on_mouse_up(
|
||||
MouseButton::Left,
|
||||
cx.listener(|this, _: &MouseUpEvent, _, _| this.finish_hue()),
|
||||
cx.listener(|this, _: &MouseUpEvent, _, cx| {
|
||||
this.finish_hue();
|
||||
cx.notify();
|
||||
}),
|
||||
)
|
||||
// `on_mouse_up` fires only while the strip is hovered, so a drag released off it would
|
||||
// keep its accent in memory and never write it.
|
||||
.on_mouse_up_out(
|
||||
MouseButton::Left,
|
||||
cx.listener(|this, _: &MouseUpEvent, _, _| this.finish_hue()),
|
||||
cx.listener(|this, _: &MouseUpEvent, _, cx| {
|
||||
this.finish_hue();
|
||||
cx.notify();
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -168,7 +174,7 @@ impl Launcher {
|
||||
fn finish_hue(&mut self) {
|
||||
if self.hue_dragging {
|
||||
self.hue_dragging = false;
|
||||
self.settings.save();
|
||||
self.save_settings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +221,7 @@ impl Launcher {
|
||||
match themes::keep_background(&backend::data_dir(), path) {
|
||||
Ok(name) => {
|
||||
self.settings.background = Some(name);
|
||||
self.settings.save();
|
||||
self.save_settings();
|
||||
self.background_error = None;
|
||||
}
|
||||
Err(e) => self.background_error = Some(e),
|
||||
@@ -242,7 +248,7 @@ impl Launcher {
|
||||
button("background-reset", "Landscape", Kind::Ghost, &t).on_click(cx.listener(
|
||||
|this, _: &ClickEvent, _, cx| {
|
||||
this.settings.background = None;
|
||||
this.settings.save();
|
||||
this.save_settings();
|
||||
cx.notify();
|
||||
},
|
||||
)),
|
||||
|
||||
@@ -24,7 +24,10 @@ impl Launcher {
|
||||
/// The game started: get out of its way as the player asked. Read from `settings.json`, so
|
||||
/// `horizon-launcher settings while-playing …` counts at once.
|
||||
pub(super) fn step_aside(&mut self, cx: &mut Context<Self>) {
|
||||
self.settings.while_playing = Settings::load().while_playing;
|
||||
// Keep a choice whose save failed available for Retry instead of replacing it from disk.
|
||||
if self.settings_save_error.is_none() {
|
||||
self.settings.while_playing = Settings::load().while_playing;
|
||||
}
|
||||
let how = self.settings.while_playing.here();
|
||||
if how == WhilePlaying::Stay {
|
||||
return;
|
||||
@@ -67,7 +70,7 @@ impl Launcher {
|
||||
move |choice, _, cx| {
|
||||
let _ = this.update(cx, |this, cx| {
|
||||
this.settings.while_playing = choice;
|
||||
this.settings.save();
|
||||
this.save_settings();
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -585,6 +585,7 @@ public final class VanillaMobAiOracle {
|
||||
new net.minecraft.world.item.ItemStack(BuiltInRegistries.ITEM.getValue(
|
||||
net.minecraft.resources.Identifier.parse("minecraft:red_harness"))));
|
||||
case "noai" -> mob.setNoAi(true);
|
||||
case "agelocked" -> method(AgeableMob.class, "setAgeLocked", boolean.class).invoke(mob, true);
|
||||
case "profession" -> {
|
||||
Villager villager = (Villager) mob;
|
||||
villager.setVillagerData(villager.getVillagerData().withProfession(level.registryAccess(),
|
||||
@@ -1109,6 +1110,7 @@ public final class VanillaMobAiOracle {
|
||||
mem.sort(null);
|
||||
sb.append("\tmem ").append(String.join(" ", mem));
|
||||
}
|
||||
if (m instanceof AgeableMob ageable) sb.append("\tage ").append(ageable.getAge());
|
||||
if (m instanceof Villager villager) sb.append("\tvx ").append(villager(villager));
|
||||
if (m instanceof net.minecraft.world.entity.animal.golem.CopperGolem golem) {
|
||||
Object opened = get(golem, "openedChestPos");
|
||||
|
||||
@@ -350,6 +350,7 @@ class StressSceneTests(unittest.TestCase):
|
||||
self.assertEqual(summons("stress_items", "item"), 2000)
|
||||
self.assertEqual(sum(summons("stress_held", k) for k in stress_scenes.HOLDERS), 1000)
|
||||
self.assertEqual(sum(summons("stress_eyes", k) for k in stress_scenes.EYED), 1000)
|
||||
self.assertEqual(sum(summons("stress_glowing", k) for k in stress_scenes.MOBS), 1000)
|
||||
self.assertEqual(summons("stress_frames", "item_frame"), 512)
|
||||
self.assertEqual(summons("stress_armor_stands", "armor_stand"), 500)
|
||||
self.assertEqual(stress_scenes.setup("stress_empty"), stress_scenes.SETTINGS + stress_scenes.floor(
|
||||
|
||||
@@ -1686,6 +1686,38 @@ def summoned_potions():
|
||||
return ops
|
||||
|
||||
|
||||
def trial_spawner_passengers():
|
||||
"""Configured passengers must exist as entities with a vehicle (#7254)."""
|
||||
ops = [*setup('/fill 1 219 1 13 219 8 stone',
|
||||
'/fill 1 220 1 13 224 8 air', '/gamerule spawn_mobs true')]
|
||||
# Exercise namespace and position independently, plus the explicit control.
|
||||
for i, (kind, pos) in enumerate([
|
||||
('chicken', ''), ('minecraft:chicken', ''),
|
||||
('chicken', ',Pos:[4.5d,220.0d,6.5d]'),
|
||||
('minecraft:chicken', ',Pos:[4.5d,220.0d,6.5d]'),
|
||||
]):
|
||||
parent, rider = f'trial_parent_{i}', f'trial_rider_{i}'
|
||||
ops += setup(
|
||||
'/setblock 4 220 4 trial_spawner[trial_spawner_state=active]'
|
||||
'{normal_config:{spawn_range:3,total_mobs:1.0f,simultaneous_mobs:1.0f,'
|
||||
'total_mobs_added_per_player:0.0f,simultaneous_mobs_added_per_player:0.0f,'
|
||||
'ticks_between_spawn:1000},spawn_data:{entity:{id:"zombie",'
|
||||
f'Tags:["{parent}"],Pos:[4.5d,220.0d,6.5d],NoAI:1b,'
|
||||
f'Passengers:[{{id:"{kind}",NoAI:1b,Tags:["{rider}"]{pos}}}]'
|
||||
'}},next_mob_spawns_at:0L}',
|
||||
'/data modify block 4 220 4 registered_players append from entity @s UUID')
|
||||
ops += [
|
||||
{'op': 'tick', 'n': 5},
|
||||
cmd(f'/execute if entity @e[tag={parent}]'),
|
||||
cmd(f'/execute if entity @e[tag={rider}]'),
|
||||
cmd(f'/execute as @e[tag={rider}] on vehicle if entity @s[tag={parent}]'),
|
||||
cmd(f'/data get entity @e[tag={parent},limit=1] Passengers[0].Tags'),
|
||||
*setup(f'/kill @e[tag={parent}]', f'/kill @e[tag={rider}]',
|
||||
'/setblock 4 220 4 air'),
|
||||
]
|
||||
return ops + setup('/fill 1 219 1 13 224 8 air', '/gamerule spawn_mobs false')
|
||||
|
||||
|
||||
def groups():
|
||||
return [
|
||||
('setup', group_setup()),
|
||||
@@ -1698,6 +1730,7 @@ def groups():
|
||||
('wither spawn', wither_spawn()),
|
||||
('riding teleport', riding_teleport()),
|
||||
('mob riding', mob_riding()),
|
||||
('trial spawner passengers', trial_spawner_passengers()),
|
||||
('camel riders', camel_riders()),
|
||||
('scores, teams and bossbars', scores()),
|
||||
('execute', execute()),
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"name": "movement_input",
|
||||
"target": "horizon_client::game::native_hooks::Game<'_>::native_movement_input",
|
||||
"neoforge": "MovementInputUpdateEvent",
|
||||
"side": "client",
|
||||
"cancellable": false,
|
||||
"mod_api": {
|
||||
"contract": "listen"
|
||||
}
|
||||
}
|
||||
],
|
||||
"rust_summary": "Local player movement input before application (`native_movement_input`).",
|
||||
"doc_summary": "`native_movement_input(forward, strafe, jump, sneak)` lets client mods change local movement after input is read and before it is applied. Positive strafe is left; servers receive direction flags rather than scalar magnitudes. Bundles need hooks API @API:movement_input@."
|
||||
}
|
||||
@@ -61,7 +61,7 @@ StandingSignBlock.getStateForPlacement port crates/horizon-world/src/level/place
|
||||
StandingSignBlock.mirror port crates/horizon-world/src/worldgen/structure.rs::StateTransforms::mirror
|
||||
StandingSignBlock.rotate port crates/horizon-world/src/worldgen/structure.rs::StateTransforms::rotate
|
||||
StandingSignBlock.updateShape port crates/horizon-world/src/level/shapes.rs::Sim::update_shape_class
|
||||
StemBlock.getCloneItemStack missing unsure: minor: middle-click on a stem may not return its seed item; no stem case found in the pick code
|
||||
StemBlock.getCloneItemStack port crates/horizon-client/src/game/block_items.rs::clone_item a pumpkin or melon stem, attached or not, picks the seed it grew from; any other block picks its own item
|
||||
StemBlock.isValidBonemealTarget port crates/horizon-world/src/level/bonemeal.rs::Sim::bonemeal_valid
|
||||
StemBlock.performBonemeal port crates/horizon-world/src/level/bonemeal.rs::Sim::perform_bonemeal
|
||||
StemBlock.randomTick port crates/horizon-world/src/level/tick.rs::Sim::stem_random_tick
|
||||
@@ -75,8 +75,8 @@ SuspiciousEffectHolder.tryGet port crates/horizon-items/src/flowers.rs::stew_eff
|
||||
TallSeagrassBlock.canSurvive port crates/horizon-world/src/level/survive.rs::Sim::can_survive_class
|
||||
TallSeagrassBlock.getStateForPlacement port crates/horizon-world/src/level/place.rs::Sim::placement_class
|
||||
TallSeagrassBlock.mayPlaceOn port crates/horizon-world/src/level/survive.rs::Sim::may_place_on
|
||||
TestBlock missing minor: game-test block for operators; Horizon keeps only its NBT
|
||||
TestInstanceBlock missing minor: game-test instance block; Horizon keeps only its NBT
|
||||
TestBlock skip:debug game-test tooling: an operator's test block runs a structure when triggered and Horizon keeps only its block entity NBT
|
||||
TestInstanceBlock skip:debug game-test tooling: the instance block runs and exports a test structure; Horizon keeps its NBT
|
||||
TintedParticleLeavesBlock.spawnFallingLeavesParticle port crates/horizon-client/src/animate_tick.rs::block_animate_tick falling tinted leaf particle
|
||||
TorchBlock.animateTick port crates/horizon-client/src/animate_tick.rs::block_animate_tick torch flame
|
||||
TorchflowerCropBlock.randomTick port crates/horizon-world/src/level/tick.rs::Sim::random_tick_block growth arm
|
||||
@@ -100,9 +100,9 @@ WallHangingSignBlock.canAttachTo inline crates/horizon-world/src/level/place.rs:
|
||||
WallHangingSignBlock.canPlace port crates/horizon-world/src/level/place.rs::Sim::wall_hanging_sign_can_place
|
||||
WallHangingSignBlock.createBlockStateDefinition skip:data states come from the generated block state table
|
||||
WallHangingSignBlock.getStateForPlacement port crates/horizon-world/src/level/place.rs::Sim::placement_class
|
||||
WallHangingSignBlock.isHittingEditableSide missing unsure: player: a hanging sign clicked on its side may not chain onto a wall hanging sign
|
||||
WallHangingSignBlock.isHittingEditableSide inline crates/horizon-world/src/level/interact.rs::Behaviours::block_use_item_on the faces of the sign it faces and its opposite are the editable ones; a hanging sign aimed at any other face passes
|
||||
WallHangingSignBlock.rotate port crates/horizon-world/src/worldgen/structure.rs::StateTransforms::rotate
|
||||
WallHangingSignBlock.shouldTryToChainAnotherHangingSign missing unsure: player: a second hanging sign used on a wall sign may not chain on
|
||||
WallHangingSignBlock.shouldTryToChainAnotherHangingSign inline crates/horizon-world/src/level/interact.rs::Behaviours::block_use_item_on passing on a non-editable face lets the held hanging sign place its own block, which chains onto the wall sign
|
||||
WallHangingSignBlock.updateShape port crates/horizon-world/src/level/shapes.rs::Sim::update_shape_class
|
||||
WallHangingSignBlock.useItemOn port crates/horizon-world/src/level/interact.rs::Behaviours::use_item_on gap: player: chaining a hanging sign onto a wall hanging sign is not ported here
|
||||
WallSignBlock.canSurvive port crates/horizon-world/src/level/survive.rs::Sim::can_survive_class
|
||||
@@ -132,7 +132,7 @@ BannerPatternLayers.removeLast inline crates/horizon-client/src/cauldron.rs::int
|
||||
BannerPatternLayers.Builder skip:java builder for banner pattern layers
|
||||
BannerPatterns skip:data registry bootstrap
|
||||
BarrelBlockEntity.loadAdditional port crates/horizon-client-core/src/block_entity_nbt.rs::canonical gap: minor: canonical NBT form only, no load step of its own
|
||||
BarrelBlockEntity.recheckOpen missing unsure: player: a barrel lid may stay open after a player walks away; only golem openers are rechecked on a timer
|
||||
BarrelBlockEntity.recheckOpen missing the lid's periodic recheck of whether its viewers are still there; Horizon draws no barrel lid at all (containers.rs holds its 27 slots), so nothing rechecks one, where a golem's openers are rechecked on a timer
|
||||
BarrelBlockEntity.saveAdditional port crates/horizon-client-core/src/block_entity_nbt.rs::canonical gap: minor: canonical NBT form only
|
||||
BarrelBlockEntity.startOpen port crates/horizon-client/src/game/block_entity_anims.rs::Game::barrel_openers_changed
|
||||
BarrelBlockEntity.stopOpen port crates/horizon-client/src/game/block_entity_anims.rs::Game::barrel_openers_changed
|
||||
@@ -162,15 +162,15 @@ PotentSulfurBlockEntity.setLevel inline crates/horizon-client/src/game/potent_su
|
||||
PotentSulfurBlockEntity.spawnGeyserParticle port crates/horizon-client/src/game/potent_sulfur.rs::Game::spawn_geyser_particle ticked in #3516; `PotentSulfurBlockEntity.spawnGeyserParticle` is cited at crates/horizon-client/src/game/potent_sulfur.rs:380
|
||||
PotentSulfurBlockEntity.spawnNoxiousGasCloudParticle port crates/horizon-client/src/game/potent_sulfur.rs::Game::spawn_noxious_gas_cloud_particle ticked in #3516; `PotentSulfurBlockEntity.spawnNoxiousGasCloudParticle` is cited at crates/horizon-client/src/game/potent_sulfur.rs:395
|
||||
RandomizableContainerBlockEntity.applyImplicitComponents port crates/horizon-client/src/block_entity_item.rs::place the item's minecraft:container_loot hands the block its LootTable and LootTableSeed
|
||||
RandomizableContainerBlockEntity.canOpen missing unsure: minor: the spectator and loot-table open checks were not found
|
||||
RandomizableContainerBlockEntity.canOpen inline crates/horizon-client/src/game/advancements.rs::Game::container_locked_for the negative of it: a spectator opens any container, and otherwise the lock decides (LockCode.canUnlock)
|
||||
RandomizableContainerBlockEntity.collectImplicitComponents port crates/horizon-client/src/block_entity_item.rs::collect the block's LootTable and LootTableSeed leave on the item as minecraft:container_loot
|
||||
RandomizableContainerBlockEntity.createMenu port crates/horizon-client/src/game.rs::Game::open_menu gap: minor: the spectator and loot-table checks are not ported here
|
||||
SculkSensorBlockEntity.loadAdditional port crates/horizon-client-render/src/sculk.rs::SculkEntity::load
|
||||
SculkSensorBlockEntity.saveAdditional port crates/horizon-client-render/src/sculk.rs::SculkEntity::to_nbt
|
||||
SculkSensorBlockEntity.VibrationUser.canReceiveVibration port crates/horizon-client-render/src/sculk.rs::valid_vibration
|
||||
SculkSensorBlockEntity.VibrationUser.onReceiveVibration port crates/horizon-client-render/src/sculk.rs::Data::schedule
|
||||
TestBlockEntity missing minor: game-test block entity; reset, trigger and logging are not ported, NBT only
|
||||
TestInstanceBlockEntity missing minor: game-test structure tooling; export and chunk forcing are not ported
|
||||
TestBlockEntity skip:debug game-test tooling: reset, trigger and logging belong to the test runner, which Horizon does not have; its NBT is kept
|
||||
TestInstanceBlockEntity skip:debug game-test tooling: export and chunk forcing belong to the test runner, which Horizon does not have
|
||||
TrialSpawnerConfig.calculateTargetSimultaneousMobs port crates/horizon-client/src/mobs/trial_spawner.rs::TrialConfig::target_simultaneous_mobs
|
||||
TrialSpawnerConfig.calculateTargetTotalMobs port crates/horizon-client/src/mobs/trial_spawner.rs::TrialConfig::target_total_mobs
|
||||
TrialSpawnerConfig.withSpawning skip:java builder-style setter on the config record
|
||||
@@ -204,7 +204,7 @@ ImposterProtoChunk skip:other wrapper so the vanilla status pipeline can read a
|
||||
LinearPalette skip:java palette storage; Horizon's chunk.rs keeps block ids in plain arrays
|
||||
PalettedContainerFactory skip:java factory for palette storage; Horizon's chunk.rs keeps block ids in plain arrays
|
||||
SingleValuePalette skip:java palette storage; Horizon's chunk.rs keeps block ids in plain arrays
|
||||
UpgradeData missing unsure: minor: block upgrades of old chunks on load are not ported
|
||||
UpgradeData missing the block upgrades an old chunk takes when it loads (the Sides bits a save carries); the datafix writes and keeps UpgradeData (datafix/chunk_leaves.rs) but no load path applies the upgrades
|
||||
ChunkDependencies.getRadiusOf inline crates/horizon-client-render/src/streaming.rs::Ranges::margins fixed margins instead of a per-status radius
|
||||
ChunkPyramid.Builder skip:java builder for the status pyramid; Horizon uses fixed margins
|
||||
ChunkStatusTasks.buildTerrain inline crates/horizon-world/src/worldgen/generator.rs::Pipeline::generate_with_context terrain pass
|
||||
|
||||
|
@@ -52,7 +52,7 @@ GameRuleMap.equals skip:java value equality of the map
|
||||
GameRuleMap.of port crates/horizon-client-core/src/gamerules.rs::GameRules::get no stored map: a rule that was never set reads its table default
|
||||
GameRuleMap.setFromIf skip:other no caller in the pinned sources (only withOther uses it)
|
||||
GameRuleMap.withOther skip:other no caller in the pinned sources
|
||||
BlendingData port crates/horizon-world/src/worldgen/blending.rs::BlendingData unpack/pack, calculateData for the border columns, heights, densities and biomes; the client reads the saved chunks (old_world.rs)
|
||||
BlendingData home crates/horizon-world/src/worldgen/blending.rs BlendingData and Blender are in that file: unpack/pack and calculated (getOrUpdateBlendingData + calculateData) for the border columns, heights, densities and biomes; the client reads the saved chunks (old_world.rs)
|
||||
BlendingData.Packed.validateArraySize skip:java codec size check
|
||||
AllOfPredicate.test port crates/horizon-world/src/worldgen/block_predicate.rs::BlockPredicate::test_inner the All arm
|
||||
AnyOfPredicate.test port crates/horizon-world/src/worldgen/block_predicate.rs::BlockPredicate::test_inner the Any arm
|
||||
|
||||
|
@@ -180,7 +180,7 @@ CollectingNeighborUpdater.addAndRun port crates/horizon-world/src/level/update.r
|
||||
CollectingNeighborUpdater.neighborChanged port crates/horizon-world/src/level/update.rs::Sim::neighbor_changed_at Simple update
|
||||
CollectingNeighborUpdater.runUpdates port crates/horizon-world/src/level/update.rs::Sim::run_updates
|
||||
CollectingNeighborUpdater.shapeUpdate port crates/horizon-world/src/level/update.rs::Sim::neighbor_shape_changed
|
||||
CollectingNeighborUpdater.updateNeighborsAtExceptFromFacing port crates/horizon-world/src/level/update.rs::Sim::update_neighbors_at_except
|
||||
CollectingNeighborUpdater.updateNeighborsAtExceptFromFacing port crates/horizon-world/src/level/update.rs::Sim::update_neighbors_at_except_oriented the Multi update with one side skipped; the orientation is threaded through here (callers pass the one they have)
|
||||
CollectingNeighborUpdater.FullNeighborUpdate.runNext port crates/horizon-world/src/level/update.rs::Update::next Full arm
|
||||
CollectingNeighborUpdater.MultiNeighborUpdate.forEachUpdatedPos inline crates/horizon-world/src/level/update.rs::Update::next Multi arm steps the index and skips the facing side
|
||||
CollectingNeighborUpdater.MultiNeighborUpdate.runNext port crates/horizon-world/src/level/update.rs::Update::next Multi arm
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
ExperimentalRedstoneWireEvaluator port crates/horizon-world/src/level/redstone/experimental.rs::Sim::experimental_wire_update two queues and insertion-ordered power changes with oriented neighbor notifications
|
||||
ExperimentalRedstoneWireEvaluator home crates/horizon-world/src/level/redstone/experimental.rs the two-queue evaluator, Evaluator in that file, is what Sim::experimental_wire_update drives: wires found with their orientation, power written in first-discovery order
|
||||
InstantNeighborUpdater skip:other never instantiated in vanilla 26.3; Level always uses CollectingNeighborUpdater
|
||||
NeighborUpdater.executeShapeUpdate port crates/horizon-world/src/level/update.rs::Sim::execute Shape arm: wire skip flag, then update_shape and update_or_destroy
|
||||
NeighborUpdater.executeUpdate port crates/horizon-world/src/level/hooks.rs::Sim::neighbor_changed per-block neighbour change dispatch
|
||||
@@ -31,7 +31,7 @@ LevelStorageSource.isNewLevelIdAcceptable port crates/horizon-client/src/title.r
|
||||
LevelStorageSource.levelExists inline crates/horizon-client/src/title.rs::Title::target_folder folder-exists check
|
||||
LevelStorageSource.loadLevelSummaries port crates/horizon-client/src/title.rs::list_worlds
|
||||
LevelStorageSource.makeLevelSummary inline crates/horizon-client/src/title.rs::list_worlds per-folder name, version, mode and last played
|
||||
LevelStorageSource.parseFeatureFlagsFromSummary missing unsure: list_worlds does not read enabled_features, so experimental worlds are not flagged
|
||||
LevelStorageSource.parseFeatureFlagsFromSummary port crates/horizon-client/src/save.rs::Level::read_summary the summary read goes through read_root, which parses WorldDataConfiguration's enabled_features; gap: minor: the world list still shows no experimental marker for them
|
||||
LevelStorageSource.parseValidator skip:other symlink allow-list (allowed_symlinks.txt); Horizon does not read that file
|
||||
LevelStorageSource.readDataConfig port crates/horizon-client/src/save.rs::Level::read DataPacks and enabled_features are read with the level (data_packs, enabled_features), and datapack::enabled_ids layers the world's own packs over the bundle with them
|
||||
LevelSummary.backupStatus inline crates/horizon-client/src/title.rs::should_backup FILE_FIXING_REQUIRED when the world needs its files fixed or comes from a newer version; UPGRADE_TO_SNAPSHOT never applies to a stable build
|
||||
|
||||
|
@@ -1,9 +1,10 @@
|
||||
SetStewEffectFunction.Builder skip:java builder for the stew effect list; Horizon keeps the effects in loot.rs Context::run (set_stew_effect)
|
||||
SetWritableBookPagesFunction missing minor: no vanilla loot table uses set_writable_book_pages; Horizon's loot run has no arm for it
|
||||
SetWrittenBookPagesFunction missing minor: no vanilla loot table uses set_written_book_pages; Horizon's loot run has no arm for it
|
||||
SetWritableBookPagesFunction missing `horizon-world/src/loot.rs::modify` has no `set_writable_book_pages` arm, and no vanilla loot table asks for one
|
||||
SetWrittenBookPagesFunction missing no `set_written_book_pages` arm in `loot.rs::modify` (the arms run from `sequence` and `set_count` through `set_potion` to `set_stew_effect`); no vanilla loot table asks for one
|
||||
SmeltItemFunction.run port crates/horizon-world/src/loot.rs::Context::run furnace_smelt arm: smelting recipe, use_input_count and stack cap
|
||||
SmeltItemFunction.smelted skip:java static builder factory
|
||||
ToggleTooltips home crates/horizon-client/src/game/commands/items.rs the `/item modify` arm toggle_tooltips writes the tooltip_display component's hide_tooltip and hidden_components as run does; the loot run has no arm, and no vanilla loot table uses it
|
||||
ToggleTooltips home crates/horizon-client/src/game/commands/items.rs the `/item modify` arm of cmd_item_modify writes the tooltip_display component's hide_tooltip and hidden_components as run does; the loot run has no arm, and no vanilla loot table uses it
|
||||
ToggleTooltips.run inline crates/horizon-client/src/game/commands/items.rs::Game::cmd_item_modify the `minecraft:toggle_tooltips` arm of the `/item modify` command adds each false toggle to `tooltip_display`'s hidden_components and keeps hide_tooltip; the loot run has no arm, so a loot table's toggle_tooltips is ignored (no vanilla loot table uses one)
|
||||
LootContextParamSets skip:data loot parameter set registry and validation; Horizon reads its loot tables
|
||||
AllOfCondition.allOf skip:java static builder
|
||||
AllOfCondition.combine inline crates/horizon-items/src/items.rs::LootContext::condition all_of arm: every term must hold
|
||||
@@ -16,11 +17,11 @@ CompositeLootItemCondition.Builder skip:java builder (addTerm)
|
||||
ConditionUserBuilder skip:java builder interface for loot conditions
|
||||
DamageSourceCondition.test port crates/horizon-world/src/loot.rs::Context::condition damage_source_properties arm (mob death loot)
|
||||
EnchantmentActiveCheck.test inline crates/horizon-client/src/game/enchant_effects.rs::Game::tick_soul_speed the soul speed modifiers switch on and off here; the ENCHANTMENT_ACTIVE flag itself is not read as a condition
|
||||
EntityHasScoreCondition.hasScore missing minor: no vanilla loot table uses entity_scores
|
||||
EntityHasScoreCondition.test missing minor: no vanilla loot table uses entity_scores
|
||||
EntityHasScoreCondition.hasScore missing `loot.rs::condition` has no `entity_scores` arm, and no vanilla loot table asks for one
|
||||
EntityHasScoreCondition.test missing same as `hasScore`: the condition dispatch (`inverted`, `all_of`, `any_of`, `entity_properties`, `killed_by_player`, the chance and location checks) has no scoreboard arm
|
||||
EntityHasScoreCondition.validate skip:data datapack validation
|
||||
EnvironmentAttributeCheck.createCodec skip:java codec
|
||||
EnvironmentAttributeCheck.test missing minor: no vanilla data uses environment_attribute_check
|
||||
EnvironmentAttributeCheck.test missing no `environment_attribute_check` arm in `loot.rs::condition`; no vanilla data uses it
|
||||
ExplosionCondition.test port crates/horizon-items/src/items.rs::LootContext::condition survives_explosion arm: 1 / radius under an explosion (block loot; only block tables use it)
|
||||
FloatValueCheck.validate skip:data datapack validation
|
||||
IntValueCheck.validate skip:data datapack validation
|
||||
@@ -39,7 +40,7 @@ MatchBlock.blockMatches skip:java static builder
|
||||
MatchBlock.getReferencedContextParams skip:data validation metadata for context params
|
||||
MatchBlock.test port crates/horizon-items/src/items.rs::LootContext::condition match_block arm: block id, state and property ranges; gap: minor: the block-entity part of the predicate is not read
|
||||
MatchTool.test port crates/horizon-items/src/items.rs::LootContext::condition match_tool arm; gap: minor: item ids, tags and enchantment levels only, other ItemPredicate parts are not read
|
||||
TimeCheck missing minor: time_check is used by no vanilla data file (datapacks only)
|
||||
TimeCheck missing no `time_check` arm in `loot.rs::condition`; no vanilla data file uses it, so only a datapack reaches it
|
||||
WeatherCheck.test port crates/horizon-client/src/game/trident.rs::Game::channel_lightning channeling's thunder check (thundering only); gap: minor: raining is not read, channeling only uses thundering
|
||||
NbtProviders skip:data registry bootstrap
|
||||
AggregateProvider skip:java codec only
|
||||
@@ -58,13 +59,13 @@ PowerProvider.mapCodec skip:java codec
|
||||
PowerProvider.validate skip:data datapack validation
|
||||
RangeProvider.mapCodec skip:java codec
|
||||
RangeProvider.validate skip:data datapack validation
|
||||
StoredNumberAccess missing minor: storage-backed numbers; no vanilla data uses storage providers
|
||||
StoredNumberAccess missing no storage-backed number provider: `loot.rs::level_value` and the int and float readers know constant, uniform, binomial, linear, clamped, fraction, levels_squared and lookup only
|
||||
UnaryProvider skip:java codec only
|
||||
floats/Average.getFloatUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_float avg arm
|
||||
floats/ContextFloatProviders skip:data registry bootstrap
|
||||
floats/Difference.getFloatUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_float sub arm
|
||||
floats/EnchantmentLevelProvider.getFloatUnsafe port crates/horizon-items/src/enchant.rs::Enchantments::condition random_chance arm: chance at the enchantment level; gap: minor: only the random_chance use is read
|
||||
floats/EnvironmentAttributeValue missing minor: no vanilla data uses environment_attribute providers
|
||||
floats/EnvironmentAttributeValue missing no `environment_attribute` provider in `loot.rs::level_value`
|
||||
floats/Length.getFloatUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_float length arm
|
||||
floats/Maximum.getFloatUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_float max arm
|
||||
floats/Minimum.getFloatUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_float min arm
|
||||
@@ -73,7 +74,7 @@ floats/Product.getFloatUnsafe port crates/horizon-client/src/game/commands/compu
|
||||
floats/Quotient.getFloatUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_float div arm
|
||||
ResolvableFloat.getFromItem port crates/horizon-client-render/src/providers.rs::Providers::speed furnace fuel speed multiplier (COOKING_FUEL); gap: minor: only the furnace fuel component is read
|
||||
ResolvableFloat.wrap skip:java codec helper
|
||||
floats/StorageValue missing minor: no vanilla data uses storage providers
|
||||
floats/StorageValue missing no `storage` provider in `loot.rs::level_value`
|
||||
floats/Sum.getFloatUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_float add arm
|
||||
floats/Truncate.getFloatUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_float truncate arm
|
||||
floats/UniformGenerator.getFloatUnsafe port crates/horizon-world/src/loot.rs::Context::float uniform arm: min + random * (max - min)
|
||||
@@ -85,7 +86,7 @@ ints/ContextIntProviders.bootstrap skip:data registry bootstrap
|
||||
ints/ContextIntProviders.compostable port crates/horizon-client/src/game/composter.rs::Game::compostable the item's compostable layer provider; evaluated when the composter takes it (Game::composter_add)
|
||||
ints/ContextIntProviders.cooking port crates/horizon-client-render/src/providers.rs::Providers::burn_time fuel burn times (the cooking/time_* providers)
|
||||
ints/Difference.getIntUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_int sub arm
|
||||
ints/EnvironmentAttributeValue missing minor: no vanilla data uses environment_attribute providers
|
||||
ints/EnvironmentAttributeValue missing no `environment_attribute` provider in the int reader (`loot.rs::int`)
|
||||
ints/FloorModulus.getIntUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_int floor_mod arm
|
||||
ints/FloorQuotient.getIntUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_int floor_div arm
|
||||
ints/Maximum.getIntUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_int max arm
|
||||
@@ -95,10 +96,10 @@ ints/Product.getIntUnsafe port crates/horizon-client/src/game/commands/compute.r
|
||||
ints/Quotient.getIntUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_int div arm
|
||||
ResolvableInt.getFromItem port crates/horizon-client-render/src/providers.rs::Providers::burn_time furnace fuel burn time (COOKING_FUEL); gap: minor: only the furnace fuel component is read
|
||||
ResolvableInt.wrap skip:java codec helper
|
||||
ints/ScoreboardValue.getIntUnsafe missing minor: no vanilla data uses score providers
|
||||
ints/ScoreboardValue.getScoreInfo missing minor: no vanilla data uses score providers
|
||||
ints/ScoreboardValue.getIntUnsafe missing no `score` provider in `loot.rs::int`
|
||||
ints/ScoreboardValue.getScoreInfo missing no `score` provider in `loot.rs::int`
|
||||
ints/ScoreboardValue.validate skip:data datapack validation
|
||||
ints/StorageValue missing minor: no vanilla data uses storage providers
|
||||
ints/StorageValue missing no `storage` provider in `loot.rs::int`
|
||||
ints/Sum.getIntUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_int add arm
|
||||
ints/UniformGenerator.getIntUnsafe port crates/horizon-world/src/loot.rs::Context::int uniform arm: inclusive random int between min and max
|
||||
ints/WeightedListValue.getIntUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_select weighted list pick (the default branch)
|
||||
@@ -118,7 +119,7 @@ DirectoryValidator.validateSymlink skip:server server check of symlinks in worl
|
||||
PathAllowList.getForFileSystem skip:server server file allow-list
|
||||
PathAllowList.readPlain skip:server server file allow-list
|
||||
PathAllowList.ConfigEntry.parse skip:server server file allow-list
|
||||
BlockHitResult.hitBorder missing minor: BlockHit has no world-border flag; vanilla projectiles check it on a border hit
|
||||
BlockHitResult.hitBorder missing `BlockHit` (`horizon-core/src/geometry/mod.rs`) carries the location, the direction, the block and `inside`, no world-border flag, so a projectile's border hit is not told apart from any other
|
||||
BlockHitResult.withDirection skip:java copy helper (Rust struct update)
|
||||
BlockHitResult.withPosition skip:java copy helper (Rust struct update)
|
||||
HitResult.distanceTo port crates/horizon-client/src/game/fishing.rs::rebound the bobber's block-hit bounce: the squared distance to the hook scales the normalised velocity (`FishingHook.onHitBlock`)
|
||||
@@ -149,7 +150,7 @@ IdenticalMerger.forMergedIndexes port crates/horizon-core/src/geometry/shape/joi
|
||||
IndirectMerger.<init> skip:java list wrapper constructor
|
||||
IndirectMerger.forMergedIndexes port crates/horizon-core/src/geometry/shape/join.rs::merge
|
||||
IndirectMerger.getList skip:java list accessor
|
||||
MinecartCollisionContext missing player: vanilla lets a rail cart pass through the block under its rail and the slope block of an ascending rail (MinecartCollisionContext); minecarts.rs move_cart has no such exclusion, so carts may bump on those blocks (read from code, not checked in game)
|
||||
MinecartCollisionContext missing `game/minecarts.rs::move_cart` has no exclusion for the block a rail rests on or the slope block of an ascending rail, so a cart's move collides with them as with any other block
|
||||
NonOverlappingMerger.forMergedIndexes port crates/horizon-core/src/geometry/shape/join.rs::disjoint
|
||||
NonOverlappingMerger.forNonSwappedIndexes port crates/horizon-core/src/geometry/shape/join.rs::disjoint
|
||||
NonOverlappingMerger.getDouble skip:java list accessor
|
||||
|
||||
|
@@ -95,7 +95,7 @@ AbstractHorseRenderer.extractRenderState port crates/horizon-client/src/mobs/equ
|
||||
AbstractMinecartRenderer.submit port crates/horizon-client/src/game/minecarts.rs::Game::cart_geometry gap: player: no hurt tilt when a cart is hit (hurt_time is not read here); minor: no per-cart offset jitter
|
||||
AbstractMinecartRenderer.newRender missing minor: experimental minecart movement (new render path) is not implemented
|
||||
AbstractMinecartRenderer.oldRender port crates/horizon-client/src/game/minecarts.rs::Game::cart_geometry rail-based draw is the path Horizon has
|
||||
AbstractMinecartRenderer.newExtractState missing minor: experimental minecart movement (new render path) is not implemented
|
||||
AbstractMinecartRenderer.newExtractState port crates/horizon-client/src/game/remote.rs::Game::remote_minecart_lerp the cart's position and rotation sampled at the frame's partial tick from the steps a NewMinecartBehavior cart has queued; None when the cart has no lerp, and the renderer then keeps the entity's own interpolated position and rotation
|
||||
AbstractMinecartRenderer.oldExtractState port crates/horizon-client/src/game/minecarts.rs::Game::cart_geometry rail front and back points are taken from the cart's rail position
|
||||
AbstractMinecartRenderer.getRenderOffset missing minor: only the new render path offsets the cart
|
||||
AbstractSkeletonRenderer.extractRenderState port crates/horizon-client/src/mobs/anim.rs::setup gap: player: a skeleton's freeze-conversion shake (its isShaking) is not drawn; the aggressive and bow flags are read in setup
|
||||
|
||||
|
@@ -9,7 +9,7 @@ EnderDragonRenderer.submit port crates/horizon-client/src/game/dragon.rs::Game::
|
||||
EnderDragonRenderer.submitRays port crates/horizon-client/src/game/dragon.rs::Game::dragon_rays
|
||||
EnderDragonRenderer.submitCrystalBeams port crates/horizon-client/src/game/dragon.rs::Game::crystal_beam
|
||||
EnderDragonRenderer.extractRenderState inline crates/horizon-client/src/game/dragon.rs::Game::dragon_geometry flap, death fade, hurt overlay, nearest-crystal beam and egg distance read there
|
||||
EndermanRenderer.getRenderOffset port crates/horizon-client/src/mobs/render.rs EndermanRenderer.getRenderOffset's random side offset for a creepy enderman
|
||||
EndermanRenderer.getRenderOffset port crates/horizon-client/src/mobs/render.rs::enderman_render_offset EndermanRenderer.getRenderOffset's random side offset for a creepy enderman
|
||||
EndermanRenderer.extractRenderState inline crates/horizon-client/src/mobs/enderman.rs::anim gap: minor: carried block is drawn by game/mob_blocks.rs, not from this pose
|
||||
EntityRenderDispatcher.getRenderer skip:other renderer lookup; Horizon dispatches by mob kind in game.rs::Game::entity_geometry and game/creatures.rs::Game::mob_geometry
|
||||
EntityRenderDispatcher.getAvatarRenderer port crates/horizon-client/src/mobs/render.rs::body slim or wide arm model by skin
|
||||
@@ -33,7 +33,7 @@ EvokerFangsRenderer.submit port crates/horizon-client/src/game/fangs.rs::Game::f
|
||||
EvokerFangsRenderer.extractRenderState inline crates/horizon-client/src/game/fangs.rs::Game::fangs_geometry yaw and bite progress read there
|
||||
ExperienceOrbRenderer.submit inline crates/horizon-client/src/game.rs::Game::entity_geometry orb billboards with the pulsing colour, in the entity pass
|
||||
ExperienceOrbRenderer.vertex inline crates/horizon-client/src/game.rs::Game::entity_geometry vertex built inline
|
||||
FallingBlockRenderer.shouldRender missing minor: a falling block inside an identical block (vanilla hides it that tick) is still drawn; no such check in game.rs or falling_block.rs
|
||||
FallingBlockRenderer.shouldRender port crates/horizon-client/src/game.rs::falling_block_visible a falling block whose own state is the state of the cell its feet are in is not drawn, so a NoGravity block or one that has just landed shows no model; the entity draw skips it and a unit test covers both cases
|
||||
FallingBlockRenderer.submit inline crates/horizon-client/src/game.rs::Game::entity_geometry falling block pass with its baked block model
|
||||
FallingBlockRenderer.extractRenderState inline crates/horizon-client/src/game.rs::Game::entity_geometry position, state, biome tint and light read there
|
||||
FireworkEntityRenderer.extractRenderState port crates/horizon-client/src/game/fireworks.rs::Game::rocket_geometry angled rocket keeps its direction
|
||||
@@ -65,7 +65,7 @@ HumanoidMobRenderer.getEquipmentIfRenderable port crates/horizon-client/src/mobs
|
||||
IllagerRenderer.extractRenderState port crates/horizon-client/src/mobs/illager.rs::holding_crossbow crossbow charge and aggressive pose
|
||||
IllusionerRenderer.extractRenderState port crates/horizon-client/src/mobs/illusioner.rs::offsets mirror-image offsets; casting flag
|
||||
IronGolemRenderer.extractRenderState port crates/horizon-client/src/mobs/golem.rs::client_tick gap: player: the offered poppy is not drawn in the golem hand and cracks from damage are not drawn
|
||||
IronGolemRenderer.setupRotations port crates/horizon-client/src/mobs/render.rs::base_transform_at (IronGolem branch) the 6.5-degree triangle wave over a 13-block walk phase
|
||||
IronGolemRenderer.setupRotations port crates/horizon-client/src/mobs/render.rs::base_transform_at the 6.5-degree triangle wave over a 13-block walk phase
|
||||
ItemEntityRenderer.extractRenderState port crates/horizon-client/src/game/item_draw.rs::Game::dropped_item_geometry bob offset, spin and spread
|
||||
ItemEntityRenderer.renderMultipleFromCount port crates/horizon-client/src/item_render.rs::cluster_poses copies of a stack laid out by depth
|
||||
ItemFrameRenderer.submit port crates/horizon-client/src/game/frames.rs::Game::frame_geometry frame model, item or map
|
||||
@@ -96,7 +96,7 @@ PaintingRenderer.vertex inline crates/horizon-client/src/game/paintings.rs::Game
|
||||
PandaRenderer.extractRenderState port crates/horizon-client/src/mobs/body_anim.rs::panda sit, lie, sneeze and roll states
|
||||
ParrotRenderer.extractRenderState port crates/horizon-client/src/mobs/body_anim.rs::parrot pose and flap angle
|
||||
PhantomRenderer.extractRenderState port crates/horizon-client/src/mobs/phantom.rs::anim gap: player: phantom size is not read, so phantoms never grow
|
||||
PhantomRenderer.scale port crates/horizon-client/src/mobs/render.rs::species_scale (Species::Phantom) 1 + 0.15 * size, with the model's 1.3125 offset
|
||||
PhantomRenderer.scale port crates/horizon-client/src/mobs/render.rs::species_scale 1 + 0.15 * size, with the model's 1.3125 offset
|
||||
PhantomRenderer.setupRotations port crates/horizon-client/src/mobs/phantom.rs::anim gap: minor: body pitch (xRot) is not applied; wings and tail are
|
||||
PigRenderer.bakeModels skip:data model baking; Horizon loads model data from files
|
||||
PigRenderer.submit port crates/horizon-client/src/mobs/render.rs::body cold or normal model by variant
|
||||
@@ -107,7 +107,7 @@ PiglinRenderer.extractRenderState port crates/horizon-client/src/mobs/anim.rs::h
|
||||
PiglinRenderer.isShaking port crates/horizon-client/src/mobs/render.rs::shaking converting piglin shakes
|
||||
PolarBearRenderer.extractRenderState port crates/horizon-client/src/mobs/body_anim.rs::polar_bear standing scale
|
||||
PufferfishRenderer.submit port crates/horizon-client/src/mobs/render.rs::body small, medium or big model by size
|
||||
PufferfishRenderer.setupRotations port crates/horizon-client/src/mobs/render.rs::pufferfish_bob / pufferfish_bob_y the 0.08-block bob
|
||||
PufferfishRenderer.setupRotations port crates/horizon-client/src/mobs/render.rs::pufferfish_bob the 0.08-block bob
|
||||
RabbitRenderer.extractRenderState port crates/horizon-client/src/mobs/body_anim.rs::rabbit hop and idle head-tilt animations; Toast name in mobs/render.rs::body
|
||||
RavagerRenderer.extractRenderState port crates/horizon-client/src/mobs/ravager.rs::anim stun, bite and roar clocks
|
||||
SalmonRenderer.setupRotations port crates/horizon-client/src/mobs/aquatic.rs::rotations the 4.3-amplitude swim wiggle and the flop
|
||||
@@ -132,7 +132,7 @@ ThrownItemRenderer.submit port crates/horizon-client/src/game/creatures.rs::Game
|
||||
ThrownItemRenderer.extractRenderState inline crates/horizon-client/src/game/creatures.rs::Game::mob_geometry item sprite read there
|
||||
ThrownTridentRenderer.submit port crates/horizon-client/src/game/trident.rs::Game::trident_geometry gap: minor: foil glint is not drawn on a thrown trident
|
||||
ThrownTridentRenderer.extractRenderState inline crates/horizon-client/src/game/trident.rs::Game::trident_geometry yaw, pitch and foil read there
|
||||
TippableArrowRenderer.extractRenderState port crates/horizon-client/src/projectiles.rs brewing::potion_color gives a tipped arrow its potion colour
|
||||
TippableArrowRenderer.extractRenderState port crates/horizon-client/src/projectiles.rs::Projectile::arrow_effect_color brewing::potion_color gives a tipped arrow its potion colour
|
||||
TntMinecartRenderer.submitMinecartContents port crates/horizon-client/src/game/minecarts.rs::Game::cart_geometry TNT block swell and white flash
|
||||
TntMinecartRenderer.submitWhiteSolidBlock inline crates/horizon-client/src/game/minecarts.rs::Game::cart_geometry white overlay built inline
|
||||
TntMinecartRenderer.extractRenderState inline crates/horizon-client/src/game/minecarts.rs::Game::cart_geometry fuse read from the cart
|
||||
|
||||
|
@@ -101,7 +101,7 @@ SpawnerRenderer.submit port crates/horizon-client/src/game/spawner.rs::Game::spa
|
||||
StandingSignRenderer.extractRenderState inline crates/horizon-client-render/src/block_entity_render.rs::BlockEntityRenderer::render_one gap: minor: the text filtering option is not read
|
||||
StandingSignRenderer.createTransformations inline crates/horizon-client-render/src/block_entity_render.rs::BlockEntityRenderer::render_one front and back text transforms per attachment
|
||||
TestInstanceRenderer.extractRenderState missing player: creative players placing a test instance block see no beacon beam, box or error markers
|
||||
TestInstanceRenderer.submit missing player: creative players placing a test instance block see no beacon beam or box
|
||||
TestInstanceRenderer.submit port crates/horizon-client/src/game/test_instances.rs::Game::test_instance_marker_geometry the per-block loop drawing the error markers and their billboards; the beam sections come from Game::test_instance_beam and the box from Game::bounding_box_gizmos, both drawn per frame
|
||||
TestInstanceRenderer.submitErrorMarker port crates/horizon-client/src/gizmos.rs::error_marker_shape the red box and its text, drawn by Game::test_instance_marker_geometry
|
||||
TestInstanceRenderer.shouldRenderOffScreen missing minor: culling policy of a renderer Horizon lacks
|
||||
TestInstanceRenderer.shouldRender missing minor: culling policy of a renderer Horizon lacks
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 13 and column 112.
|
@@ -5,7 +5,7 @@ CloudRenderer.isEastEmpty inline crates/horizon-client-render/src/clouds.rs::Mes
|
||||
CloudRenderer.isSouthEmpty inline crates/horizon-client-render/src/clouds.rs::Mesh::prepare neighbour-empty flags passed to face_mask in N,E,S,W order
|
||||
CloudRenderer.isWestEmpty inline crates/horizon-client-render/src/clouds.rs::Mesh::prepare neighbour-empty flags passed to face_mask in N,E,S,W order
|
||||
CloudRenderer.render port crates/horizon-client-render/src/gfx.rs::Renderer::draw_frame gap: minor: one cloud pipeline; fancy vs flat is chosen by the mesh fast flag, not by pipeline
|
||||
CloudRenderer.renderOit port crates/horizon-client-render/src/gfx/oit.rs the clouds draw through the OIT pipeline there, with their own depth beside the world's
|
||||
CloudRenderer.renderOit port crates/horizon-client-render/src/gfx/oit.rs::Oit::render_clouds the clouds draw through the OIT pipeline there, with their own depth beside the world's
|
||||
CloudRenderer.buildMesh port crates/horizon-client-render/src/clouds.rs::Mesh::prepare cell ring walk via cells(radius), nearest first
|
||||
CloudRenderer.tryBuildCell inline crates/horizon-client-render/src/clouds.rs::Mesh::prepare skip empty cells, then build the cell's faces
|
||||
CloudRenderer.encodeFace inline crates/horizon-client-render/src/clouds.rs::Mesh::prepare 3-byte face records replaced by vertex quads
|
||||
@@ -40,7 +40,7 @@ FaceInfo.Extent.select skip:data corner selection; Horizon's unit-cube corner t
|
||||
FaceInfo.VertexInfo.select skip:data corner selection; Horizon's unit-cube corner table PORTAL_FACES in block_entity_render.rs has the same order
|
||||
FirstPersonHandsAndItemsRenderer.calculateMapTilt inline crates/horizon-client/src/game/maps.rs::Game::two_handed_map_geometry tilt computed inline with the calculateMapTilt comment
|
||||
FirstPersonHandsAndItemsRenderer.renderPlayerHand port crates/horizon-client/src/game.rs::Game::hand_geometry empty main hand's arm model with the player skin
|
||||
FirstPersonHandsAndItemsRenderer.renderMapHand port crates/horizon-client/src/first_person.rs::map_arm_poses (MapHold::TwoHanded) both arms behind a two-handed map, at the jar's rotations and offsets
|
||||
FirstPersonHandsAndItemsRenderer.renderMapHand port crates/horizon-client/src/first_person.rs::map_arm_poses both arms behind a two-handed map, at the jar's rotations and offsets
|
||||
FirstPersonHandsAndItemsRenderer.renderOneHandedMap port crates/horizon-client/src/first_person.rs::one_handed_map_pose gap: player: the arm holding a map is not drawn; the map hand branch skips the arm
|
||||
FirstPersonHandsAndItemsRenderer.renderTwoHandedMap port crates/horizon-client/src/game/maps.rs::Game::two_handed_map_geometry gap: player: the arms that hold a two-handed map are not drawn
|
||||
FirstPersonHandsAndItemsRenderer.renderMap port crates/horizon-client/src/game/maps.rs::Game::held_map map background with its 7-pixel border, then the map contents
|
||||
@@ -57,17 +57,17 @@ GameRenderer.processBlurEffect port crates/horizon-client-render/src/menu_blur.r
|
||||
GameRenderer.preloadUiShader skip:other GPU pipeline precompile at load; Horizon builds its pipelines in Renderer::new
|
||||
GameRenderer.update port crates/horizon-client-render/src/camera.rs::AvatarBob::update_bob gap: minor: the requested post-effect list (from /postEffect) is not built
|
||||
GameRenderer.extract skip:other render-state copy for the vanilla render thread; Horizon renders from game state
|
||||
GameRenderer.preparePostEffects port crates/horizon-client-render/src/gfx.rs::post_effects (post_chain::PostEffects, built at gfx.rs:8424) vanilla post chains are made on first use from the request the game builds
|
||||
GameRenderer.applyPostEffects port crates/horizon-client-render/src/gfx.rs::post_effects the same chain runs after the world pass
|
||||
GameRenderer.tryTakeScreenshotIfNeeded port crates/horizon-client/src/game.rs::tick_world_icon (called at game.rs:17852) a local world without an icon requests a 64x64 ScreenshotRequest for saves/<folder>/icon.png once the streamer, uploads and backlog are idle and more than 10 sections are drawn
|
||||
GameRenderer.preparePostEffects port crates/horizon-client-render/src/post_chain.rs::PostEffects::apply vanilla post chains are made on first use from the request the game builds
|
||||
GameRenderer.applyPostEffects port crates/horizon-client-render/src/post_chain.rs::PostEffects::apply the same chain runs after the world pass
|
||||
GameRenderer.tryTakeScreenshotIfNeeded port crates/horizon-client/src/game.rs::Game::tick_world_icon a local world without an icon requests a 64x64 ScreenshotRequest for saves/<folder>/icon.png once the streamer, uploads and backlog are idle and more than 10 sections are drawn
|
||||
GameRenderer.takeAutoScreenshot inline crates/horizon-client/src/game.rs::tick_world_icon the same retry, once a second until the file exists; the path comes from game/integrated.rs:485
|
||||
GameRenderer.integrate3DHudDepth inline crates/horizon-client-render/src/gfx.rs::post_effects runs with a post chain applied, which is vanilla's consistentDepthRequired condition
|
||||
GameRenderer.integrate3DHudDepth inline crates/horizon-client-render/src/post_chain.rs::PostEffects::apply runs with a post chain applied, which is vanilla's consistentDepthRequired condition
|
||||
GameRenderer.extractWindow skip:other render-state copy of the window size; Horizon reads the window directly
|
||||
GameRenderer.extractOptions skip:other render-state copy of options; Horizon reads settings directly
|
||||
GameRenderer.extractCamera skip:other render-state copy of camera and fog; Horizon computes them in camera.rs and fog.rs
|
||||
GameRenderer.resetData inline crates/horizon-client/src/game.rs::Game::finish_close_world boss overlay reset on world close; camera and map-texture resets not verified here
|
||||
GameRenderer.projectPointToScreen skip:other no callers in vanilla 26.3
|
||||
GameRenderer.useImprovedTransparency port crates/horizon-client-render/src/gfx.rs::improved_transparency / oit: Option<oit::Oit> the option builds the OIT pass
|
||||
GameRenderer.useImprovedTransparency port crates/horizon-client-render/src/gfx.rs::Renderer::draw_frame the option builds the OIT pass
|
||||
GlobalSettingsUniform.update skip:other global uniform (window size, time, gamma, glint) written per pass; Horizon sets its own uniforms
|
||||
GpuWarnlistManager.getAllWarnings missing minor: no GPU driver warnlist; Horizon does not check the driver against known-bad lists
|
||||
GpuWarnlistManager.compilePatterns missing minor: no GPU driver warnlist patterns
|
||||
@@ -82,9 +82,9 @@ LevelRenderer.addMainPass port crates/horizon-client-render/src/gfx.rs::Renderer
|
||||
LevelRenderer.executeAlwaysOnTop port crates/horizon-client/src/game.rs::Game::render always-on-top debug lines and crosshair drawn over the world
|
||||
LevelRenderer.executeSolid port crates/horizon-client-render/src/gfx.rs::Renderer::draw_sections solid section draws in arena page order
|
||||
LevelRenderer.prepareTranslucents port crates/horizon-client-render/src/gfx/translucency.rs::Sorter::sort translucent quads sorted from the camera each frame
|
||||
LevelRenderer.executeOit port crates/horizon-client-render/src/gfx/oit.rs + gfx::mark_oit (gfx/entity_batch.rs:31) translucent geometry is marked and resolved by the OIT pass
|
||||
LevelRenderer.executeDepthBoundsCull inline crates/horizon-client-render/src/gfx/oit.rs part of the same pass
|
||||
LevelRenderer.executeOitWaterMask port crates/horizon-client-render/src/gfx.rs::WATER_MASK_OVERLAY the water mask overlay of the same pass
|
||||
LevelRenderer.executeOit port crates/horizon-client-render/src/gfx/oit.rs::Oit::run translucent geometry is marked and resolved by the OIT pass
|
||||
LevelRenderer.executeDepthBoundsCull inline crates/horizon-client-render/src/gfx/oit.rs::Oit::execute_depth_bounds_cull part of the same pass
|
||||
LevelRenderer.executeOitWaterMask port crates/horizon-client-render/src/gfx/oit.rs::Oit::execute_water_mask the water mask overlay of the same pass
|
||||
LevelRenderer.extractSectionDrawGroups port crates/horizon-client-render/src/gfx.rs::Renderer::draw_frame section draws grouped per layer and arena page from the visible sections
|
||||
LevelRenderer.prepareChunkRenders port crates/horizon-client-render/src/gfx.rs::Renderer::draw_frame per-layer draw list built for the frame, reversed for translucent layers
|
||||
LevelRenderer.performTransientBlockRemovals inline crates/horizon-client/src/game/transient_blocks.rs::add_transient_block transient blocks are tracked and dropped by that list
|
||||
@@ -93,7 +93,7 @@ LevelRenderer.compileSections port crates/horizon-client-render/src/streaming.rs
|
||||
LevelRenderer.submitEntities port crates/horizon-client-render/src/gfx.rs::Renderer::draw_frame entity models drawn with their batches
|
||||
LevelRenderer.submitBlockEntities port crates/horizon-client-render/src/gfx.rs::Renderer::block_entities block entity list submitted for drawing
|
||||
LevelRenderer.submitBlockDestroyAnimation port crates/horizon-client-render/src/gfx.rs::Renderer::draw_frame crumbling overlay drawn on the block being broken
|
||||
LevelRenderer.submitTransientBlocks port crates/horizon-client/src/game/remote.rs: C::AddTransientBlock => add_transient_block the packet is handled and the block is drawn
|
||||
LevelRenderer.submitTransientBlocks port crates/horizon-client/src/game/transient_blocks.rs::Game::transient_block_geometry the packet is handled and the block is drawn
|
||||
LevelRenderer.submitHitOutline port crates/horizon-client/src/game.rs::Game::outline_boxes outline of the block under the crosshair
|
||||
LevelRenderer.close skip:other GPU resource teardown
|
||||
LevelRenderer.invalidateCompiledGeometry port crates/horizon-client-render/src/gfx.rs::Renderer::clear_sections all section meshes dropped and rebuilt when settings are applied
|
||||
@@ -134,11 +134,11 @@ PlayerSkinRenderCache.RenderInfo.textureView skip:other GPU texture view cache;
|
||||
PlayerSkinRenderCache.RenderInfo.glyphRenderTypes skip:other font render types for name tags; Horizon's text uses its own glyph pipeline
|
||||
PlayerSkinRenderCache.RenderInfo.equals skip:java value equality for a cache key
|
||||
PlayerSkinRenderCache.RenderInfo.hashCode skip:java hash for a cache key
|
||||
PostChain.load missing minor: no data-driven post chain loader; only the blur and entity outline chains exist, hard-coded
|
||||
PostChain.getReferencedExternalTargets skip:other config validation for data-driven post chains
|
||||
PostChain.createPass missing minor: no data-driven post pass creation; the outline and blur passes are fixed
|
||||
PostChain.addToFrame port crates/horizon-client-render/src/gfx/entity_outline.rs::EntityOutline::draw gap: minor: runs the entity outline chain only; other chains are not built
|
||||
PostChain.process port crates/horizon-client-render/src/menu_blur.rs::MenuBlur::apply gap: minor: runs the menu blur only; other chains are not run
|
||||
PostChain.load port crates/horizon-client-render/src/post_chain.rs::PostEffects::load reads the chain's JSON, refuses a target other than the frame ("Referenced external targets are not available in this context"), builds its internal targets and passes; None when the file is absent
|
||||
PostChain.getReferencedExternalTargets inline crates/horizon-client-render/src/post_chain.rs::PostEffects::load the external targets a chain names, checked against the frame the only external target Horizon has
|
||||
PostChain.createPass port crates/horizon-client-render/src/post_chain.rs::PostEffects::translated_pass the pass's GLSL translated, its targets and images bound, its uniforms packed, and a pass reading the target it writes refused
|
||||
PostChain.addToFrame port crates/horizon-client-render/src/gfx/entity_outline.rs::EntityOutline::draw gap: minor: this is the silhouette pass; a chain's own passes run in post_chain.rs::PostEffects::apply, which builds whatever chain id the frame's request names
|
||||
PostChain.process port crates/horizon-client-render/src/menu_blur.rs::MenuBlur::apply gap: minor: this is the menu's own two-pass blur; the chain runner is post_chain.rs::PostEffects::apply
|
||||
PostChain.getOrCreatePersistentTarget skip:other GPU persistent target cache
|
||||
PostChain.close skip:other GPU resource teardown
|
||||
PostChain.TargetBundle.getOrThrow skip:other frame-graph target lookup; Horizon has no frame graph
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 137 and column 150.
|
@@ -12,8 +12,8 @@ ChatComponent.scrollChat port crates/horizon-client-render/src/chat.rs::Chat::sc
|
||||
ChatComponent.getHeight port crates/horizon-client-render/src/chat.rs::ChatOptions::height_px focused or unfocused chat height in px
|
||||
ChatComponent.defaultUnfocusedPct inline crates/horizon-client-render/src/chat.rs::ChatOptions::default the default 70/160 is stored as a constant, not computed from the height formula
|
||||
ChatComponent.getLineHeight port crates/horizon-client-render/src/chat.rs::ChatOptions::line_height 9 * (line spacing + 1)
|
||||
ChatComponent.saveAsDraft missing player: Save Chat Drafts does nothing (options_ui.rs notes no drafts yet); text left in the box is not kept for the next open
|
||||
ChatComponent.createScreen port crates/horizon-client/src/game/chat.rs::Game::open_chat gap: player: no draft is restored; the chat opens with the initial text its caller passes
|
||||
ChatComponent.saveAsDraft port crates/horizon-client/src/game/chat.rs::Game::keep_chat_draft the text left in the box becomes the draft unless the screen was closed on purpose without Save Chat Drafts or the text was sent
|
||||
ChatComponent.createScreen port crates/horizon-client/src/game/chat.rs::Game::open_chat_method T or / opens the chat with the draft kept last time, a command draft only for /, else with its prefix, and records whether the box starts on a draft
|
||||
ChatComponent.preserveCurrentChatScreen missing minor: unsure: Horizon keeps no chat screen across a dimension change; game/nether_portal.rs:577 only leaves Chat alone during a portal effect, the change path was not checked
|
||||
ChatComponent.storeState inline crates/horizon-client/src/game/remote.rs::Game::poll_remote no snapshot: the Reconfigured arm only resets boss bars and registries, so the chat survives a reconfigure without a copy
|
||||
ChatComponent.restoreState inline crates/horizon-client/src/game/remote.rs::Game::poll_remote no restore: the chat is never cleared on Reconfigured, so there is nothing to put back
|
||||
|
||||
|
@@ -32,11 +32,11 @@ TabNavigationBar.narrationPriority inline crates/horizon-client/src/screen_narra
|
||||
TabNavigationBar.updateWidgetNarration port crates/horizon-client/src/screen_narrator.rs::narrate the tabs list's narration: gui.narrate.tab of the tab under the mouse else the open one, plus narration.tab_navigation.usage while the row has focus
|
||||
TabNavigationBar.narrateListElementPosition inline crates/horizon-client/src/screen_narrator.rs::narrate narrator.position.tab with the tab's index and the tab count, in the same branch
|
||||
TabNavigationBar.extractWidgetRenderState port crates/horizon-client/src/title/create_world.rs::Title::draw_create tab row drawn in draw_create
|
||||
TabNavigationBar.selectTab port crates/horizon-client/src/title.rs::Title::click gap: minor: only clicks pick a tab; no select by index (Ctrl+number, init)
|
||||
TabNavigationBar.selectTab port crates/horizon-client/src/title.rs::Title::select_create_tab the tab set by index, the focus cleared, from a click on the row and from Ctrl+number
|
||||
TabNavigationBar.setTabActiveState inline crates/horizon-client/src/game/stats_screen.rs::Game::stats_tab_active the Items and Mobs tabs are active only while their list has rows: an inactive tab does not click (stats_screen.rs:582) and its label draws inactive (stats_screen.rs:317-337)
|
||||
TabNavigationBar.setTabTooltip missing minor: the tab row has no per-tab tooltip
|
||||
TabNavigationBar.keyPressed missing minor: Ctrl+1 to 9 and Ctrl+Tab do not switch create-world tabs
|
||||
TabNavigationBar.getNextTabIndex missing minor: index math for Ctrl+number and Ctrl+Tab, which Horizon lacks
|
||||
TabNavigationBar.keyPressed port crates/horizon-client/src/title.rs::Title::tab_bar_key Ctrl+1 to Ctrl+0 and Ctrl+Tab (back with Shift) switch the Create World tabs, a digit past the last clamping to it; the rules and experiments screens keep the keys first
|
||||
TabNavigationBar.getNextTabIndex port crates/horizon-client/src/title/create_world.rs::next_tab_index the tab Ctrl plus a digit asks for, Ctrl+1 the first and Ctrl+0 the tenth, or one step for Tab, wrapping; the caller clamps to the tabs there are
|
||||
TabNavigationBar.currentTabIndex skip:other accessor; Horizon keeps the open tab in options.tab
|
||||
TabNavigationBar.currentTabButton skip:other accessor; Horizon draws the tabs from options.tab
|
||||
TabNavigationBar.Builder.addTab skip:java builder
|
||||
|
||||
|
@@ -37,13 +37,13 @@ ServerSelectionList.updateOnlineServers inline crates/horizon-client/src/multipl
|
||||
ServerSelectionList.updateNetworkServers inline crates/horizon-client/src/multiplayer.rs::rows LAN games are read each frame in rows; one found since the last call narrates multiplayer.lan.server_found with its getServerNarration (Screen::lan_found_narrations)
|
||||
ServerSelectionList.LANHeader.extractContent port crates/horizon-client/src/multiplayer.rs::Screen::draw scanning label and loading dots drawn in the list
|
||||
ServerSelectionList.NetworkServerEntry.extractContent port crates/horizon-client/src/multiplayer.rs::Screen::draw gap: minor: the address is always shown, Hide Server Address (hideServerAddress) is not read; LAN World title, MOTD and address
|
||||
ServerSelectionList.NetworkServerEntry.keyPressed missing minor: no keyboard join on the server list (Enter)
|
||||
ServerSelectionList.NetworkServerEntry.keyPressed port crates/horizon-client/src/multiplayer.rs::Screen::confirm Enter joins the selected LAN game: the index past the saved servers picks it out of the scanner and connects to its address
|
||||
ServerSelectionList.NetworkServerEntry.join port crates/horizon-client/src/multiplayer.rs::Screen::click LAN game joined by a click on its row (Row::Lan) or a second click on the selected row
|
||||
ServerSelectionList.NetworkServerEntry.matches skip:java selection is kept by index, so no entry identity match is needed
|
||||
ServerSelectionList.OnlineServerEntry.extractContent port crates/horizon-client/src/multiplayer.rs::Screen::draw player: name, MOTD, ping bars, player count, hover join and move sprites; a server on another protocol shows its version in red in place of the player count and the count carries the playerList tooltip
|
||||
ServerSelectionList.OnlineServerEntry.refreshStatus port crates/horizon-client/src/multiplayer.rs::ping_tooltip ping tooltip: ms, no connection or pinging; the player count's playerList tooltip is player_list_tooltip; the bar choice is in Screen::draw
|
||||
ServerSelectionList.OnlineServerEntry.extractIcon port crates/horizon-client/src/multiplayer.rs::draw_server_icon server icon at 32x32
|
||||
ServerSelectionList.OnlineServerEntry.keyPressed missing minor: no keyboard path: Enter does not join and Shift+Up/Down does not move a server
|
||||
ServerSelectionList.OnlineServerEntry.keyPressed port crates/horizon-client/src/multiplayer.rs::Screen::confirm Enter joins the selected server as a click on it does; Shift+Up/Down swaps it with its neighbour and saves the list in Screen::arrow
|
||||
ServerSelectionList.OnlineServerEntry.swap port crates/horizon-client/src/multiplayer.rs::Screen::click icon arrow click swaps the server up or down and saves
|
||||
ServerSelectionList.OnlineServerEntry.getNarration port crates/horizon-client/src/multiplayer.rs::Screen::server_narration the entry's name then its status, joined for the screen narrator
|
||||
ServerSelectionList.OnlineServerEntry.matches skip:java selection is kept by index, so no entry identity match is needed
|
||||
|
||||
|
@@ -130,7 +130,7 @@ ServerPlayer.getIpAddress skip:server for ban-ip and the dedicated server log
|
||||
ServerPlayer.updateOptions port crates/horizon-client/src/lan/hub.rs::Hub::process_inbox the ClientInformation arm: view distance, main hand, model parts and chat visibility are kept
|
||||
ServerPlayer.clientInformation skip:java getter
|
||||
ServerPlayer.sendServerStatus port crates/horizon-client/src/lan/hub.rs::Hub::place_new_player ServerData with the motd and icon at join
|
||||
ServerPlayer.updateInvisibilityStatus inline crates/horizon-client/src/game/lan.rs::Game::lan_host_state
|
||||
ServerPlayer.updateInvisibilityStatus inline crates/horizon-client/src/lan/hub.rs::guest_body a spectator is always invisible, else the invisibility effect decides; guest_body feeds the shared flags sent by ServerEntity.sendDirtyEntityData (shared_flags)
|
||||
ServerPlayer.drop port crates/horizon-client/src/game.rs::Game::player_drop item_dropped and drop statistics, thrower set
|
||||
ServerPlayer.calculateGameModeForNewPlayer port crates/horizon-client/src/game/lan.rs::Game::lan_guest_joined default or forced game mode for a new or force-gamemode guest
|
||||
ServerPlayer.storeGameTypes port crates/horizon-client/src/save.rs::Level::player_tag both are saved: playerGameType, and previousPlayerGameType (absent when unset, as storeNullable is), read back into previous_game_mode
|
||||
|
||||
|
@@ -71,19 +71,19 @@ DolphinJumpGoal.surfaceIsClear inline crates/horizon-client/src/mobs/dolphin.rs:
|
||||
DolphinJumpGoal.canContinueToUse port crates/horizon-client/src/mobs/dolphin.rs::Jump::can_continue
|
||||
DoorInteractGoal.start inline crates/horizon-client/src/mobs/doors.rs::RaiderOpenDoor::start passed and the door direction only feed canContinueToUse, which both subclasses override, so they are not kept
|
||||
DoorInteractGoal.tick inline crates/horizon-client/src/mobs/doors.rs::BreakDoor::tick the passed-the-door dot test is not kept: BreakDoor and RaiderOpenDoor both override can_continue
|
||||
FollowFlockLeaderGoal port crates/horizon-client/src/mobs/aquatic.rs::FollowFlockLeader tick for tick with vanilla in mob_ai cod/salmon/tropical_fish_school; recruits in vanilla getEntitiesOfClass order (Mob.section)
|
||||
FollowMobGoal port crates/horizon-client/src/mobs/goals.rs::FollowMob the mutual-look branch is left out: only parrots have the goal
|
||||
FollowPlayerRiddenEntityGoal port crates/horizon-client/src/mobs/dolphin.rs::FollowPlayerRidden only the player the mob sees (the nearest) is considered; tick for tick with vanilla in mob_ai dolphin_boat_*
|
||||
FollowFlockLeaderGoal home crates/horizon-client/src/mobs/aquatic.rs FollowFlockLeader in that file, tick for tick with vanilla in mob_ai cod/salmon/tropical_fish_school; recruits in vanilla getEntitiesOfClass order (Mob.section)
|
||||
FollowMobGoal home crates/horizon-client/src/mobs/goals.rs FollowMob in that file; the mutual-look branch is left out: only parrots have the goal
|
||||
FollowPlayerRiddenEntityGoal home crates/horizon-client/src/mobs/dolphin.rs FollowPlayerRidden in that file; only the player the mob sees (the nearest) is considered; tick for tick with vanilla in mob_ai dolphin_boat_*
|
||||
GoalSelector.removeAllGoals port crates/horizon-client/src/mobs/goals.rs::GoalSelector::clear gap: minor: clear() takes no predicate and does not stop running goals
|
||||
GoalSelector.removeGoal missing minor: no per-goal removal; callers (cat and ocelot trust, zombie break door, skeleton weapon swap, skeleton horse trap) fold the condition into can_use
|
||||
GoalSelector.goalContainsAnyFlags inline crates/horizon-client/src/mobs/goals.rs::GoalSelector::tick flags & disabled
|
||||
GoalSelector.goalCanBeReplacedForAllFlags inline crates/horizon-client/src/mobs/goals.rs::GoalSelector::tick the replaceable check over the locked slots
|
||||
GoalSelector.tickRunningGoals port crates/horizon-client/src/mobs/goals.rs::GoalSelector::tick_running
|
||||
GolemRandomStrollInVillageGoal inline crates/horizon-client/src/mobs/golem.rs::register_iron a named Stroll with a gate and a position; tick for tick with vanilla in mob_ai iron_golem_village_*
|
||||
GolemRandomStrollInVillageGoal home crates/horizon-client/src/mobs/golem.rs a Stroll with interval 240 and village_stroll_position that register_iron adds at 4; tick for tick with vanilla in mob_ai iron_golem_village_*
|
||||
MeleeAttackGoal.checkAndPerformAttack inline crates/horizon-client/src/mobs/goals.rs::MeleeAttack::tick reset cooldown, swing, do_hurt_target
|
||||
MeleeAttackGoal.canPerformAttack inline crates/horizon-client/src/mobs/goals.rs::MeleeAttack::tick cooldown over, within melee range, line of sight
|
||||
MoveBackToVillageGoal inline crates/horizon-client/src/mobs/golem.rs::register_iron a named Stroll with a gate and a position; tick for tick with vanilla in mob_ai iron_golem_village_*
|
||||
MoveThroughVillageGoal inline crates/horizon-client/src/mobs/undead.rs::MoveThroughVillage zombies register it at priority 6 (the raider variant is raider.rs::MoveThroughVillage); no tick parity check yet
|
||||
MoveBackToVillageGoal home crates/horizon-client/src/mobs/golem.rs the back Stroll register_iron adds at 2: interval 10, the outside_village gate and back_to_village_position; tick for tick with vanilla in mob_ai iron_golem_village_*
|
||||
MoveThroughVillageGoal home crates/horizon-client/src/mobs/undead.rs MoveThroughVillage in that file: zombies add it at 6; the raider's nested Raider$RaiderMoveThroughVillageGoal is the separate MoveThroughVillage in raider.rs; no tick parity check yet
|
||||
MoveToBlockGoal.moveMobToBlock inline crates/horizon-client/src/mobs/goals.rs::MoveToBlock::start nav_move_to the block centre one up
|
||||
MoveTowardsTargetGoal.start inline crates/horizon-client/src/mobs/golem.rs::MoveTowardsTarget::can_use gap: minor: paths straight to the target entity in can_use, not to a random position towards it chosen in canUse
|
||||
OpenDoorGoal.canContinueToUse port crates/horizon-client/src/mobs/doors.rs::RaiderOpenDoor::can_continue the raider goal does not close the door after, so it never continues
|
||||
|
||||
|
@@ -4,12 +4,12 @@ ItemFrame.recalculateBoundingBox inline crates/horizon-client/src/entities.rs::I
|
||||
ItemFrame.shouldDamageDropItem inline crates/horizon-client/src/game/explosion.rs::Game::explosion_hurt_entities a blast breaks a frame in reach and drops its item (`explosion_breaks_frames`)
|
||||
ItemFrame.canHurtWhenFixed port crates/horizon-client/src/game/frames.rs::attack_frame a fixed frame refuses a non-creative hit (attack_ticks reset, no break) and lets a creative player break it
|
||||
ItemFrame.hurtClient inline crates/horizon-client/src/game/frames.rs::Game::attack_frame gap: minor: no separate client-side fixed-frame gate; the creative exception is missing
|
||||
ItemFrame.removeFramedMap inline crates/horizon-client/src/game/maps.rs::Game::lan_map_updates frame map markers come from item_frames each tick, so a removed frame needs no step
|
||||
ItemFrame.removeFramedMap port crates/horizon-client/src/game/maps.rs::Game::remove_framed_map the map the frame holds forgets the frame (`MapItemSavedData.removedFromFrame`: the decoration and the frames entry); drop_frame_items calls it when the frame breaks, and skips it only when the drops are off and a player caused the break, which is vanilla's null `causedBy`
|
||||
ItemFrame.getSlot inline crates/horizon-client/src/game/commands/items.rs::Game::slot_get slots_named maps container.0 and contents on a frame to Slot::Frame; slot_get reads the frame's item and slot_set writes it
|
||||
ItemFrame.onSyncedDataUpdated skip:other network data-sync hook; the item is read from the frame field on demand
|
||||
ItemFrame.setRotation inline crates/horizon-client/src/game/frames.rs::Game::interact_frame the rotate-by-one step; the neighbour update is in frame_output_changed
|
||||
ItemFrame.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::item_frame_from_nbt gap: minor: Facing defaults to 2 where vanilla defaults to DOWN
|
||||
ItemFrame.getFrameItemStackWithData missing minor: a broken frame never carries its custom name; drop_frame_items builds a plain item_frame stack
|
||||
ItemFrame.getFrameItemStackWithData missing `game/frames.rs::drop_frame_items` builds a plain `item_frame` stack, so a broken frame never carries its custom name
|
||||
ItemFrame.getVisualRotationYInDegrees inline crates/horizon-client/src/game/frames.rs::Game::frame_geometry the 45 degree step per rotation step is drawn in frame_geometry
|
||||
LeashFenceKnotEntity.recalculateBoundingBox port crates/horizon-client/src/mobs/leash.rs::knot_state position and box from the block position
|
||||
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
|
||||
@@ -35,15 +35,15 @@ ItemEntity.hurtClient skip:other client-side hurt prediction; items are not att
|
||||
ItemEntity.hurtServer inline crates/horizon-client/src/game/explosion.rs::Game::explosion_hurt_entities gap: minor: explosions destroy items at damage 5 without subtracting health; fire and lava hurt are in environment_tick
|
||||
ItemEntity.addAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::item_entity_to_nbt
|
||||
ItemEntity.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::item_entity_from_nbt a missing Item returns None, which matches the discard
|
||||
ItemEntity.getName missing minor: a dropped item's name (custom or item name) is not used anywhere in Horizon
|
||||
ItemEntity.getName missing a dropped item's name (custom or the item's own) is not read anywhere
|
||||
ItemEntity.teleport inline crates/horizon-client/src/entities.rs::merge_with_neighbours the arrival merge is the normal per-tick merge, not a teleport step
|
||||
ItemEntity.getVisualRotationYInDegrees inline crates/horizon-client/src/game/item_draw.rs::Game::dropped_item_geometry spin is applied in radians there; no degree conversion
|
||||
ItemEntity.getSlot missing unsure: no slot 0 access for dropped items found in commands.rs
|
||||
ItemEntity.getSlot missing a dropped item is not addressable as an entity slot: `game/commands.rs` has no item entity case
|
||||
PrimedTnt.defineSynchedData skip:other fuse and block state are struct fields; spawn sets fuse 80; the block is always TNT
|
||||
PrimedTnt.explode inline crates/horizon-client/src/game/tnt.rs::Game::tick_tnt the power is the tnt's own field (`explosion_power`, 4 by default); gap: minor: no used-portal damage calculator
|
||||
PrimedTnt.addAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::tnt_to_nbt the fuse and the block state are saved
|
||||
PrimedTnt.restoreFrom missing minor: PrimedTnt has no owner field, so the igniter is not kept through portal moves
|
||||
PrimedTnt.teleport missing unsure: no used-portal flag on PrimedTnt; the portal damage calculator is not applied
|
||||
PrimedTnt.restoreFrom missing `PrimedTnt` has no owner field, so the igniter is not kept through a dimension change
|
||||
PrimedTnt.teleport missing `PrimedTnt` carries no used-portal flag and the portal damage calculator is not applied to it
|
||||
Blaze.registerGoals port crates/horizon-client/src/mobs/nether.rs::register_blaze gap: minor: MoveTowardsRestriction goal (priority 5) is not registered
|
||||
Blaze.createAttributes skip:data attribute defaults come from data tables
|
||||
Blaze.isCharged inline crates/horizon-client/src/mobs/nether.rs::BlazeAttack::tick the charged state is m.extra.flag, set in the attack goal tick
|
||||
@@ -88,7 +88,7 @@ Guardian.checkGuardianSpawnRules port crates/horizon-client/src/mobs/spawn_rules
|
||||
Guardian.GuardianAttackGoal.canContinueToUse port crates/horizon-client/src/mobs/guardian.rs::GuardianAttack::can_continue
|
||||
Guardian.GuardianAttackSelector.test inline crates/horizon-client/src/mobs/guardian.rs::register the selector is the player, squid and axolotl beyond 9 filters in register
|
||||
Monster.updateNoActionTime port crates/horizon-client/src/mobs/ai.rs::server_ai_step the idle counter is counted up there and read by the despawn path in raids.rs
|
||||
Monster.getProjectile missing unsure: no held-ammo choice for mobs found in mobs/*.rs
|
||||
Monster.getProjectile missing no held-ammo choice in `mobs/*.rs`: a monster's shot takes its projectile kind from the species, not from the stack it holds
|
||||
PatrollingMonster.addAdditionalSaveData port crates/horizon-client/src/mobs/raider.rs::write_nbt
|
||||
PatrollingMonster.readAdditionalSaveData port crates/horizon-client/src/mobs/raider.rs::read_nbt
|
||||
PatrollingMonster.checkPatrollingMonsterSpawnRules port crates/horizon-client/src/mobs/spawn_rules.rs::check_spawn_rules the Pillager arm: block light 8 or less, then mob rules
|
||||
@@ -103,12 +103,12 @@ Phantom.PhantomBodyRotationControl.clientTick inline crates/horizon-client/src/m
|
||||
Phantom.PhantomMoveTargetGoal.touchingTarget inline crates/horizon-client/src/mobs/phantom.rs::CircleAroundAnchor::tick the squared distance under 4 check near the move target
|
||||
Phantom.PhantomSweepAttackGoal.canContinueToUse port crates/horizon-client/src/mobs/phantom.rs::SweepAttack::can_continue the cat hiss and scare check
|
||||
Ravager.registerGoals port crates/horizon-client/src/mobs/ravager.rs::register
|
||||
Ravager.updateControlFlags missing unsure: no rider control-flag switch for ravagers in ravager.rs
|
||||
Ravager.updateControlFlags missing the goal set has no control flags, so a mob driving a boat does not switch off the ravager's own move, jump and look goals
|
||||
Ravager.createAttributes skip:data attribute defaults come from data tables
|
||||
Ravager.addAdditionalSaveData inline crates/horizon-client/src/entity_nbt.rs::mob_to_nbt AttackTick, StunTick and RoarTick are written there
|
||||
Ravager.readAdditionalSaveData inline crates/horizon-client/src/entity_nbt.rs::mob_from_nbt the same three tick values are read there
|
||||
Ravager.stunEffect port crates/horizon-client/src/mobs/ravager.rs::ai_step the stun particle is emitted from ai_step
|
||||
Ravager.hasLineOfSight missing unsure: no stunned or roaring line-of-sight cut-off in ravager.rs
|
||||
Ravager.hasLineOfSight missing `ravager::immobile` knows the biting, stunned and roaring states, but no target goal is gated on it, so an immobile ravager still sees and takes targets
|
||||
Ravager.applyRoarKnockbackClient inline crates/horizon-client/src/mobs/ravager.rs::roar the roar push is applied in roar
|
||||
Ravager.strongKnockback inline crates/horizon-client/src/mobs/ravager.rs::roar the push of 4 along the direction is in roar
|
||||
Ravager.addRoarParticleEffects inline crates/horizon-client/src/mobs/ravager.rs::roar the poof particles are emitted in roar
|
||||
@@ -119,18 +119,18 @@ Shulker.addAdditionalSaveData inline crates/horizon-client/src/entity_nbt.rs::mo
|
||||
Shulker.findNewAttachment inline crates/horizon-client/src/mobs/shulker.rs::tick keeps its face or teleports in tick, through attachable and teleport
|
||||
Shulker.makeBoundingBox port crates/horizon-client/src/mobs/shulker.rs::progress_aabb
|
||||
Shulker.getDefaultDimensions inline crates/horizon-client/src/mobs/shulker.rs::progress_aabb gap: minor: the down-face height scale is not seen here
|
||||
Shulker.startRiding missing unsure: no attach-face reset to DOWN when a shulker mounts a vehicle in shulker.rs
|
||||
Shulker.stopRiding missing unsure: no body yaw reset after a shulker dismounts in shulker.rs
|
||||
Shulker.move missing unsure: no shulker-box move type (teleport) found in the piston code
|
||||
Shulker.startRiding missing no attach-face reset on boarding: `mobs/shulker.rs` sets `extra.variant3` from `Attachable` and the save only, so a shulker on a vehicle keeps the face it had
|
||||
Shulker.stopRiding inline crates/horizon-client/src/mobs/shulker.rs::tick the body yaw is pinned to 0 on every tick (`shulker::tick`, and `client_tick` for a remote one), so a dismounted shulker shows no leftover body yaw without a reset of its own
|
||||
Shulker.move missing the `SHULKER_BOX` mover type has no counterpart: `shulker::teleport` runs from the tick and the bullet hit only, so a shulker box pushed by a piston never takes its shulker along
|
||||
Shulker.findAttachableSurface port crates/horizon-client/src/mobs/shulker.rs::attachable
|
||||
Shulker.isPositionBlocked inline crates/horizon-client/src/mobs/shulker.rs::can_stay_at the blocked-position check is part of can_stay_at
|
||||
Shulker.onSyncedDataUpdated skip:other network sync hook; the box is computed from the face on demand
|
||||
Shulker.getRenderPosition missing unsure: no render slide after a shulker teleport in shulker.rs
|
||||
Shulker.getRenderPosition missing no render slide: `client_tick` eases the lid and squares the body, and nothing interpolates the position across a teleport
|
||||
Shulker.setVariant inline crates/horizon-client/src/mobs/shulker.rs::register colour comes from spawn data; NO_COLOR when unset
|
||||
Shulker.getColor inline crates/horizon-client/src/mobs/shulker.rs::texture colour is read from extra.color for the texture
|
||||
Shulker.get skip:other component getter; Horizon reads extra.color directly
|
||||
Shulker.applyImplicitComponent missing minor: a shulker spawned with a SHULKER_COLOR component ignores it
|
||||
Shulker.ShulkerDefenseAttackGoal.getTargetSearchArea missing minor: shulkers do not defend nearby shulkers; there is no defense target goal
|
||||
Shulker.applyImplicitComponent missing a shulker spawned from a shulker box item ignores its colour: the implicit components are applied to a mob coming out of a bucket only (`entity_nbt::bucket_components`)
|
||||
Shulker.ShulkerDefenseAttackGoal.getTargetSearchArea missing no defense target goal: `mobs/shulker.rs::register` gives the shulker `HurtByTarget` with `alert` (a hurt shulker wakes the targetless ones around it) and a player search box 4 deep along the attach axis, nothing that defends another shulker
|
||||
Shulker.ShulkerNearestAttackGoal.getTargetSearchArea inline crates/horizon-client/src/mobs/shulker.rs::register the 4-deep search box is the when filter in register
|
||||
Shulker.ShulkerPeekGoal.canContinueToUse port crates/horizon-client/src/mobs/shulker.rs::Peek::can_continue
|
||||
Silverfish.registerGoals port crates/horizon-client/src/mobs/hostile.rs::register
|
||||
|
||||
|
@@ -11,18 +11,18 @@ Strider.getDismountLocationForPassenger port crates/horizon-client/src/game/moun
|
||||
Strider.tickRidden port crates/horizon-client/src/mobs/equine.rs::ridden_step strider branch: rider yaw, half pitch, boost factor, cold slowdown
|
||||
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 minor: a player can still board a saddled strider whose head is in lava; vanilla refuses (isEyeInFluid LAVA)
|
||||
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.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
|
||||
Vex.restoreFrom missing minor: a vex restored after a dimension trip keeps no owner link; Horizon has no mob restoreFrom
|
||||
Vex.restoreFrom missing no mob `restoreFrom`: a vex that changes dimension keeps no owner link
|
||||
Vex.getVexFlag inline crates/horizon-client/src/mobs/vex.rs::Charge::can_continue the charging bit is the plain extra.flag; there is no flags byte
|
||||
Vex.setVexFlag inline crates/horizon-client/src/mobs/vex.rs::Charge::start start sets extra.flag; stop and tick clear it
|
||||
Vex.finalizeSpawn port crates/horizon-client/src/mobs/finalize.rs::Ctx::finalize Vex arm: populate (sword) then enchant
|
||||
Vex.populateDefaultEquipmentSlots port crates/horizon-client/src/mobs/finalize.rs::Ctx::populate iron sword in hand with drop chance 0
|
||||
Witch.registerGoals port crates/horizon-client/src/mobs/witch.rs::register gap: minor: raiders that hurt a witch are still targeted back (vanilla ignores them)
|
||||
Witch.handleEntityEvent missing player: a witch shows purple magic puffs about once a minute while idle; not drawn here (the RNG draw is kept in ai_step)
|
||||
Witch.handleEntityEvent port crates/horizon-client/src/game/remote_mob_particles.rs::Game::witch_magic_particles event 15: `nextInt(35) + 10` purple puffs half a block over the head, the count drawn again every pass as the Java loop condition does; `game/remote.rs:3949` hands it every witch entity event (the rest of the handler is LivingEntity's)
|
||||
Witch.getDamageAfterMagicAbsorb port crates/horizon-client/src/mobs.rs::Mob::damage_with 85% less from #witch_resistant_to, nothing from its own hit
|
||||
Zoglin.initCoreActivity inline crates/horizon-client/src/mobs/piglin.rs::register_zoglin gap: minor: look-at-target and move-to-target sinks are not separate; the goal set stands in for them
|
||||
Zoglin.initIdleActivity inline crates/horizon-client/src/mobs/piglin.rs::register_zoglin gap: minor: idle is a stroll only; no look-target sometimes and no do-nothing pauses
|
||||
@@ -33,7 +33,7 @@ Zoglin.createAttributes skip:data attribute defaults come from the data tables
|
||||
Zoglin.doHurtTarget port crates/horizon-client/src/mobs/piglin.rs::hoglin_hurt_target headbutt event, zoglin attack sound, hurt and throw
|
||||
Zoglin.hurtServer inline crates/horizon-client/src/mobs/piglin.rs::register_zoglin gap: minor: HurtByTarget sets the attacker as target with no 4-block check against a much nearer current target
|
||||
Zoglin.setAttackTarget inline crates/horizon-client/src/mobs/piglin.rs::register_zoglin gap: minor: target has no 200-tick expiry; Horizon sets the target directly
|
||||
Zoglin.updateActivity missing player: a zoglin that starts a fight does not play its angry grunt (zoglin.angry)
|
||||
Zoglin.updateActivity port crates/horizon-client/src/mobs/piglin.rs::zoglin_step on entering the fight activity it plays `entity.zoglin.angry`; `mobs/ai.rs:1718` runs it for every zoglin after the brain
|
||||
Zoglin.customServerAiStep inline crates/horizon-client/src/mobs/piglin.rs::ai_step gap: minor: the brain tick is not run; the zoglin goal set runs in the general AI loop
|
||||
Zoglin.setBaby port crates/horizon-client/src/mobs/finalize.rs::set_baby Zoglin arm: non-ageable baby age and 0.5 attack damage
|
||||
Zoglin.handleEntityEvent port crates/horizon-client/src/mobs/piglin.rs::client_event event 4 starts the headbutt; the attack sound comes from hoglin_hurt_target
|
||||
@@ -42,8 +42,8 @@ Breeze.createAttributes skip:data attribute defaults come from the data tables
|
||||
Breeze.makeBrain port crates/horizon-client/src/mobs/monster_brain.rs::init breezes get the fight default activity
|
||||
Breeze.onSyncedDataUpdated port crates/horizon-client/src/mobs/breeze.rs::ai_step client pose change restarts the matching animation
|
||||
Breeze.resetAnimations inline crates/horizon-client/src/mobs/breeze.rs::ai_step the stop calls of the pose-change branch
|
||||
Breeze.emitJumpTrailParticles missing player: no block-dust trail behind a long jump (about 5 ticks of particles)
|
||||
Breeze.emitGroundParticles missing player: no block-dust puffs at its feet while it stands, shoots, inhales or lands; Horizon only draws the random number
|
||||
Breeze.emitJumpTrailParticles missing `mobs/breeze.rs` has no particle call (only `dangerous_block`), so no block dust follows a long jump
|
||||
Breeze.emitGroundParticles missing same as `emitJumpTrailParticles`: no ground puffs at its feet while it stands, shoots, inhales or lands
|
||||
Breeze.playWhirlSound port crates/horizon-client/src/mobs/breeze.rs::tick whirl sound on a random clock, as a local sound
|
||||
Breeze.deflection port crates/horizon-client/src/game/creatures.rs::Game::tick_projectiles breeze turns projectiles back, except wind charges
|
||||
Breeze.getHurtBy port crates/horizon-client/src/mobs/monster_brain.rs::hurt_by_entity the living source of the last hurt
|
||||
@@ -68,19 +68,19 @@ Creaking.defineSynchedData skip:java SynchedEntityData builder; the synced valu
|
||||
Creaking.createAttributes skip:data attribute defaults come from the data tables (mobs.rs attr lookup)
|
||||
Creaking.setupAnimationStates port crates/horizon-client/src/mobs/creaking.rs::animate attack, invulnerability and tear-down animations
|
||||
Creaking.creakingDeathEffects port crates/horizon-client/src/mobs/creaking_heart.rs::remove_protector gap: minor: no blameSourceForDamage or die(); health is set to 0 and the twitch sound plays
|
||||
Creaking.canUsePortal missing minor: a heart-bound creaking can still go through a portal; game/nether_portal.rs mob_can_use_portal has no heart check
|
||||
Creaking.canUsePortal missing `game/nether_portal.rs::mob_can_use_portal` tests alive, no mount, not ridden, the guest-ride rule and the wither; nothing about the heart, so a heart-bound creaking can still go through a portal
|
||||
Creaking.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::mob_from_nbt home_pos is read back as the transient home
|
||||
Creaking.checkEyeBlink inline crates/horizon-client/src/mobs/creaking.rs::animate eye flicker timing on the dying creaking
|
||||
Creaking.knockback port crates/horizon-client/src/mobs.rs::Mob::knockback
|
||||
Creaking.CreakingPathNavigation.createPathFinder missing minor: no home-distance node evaluator; paths do not block nodes more than 32 blocks from the heart
|
||||
Creaking.HomeNodeEvaluator.getPathType missing minor: nodes more than 32 blocks from home are not marked blocked
|
||||
Creaking.CreakingPathNavigation.createPathFinder missing no home-distance node evaluator: `m.extra.home` is read for damage, tearing down and the save, never by the pathfinder
|
||||
Creaking.HomeNodeEvaluator.getPathType missing no node is marked blocked for being more than 32 blocks from the heart
|
||||
CreakingAi.initCoreActivity port crates/horizon-client/src/mobs/monster_brain.rs::creaking_packages core swim, look-at-target and move sink
|
||||
CreakingAi.initIdleActivity port crates/horizon-client/src/mobs/monster_brain.rs::creaking_packages idle: attack active creakings, look sometimes, stroll, look-walk or do nothing
|
||||
CreakingAi.initFightActivity port crates/horizon-client/src/mobs/monster_brain.rs::creaking_packages fight: walk to target, melee on a 40 tick cooldown, stop when unreachable
|
||||
AbstractCubeMob.registerGoals port crates/horizon-client/src/mobs/cube.rs::register float, random direction, keep jumping, attack goal and player and golem targets
|
||||
AbstractCubeMob.addAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::mob_to_nbt Size stored minus one, and wasOnGround
|
||||
AbstractCubeMob.refreshDimensions skip:other bounds are read from the size on each use, so there is no separate refresh; the position is never reset
|
||||
AbstractCubeMob.onSyncedDataUpdated missing unsure: remote.rs applies no size sync to cubes; local play sets the size directly
|
||||
AbstractCubeMob.onSyncedDataUpdated missing no size sync from a spawn or a data update: `mobs/living.rs` takes a mob's dimensions from its own size, and `game/remote.rs` applies no cube size
|
||||
AbstractCubeMob.setUpSplitCube port crates/horizon-client/src/game/creatures.rs::Game::mob_died gap: minor: children get half size and the same fixed offsets, but no random yaw
|
||||
AbstractCubeMob.push inline crates/horizon-client/src/mobs/cube.rs::hurt_touching_golems an iron golem whose box overlaps a cube that can deal damage is hurt through dealDamage (MobEvent::CubeHurtMob, with the attack sound after a landed hit)
|
||||
AbstractCubeMob.dealDamage inline crates/horizon-client/src/mobs/cube.rs::ai_step the player touch (CubeHurtPlayer) and a golem's overlap (CubeHurtMob, hurt_touching_golems): alive, teams allow damage, in melee range, line of sight, then mobAttack for getAttackDamage
|
||||
@@ -136,12 +136,12 @@ HoglinAi.initIdleActivity inline crates/horizon-client/src/mobs/piglin.rs::regis
|
||||
HoglinAi.initFightActivity inline crates/horizon-client/src/mobs/piglin.rs::register_hoglin MeleeAttack.create(40) for adults and create(15) for babies (`MeleeAttack::intervals(40, 15)`); the brain's own order has no tick parity here (the probe has no hoglin brain)
|
||||
HoglinAi.initRetreatActivity port crates/horizon-client/src/mobs/piglin.rs::register_hoglin
|
||||
HoglinAi.createIdleMovementBehaviors inline crates/horizon-client/src/mobs/piglin.rs::register_hoglin gap: minor: no look-target walk or do-nothing pauses; a plain stroll only
|
||||
HoglinAi.updateActivity missing player: no grunt when a hoglin switches activity (angry or retreat); only the ambient roll plays
|
||||
HoglinAi.updateActivity port crates/horizon-client/src/mobs/piglin.rs::update_activity takes the first valid activity and voices the one it changed to (`hoglin_sound`: angry in a fight, retreat when avoiding or converting, else ambient); the same fn covers PiglinAi and PiglinBruteAi
|
||||
HoglinAi.onHitTarget inline crates/horizon-client/src/mobs/piglin.rs::hoglin_hurt_target gap: player: no retreat when piglins outnumber hoglins; the hit only attacks
|
||||
HoglinAi.retreatFromNearestTarget missing player: no retreat from the nearest piglin or hoglin target (part of the missing retreat activity)
|
||||
HoglinAi.setAvoidTarget missing player: no avoid target is set, so a hoglin never backs off after a piglin hit (retreat activity missing)
|
||||
HoglinAi.retreatFromNearestTarget port crates/horizon-client/src/mobs/piglin.rs::retreat_from_nearest the nearest of the new target, the avoid target and the attack target retreats `who` (`BehaviorUtils.getNearestTarget`: strictly closer wins), then a hoglin sets an avoid target and a piglin also stops hunting
|
||||
HoglinAi.setAvoidTarget port crates/horizon-client/src/mobs/piglin.rs::set_avoid_target forgets the attack target and the walk and avoids `who` for a 5 to 20 second retreat, sampled from the level random
|
||||
HoglinAi.isPosNearNearestRepellent inline crates/horizon-client/src/mobs/piglin.rs::HoglinAvoidRepellent::can_use the 8-block repellent distance check
|
||||
HoglinAi.piglinsOutnumberHoglins missing player: the count of piglins against hoglins that starts a retreat is not kept
|
||||
HoglinAi.piglinsOutnumberHoglins port crates/horizon-client/src/mobs/piglin.rs::piglins_outnumber_hoglins an adult hoglin counts itself and sees more adult piglins than hoglins (`piglins > hoglins + 1`)
|
||||
HoglinAi.maybeRetaliate port crates/horizon-client/src/mobs/piglin.rs::register_hoglin gap: minor: no check for hoglin attackers or the 4-block nearer-target rule; HurtByTarget alerts others
|
||||
HoglinAi.setAttackTarget inline crates/horizon-client/src/mobs/piglin.rs::register_hoglin gap: minor: target has no 200-tick expiry and the breed target is not cleared
|
||||
HoglinAi.setAttackTargetIfCloserThanCurrent inline crates/horizon-client/src/mobs/piglin.rs::register_hoglin NearestTarget keeps the closest target and skips pacified hoglins
|
||||
|
||||
|
@@ -15,7 +15,7 @@ SpellcasterIllager.getCurrentSpell skip:other getter; Horizon reads extra.varia
|
||||
SpellcasterIllager.SpellcasterUseSpellGoal.canContinueToUse port crates/horizon-client/src/mobs/illager.rs::UseSpell::can_continue target alive and warm-up left
|
||||
Vindicator.registerGoals port crates/horizon-client/src/mobs/illager.rs::register_vindicator break door, open door, hold ground, melee, Johnny goal
|
||||
Vindicator.createAttributes skip:data attribute defaults (speed 0.35, follow 12, health 24, attack 5) live in data tables
|
||||
Vindicator.addAdditionalSaveData missing minor: the Johnny flag is not saved; Horizon's Johnny goal tests the custom name, so only a vindicator renamed after being named Johnny differs
|
||||
Vindicator.addAdditionalSaveData missing no `Johnny` flag is written: `mobs/illager.rs:78` tests the custom name itself, so only a vindicator renamed after being named Johnny differs
|
||||
Vindicator.finalizeSpawn port crates/horizon-client/src/mobs/finalize.rs::Ctx::finalize raider spawn, then equipment and enchantments; the door flag it sets each tick comes from vindicator_server_ai_step
|
||||
Vindicator.populateDefaultEquipmentSlots port crates/horizon-client/src/mobs/finalize.rs::Ctx::populate iron axe unless the mob is in a raid
|
||||
Vindicator.setCustomName inline crates/horizon-client/src/mobs/illager.rs::register_vindicator gap: minor: the Johnny goal reads the current custom name each tick; vanilla latches the flag once a name is Johnny
|
||||
@@ -25,7 +25,7 @@ AbstractPiglin.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.
|
||||
AbstractPiglin.customServerAiStep port crates/horizon-client/src/mobs/piglin.rs::ai_step gap: minor: zombification runs outside the Nether by the nether flag, not the PIGLINS_ZOMBIFY attribute
|
||||
AbstractPiglin.finishConversion port crates/horizon-client/src/mobs/piglin.rs::ai_step spawns the zombified piglin with nausea and removes the old one
|
||||
Piglin.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::mob_from_nbt gap: minor: CannotHunt is not read, so hunting ignores it; baby and inventory are read
|
||||
Piglin.getSlot missing minor: item commands cannot reach a piglin's inventory slots (300 and up); 300 and up only address the open container
|
||||
Piglin.getSlot missing item commands stop at the open container: a piglin's own inventory slots (300 and up) are not addressable
|
||||
Piglin.defineSynchedData skip:java synched-data registration; Horizon keeps baby, crossbow and dancing state in Mob fields
|
||||
Piglin.createAttributes skip:data attribute defaults (health 16, speed 0.35, attack 5) live in data tables
|
||||
Piglin.checkPiglinSpawnRules port crates/horizon-client/src/mobs/spawn_rules.rs::check_spawn_rules not spawning on a nether wart block
|
||||
@@ -39,7 +39,7 @@ Piglin.hurtServer inline crates/horizon-client/src/mobs/piglin.rs::celebrate gap
|
||||
Piglin.canUseNonMeleeWeapon port crates/horizon-client/src/game/piglins.rs::Game::piglin_wanted gap: minor: checks the preferred-weapons tag only, not the usable-crossbow tag or the kinetic weapon component
|
||||
Piglin.holdInOffHand port crates/horizon-client/src/game/piglins.rs::Game::start_admiring gap: minor: does not drop an item already in the off hand and does not set persistence
|
||||
Piglin.wantsToPickUp port crates/horizon-client/src/game/piglins.rs::Game::piglin_wants gap: minor: does not check the admiring-disabled wait after a hit by a player
|
||||
Piglin.startRiding missing minor: baby piglins never mount a baby hoglin (see PiglinAi.babySometimesRideBabyHoglin)
|
||||
Piglin.startRiding port crates/horizon-client/src/mobs/riders.rs::start_riding the boarding tests (not the vehicle it is on, not one it carries, the boarding cooldown, a free vehicle) plus the `Piglin` case: a baby piglin on a hoglin goes on top of the up-to-three piglins already there
|
||||
Piglin.getTopPassenger port crates/horizon-client/src/mobs/riders.rs::top_passenger the top of the passenger chain, capped at four passengers on a hoglin
|
||||
Piglin.getAmbientSound port crates/horizon-client/src/mobs/piglin.rs::piglin_ambient the sound of the activity: angry, retreat, admiring, celebrate, jealous, or the plain grunt
|
||||
PiglinAi.initCoreActivity inline crates/horizon-client/src/mobs/piglin.rs::register_piglin gap: minor: no babyAvoidNemesis (babies do not flee a nemesis); avoidZombified and admiring stop are covered by goals
|
||||
@@ -71,12 +71,12 @@ PiglinAi.getSoundForActivity port crates/horizon-client/src/mobs/piglin.rs::pigl
|
||||
PiglinAi.isNearAvoidTarget inline crates/horizon-client/src/mobs/piglin.rs::piglin_sound the avoid target within 12 blocks picks the retreat grunt
|
||||
PiglinAi.babySometimesRideBabyHoglin port crates/horizon-client/src/mobs/piglin.rs::baby_sometimes_ride_baby_hoglin the idle activity copies NEAREST_VISIBLE_BABY_HOGLIN into RIDE_TARGET for 10 to 30 seconds
|
||||
PiglinAi.broadcastAngerTarget inline crates/horizon-client/src/mobs/goals.rs::HurtByTarget::start gap: minor: alerts same-kind mobs nearby; no closer-target check and no hoglin rule
|
||||
PiglinAi.broadcastUniversalAnger missing minor: the universal_anger game rule is stored but piglins ignore it
|
||||
PiglinAi.broadcastUniversalAnger missing the rule reaches the mob world (`creatures.rs:788` sets `universal_anger`) and the wolf's target goal reads it (`tame.rs:81`, `NeutralMob.isAngryAt`), but nothing in `mobs/piglin.rs`: the piglin anger broadcast is `broadcast_anger` (`broadcastAngerTarget`) only, so no piglin is angered for a rule that is on
|
||||
PiglinAi.setAngerTarget port crates/horizon-client/src/game/piglins.rs::Game::anger_piglins gap: minor: only the player path (600-tick anger); a hoglin target does not set the hunt wait
|
||||
PiglinAi.setAngerTargetToNearestTargetablePlayerIfFound missing minor: only for universal anger, which Horizon does not apply
|
||||
PiglinAi.setAngerTargetToNearestTargetablePlayerIfFound missing part of `broadcastUniversalAnger`, which the piglin AI does not run; `tame.rs:81` is the one place the rule picks a player, for the wolf
|
||||
PiglinAi.setAngerTargetIfCloserThanCurrent inline crates/horizon-client/src/mobs/goals.rs::HurtByTarget::start gap: minor: alerts nearby piglins but never keeps a closer target
|
||||
PiglinAi.getAvoidTarget port crates/horizon-client/src/mobs/piglin.rs::avoid_target the avoid target while its memory is there
|
||||
PiglinAi.getNearestVisibleTargetablePlayer missing minor: only used by universal anger, which Horizon does not apply
|
||||
PiglinAi.getNearestVisibleTargetablePlayer missing only used by `broadcastUniversalAnger` and its anger target, neither of which the piglin AI runs
|
||||
PiglinAi.broadcastRetreat port crates/horizon-client/src/mobs/piglin.rs::broadcast_retreat every adult piglin in range takes the retreat
|
||||
PiglinAi.retreatFromNearestTarget port crates/horizon-client/src/mobs/piglin.rs::retreat_from_nearest RETREAT_DURATION of avoiding the nearest target
|
||||
PiglinAi.wantsToStopFleeing port crates/horizon-client/src/mobs/piglin.rs::wants_to_stop_fleeing the avoid target is gone or nearby and no hoglin outnumbers the piglin
|
||||
@@ -100,26 +100,26 @@ Bogged.spawnShearedMushrooms port crates/horizon-client/src/game/creatures.rs::G
|
||||
WitherSkeleton.registerGoals port crates/horizon-client/src/mobs/undead.rs::register piglin and piglin brute target at priority 3, then the skeleton goals
|
||||
WitherSkeleton.canHoldItem port crates/horizon-client/src/mobs/equip.rs::wants_to_pick_up the wither_skeleton_disliked_weapons tag is refused
|
||||
WitherSkeleton.finalizeSpawn port crates/horizon-client/src/mobs/finalize.rs::Ctx::skeleton attack damage 4 for wither skeletons
|
||||
WitherSkeleton.getArrow missing minor: arrows from a wither skeleton are not set on fire for 100 seconds
|
||||
WitherSkeleton.getArrow missing an arrow shot by a wither skeleton is not set on fire for 100 seconds
|
||||
Spider.registerGoals port crates/horizon-client/src/mobs/hostile.rs::register spider goals: leap at target and the spider melee that gives up in bright light
|
||||
Spider.isClimbing skip:other getter; Horizon reads the mob's climbing field
|
||||
Spider.setClimbing inline crates/horizon-client/src/mobs/ai.rs::ai_step climbing set from the horizontal collision each tick
|
||||
Spider.getVehicleAttachmentPoint port crates/horizon-client/src/mobs/riders.rs::passenger_attachment gap: minor: always the spider's fixed seat; vanilla uses a lower seat for riders narrower than the spider
|
||||
Spider.SpiderEffectsGroupData.setRandomEffect port crates/horizon-client/src/mobs/finalize.rs::Ctx::spider hard-difficulty effect: speed, strength, regeneration or invisibility
|
||||
AngerManagement.codec missing minor: warden anger is not saved; the anger tag is written empty, so a reloaded warden forgets its suspects
|
||||
AngerManagement.createUuidAngerPairs missing minor: part of the anger save, which Horizon does not write
|
||||
AngerManagement.convertFromUuids missing minor: part of the anger load, which Horizon does not read
|
||||
AngerManagement.codec missing `entity_nbt.rs::persistent_anger` lists wolf, iron golem, polar bear, enderman and zombified piglin; a warden has none of it, so its anger is never written and never read
|
||||
AngerManagement.createUuidAngerPairs missing part of the warden's anger save, which `entity_nbt.rs` does not write (see AngerManagement.codec)
|
||||
AngerManagement.convertFromUuids missing part of the warden's anger load, which `entity_nbt.rs` does not read
|
||||
AngerManagement.getTopSuspect port crates/horizon-client/src/mobs/warden.rs::AngerManagement::active_entity first suspect the filter accepts
|
||||
AngerManagement.Sorter.compare port crates/horizon-client/src/mobs/warden.rs::AngerManagement::sort_and_update_highest angry first, then players, then the angriest
|
||||
Warden.<init> port crates/horizon-client/src/mobs/warden.rs::register pathfinding maluses and can-float; the anger and vibration state start empty
|
||||
Warden.getAddEntityPacket missing unsure: the spawn packet's emerging flag (data 1) has no Horizon counterpart that I found
|
||||
Warden.recreateFromPacket missing unsure: no client-side emerging pose from a spawn packet that I found
|
||||
Warden.getAddEntityPacket missing the emerging bit is extra entity data on the spawn packet; Horizon's add-entity payload carries no warden data, so a warden spawned from a packet is never emerging
|
||||
Warden.recreateFromPacket missing the client half of the same bit: nothing in `game/remote.rs` applies warden data from a spawn, and `mobs/living.rs` shortens a warden only when its own `is_emerging` memory says so
|
||||
Warden.createAttributes skip:data attribute defaults (health 500, speed 0.3, damage 30) live in data tables
|
||||
Warden.handleEntityEvent inline crates/horizon-client/src/mobs/warden.rs::animate gap: minor: could not confirm the attack animation (event 4) is started here; tendril and sonic boom are
|
||||
Warden.onSyncedDataUpdated inline crates/horizon-client/src/mobs/warden.rs::set_pose starts the emerge, dig, roar or sniff animation on pose change
|
||||
Warden.updateDynamicGameEventListener port crates/horizon-client/src/mobs/warden.rs::listen vibrations reach the warden through listen; there is no registration step
|
||||
Warden.addAdditionalSaveData missing minor: warden anger is not saved (the anger tag is written empty)
|
||||
Warden.readAdditionalSaveData missing minor: warden anger is not read back, so a reloaded warden is calm
|
||||
Warden.addAdditionalSaveData missing a warden's anger is not saved: `persistent_anger` leaves the warden out, so `anger_end_time` and `angry_at` are never written for it
|
||||
Warden.readAdditionalSaveData missing a warden's anger is not read back, so a reloaded warden is calm
|
||||
Warden.playListeningSound inline crates/horizon-client/src/mobs/warden.rs::increase_anger plays the listening sound for the anger level, unless roaring
|
||||
WardenAi.initCoreActivity inline crates/horizon-client/src/mobs/warden_brain.rs::activities swim, look at target, look sink, move sink
|
||||
WardenAi.initDiggingActivity inline crates/horizon-client/src/mobs/warden_brain.rs::activities force unmount and digging
|
||||
@@ -133,7 +133,7 @@ WardenSpawnTracker.copyData inline crates/horizon-client-render/src/sculk.rs::Wa
|
||||
Drowned.checkDrownedSpawnRules port crates/horizon-client/src/mobs/spawn_rules.rs::check_spawn_rules water below, spawner or deep enough, dark enough, frequency roll
|
||||
Drowned.isDeepEnoughToSpawn inline crates/horizon-client/src/mobs/spawn_rules.rs::check_spawn_rules below sea level minus 5
|
||||
Drowned.populateDefaultEquipmentSlots port crates/horizon-client/src/mobs/finalize.rs::Ctx::populate 10 percent chance of a trident (10 in 16) or fishing rod
|
||||
Drowned.canReplaceCurrentItem missing minor: no mob item pickup in Horizon, so the nautilus shell guard never applies
|
||||
Drowned.canReplaceCurrentItem missing no mob item pickup, so the nautilus shell guard never applies
|
||||
Drowned.rangedAttackUncertainty port crates/horizon-client/src/mobs/undead.rs::drowned_throw gap: minor: fixed uncertainty 6 (normal difficulty), not 14 minus 4 times the difficulty
|
||||
Drowned.rideTick port crates/horizon-client/src/game/passengers.rs::position_riders a rider takes its vehicle's body yaw every tick
|
||||
Drowned.wantsToPickUp port crates/horizon-client/src/mobs/equip.rs::wants_to_pick_up spears are refused, then the zombie checks
|
||||
|
||||
|
@@ -6,7 +6,7 @@ ZombieVillager.initializeZombieVillagerData port crates/horizon-client/src/mobs/
|
||||
ZombieVillager.handleEntityEvent port crates/horizon-client/src/mobs/zombie_villager.rs::start_converting cure sound (event 16) is queued when the cure starts
|
||||
ZombieVillager.setVillagerData inline crates/horizon-client/src/game/zombie_villagers.rs::carry_over gap: minor: the cure path copies type and profession there; no setter for the implicit component path
|
||||
ZombieVillager.get skip:other DataComponents getter for the villager variant; Horizon has no entity component map
|
||||
ZombieVillager.applyImplicitComponent missing minor: a zombie villager spawned from an item carrying a villager variant component keeps a random type; Horizon applies implicit components only to buckets (load_bucket_data)
|
||||
ZombieVillager.applyImplicitComponent missing an implicit component is applied only to a mob coming out of a bucket (`entity_nbt::bucket_components`), so a zombie villager spawned from an item carrying a villager variant component keeps a random type
|
||||
ZombifiedPiglin.createAttributes skip:data attribute defaults are entity-type data read in Mob::new (mobs.rs)
|
||||
ZombifiedPiglin.maybePlayFirstAngerSound port crates/horizon-client/src/mobs/undead.rs::zombified_piglin_step first-anger counter and sound run in the step
|
||||
ZombifiedPiglin.maybeAlertOthers port crates/horizon-client/src/mobs/undead.rs::zombified_piglin_step alert timer and line-of-sight check run in the step
|
||||
@@ -39,8 +39,8 @@ Villager.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::re
|
||||
Villager.getAmbientSound port crates/horizon-client/src/mobs/ai.rs::play_ambient gap: minor: the trade ambient sound is not played while trading
|
||||
Villager.rewardTradeXp port crates/horizon-client/src/merchant.rs::Session::notify_trade gap: minor: no 10 second Regeneration on the villager at level-up
|
||||
Villager.die port crates/horizon-client/src/game/villagers.rs::Game::villager_murdered gap: minor: releaseAllPois is not done on death
|
||||
Villager.releaseAllPois missing unsure: releasePoi frees the bed, job site, potential job site and meeting point claims; Horizon has no claim store, only the block occupied property, so the effect is unclear
|
||||
Villager.releasePoi missing unsure: same as releaseAllPois; the claim store is not clear in Horizon
|
||||
Villager.releaseAllPois missing claims are per-mob fields (`extra.bed`, `extra.home` for the job site, `extra.meeting`) with no world-side point-of-interest store to release from; freeing a claim for another villager while this one lives has no counterpart
|
||||
Villager.releasePoi missing same as `releaseAllPois`: a single claim is that mob's field, dropped with the mob
|
||||
Villager.eatUntilFull port crates/horizon-client/src/mobs/villager.rs::Ext::eat_and_digest
|
||||
Villager.shouldIncreaseLevel inline crates/horizon-client/src/merchant.rs::Session::notify_trade level-up check: can level up and xp at the maximum
|
||||
Villager.increaseMerchantCareer inline crates/horizon-client/src/merchant.rs::Session::notify_trade level plus the new level trade set
|
||||
@@ -53,7 +53,7 @@ Villager.updateTrades port crates/horizon-client/src/game/trading.rs::Game::ensu
|
||||
Villager.maybeDecayGossip port crates/horizon-client/src/mobs/villager.rs::Ext::maybe_decay
|
||||
Villager.golemSpawnConditionsMet port crates/horizon-client/src/mobs/villager.rs::Ext::wants_golem sleep memory check; the golem cooldown is an extra check here
|
||||
Villager.get skip:other DataComponents getter for the villager variant; Horizon has no entity component map
|
||||
Villager.applyImplicitComponent missing minor: a villager spawned from an item carrying a villager variant component keeps its biome type; Horizon applies implicit components only to buckets
|
||||
Villager.applyImplicitComponent missing the same bucket-only path (`entity_nbt::bucket_components`), so a villager spawned from an item keeps the type its biome gives it
|
||||
VillagerData.withType skip:java immutable record copy; Horizon keeps type and profession as Mob fields
|
||||
VillagerData.withProfession skip:java immutable record copy; the profession change is villager::set_profession
|
||||
VillagerProfession.bootstrap skip:data registry bootstrap of professions; Horizon reads them from data tables
|
||||
@@ -87,9 +87,9 @@ Inventory.removeFromSelected port crates/horizon-world/src/player_inventory.rs::
|
||||
Player.defineSynchedData skip:java synched data definitions; Horizon keeps the fields in game and player state
|
||||
Player.turtleHelmetTick port crates/horizon-client/src/game/potions.rs::Game::turtle_helmet_tick
|
||||
Player.isEquipped inline crates/horizon-client/src/game/potions.rs::Game::turtle_helmet_tick the head-slot check for the turtle helmet
|
||||
Player.canPlayerFitWithinBlocksAndEntitiesWhen missing unsure: the pose fit check (no collision in the new pose box) was not found in Horizon
|
||||
Player.canPlayerFitWithinBlocksAndEntitiesWhen port crates/horizon-client/src/pose.rs::MotionWorld::can_fit_pose the box of the pose's dimensions at the player's position, deflated by 1e-7, tested against the player's collision; `update_player_pose` asks it for the swimming, the desired and the crouching pose in that order, as `updatePlayerPose` does
|
||||
Player.increaseScore skip:other the score is experience.total in Horizon (death screen); there is no separate score counter
|
||||
Player.getWeaponItem missing unsure: the riptide spin-attack item (autoSpinAttackItemStack) was not found; Horizon keeps spin_attack parts only
|
||||
Player.getWeaponItem missing the riptide spin attack keeps only `auto_spin_attack_ticks` and `auto_spin_attack_dmg`, not the stack it started with (`autoSpinAttackItemStack`), so there is no item to answer with mid-spin
|
||||
Player.destroyVanishingCursedItems port crates/horizon-client/src/game.rs::Game::drop_inventory vanishing-curse stacks are removed from the death drops
|
||||
Player.hasCorrectToolForDrops port crates/horizon-client/src/game.rs::Game::tick_mining the correct-tool test for harvest drops
|
||||
Player.readAdditionalSaveData port crates/horizon-client/src/save.rs::Level::read_player
|
||||
@@ -100,12 +100,12 @@ Player.canFallAtLeast inline crates/horizon-world/src/edge_backoff.rs::back_off_
|
||||
Player.deflectProjectile port crates/horizon-client/src/game/deflect.rs::Game::attack_redirectable
|
||||
Player.baseDamageScaleFactor inline crates/horizon-client/src/game/mace.rs::Game::sweep_damage
|
||||
Player.isEffectiveAi skip:other client-side AI gate; Horizon has no effective-AI check in its loop
|
||||
Player.hasContainerOpen missing unsure: the test for an open container menu (not the inventory menu) was not located
|
||||
Player.hasContainerOpen missing no test for an open container menu: screens are `self.screen`, and nothing distinguishes a container menu from the inventory menu
|
||||
Player.startSleepInBed port crates/horizon-client/src/game.rs::Game::try_sleep
|
||||
Player.isSleepingLongEnough port crates/horizon-client/src/game/lan_actions.rs::sleep_counts 100 ticks in bed is what counts a sleeper as deep for SleepStatus.update
|
||||
Player.updateSwimming port crates/horizon-client/src/motion.rs::MotionWorld::swimming
|
||||
Player.getXpNeededForNextLevel port crates/horizon-client/src/entities.rs::Experience::needed
|
||||
Player.hasEnoughFoodToDoExhaustiveManoeuvres missing unsure: the food-or-flying test for exhaustive moves was not found in motion.rs
|
||||
Player.hasEnoughFoodToDoExhaustiveManoeuvres missing no food-or-flying gate anywhere in `motion.rs`: the exhaustion of a jump or a move is added without asking the food level
|
||||
Player.mayUseItemAt port crates/horizon-client/src/game.rs::Game::use_hand the may-build test before use-on in adventure mode
|
||||
Player.isSwimming port crates/horizon-client/src/motion.rs::MotionWorld::swimming
|
||||
Player.decorateDisplayNameComponent port crates/horizon-client/src/game/commands.rs::player_display_name click suggests /tell, hover shows the player entity, plus the insertion
|
||||
@@ -133,10 +133,10 @@ StackedContents.RecipePicker.getVisitedIngredientIndex skip:java bitset index a
|
||||
StackedContents.RecipePicker.getVisitiedItemIndex skip:java bitset index arithmetic; plain bool vectors in Horizon
|
||||
StackedContents.RecipePicker.clearAllVisited port crates/horizon-client/src/menu/place.rs::Picker::clear_visited
|
||||
StackedItemContents.getBiggestCraftableStack port crates/horizon-client/src/menu/place.rs::Stacked::try_pick_all the largest craftable count, found by a bounded search
|
||||
EvokerFangs.readAdditionalSaveData missing minor: fangs are not saved; a save in mid-bite drops them
|
||||
EvokerFangs.addAdditionalSaveData missing minor: fangs are not saved; the warmup and owner are not written
|
||||
EvokerFangs.readAdditionalSaveData missing fangs are not saved: `entity_nbt.rs` has no evoker fangs writer, so a save in mid-bite drops them
|
||||
EvokerFangs.addAdditionalSaveData missing no writer for the warmup delay or the owner either
|
||||
EvokerFangs.getAnimationProgress port crates/horizon-client/src/game/fangs.rs::Game::fangs_geometry the bite progress that positions the fang model
|
||||
EyeOfEnder.setItem skip:ui the item sprite a flying eye shows; Horizon draws the eye itself
|
||||
EyeOfEnder.shouldRenderAtSqrDistance skip:ui render distance culling only
|
||||
EyeOfEnder.spawnParticles port crates/horizon-client/src/game/end.rs::Game::eye_break_particles the portal crumbs drawn in toward the eye
|
||||
EyeOfEnder.readAdditionalSaveData missing minor: flying eyes are not saved; a save mid-flight loses them
|
||||
EyeOfEnder.readAdditionalSaveData missing a flying eye of ender is not saved: `entity_nbt.rs` has no writer for it (nor `saved_with_chunk` a kind for it), so a save mid-flight loses it
|
||||
|
@@ -20,16 +20,16 @@ FishingHook.handleEntityEvent inline crates/horizon-client/src/game/fishing.rs::
|
||||
FishingHook.getPlayerOwner skip:other owner is implicit: the host's hook is self.hook and a guest's is in that guest's play state
|
||||
FishingHook.getAddEntityPacket port crates/horizon-client/src/game/fishing.rs::Hook::track add-entity payload: the owner entity id is the bobber's data field
|
||||
LlamaSpit.onHitEntity port crates/horizon-client/src/game/creatures.rs::Game::projectile_hit spit hit: 1.0 damage with the spit source to a player (mob_hits_player) or a mob
|
||||
Projectile.applyImplicitComponent missing minor: a projectile given a can_break component by its item keeps no break predicate; Horizon applies implicit components only to buckets
|
||||
Projectile.applyImplicitComponent missing an implicit component is applied only to a mob coming out of a bucket (`entity_nbt::bucket_components`, `game/creatures.rs:5058`), so a projectile spawned from an item with a `can_break` component keeps no break predicate
|
||||
Projectile.addAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::put_projectile Owner and LeftOwner only when set, HasBeenShot once it has ticked (a save before the first tick keeps what it loaded)
|
||||
Projectile.ownedBy inline crates/horizon-client/src/projectiles.rs::Projectile::tick the owner is skipped in the target loop until the projectile has left it
|
||||
Projectile.readAdditionalSaveData inline crates/horizon-client/src/entity_nbt.rs::throwable_from_nbt no shared reader: every kind's own reader takes Owner, LeftOwner and NoGravity (this one, `fireball_from_nbt`, `arrow_from_nbt`); HasBeenShot is read for arrows only, and no projectile keeps a `can_break` predicate
|
||||
Projectile.restoreFrom missing minor: the owner is not carried over a dimension change; Horizon has no projectile dimension change
|
||||
Projectile.restoreFrom missing no projectile `restoreFrom`: the owner is not carried over a dimension change
|
||||
Projectile.isOutsideOwnerCollisionRange inline crates/horizon-client/src/projectiles.rs::Projectile::tick gap: minor: left_owner tests the owner's box inflated 1.0 at the position only; vanilla sweeps along the motion and checks the owner's passengers too
|
||||
Projectile.getMovementToShoot port crates/horizon-client/src/projectiles.rs::Projectile::shoot direction plus triangle spread 0.0172275 times uncertainty, scaled by power
|
||||
Projectile.onAboveBubbleColumn port crates/horizon-client/src/arrows.rs::Ticker::bubble_column gap: minor: arrows only; thrown ender pearls get their push in creatures.rs tick_projectiles and other projectiles get none
|
||||
Projectile.onInsideBubbleColumn port crates/horizon-client/src/arrows.rs::Ticker::bubble_column gap: minor: arrows only; thrown ender pearls get their push in creatures.rs tick_projectiles and other projectiles get none
|
||||
Projectile.canBreakBlockInAdventureMode missing minor: the can_break predicate and adventure-mode rules for projectile block breaking are not modelled
|
||||
Projectile.canBreakBlockInAdventureMode missing a projectile's block breaking ignores the `can_break` predicate and the adventure-mode rules
|
||||
Projectile.spawnProjectileFromRotation port crates/horizon-client/src/projectiles.rs::Projectile::shoot_from_rotation gap: minor: the aim and inherited velocity only; the add-to-world step is the caller's
|
||||
Projectile.spawnProjectile inline crates/horizon-client/src/game/creatures.rs::Game::throw_item each shooter adds its own projectile (throw_item, shoot_crossbow, shoot_projectile_item, the bow in game.rs); throw_item is the player throw path
|
||||
Projectile.applyOnProjectileSpawned port crates/horizon-client/src/game/enchant_effects.rs::Game::bow_projectile_ignite gap: minor: Flame ignite only; Piercing and Punch go through Projectile::set_bonus
|
||||
@@ -43,14 +43,14 @@ ProjectileUtil.getHitResultOnViewVector port crates/horizon-client/src/game/pick
|
||||
ProjectileUtil.getHitResult port crates/horizon-client/src/projectiles.rs::Projectile::tick block clip first, then the nearest entity box on the segment; same core as the move-vector sweep
|
||||
ProjectileUtil.rotateTowardsMovement port crates/horizon-client/src/projectiles.rs::Projectile::update_rotation gap: minor: rotation is set at once, vanilla lerps it by rotationSpeed
|
||||
ProjectileUtil.getMobArrow port crates/horizon-client/src/mobs/undead.rs::perform_ranged_attack gap: minor: builds the skeleton-family arrow with base damage from power; illager crossbow arrows are built in mobs/illager.rs::shoot_crossbow
|
||||
ShulkerBullet.addAdditionalSaveData missing minor: the final target and move direction are not saved; Horizon does not save flying projectiles
|
||||
ShulkerBullet.readAdditionalSaveData missing minor: the final target and move direction are not read back; Horizon does not save flying projectiles
|
||||
ShulkerBullet.canHitEntity missing minor: no-physics entities are not skipped by the bullet's target search; Projectile::tick checks owner and boxes only
|
||||
ShulkerBullet.addAdditionalSaveData missing `entity_nbt.rs::saved_with_chunk` leaves `ShulkerBullet` out of the kinds a chunk writes, so a bullet in flight is dropped with the save; the final target and the move direction have no writer
|
||||
ShulkerBullet.readAdditionalSaveData missing the same gap on the load side: nothing reads a bullet's final target or move direction back
|
||||
ShulkerBullet.canHitEntity missing the bullet's target search (`Projectile::tick` on the client, `projectiles` in `game/creatures.rs`) tests the owner and the boxes only; a no-physics entity is not skipped
|
||||
ShulkerBullet.onHitBlock port crates/horizon-client/src/game/creatures.rs::Game::projectile_hit shulker bullet block hit: hit sound and two explosion particles at the bullet
|
||||
ShulkerBullet.destroy inline crates/horizon-client/src/game/creatures.rs::Game::tick_projectiles the bullet is dropped from the flight list on any hit; the game event is not sent
|
||||
ShulkerBullet.hurtServer port crates/horizon-client/src/game/deflect.rs::Game::hit_shulker_bullet
|
||||
ThrowableProjectile.applyInertia port crates/horizon-client/src/projectiles.rs::Projectile::tick default arm: gravity, then drag 0.8 in water and 0.99 in air; gap: minor: no bubble particles in water
|
||||
ThrowableProjectile.handleFirstTickBubbleColumn missing minor: a thrown item that spawns inside a bubble column is not pushed on its first tick
|
||||
ThrowableProjectile.handleFirstTickBubbleColumn missing a thrown item spawned inside a bubble column is not pushed on its first tick: `game/creatures.rs` has bubble-column particles and the pearl's own column hooks, nothing for a throwable's first tick
|
||||
AbstractArrow.<init> port crates/horizon-client/src/arrows.rs::ArrowState::new arrow defaults: pickup, pickup item, shake, pierce; the owner and drawn ammo come from set_owner and set_item
|
||||
AbstractArrow.defineSynchedData skip:java flags, pierce level and in-ground sync fields; Horizon keeps them on ArrowState and Projectile
|
||||
AbstractArrow.lerpMotion skip:other client interpolation of a server motion packet; Horizon's remote arrows take the server position each update
|
||||
@@ -68,7 +68,7 @@ AbstractArrow.tryPickup port crates/horizon-client/src/arrows.rs::ArrowState::pl
|
||||
AbstractArrow.setFlag port crates/horizon-client/src/projectiles.rs::Projectile::set_critical gap: minor: only the critical flag; the no-physics flag is a plain field
|
||||
AbstractArrow.setPickupItemStack port crates/horizon-client/src/arrows.rs::ArrowState::set_item an empty stack falls back to the default pickup item
|
||||
AbstractArrow.isNoPhysics inline crates/horizon-client/src/arrows.rs::Ticker::tick no_physics is a field read by the tick
|
||||
AbstractArrow.getSlot missing minor: the arrow's pickup stack is not reachable as an entity slot, so item commands on it do nothing
|
||||
AbstractArrow.getSlot missing an arrow's pickup stack is not addressable as an entity slot, so item commands on an arrow do nothing
|
||||
AbstractArrow.Pickup.byOrdinal port crates/horizon-client/src/arrows.rs::Pickup::from_byte out of range is DISALLOWED
|
||||
Arrow.updateColor port crates/horizon-client/src/projectiles.rs::Projectile::arrow_effect_color the tipped arrow's opaque potion colour for its flight and hit particles (-1 for none)
|
||||
Arrow.makeParticle port crates/horizon-client/src/game/remote_projectiles.rs::Game::arrow_makeparticle ticked in #3514; `Arrow.makeParticle` is cited at crates/horizon-client/src/game/remote_projectiles.rs:218
|
||||
@@ -82,11 +82,11 @@ ThrownTrident.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.r
|
||||
ThrownTrident.getLoyaltyFromItem port crates/horizon-client/src/game/trident.rs::Game::release_trident loyalty level read from the thrown stack when the trident is released
|
||||
ThrownTrident.tickDespawn inline crates/horizon-client/src/arrows.rs::Ticker::tick gap: minor: the 1200-tick life limit applies to every trident; the loyalty exemption is not checked
|
||||
AbstractHurtingProjectile.shouldRenderAtSqrDistance skip:ui distance culling for the fireball model; Horizon culls with its own range check
|
||||
AbstractHurtingProjectile.canHitEntity missing minor: no-physics entities are not skipped by the fireball's target search
|
||||
AbstractHurtingProjectile.canHitEntity missing same as `ShulkerBullet.canHitEntity`: the fireball's target search has no no-physics filter
|
||||
WitherSkull.getBlockExplosionResistance port crates/horizon-client/src/game/explosion.rs::blast_resistance a dangerous skull's blast caps a breakable block's resistance at 0.8; `#wither_immune` blocks keep theirs
|
||||
WitherSkull.onHit port crates/horizon-client/src/game/wither.rs::Game::wither_skull_hit skull hit damage, wither effect, then the blast at the impact point
|
||||
AbstractWindCharge.makeBoundingBox port crates/horizon-client/src/projectiles.rs::ProjectileKind::size gap: minor: gives the width only; the box sits 0.15 below the position in vanilla
|
||||
AbstractWindCharge.canHitEntity missing minor: no filter for end crystals or other wind charges found in the target list; unsure whether the caller excludes them
|
||||
AbstractWindCharge.canHitEntity missing the wind charge's target list is `creatures.rs::wind_charge_hitboxes`, which collects the other live, unstuck wind charges as hittable boxes: nothing there drops a wind charge or an end crystal, so the filter `canHitEntity` makes is not applied
|
||||
AbstractWindCharge.onHitBlock port crates/horizon-client/src/game/wind.rs::Game::wind_charge_hit a burst a quarter block out along the hit face
|
||||
WindCharge.deflect port crates/horizon-client/src/game/wind.rs::reverse gap: minor: turns the charge at half speed; the noDeflectTicks delay is not modelled
|
||||
WindCharge.shouldRenderAtSqrDistance skip:ui distance culling for the wind charge model; Horizon culls with its own range check
|
||||
@@ -97,10 +97,10 @@ ThrownEnderpearl.getOwner skip:other owner is kept as a Who value and resolved
|
||||
ThrownEnderpearl.findOwnerIncludingDeadPlayer skip:other owner lookup by UUID across dead players; Horizon keeps the owner id directly
|
||||
ThrownEnderpearl.onHitEntity port crates/horizon-client/src/game/creatures.rs::Game::projectile_hit a mob hit takes zero damage; the pearl still lands
|
||||
ThrownEnderpearl.onHit port crates/horizon-client/src/game/creatures.rs::Game::projectile_hit gap: minor: no 32 portal particles and no dimension change; the owner is teleported to where the pearl was, with 5 damage and a 5 percent endermite
|
||||
ThrownEnderpearl.isAllowedToTeleportOwner missing minor: a pearl whose owner is dead or asleep when it lands still teleports the owner
|
||||
ThrownEnderpearl.isAllowedToTeleportOwner missing `game/creatures.rs` teleports the owner on a pearl's hit without asking whether they are dead or sleeping
|
||||
ThrownEnderpearl.playSound port crates/horizon-client/src/game/creatures.rs::Game::projectile_hit teleport sound at the landing point
|
||||
ThrownEnderpearl.teleport port crates/horizon-client/src/game/creatures.rs::Game::projectile_hit moves the player to the pearl's previous position and clears velocity and fall distance
|
||||
ThrownEnderpearl.canTeleport missing minor: a pearl from the End to the Overworld needs the owner's seen-credits flag before it may teleport
|
||||
ThrownEnderpearl.canTeleport missing no seen-credits test: a pearl whose owner came from the End teleports them back to the Overworld regardless
|
||||
ThrownEnderpearl.onInsideBlock port crates/horizon-client/src/game/end.rs::Game::pearl_owner_in_gateway a pearl that flies into an End gateway sends its owner through and fires enter_block
|
||||
Raid.<init> port crates/horizon-client/src/raid.rs::Raid::new centre, difficulty-based group count, cooldown and active flag
|
||||
Raid.getAllRaiders port crates/horizon-client/src/game/raids.rs::Game::raid_members the raiders of this raid, from the mobs carrying its id
|
||||
|
||||
|
@@ -1,11 +1,11 @@
|
||||
AbstractBoat.setInitialPos port crates/horizon-client/src/game/boats.rs::Boat::new spawn sets the position and its previous copy; velocity and yaw start at zero
|
||||
AbstractBoat.defineSynchedData skip:java SynchedEntityData definitions; Horizon keeps paddle, hurt and input state on the Boat struct
|
||||
AbstractBoat.getPassengerAttachmentPoint port crates/horizon-client/src/game/boats.rs::Boat::seat seat height and the two-seat offsets, +0.2 for an animal; chest boat's 0.15 is included
|
||||
AbstractBoat.onAboveBubbleColumn missing player: a boat over a bubble column is not lifted or pushed down and no splash plays on it; Horizon has no bubble column for boats
|
||||
AbstractBoat.onAboveBubbleColumn port crates/horizon-client/src/game/boats.rs::Game::boat_above_bubble_column a column whose surface the boat is over marks it above a column and starts the 60-tick bubble time, down or up; out of water, one roll in a hundred plays the swim splash sound and particle and posts the splash game event
|
||||
AbstractBoat.animateHurt port crates/horizon-client/src/game/boats.rs::Game::hurt_boat sets the hurt wobble and time; gap: minor: uses the server damage x10, no separate client x11 wobble value
|
||||
AbstractBoat.tickBubbleColumn missing player: a boat in a bubble column does not rise or sink and ejects no passengers from a down column; Horizon has no bubble time on boats
|
||||
AbstractBoat.handleEntityEvent missing player: the bubble column effect event (71 or 72) never reaches a boat, same gap as the bubble column lift
|
||||
AbstractBoat.handleBubbleColumnEffect missing player: a boat in an up or down bubble column keeps its speed; Horizon never changes its vertical velocity from a column
|
||||
AbstractBoat.tickBubbleColumn port crates/horizon-client/src/game/boats.rs::Game::tick_boat_bubble_column the countdown (`Boat::tick_bubble_countdown`) running out throws the boat up or pulls it down after BUBBLE_TIME ticks, lets the riders out of a down column first and broadcasts event 71 or 72; the client's wobble is `Boat::tick_bubble_wobble`
|
||||
AbstractBoat.handleEntityEvent port crates/horizon-client/src/game/remote_vehicles.rs::Game::remote_boat_event event 71 drags the boat down and 72 throws it up, on the boat the client drives; every other id is left to the super call
|
||||
AbstractBoat.handleBubbleColumnEffect port crates/horizon-client/src/game/boats.rs::Game::handle_bubble_column_effect an up column sets vertical velocity 0.6 and a down column adds -0.7, only while the window simulates the boat's movement
|
||||
AbstractBoat.setPaddleState inline crates/horizon-client/src/game/boats.rs::Game::tick_boat paddle flags are set from the rider's input inside the controlled branch
|
||||
AbstractBoat.getLeashOffset port crates/horizon-client/src/mobs/leash.rs::Holder::boat_leash_offset 1.375 by 0.5625 for a boat or raft
|
||||
AbstractBoat.getStatus port crates/horizon-client/src/game/boats.rs::Game::boat_status in air, in water, under water or on land
|
||||
@@ -18,7 +18,7 @@ AbstractBoat.controlBoat inline crates/horizon-client/src/game/boats.rs::Game::t
|
||||
AbstractBoat.getDismountLocationForPassenger port crates/horizon-client/src/game/boats.rs::Game::boat_dismount_target gap: minor: no dismount pose check; the floor and water tests are there
|
||||
AbstractBoat.clampRotation inline crates/horizon-client/src/game/remote_vehicles.rs::Game::remote_ride the rider's yaw is clamped to 105 degrees off the boat; mobs get the head clamp in boat_passengers.rs seat_boat_mobs
|
||||
AbstractBoat.calculatePassengerBodyYRot inline crates/horizon-client/src/game/boat_passengers.rs::Game::seat_boat_mobs animal turned 90 or 270 degrees when the boat is full
|
||||
AbstractBoat.onPassengerTurned inline crates/horizon-client/src/game/boats.rs::Game::tick_riding the rider's turn is clamped each tick
|
||||
AbstractBoat.onPassengerTurned inline crates/horizon-client/src/game/remote_vehicles.rs::Game::remote_ride the rider's turn is clamped each tick
|
||||
AbstractBoat.remove inline crates/horizon-client/src/game/boats.rs::Game::hurt_boat destroyed boats eject mobs and drop the boat; gap: minor: no leash drop, since boats cannot be leashed here
|
||||
AbstractBoat.checkFallDamage missing minor: boats keep no fall distance; boats take no fall damage either way
|
||||
AbstractBoat.getPaddleState port crates/horizon-client/src/game/boats.rs::Boat::rowing_time paddle flag, set only while a rider drives
|
||||
|
||||
|
@@ -34,7 +34,7 @@ StonecutterMenu.removed port crates/horizon-client/src/game/stations.rs::Game::c
|
||||
ArrowItem.asProjectile port crates/horizon-client/src/game/dispenser.rs::Game::shoot_projectile_item dispensed arrows are built in the arrow arm of the item shot
|
||||
BlockItem.updateBlockEntityComponents port crates/horizon-client/src/block_entity_item.rs::place the placed block entity takes the item's components
|
||||
BlockItem.canPlace port crates/horizon-world/src/level/place.rs::Sim::place the placement checks, including the survive check, run in Sim::place
|
||||
BlockItem.onDestroyed missing unsure: a container item whose item entity is destroyed should spill its contents; no Horizon code does this
|
||||
BlockItem.onDestroyed missing no `onDestroyed` on any item: an item entity of a shulker box or a bundle destroyed by fire or lava spills nothing
|
||||
BoatItem.getBoat port crates/horizon-client/src/game/boats.rs::Game::place_boat boat placed at the hit point with the item's stack config
|
||||
BucketItem.playEmptySound port crates/horizon-world/src/level/buckets.rs::Sim::empty_sound
|
||||
BundleItem.getWeightSafe port crates/horizon-items/src/bundle.rs::Contents::weight_fraction float fraction; vanilla compares an exact Fraction
|
||||
@@ -42,7 +42,7 @@ BundleItem.getBarWidth port crates/horizon-items/src/bundle.rs::Contents::bar
|
||||
BundleItem.getBarColor port crates/horizon-items/src/bundle.rs::Contents::bar
|
||||
BundleItem.removeOneItemFromBundle port crates/horizon-items/src/bundle.rs::Contents::remove_one
|
||||
BundleItem.getTooltipImage port crates/horizon-items/src/bundle.rs::tooltip_layout gap: minor: the tooltip_display rule that hides the bundle contents is not checked here
|
||||
BundleItem.onDestroyed missing unsure: a bundle whose item entity is destroyed should spill its contents; no Horizon code does this
|
||||
BundleItem.onDestroyed missing same as `BlockItem.onDestroyed`: nothing spills a destroyed item entity's contents
|
||||
BundleItem.playRemoveOneSound port crates/horizon-client/src/game.rs::Game::drop_bundle_content the menu queues the same sound in menu/bundle.rs
|
||||
BundleItem.playInsertSound port crates/horizon-client/src/menu/bundle.rs::Menu::carried_bundle_click queues the insert or insert-fail sound
|
||||
BundleItem.playDropContentsSound port crates/horizon-client/src/game.rs::Game::drop_bundle_content
|
||||
@@ -54,8 +54,8 @@ CrossbowItem.createProjectile port crates/horizon-client/src/arrows.rs::Ticker::
|
||||
CrossbowItem.getShotPitch inline crates/horizon-client/src/game/crossbow.rs::Game::shoot_crossbow the shot pitch per index is inline
|
||||
CrossbowItem.getRandomShotPitch inline crates/horizon-client/src/game/crossbow.rs::Game::shoot_crossbow the random pitch formula is inline
|
||||
CrossbowItem.getPowerForTime inline crates/horizon-client/src/game/crossbow.rs::Game::tick_crossbow the charge fraction capped at one is the tick check against the charge time
|
||||
DyeColor.getMixedColor missing player: a baby sheep, wolf or cat from parents of two dye colours gets a mixed colour; Horizon has no breeding dye mix (only sheep_color for spawns)
|
||||
DyeColor.findColorMixInRecipes missing player: part of the same breeding dye mix; the crafting recipe lookup for two dyes is not done for offspring
|
||||
DyeColor.getMixedColor port crates/horizon-client/src/game/creatures.rs::Game::dye_mix the colour of the crafting recipe that mixes the two dyes, else one of the two picked at random (`next_boolean`); `creatures.rs:3153`, `:3176` and `:3222` give the offspring of a sheep, wolf or cat its colour with it
|
||||
DyeColor.findColorMixInRecipes port crates/horizon-client/src/game/creatures.rs::Game::dye_color_mix_in_recipes the two dyes laid out as a 2x2 crafting grid through `Recipes::find`, and the result read back as a dye colour by its `_dye` suffix
|
||||
FireworkRocketItem.createDispenseConfig port crates/horizon-client/src/game/dispenser.rs::Game::dispense_projectile rocket config: offset 0.5000099, uncertainty 1, power 0.5, event 1004
|
||||
FireworkRocketItem.getEntityJustOutsideOfBlockPos port crates/horizon-client/src/game/dispenser.rs::Game::dispense_position the position just outside the dispenser on the facing side
|
||||
HangingEntityItem.mayPlace port crates/horizon-client/src/game/paintings.rs::Game::place_painting paintings take no vertical face; an item frame (ItemFrameItem.mayPlace) may go on one and only needs the build height, which place_item_frame checks
|
||||
@@ -84,7 +84,7 @@ Item.Properties.horseArmor skip:data registration builder
|
||||
Item.Properties.nautilusArmor skip:data registration builder
|
||||
Item.Properties.requiredFeatures skip:data registration builder
|
||||
Item.Properties.delayedHolderComponent skip:data registration builder
|
||||
Item.Properties.loweredMobVisibility missing unsure: the mob_visibility component is not read by Horizon's mob targeting
|
||||
Item.Properties.loweredMobVisibility missing the `mob_visibility` component is parsed (it is in `item_model.rs`'s component table and the protocol's item hash), but no mob targeting reads it
|
||||
Item.Properties.finalizeInitializer skip:data registration builder
|
||||
Item.TooltipContext.of skip:java tooltip context factory; Horizon passes the level and player to the tooltip functions
|
||||
ItemStack.validateStrict skip:java codec-side validation of components and stack size
|
||||
@@ -97,7 +97,7 @@ ItemStack.isBroken inline crates/horizon-client/src/game.rs::Game::damage_held t
|
||||
ItemStack.nextDamageWillBreak inline crates/horizon-client/src/game/trident.rs::Game::use_trident gap: minor: only the trident's check is ported here
|
||||
ItemStack.processDurabilityChange port crates/horizon-client/src/game/enchant_effects.rs::Game::durability_loss the Unbreaking-adjusted loss
|
||||
ItemStack.applyDamage port crates/horizon-client/src/game.rs::Game::damage_held gap: minor: no item durability advancement trigger fires
|
||||
ItemStack.hurtAndConvertOnBreak missing unsure: a carrot or fungus on a stick that breaks should become a fishing rod; no conversion was found
|
||||
ItemStack.hurtAndConvertOnBreak missing nothing converts a broken carrot on a stick or fungus on a stick into a fishing rod
|
||||
ItemStack.hurtEnemy port crates/horizon-client/src/game/mace.rs::Game::weapon_wear the per-hit durability loss of a weapon
|
||||
ItemStack.interactLivingEntity port crates/horizon-client/src/game.rs::Game::interact_entity equip-on-interact is in the mob paths (nautili.rs, ghasts.rs)
|
||||
ItemStack.copy skip:java value copy
|
||||
@@ -111,21 +111,21 @@ ItemStack.hashStackList skip:java hash for collections
|
||||
ItemStack.onCraftedBy inline crates/horizon-client/src/game.rs::Game::click_inventory gap: minor: the crafted statistic is added only on the inventory grid craft path here
|
||||
ItemStack.set skip:other component write; Horizon stores component data in typed fields
|
||||
ItemStack.update skip:java functional update helper
|
||||
ItemStack.applyComponentsAndValidate missing unsure: a validated component patch has no single Horizon counterpart; /item modify is a separate path
|
||||
ItemStack.applyComponentsAndValidate missing a validated component patch has no single counterpart: `/item modify` goes through its own path
|
||||
ItemStack.getStyledHoverName port crates/horizon-client/src/game/stations.rs::Game::stack_hover_name gap: minor: plain text; the rarity colour and italic style are not applied here
|
||||
ItemStack.addToTooltip port crates/horizon-client/src/item_tooltip.rs::lines
|
||||
ItemStack.addUnitComponentToTooltip inline crates/horizon-client/src/game/stations.rs::Game::stack_tooltip_lines unit lines such as unbreakable, gated by the tooltip display
|
||||
ItemStack.canPlaceOnBlockInAdventureMode port crates/horizon-client/src/game/game_mode.rs::Game::can_place_on_in_adventure the can_place_on predicates gate a placement in adventure mode
|
||||
ItemStack.canBreakBlockInAdventureMode missing player: in adventure mode a block can be broken only when the held item's can_break list allows it; Horizon does not read that component
|
||||
ItemStack.getDamageSource missing unsure: a stack's damage_type component is not read when a weapon hits; no Horizon reader found
|
||||
ItemStack.canBreakBlockInAdventureMode port crates/horizon-client/src/game/game_mode.rs::Game::adventure_predicate the main-hand stack's `can_break` predicate against the block at the position (one predicate or a list, block ids or tags, exact or ranged state properties; a predicate with `nbt` or `components` never matches), read by `game_mode.rs:200`; the same fn answers `can_place_on`
|
||||
ItemStack.getDamageSource missing a stack's `damage_type` component is not read when a weapon hits: the attack builds its damage from the mob's own sources (`mobs::Damage`), and `damage_type` is only used for the enchantment damage tags
|
||||
ItemStackTemplate.fromNonEmptyStack port crates/horizon-items/src/item_data.rs::template_tag stack to template tag
|
||||
ItemStackTemplate.withCount skip:java record copy with a new count
|
||||
ItemStackTemplate.apply port crates/horizon-assets/src/item_model.rs::stack_from_template
|
||||
ItemUtils.onContainerDestroyed missing unsure: destroyed container items do not spill their contents; see BlockItem.onDestroyed
|
||||
ItemUtils.onContainerDestroyed missing the shared spill of a destroyed container item has no counterpart; see `BlockItem.onDestroyed`
|
||||
Items.registerBlock skip:data item registry bootstrap
|
||||
Items.registerItem skip:data item registry bootstrap
|
||||
JukeboxSong.hasFinished port crates/horizon-items/src/items.rs::Song::finished
|
||||
JukeboxSongPlayer.setSongWithoutPlaying missing unsure: no load path restores a jukebox's song start tick
|
||||
JukeboxSongPlayer.setSongWithoutPlaying inline crates/horizon-client/src/save.rs::decode_block_entity the load sets the record and `ticks_since_song_started` straight into the `Jukebox` block entity, which is the whole of `setSongWithoutPlaying`; the song carries on from that tick in `tick_jukebox`
|
||||
JukeboxSongPlayer.play port crates/horizon-client/src/game/jukebox.rs::Game::start_song the song starts at the jukebox with its sound
|
||||
JukeboxSongPlayer.shouldEmitJukeboxPlayingEvent inline crates/horizon-client/src/game/jukebox.rs::Game::tick_jukebox
|
||||
JukeboxSongPlayer.spawnMusicParticles inline crates/horizon-client/src/game/jukebox.rs::Game::tick_jukebox the note particle is emitted in tick_jukebox
|
||||
@@ -138,7 +138,7 @@ MapItem.createNewSavedData port crates/horizon-client/src/game/maps.rs::MapData:
|
||||
MapItem.getCorrectStateForFluidBlock inline crates/horizon-client/src/game/maps.rs::Game::update_map the map colour pass
|
||||
MapItem.inventoryTick port crates/horizon-client/src/game/maps.rs::Game::tick_maps
|
||||
MapItem.lockMap port crates/horizon-client/src/menu.rs::Menu::map_crafted the lock branch of the post processing
|
||||
MapItem.useOn missing player: using a map on a banner block should toggle that banner's marker on the map; Horizon has no banner toggle
|
||||
MapItem.useOn port crates/horizon-client/src/game/maps.rs::Game::use_map_on_banner a map used on a block in the `minecraft:banners` tag toggles that banner's marker (`MapData::toggle_banner`); a failed toggle ends the use without a swing, and `game/remote.rs` predicts the same for its own map
|
||||
MobBucketItem.emptyContents port crates/horizon-world/src/level/buckets.rs::Sim::bucket_empty the MobWater and Nothing contents
|
||||
PotionItem.getDefaultInstance skip:data default stack data
|
||||
PotionItem.useOn port crates/horizon-client/src/game/potions.rs::Game::use_water_bottle_on_block a water bottle poured on #convertible_to_mud makes mud and empties the bottle
|
||||
|
||||
|
@@ -68,7 +68,7 @@ ItemContainerContents.itemCopies skip:other no callers in the vanilla source
|
||||
ItemContainerContents.asMutable skip:other no callers in the vanilla source
|
||||
ItemContainerContents.equals skip:java value equality; Horizon compares the item data directly
|
||||
KineticWeapon.makeLocalHitSound port crates/horizon-client/src/game/spear.rs::Game::kinetic_hit_feedback the local hit sound, at most once per ten ticks per entity
|
||||
KineticWeapon.computeDamageUseDuration missing unsure: the use duration (delay plus the damage window) is not computed in Horizon's use_spear or spear_tick; the windows are checked one by one
|
||||
KineticWeapon.computeDamageUseDuration port crates/horizon-client/src/spear.rs::Kinetic::damage_use_duration the delay plus the damage window's duration, which `SpearUseGoal` and `SpearAttack` read as the spear's use duration
|
||||
KineticWeapon.Condition.test port crates/horizon-client/src/spear.rs::Condition::test tick, attacker speed and relative speed against the scaled minimums
|
||||
PiercingWeapon.makeHitSound port crates/horizon-client/src/game/spear.rs::Game::jab gap: minor: the hit sound plays at this player's feet only; vanilla plays it for everyone nearby
|
||||
ResolvableProfile.createUnresolved port crates/horizon-client-render/src/skins.rs::parse_profile keeps a name or id as the unresolved key, resolved later
|
||||
@@ -97,7 +97,7 @@ Ingredient.testOptionalIngredient port crates/horizon-client/src/crafting.rs::In
|
||||
Ingredient.equals skip:java value equality
|
||||
Ingredient.of port crates/horizon-client/src/crafting.rs::Ingredient::of ingredient from item ids
|
||||
Ingredient.displayForSingleItem skip:other no callers in the vanilla source
|
||||
Ingredient.getSingleItem missing unsure: returns the one item of a single-item ingredient; no Horizon function found in this pass
|
||||
Ingredient.getSingleItem skip:data only `BrewingRecipeBuilder.defaultId` (datagen) asks for it; the brewing recipes Horizon reads come from the JSON tables
|
||||
PlacementInfo.create port crates/horizon-client/src/crafting.rs::Placement::of placement for one ingredient, none when it is empty
|
||||
PotionIngredient.test port crates/horizon-client-render/src/brewing.rs::PotionIngredient::test item id and the potion filter
|
||||
PotionIngredient.of port crates/horizon-client-render/src/brewing.rs::PotionIngredient::parse built from the recipe JSON, with the potion filter
|
||||
@@ -116,24 +116,24 @@ ShapedRecipePattern.unpack port crates/horizon-client/src/crafting.rs::Recipes::
|
||||
ShapedRecipePattern.firstNonEmpty port crates/horizon-client/src/crafting.rs::shrink trims empty rows and columns at both ends
|
||||
ShapedRecipePattern.lastNonEmpty port crates/horizon-client/src/crafting.rs::shrink trims empty rows and columns at both ends
|
||||
ShapedRecipePattern.matches port crates/horizon-client/src/crafting.rs::Recipes::assemble_if_matches shaped arm: exact size, cell tests, mirrored match
|
||||
SmithingTransformRecipe.display missing unsure: the recipe book slot displays for a smithing transform are not built in Horizon's recipe book; the smithing table uses StationRecipes::smithing_result
|
||||
SmithingTrimRecipe.display missing unsure: the recipe book slot displays for a smithing trim are not built in Horizon's recipe book; the smithing table uses StationRecipes::smithing_result
|
||||
SmithingTransformRecipe.display missing the book's entries are built from shaped, shapeless, furnace and special recipes only (Book::build), so a smithing transform gets its display count (1) and its station in RecipeIndex and no slot displays
|
||||
SmithingTrimRecipe.display missing same as SmithingTransformRecipe.display: the table's own screens come from StationRecipes::smithing_result, the book builds no smithing entry
|
||||
TransmuteRecipe.computeResultSize port crates/horizon-client/src/crafting.rs::Special::assemble result count plus the material count when it is added
|
||||
TransmuteRecipe.matches port crates/horizon-client/src/crafting.rs::Special::matches Transmute arm: one input and the material list
|
||||
TransmuteRecipe.assemble port crates/horizon-client/src/crafting.rs::Special::assemble Transmute arm: result with the input's components
|
||||
TransmuteRecipe.resultDisplay port crates/horizon-client/src/recipe_book.rs::RecipeIndex::result the recipe's result id
|
||||
TransmuteRecipe.createPlacementInfo port crates/horizon-client/src/crafting.rs::Recipes::placement the placement of the input and the materials
|
||||
RecipeDisplay.isEnabled missing minor: no feature flag check, so a display for a disabled feature still shows
|
||||
RecipeDisplay.isEnabled missing no feature flag check: RecipeIndex::load and Book::build skip the level's enabled_features, so a display of a disabled feature still shows
|
||||
SlotDisplay.applyDemoTransformation port crates/horizon-client/src/stations.rs::StationRecipes::trim_demo gap: minor: only the trim demo draws (up to 256 tries); the dyed demo is not modelled
|
||||
SlotDisplay.AnyFuel.resolve port crates/horizon-client/src/recipe_book.rs::ghost every item with a cooking fuel component, grouped by component value in registry order
|
||||
SlotDisplay.Composite.resolve missing unsure: no composite display list found in Horizon's recipe book
|
||||
SlotDisplay.Composite.resolve missing the synced display parses SlotDisplay::Composite (horizon-protocol/src/packets/recipes.rs) but nothing resolves it into stacks; Book::items_of reads an Ingredient's own item list
|
||||
SlotDisplay.DyedSlotDemo.resolve port crates/horizon-client/src/crafting.rs::book_displays the targets cycle fastest and the list stops at 16 stacks
|
||||
SlotDisplay.ItemSlotDisplay.resolve port crates/horizon-client/src/recipe_book.rs::Book::items_of the item of a single-item slot
|
||||
SlotDisplay.ItemStackSlotDisplay.resolve port crates/horizon-client/src/recipe_book.rs::Book::items_of gap: minor: keeps item ids only; the stack's components are not carried into the display
|
||||
SlotDisplay.OnlyWithComponent.resolve missing unsure: no display that filters by an item component found in Horizon's recipe book
|
||||
SlotDisplay.OnlyWithComponent.resolve missing the book's displays hold item ids, not stacks (Book::items_of), so there is no component for a predicate to test
|
||||
SlotDisplay.SmithingTrimDemoSlotDisplay.resolve port crates/horizon-client/src/stations.rs::StationRecipes::trim_demo a random base trimmed by a random material
|
||||
SlotDisplay.WithAnyPotion.resolve port crates/horizon-client/src/crafting.rs::book_displays every potion resolved onto the source item
|
||||
SlotDisplay.WithRemainder.resolve missing unsure: no remainder display found in Horizon's recipe book; remainders exist in Recipes::remainders for play
|
||||
SlotDisplay.WithRemainder.isEnabled missing minor: no feature flag check for the remainder display
|
||||
SlotDisplay.WithRemainder.resolve missing remainders are kept for play (Recipes::remainders), and the book's displays never apply them
|
||||
SlotDisplay.WithRemainder.isEnabled missing no feature flag check: the recipe load and the book never read the level's enabled_features
|
||||
BlockPlaceContext.at port crates/horizon-world/src/level/place.rs::Sim::placement_at the placement position and face from the clicked block
|
||||
UseOnContext.isSecondaryUseActive port crates/horizon-client/src/game.rs::Game::use_hand the shift key sets the context's sneaking flag
|
||||
|
@@ -118,7 +118,7 @@ CollisionGetter.noBorderCollision port crates/horizon-client-render/src/collisio
|
||||
CollisionGetter.getPreMoveCollisions inline crates/horizon-client-render/src/collisions.rs::SceneCollisions::resolve_with_context gap: minor: entity and block shapes are not returned as a list; the resolver takes them together
|
||||
CollisionGetter.getBlockAndLiquidCollisions port crates/horizon-client-render/src/collisions.rs::no_collision_or_liquid gap: minor: a yes or no test, not the shape list
|
||||
CollisionGetter.getBlockCollisionsFromContext port crates/horizon-client-render/src/collisions.rs::SceneCollisions::collision_block gap: minor: per-block shape; the box walk is not here
|
||||
CollisionGetter.clipIncludingBorder missing unsure: no world-border clamp of a ray found in clip_ray
|
||||
CollisionGetter.clipIncludingBorder port crates/horizon-client/src/projectiles.rs::clip_including_border a ray that starts inside the world border and ends outside it stops on the border at WorldBorder.clampVec3ToBound of that point, facing the nearest direction of travel; the projectile clip in creatures.rs calls it around its block clip
|
||||
CollisionGetter.collidesWithSuffocatingBlock port crates/horizon-client-render/src/collisions.rs::SceneCollisions::is_suffocating
|
||||
EntityGetter.getEntitiesOfClass skip:other class filter repeated at each caller over the entity lists; no shared lookup
|
||||
EntityGetter.getPlayerByUUID skip:other uuid find repeated at each caller (e.g. allays.rs)
|
||||
|
||||
|
Loaded 100 of 118 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user