Merge origin/main into entity occlusion branch

This commit is contained in:
ElyTheCat committed 2026-11-02 18:47:38 +03:30
commit 7bee9ec3cf
84 files changed
+1178 -231

No files matched your search

+10 -2
View File
@@ -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));
}
}
}
+11 -1
View File
@@ -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),
})
}
+15 -4
View File
@@ -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),
);
+3
View File
@@ -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.
+3
View File
@@ -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)",
+145 -6
View File
@@ -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"))
@@ -5312,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
@@ -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)?;
+24 -8
View File
@@ -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)
+1 -1
View File
@@ -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],
+6 -2
View File
@@ -622,8 +622,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() {
@@ -634,6 +634,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 if let Some(culling) = culling {
self.finish_entity_occlusion(culling, item, out, entity_start);
} else {
+9
View File
@@ -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<_>>()
}),
})
}
}
@@ -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.
+18 -2
View File
@@ -359,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"
@@ -402,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,
@@ -435,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);
@@ -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)),
+14
View File
@@ -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],
+4 -1
View File
@@ -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`
+35
View File
@@ -4512,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]
@@ -13799,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;
}
+57 -7
View File
@@ -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);
+29 -1
View File
@@ -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));
}
+4
View File
@@ -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
+269
View File
@@ -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);
}
}
+9 -1
View File
@@ -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);
}
+13
View File
@@ -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;
+4 -4
View File
@@ -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 {
+2 -2
View File
@@ -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.
+2 -2
View File
@@ -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 {
+2
View File
@@ -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
+3
View File
@@ -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}",
+3
View File
@@ -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}",
+3
View File
@@ -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}",
+3
View File
@@ -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": "アップデート",
+3
View File
@@ -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}",
+3
View File
@@ -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",
+3
View File
@@ -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}",
+3
View File
@@ -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",
+3
View File
@@ -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}",
+3
View File
@@ -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
View File
@@ -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(())
}
}
+1 -1
View File
@@ -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
View File
@@ -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;
+1 -1
View File
@@ -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;
}
+1 -1
View File
@@ -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();
},
));
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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();
})),
),
+14 -5
View File
@@ -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)
}
+7 -4
View File
@@ -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();
}
+1 -1
View File
@@ -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
View File
@@ -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))
}
+2 -2
View File
@@ -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();
});
}
+1 -1
View File
@@ -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();
}
}
+1 -1
View File
@@ -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();
},
)),
+12 -6
View File
@@ -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();
},
)),
+5 -2
View File
@@ -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();
});
}
+1
View File
@@ -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(
@@ -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@."
}
+10 -10
View File
@@ -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
1 RodBlock.mirror port crates/horizon-world/src/worldgen/structure.rs::StateTransforms::mirror rod facing mirrored through the state table
61 StandingSignBlock.mirror port crates/horizon-world/src/worldgen/structure.rs::StateTransforms::mirror
62 StandingSignBlock.rotate port crates/horizon-world/src/worldgen/structure.rs::StateTransforms::rotate
63 StandingSignBlock.updateShape port crates/horizon-world/src/level/shapes.rs::Sim::update_shape_class
64 StemBlock.getCloneItemStack missing port crates/horizon-client/src/game/block_items.rs::clone_item unsure: minor: middle-click on a stem may not return its seed item; no stem case found in the pick code a pumpkin or melon stem, attached or not, picks the seed it grew from; any other block picks its own item
65 StemBlock.isValidBonemealTarget port crates/horizon-world/src/level/bonemeal.rs::Sim::bonemeal_valid
66 StemBlock.performBonemeal port crates/horizon-world/src/level/bonemeal.rs::Sim::perform_bonemeal
67 StemBlock.randomTick port crates/horizon-world/src/level/tick.rs::Sim::stem_random_tick
75 TallSeagrassBlock.canSurvive port crates/horizon-world/src/level/survive.rs::Sim::can_survive_class
76 TallSeagrassBlock.getStateForPlacement port crates/horizon-world/src/level/place.rs::Sim::placement_class
77 TallSeagrassBlock.mayPlaceOn port crates/horizon-world/src/level/survive.rs::Sim::may_place_on
78 TestBlock missing skip:debug minor: game-test block for operators; Horizon keeps only its NBT game-test tooling: an operator's test block runs a structure when triggered and Horizon keeps only its block entity NBT
79 TestInstanceBlock missing skip:debug minor: game-test instance block; Horizon keeps only its NBT game-test tooling: the instance block runs and exports a test structure; Horizon keeps its NBT
80 TintedParticleLeavesBlock.spawnFallingLeavesParticle port crates/horizon-client/src/animate_tick.rs::block_animate_tick falling tinted leaf particle
81 TorchBlock.animateTick port crates/horizon-client/src/animate_tick.rs::block_animate_tick torch flame
82 TorchflowerCropBlock.randomTick port crates/horizon-world/src/level/tick.rs::Sim::random_tick_block growth arm
100 WallHangingSignBlock.canPlace port crates/horizon-world/src/level/place.rs::Sim::wall_hanging_sign_can_place
101 WallHangingSignBlock.createBlockStateDefinition skip:data states come from the generated block state table
102 WallHangingSignBlock.getStateForPlacement port crates/horizon-world/src/level/place.rs::Sim::placement_class
103 WallHangingSignBlock.isHittingEditableSide missing inline crates/horizon-world/src/level/interact.rs::Behaviours::block_use_item_on unsure: player: a hanging sign clicked on its side may not chain onto a wall hanging sign the faces of the sign it faces and its opposite are the editable ones; a hanging sign aimed at any other face passes
104 WallHangingSignBlock.rotate port crates/horizon-world/src/worldgen/structure.rs::StateTransforms::rotate
105 WallHangingSignBlock.shouldTryToChainAnotherHangingSign missing inline crates/horizon-world/src/level/interact.rs::Behaviours::block_use_item_on unsure: player: a second hanging sign used on a wall sign may not chain on passing on a non-editable face lets the held hanging sign place its own block, which chains onto the wall sign
106 WallHangingSignBlock.updateShape port crates/horizon-world/src/level/shapes.rs::Sim::update_shape_class
107 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
108 WallSignBlock.canSurvive port crates/horizon-world/src/level/survive.rs::Sim::can_survive_class
132 BannerPatternLayers.Builder skip:java builder for banner pattern layers
133 BannerPatterns skip:data registry bootstrap
134 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
135 BarrelBlockEntity.recheckOpen missing unsure: player: a barrel lid may stay open after a player walks away; only golem openers are rechecked on a timer 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
136 BarrelBlockEntity.saveAdditional port crates/horizon-client-core/src/block_entity_nbt.rs::canonical gap: minor: canonical NBT form only
137 BarrelBlockEntity.startOpen port crates/horizon-client/src/game/block_entity_anims.rs::Game::barrel_openers_changed
138 BarrelBlockEntity.stopOpen port crates/horizon-client/src/game/block_entity_anims.rs::Game::barrel_openers_changed
162 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
163 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
164 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
165 RandomizableContainerBlockEntity.canOpen missing inline crates/horizon-client/src/game/advancements.rs::Game::container_locked_for unsure: minor: the spectator and loot-table open checks were not found the negative of it: a spectator opens any container, and otherwise the lock decides (LockCode.canUnlock)
166 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
167 RandomizableContainerBlockEntity.createMenu port crates/horizon-client/src/game.rs::Game::open_menu gap: minor: the spectator and loot-table checks are not ported here
168 SculkSensorBlockEntity.loadAdditional port crates/horizon-client-render/src/sculk.rs::SculkEntity::load
169 SculkSensorBlockEntity.saveAdditional port crates/horizon-client-render/src/sculk.rs::SculkEntity::to_nbt
170 SculkSensorBlockEntity.VibrationUser.canReceiveVibration port crates/horizon-client-render/src/sculk.rs::valid_vibration
171 SculkSensorBlockEntity.VibrationUser.onReceiveVibration port crates/horizon-client-render/src/sculk.rs::Data::schedule
172 TestBlockEntity missing skip:debug minor: game-test block entity; reset, trigger and logging are not ported, NBT only game-test tooling: reset, trigger and logging belong to the test runner, which Horizon does not have; its NBT is kept
173 TestInstanceBlockEntity missing skip:debug minor: game-test structure tooling; export and chunk forcing are not ported game-test tooling: export and chunk forcing belong to the test runner, which Horizon does not have
174 TrialSpawnerConfig.calculateTargetSimultaneousMobs port crates/horizon-client/src/mobs/trial_spawner.rs::TrialConfig::target_simultaneous_mobs
175 TrialSpawnerConfig.calculateTargetTotalMobs port crates/horizon-client/src/mobs/trial_spawner.rs::TrialConfig::target_total_mobs
176 TrialSpawnerConfig.withSpawning skip:java builder-style setter on the config record
204 LinearPalette skip:java palette storage; Horizon's chunk.rs keeps block ids in plain arrays
205 PalettedContainerFactory skip:java factory for palette storage; Horizon's chunk.rs keeps block ids in plain arrays
206 SingleValuePalette skip:java palette storage; Horizon's chunk.rs keeps block ids in plain arrays
207 UpgradeData missing unsure: minor: block upgrades of old chunks on load are not ported 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
208 ChunkDependencies.getRadiusOf inline crates/horizon-client-render/src/streaming.rs::Ranges::margins fixed margins instead of a per-status radius
209 ChunkPyramid.Builder skip:java builder for the status pyramid; Horizon uses fixed margins
210 ChunkStatusTasks.buildTerrain inline crates/horizon-world/src/worldgen/generator.rs::Pipeline::generate_with_context terrain pass
+1 -1
View File
@@ -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
1 RegionFileVersion.configure missing minor: region-file-compression is read from server.properties (horizon-server-files properties.rs) but nothing applies it; chunk saves always write zlib (save.rs compress_records)
52 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
53 GameRuleMap.setFromIf skip:other no caller in the pinned sources (only withOther uses it)
54 GameRuleMap.withOther skip:other no caller in the pinned sources
55 BlendingData port home crates/horizon-world/src/worldgen/blending.rs::BlendingData crates/horizon-world/src/worldgen/blending.rs unpack/pack, calculateData for the border columns, heights, densities and biomes; the client reads the saved chunks (old_world.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)
56 BlendingData.Packed.validateArraySize skip:java codec size check
57 AllOfPredicate.test port crates/horizon-world/src/worldgen/block_predicate.rs::BlockPredicate::test_inner the All arm
58 AnyOfPredicate.test port crates/horizon-world/src/worldgen/block_predicate.rs::BlockPredicate::test_inner the Any arm
+1 -1
View File
@@ -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 RepeatingPlacement.modify port crates/horizon-world/src/worldgen/placement.rs::PlacementModifier::modify_inner repeat arm for Count and the noise counts emits the count
180 CollectingNeighborUpdater.neighborChanged port crates/horizon-world/src/level/update.rs::Sim::neighbor_changed_at Simple update
181 CollectingNeighborUpdater.runUpdates port crates/horizon-world/src/level/update.rs::Sim::run_updates
182 CollectingNeighborUpdater.shapeUpdate port crates/horizon-world/src/level/update.rs::Sim::neighbor_shape_changed
183 CollectingNeighborUpdater.updateNeighborsAtExceptFromFacing port crates/horizon-world/src/level/update.rs::Sim::update_neighbors_at_except 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)
184 CollectingNeighborUpdater.FullNeighborUpdate.runNext port crates/horizon-world/src/level/update.rs::Update::next Full arm
185 CollectingNeighborUpdater.MultiNeighborUpdate.forEachUpdatedPos inline crates/horizon-world/src/level/update.rs::Update::next Multi arm steps the index and skips the facing side
186 CollectingNeighborUpdater.MultiNeighborUpdate.runNext port crates/horizon-world/src/level/update.rs::Update::next Multi arm
+2 -1
View File
@@ -3,7 +3,8 @@ SetWritableBookPagesFunction missing `horizon-world/src/loot.rs::modify` has no
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
1 SetStewEffectFunction.Builder skip:java builder for the stew effect list; Horizon keeps the effects in loot.rs Context::run (set_stew_effect)
3 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
4 SmeltItemFunction.run port crates/horizon-world/src/loot.rs::Context::run furnace_smelt arm: smelting recipe, use_input_count and stack cap
5 SmeltItemFunction.smelted skip:java static builder factory
6 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 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
7 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)
8 LootContextParamSets skip:data loot parameter set registry and validation; Horizon reads its loot tables
9 AllOfCondition.allOf skip:java static builder
10 AllOfCondition.combine inline crates/horizon-items/src/items.rs::LootContext::condition all_of arm: every term must hold
+1 -1
View File
@@ -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
1 ServerData.write port crates/horizon-protocol/src/servers.rs::ServerEntry::to_tag gap: minor: no acceptedCodeOfConduct entry (Horizon never stores code of conduct acceptance)
95 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
96 AbstractMinecartRenderer.newRender missing minor: experimental minecart movement (new render path) is not implemented
97 AbstractMinecartRenderer.oldRender port crates/horizon-client/src/game/minecarts.rs::Game::cart_geometry rail-based draw is the path Horizon has
98 AbstractMinecartRenderer.newExtractState missing port crates/horizon-client/src/game/remote.rs::Game::remote_minecart_lerp minor: experimental minecart movement (new render path) is not implemented 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
99 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
100 AbstractMinecartRenderer.getRenderOffset missing minor: only the new render path offsets the cart
101 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
+1 -1
View File
@@ -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
1 DolphinRenderer.extractRenderState port crates/horizon-client/src/game/mob_blocks.rs::Game::held_item_geometry gap: minor: isMoving (dolphin motion flag) is not read
33 EvokerFangsRenderer.extractRenderState inline crates/horizon-client/src/game/fangs.rs::Game::fangs_geometry yaw and bite progress read there
34 ExperienceOrbRenderer.submit inline crates/horizon-client/src/game.rs::Game::entity_geometry orb billboards with the pulsing colour, in the entity pass
35 ExperienceOrbRenderer.vertex inline crates/horizon-client/src/game.rs::Game::entity_geometry vertex built inline
36 FallingBlockRenderer.shouldRender missing port crates/horizon-client/src/game.rs::falling_block_visible 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 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
37 FallingBlockRenderer.submit inline crates/horizon-client/src/game.rs::Game::entity_geometry falling block pass with its baked block model
38 FallingBlockRenderer.extractRenderState inline crates/horizon-client/src/game.rs::Game::entity_geometry position, state, biome tint and light read there
39 FireworkEntityRenderer.extractRenderState port crates/horizon-client/src/game/fireworks.rs::Game::rocket_geometry angled rocket keeps its direction
+1 -1
View File
@@ -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 -5
View File
@@ -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.
+6 -6
View File
@@ -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
1 UseBonemeal.checkExtraStartConditions inline crates/horizon-client/src/mobs/villager_brain.rs::B::try_start every 10 ticks and not within 160 of the last session, with bone meal in the inventory and a growing crop in the 3x3x3
71 DolphinJumpGoal.canContinueToUse port crates/horizon-client/src/mobs/dolphin.rs::Jump::can_continue
72 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
73 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
74 FollowFlockLeaderGoal port home crates/horizon-client/src/mobs/aquatic.rs::FollowFlockLeader crates/horizon-client/src/mobs/aquatic.rs tick for tick with vanilla in mob_ai cod/salmon/tropical_fish_school; recruits in vanilla getEntitiesOfClass order (Mob.section) FollowFlockLeader in that file, tick for tick with vanilla in mob_ai cod/salmon/tropical_fish_school; recruits in vanilla getEntitiesOfClass order (Mob.section)
75 FollowMobGoal port home crates/horizon-client/src/mobs/goals.rs::FollowMob crates/horizon-client/src/mobs/goals.rs the mutual-look branch is left out: only parrots have the goal FollowMob in that file; the mutual-look branch is left out: only parrots have the goal
76 FollowPlayerRiddenEntityGoal port home crates/horizon-client/src/mobs/dolphin.rs::FollowPlayerRidden crates/horizon-client/src/mobs/dolphin.rs only the player the mob sees (the nearest) is considered; tick for tick with vanilla in mob_ai dolphin_boat_* FollowPlayerRidden in that file; only the player the mob sees (the nearest) is considered; tick for tick with vanilla in mob_ai dolphin_boat_*
77 GoalSelector.removeAllGoals port crates/horizon-client/src/mobs/goals.rs::GoalSelector::clear gap: minor: clear() takes no predicate and does not stop running goals
78 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
79 GoalSelector.goalContainsAnyFlags inline crates/horizon-client/src/mobs/goals.rs::GoalSelector::tick flags & disabled
80 GoalSelector.goalCanBeReplacedForAllFlags inline crates/horizon-client/src/mobs/goals.rs::GoalSelector::tick the replaceable check over the locked slots
81 GoalSelector.tickRunningGoals port crates/horizon-client/src/mobs/goals.rs::GoalSelector::tick_running
82 GolemRandomStrollInVillageGoal inline home crates/horizon-client/src/mobs/golem.rs::register_iron crates/horizon-client/src/mobs/golem.rs a named Stroll with a gate and a position; tick for tick with vanilla in mob_ai iron_golem_village_* 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_*
83 MeleeAttackGoal.checkAndPerformAttack inline crates/horizon-client/src/mobs/goals.rs::MeleeAttack::tick reset cooldown, swing, do_hurt_target
84 MeleeAttackGoal.canPerformAttack inline crates/horizon-client/src/mobs/goals.rs::MeleeAttack::tick cooldown over, within melee range, line of sight
85 MoveBackToVillageGoal inline home crates/horizon-client/src/mobs/golem.rs::register_iron crates/horizon-client/src/mobs/golem.rs a named Stroll with a gate and a position; tick for tick with vanilla in mob_ai iron_golem_village_* 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_*
86 MoveThroughVillageGoal inline home crates/horizon-client/src/mobs/undead.rs::MoveThroughVillage crates/horizon-client/src/mobs/undead.rs zombies register it at priority 6 (the raider variant is raider.rs::MoveThroughVillage); no tick parity check yet 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
87 MoveToBlockGoal.moveMobToBlock inline crates/horizon-client/src/mobs/goals.rs::MoveToBlock::start nav_move_to the block centre one up
88 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
89 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
+1 -1
View File
@@ -4,7 +4,7 @@ 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
1 ItemFrame.defineSynchedData skip:other entity data defaults; ItemFrame starts with item None and rotation 0 in place_item_frame
4 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`)
5 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
6 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
7 ItemFrame.removeFramedMap inline port crates/horizon-client/src/game/maps.rs::Game::lan_map_updates crates/horizon-client/src/game/maps.rs::Game::remove_framed_map frame map markers come from item_frames each tick, so a removed frame needs no step 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`
8 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
9 ItemFrame.onSyncedDataUpdated skip:other network data-sync hook; the item is read from the frame field on demand
10 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
+5 -5
View File
@@ -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
1 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
2 AbstractBoat.defineSynchedData skip:java SynchedEntityData definitions; Horizon keeps paddle, hurt and input state on the Boat struct
3 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
4 AbstractBoat.onAboveBubbleColumn missing port crates/horizon-client/src/game/boats.rs::Game::boat_above_bubble_column 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 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
5 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
6 AbstractBoat.tickBubbleColumn missing port crates/horizon-client/src/game/boats.rs::Game::tick_boat_bubble_column 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 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`
7 AbstractBoat.handleEntityEvent missing port crates/horizon-client/src/game/remote_vehicles.rs::Game::remote_boat_event player: the bubble column effect event (71 or 72) never reaches a boat, same gap as the bubble column lift 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
8 AbstractBoat.handleBubbleColumnEffect missing port crates/horizon-client/src/game/boats.rs::Game::handle_bubble_column_effect player: a boat in an up or down bubble column keeps its speed; Horizon never changes its vertical velocity from a column an up column sets vertical velocity 0.6 and a down column adds -0.7, only while the window simulates the boat's movement
9 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
10 AbstractBoat.getLeashOffset port crates/horizon-client/src/mobs/leash.rs::Holder::boat_leash_offset 1.375 by 0.5625 for a boat or raft
11 AbstractBoat.getStatus port crates/horizon-client/src/game/boats.rs::Game::boat_status in air, in water, under water or on land
18 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
19 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
20 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
21 AbstractBoat.onPassengerTurned inline crates/horizon-client/src/game/boats.rs::Game::tick_riding crates/horizon-client/src/game/remote_vehicles.rs::Game::remote_ride the rider's turn is clamped each tick
22 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
23 AbstractBoat.checkFallDamage missing minor: boats keep no fall distance; boats take no fall damage either way
24 AbstractBoat.getPaddleState port crates/horizon-client/src/game/boats.rs::Boat::rowing_time paddle flag, set only while a rider drives
+1 -1
View File
@@ -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)
1 Enchantment.getSlotItems inline crates/horizon-world/src/loot.rs::LootData::enchantment_level slot filter and item lookup folded into the best-level loop
118 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
119 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
120 CollisionGetter.getBlockCollisionsFromContext port crates/horizon-client-render/src/collisions.rs::SceneCollisions::collision_block gap: minor: per-block shape; the box walk is not here
121 CollisionGetter.clipIncludingBorder missing port crates/horizon-client/src/projectiles.rs::clip_including_border unsure: no world-border clamp of a ray found in clip_ray 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
122 CollisionGetter.collidesWithSuffocatingBlock port crates/horizon-client-render/src/collisions.rs::SceneCollisions::is_suffocating
123 EntityGetter.getEntitiesOfClass skip:other class filter repeated at each caller over the entity lists; no shared lookup
124 EntityGetter.getPlayerByUUID skip:other uuid find repeated at each caller (e.g. allays.rs)
+1 -1
View File
@@ -126,7 +126,7 @@ BannerBlockEntity.applyImplicitComponents port crates/horizon-client/src/block_e
BannerBlockEntity.collectImplicitComponents port crates/horizon-client/src/block_entity_item.rs::collect banner patterns and name to item components
BaseContainerBlockEntity.loadAdditional port crates/horizon-client-core/src/block_entity_nbt.rs::items container slots from the saved list
BaseContainerBlockEntity.saveAdditional port crates/horizon-client-core/src/block_entity_nbt.rs::canonical container NBT in its saved form
BaseContainerBlockEntity.sendChestLockedNotifications missing minor: a chest with a Lock tag should refuse to open with a message. Horizon has no Lock check; chest_menu_blocked (game/storage.rs) only looks at the block above.
BaseContainerBlockEntity.sendChestLockedNotifications port crates/horizon-client-render/src/containers.rs::locked_message the container.isLocked action-bar text, naming the container by its display name; the menu path in game/lan_actions.rs refuses a container whose Lock the held item does not open, sends this and plays block.chest.locked
BaseContainerBlockEntity.isEmpty port crates/horizon-client/src/game/machines.rs::Containers::is_empty
BaseContainerBlockEntity.removeItem port crates/horizon-client/src/game/machines.rs::Game::take_one gap: minor: takes one item, not a chosen count.
BaseContainerBlockEntity.createMenu port crates/horizon-client/src/game.rs::Game::open_menu opens the container's screen
1 SpeleothemBlock.createBlockStateDefinition skip:data block state definition is data (blocks table in horizon-items)
126 BannerBlockEntity.collectImplicitComponents port crates/horizon-client/src/block_entity_item.rs::collect banner patterns and name to item components
127 BaseContainerBlockEntity.loadAdditional port crates/horizon-client-core/src/block_entity_nbt.rs::items container slots from the saved list
128 BaseContainerBlockEntity.saveAdditional port crates/horizon-client-core/src/block_entity_nbt.rs::canonical container NBT in its saved form
129 BaseContainerBlockEntity.sendChestLockedNotifications missing port crates/horizon-client-render/src/containers.rs::locked_message minor: a chest with a Lock tag should refuse to open with a message. Horizon has no Lock check; chest_menu_blocked (game/storage.rs) only looks at the block above. the container.isLocked action-bar text, naming the container by its display name; the menu path in game/lan_actions.rs refuses a container whose Lock the held item does not open, sends this and plays block.chest.locked
130 BaseContainerBlockEntity.isEmpty port crates/horizon-client/src/game/machines.rs::Containers::is_empty
131 BaseContainerBlockEntity.removeItem port crates/horizon-client/src/game/machines.rs::Game::take_one gap: minor: takes one item, not a chosen count.
132 BaseContainerBlockEntity.createMenu port crates/horizon-client/src/game.rs::Game::open_menu opens the container's screen
+6 -6
View File
@@ -19,7 +19,7 @@ MobEffectsPredicate.matches inline crates/horizon-client/src/advancements.rs::en
MobEffectsPredicate.Builder.and skip:java builder for effect predicates in code
MobEffectsPredicate.MobEffectInstancePredicate.matches inline crates/horizon-client/src/advancements.rs::entity gap: minor: ambient and visible flags are not checked; amplifier and duration are
NbtPredicate.matches missing minor: custom_data and entity NBT predicates are not read; no vanilla advancement uses them
SingleComponentItemPredicate.matches inline crates/horizon-client/src/advancements.rs::item gap: minor: only enchantments, damage and potion contents are read; other components are ignored
SingleComponentItemPredicate.matches inline crates/horizon-client/src/advancements.rs::item gap: minor: enchantments, damage and potion contents are read from components and predicates; adventure/play_jukebox_in_meadows asks for minecraft:jukebox_playable, which is ignored, so any tool passes that match_tool test
SlotsPredicate.matches missing minor: slot-range item test is not read; no vanilla advancement uses it
SlotsPredicate.matchSlots missing minor: helper of SlotsPredicate.matches; not read
StatePropertiesPredicate.matches port crates/horizon-client/src/advancements.rs::block the state property test of block()
@@ -32,8 +32,8 @@ StatePropertiesPredicate.RangedMatcher.match inline crates/horizon-client/src/ad
TagPredicate.codec skip:java codec
TagPredicate.is skip:java builder
TagPredicate.isNot skip:java builder
TagPredicate.matches inline crates/horizon-client/src/advancements.rs::id_matches gap: minor: the #tag form is read; the negated expected=false form is not kept
CubeMobPredicate.matches missing minor: type_specific/cube_mob (slime size) is not read; no vanilla advancement uses it
TagPredicate.matches inline crates/horizon-client/src/advancements.rs::damage_source each of the damage type's tags compared against the predicate's expected flag; DamageSourcePredicate.matches is the only caller (adventure/blowback asks for #minecraft:is_projectile with expected true)
CubeMobPredicate.matches inline crates/horizon-world/src/loot.rs::Context::entity_matches the loot reader's minecraft:type_specific/cube_mob arm, the size bounds against the mob's own size; entity() in advancements.rs has no arm for it and answers false, so an advancement asking for one never matches
DistanceToPlayerPredicate.matches port crates/horizon-client/src/advancements.rs::distance the distance key of entity(): from the player to the entity
EntityEquipmentPredicate.captainPredicate skip:data datagen builder for the raid captain predicate (VanillaAdventureAdvancements)
EntityEquipmentPredicate.matches inline crates/horizon-client/src/advancements.rs::entity the equipment key: the seven slots
@@ -55,7 +55,7 @@ EntityTagPredicate.containsAllOf missing minor: helper of entity_tags; not read
EntityTypePredicate.of skip:java builder from an entity type or tag
EntityTypePredicate.matches port crates/horizon-client/src/advancements.rs::id_matches the type key of entity(): an id or #tag
FishingHookPredicate.matches inline crates/horizon-client/src/advancements.rs::entity the type_specific/fishing_hook arm: open-water test
LightningBoltPredicate.matches inline crates/horizon-client/src/advancements.rs::entity gap: minor: blocks_set_on_fire is checked against 0, not the bolt's own count; the lightning trigger never fires
LightningBoltPredicate.matches inline crates/horizon-client/src/advancements.rs::entity the minecraft:type_specific/lightning arm: blocks_set_on_fire against the bolt's own count (game/lightning.rs fills it from the fires the bolt set) and entity_struck against the mobs around it
MovementAffectedByPredicate.matches missing minor: movement_affected_by key is not read; no vanilla advancement uses it
MovementPredicate.matches missing minor: movement key (speed, fall distance) is not read; no vanilla advancement uses it
PassengerPredicate.matches inline crates/horizon-client/src/advancements.rs::entity the `passenger` key: any passenger of the vehicle matches the nested predicate
@@ -66,8 +66,8 @@ PlayerPredicate.Builder.checkAdvancementDone skip:java builder
PlayerPredicate.Builder.checkAdvancementCriterions skip:java builder
PlayerPredicate.Builder.build skip:java builder
PlayerPredicate.StatMatcher.createTypedCodec skip:java codec
RaiderPredicate.matches missing minor: type_specific/raider is not read in entity(); no vanilla advancement uses it
SheepPredicate.matches missing minor: type_specific/sheep is not read; no vanilla advancement uses it
RaiderPredicate.matches inline crates/horizon-world/src/loot.rs::Context::entity_matches the loot reader's minecraft:type_specific/raider arm: captain matches, and has_raid matches no raid running here; the advancement reader answers false for the key
SheepPredicate.matches inline crates/horizon-world/src/loot.rs::Context::entity_matches the loot reader's minecraft:type_specific/sheep arm, the sheared flag; the advancement reader answers false for the key
SteppingOnPredicate.matches inline crates/horizon-client/src/advancements.rs::entity gap: minor: no on-ground test; the block is read 0.5 below the feet
TargetedEntityPredicate.matches missing minor: targeted_entity key is not read; no vanilla advancement uses it
TeamPredicate.matches missing minor: team key is not read though scoreboard teams exist; no vanilla advancement uses it
1 InputPredicate.matches inline crates/horizon-client/src/advancements.rs::input gap: only the sneak input is read; forward, back, jump and sprint have no source here yet and count as unconstrained
19 MobEffectsPredicate.Builder.and skip:java builder for effect predicates in code
20 MobEffectsPredicate.MobEffectInstancePredicate.matches inline crates/horizon-client/src/advancements.rs::entity gap: minor: ambient and visible flags are not checked; amplifier and duration are
21 NbtPredicate.matches missing minor: custom_data and entity NBT predicates are not read; no vanilla advancement uses them
22 SingleComponentItemPredicate.matches inline crates/horizon-client/src/advancements.rs::item gap: minor: only enchantments, damage and potion contents are read; other components are ignored gap: minor: enchantments, damage and potion contents are read from components and predicates; adventure/play_jukebox_in_meadows asks for minecraft:jukebox_playable, which is ignored, so any tool passes that match_tool test
23 SlotsPredicate.matches missing minor: slot-range item test is not read; no vanilla advancement uses it
24 SlotsPredicate.matchSlots missing minor: helper of SlotsPredicate.matches; not read
25 StatePropertiesPredicate.matches port crates/horizon-client/src/advancements.rs::block the state property test of block()
32 TagPredicate.codec skip:java codec
33 TagPredicate.is skip:java builder
34 TagPredicate.isNot skip:java builder
35 TagPredicate.matches inline crates/horizon-client/src/advancements.rs::id_matches crates/horizon-client/src/advancements.rs::damage_source gap: minor: the #tag form is read; the negated expected=false form is not kept each of the damage type's tags compared against the predicate's expected flag; DamageSourcePredicate.matches is the only caller (adventure/blowback asks for #minecraft:is_projectile with expected true)
36 CubeMobPredicate.matches missing inline crates/horizon-world/src/loot.rs::Context::entity_matches minor: type_specific/cube_mob (slime size) is not read; no vanilla advancement uses it the loot reader's minecraft:type_specific/cube_mob arm, the size bounds against the mob's own size; entity() in advancements.rs has no arm for it and answers false, so an advancement asking for one never matches
37 DistanceToPlayerPredicate.matches port crates/horizon-client/src/advancements.rs::distance the distance key of entity(): from the player to the entity
38 EntityEquipmentPredicate.captainPredicate skip:data datagen builder for the raid captain predicate (VanillaAdventureAdvancements)
39 EntityEquipmentPredicate.matches inline crates/horizon-client/src/advancements.rs::entity the equipment key: the seven slots
55 EntityTypePredicate.of skip:java builder from an entity type or tag
56 EntityTypePredicate.matches port crates/horizon-client/src/advancements.rs::id_matches the type key of entity(): an id or #tag
57 FishingHookPredicate.matches inline crates/horizon-client/src/advancements.rs::entity the type_specific/fishing_hook arm: open-water test
58 LightningBoltPredicate.matches inline crates/horizon-client/src/advancements.rs::entity gap: minor: blocks_set_on_fire is checked against 0, not the bolt's own count; the lightning trigger never fires the minecraft:type_specific/lightning arm: blocks_set_on_fire against the bolt's own count (game/lightning.rs fills it from the fires the bolt set) and entity_struck against the mobs around it
59 MovementAffectedByPredicate.matches missing minor: movement_affected_by key is not read; no vanilla advancement uses it
60 MovementPredicate.matches missing minor: movement key (speed, fall distance) is not read; no vanilla advancement uses it
61 PassengerPredicate.matches inline crates/horizon-client/src/advancements.rs::entity the `passenger` key: any passenger of the vehicle matches the nested predicate
66 PlayerPredicate.Builder.checkAdvancementCriterions skip:java builder
67 PlayerPredicate.Builder.build skip:java builder
68 PlayerPredicate.StatMatcher.createTypedCodec skip:java codec
69 RaiderPredicate.matches missing inline crates/horizon-world/src/loot.rs::Context::entity_matches minor: type_specific/raider is not read in entity(); no vanilla advancement uses it the loot reader's minecraft:type_specific/raider arm: captain matches, and has_raid matches no raid running here; the advancement reader answers false for the key
70 SheepPredicate.matches missing inline crates/horizon-world/src/loot.rs::Context::entity_matches minor: type_specific/sheep is not read; no vanilla advancement uses it the loot reader's minecraft:type_specific/sheep arm, the sheared flag; the advancement reader answers false for the key
71 SteppingOnPredicate.matches inline crates/horizon-client/src/advancements.rs::entity gap: minor: no on-ground test; the block is read 0.5 below the feet
72 TargetedEntityPredicate.matches missing minor: targeted_entity key is not read; no vanilla advancement uses it
73 TeamPredicate.matches missing minor: team key is not read though scoreboard teams exist; no vanilla advancement uses it
+2 -2
View File
@@ -23,7 +23,7 @@ RegistrySetBuilder.BuildState.throwOnError skip:data bootstrap and datagen regi
RegistrySetBuilder.BuildState.createBootstrap skip:data bootstrap and datagen registry-set building; Horizon reads registry data from tables
RegistrySynchronization.packRegistries inline crates/horizon-client/src/lan/data.rs::SyncData::registry_packets gap: minor: packs every registry SyncData holds; the synchronized-registry list is not checked here
RegistrySynchronization.packRegistry port crates/horizon-client/src/lan/data.rs::SyncData::registry_packets gap: minor: known_pack leaves out contents for every entry; vanilla checks each entry's known pack
RegistrySynchronization.networkSafeRegistries inline crates/horizon-client/src/lan/data.rs::SyncData::registry_packets gap: minor: unsure which registries count as networkable; decided when SyncData is built
RegistrySynchronization.networkSafeRegistries inline crates/horizon-client/src/lan/data.rs::SyncData::registry_packets the networkable set is `SYNCHRONIZED` (lan/data.rs:18: 32 registries, `RegistryDataLoader.SYNCHRONIZED_REGISTRIES` in vanilla's order) and registry_packets packs every one of them
SectionPos.offset inline crates/horizon-client-render/src/light.rs::Region::slot gap: minor: the light engine reaches neighbour sections by block coordinate; the SectionTracker walk is not checked
SectionPos.sectionRelativePos port crates/horizon-core/src/position.rs::BlockPos::section_relative
SectionPos.relativeToBlockPos port crates/horizon-core/src/position.rs::SectionPos::relative_to_block
@@ -108,7 +108,7 @@ PatchedDataComponentMap.size skip:java generic component map; Horizon sets type
PatchedDataComponentMap.asPatch skip:java generic component map; Horizon sets typed StackData fields and ItemStack::tidy drops default-equal data
PatchedDataComponentMap.copy skip:java generic component map; Horizon sets typed StackData fields and ItemStack::tidy drops default-equal data
PatchedDataComponentMap.equals skip:java generic component map; Horizon sets typed StackData fields and ItemStack::tidy drops default-equal data
TypedDataComponent.encodeValue skip:java codec
TypedDataComponent.encodeValue port crates/horizon-protocol/src/item/hash.rs::Component::code the component's own codec form, hashed; one arm per component kind, with the kinds that have no persistent codec (map_post_processing, creative_slot_lock) as the None
AttributeModifiersPredicate.matches missing `advancements.rs::item` reads `items`, `count`, the `potion_contents` and `damage` components and the `enchantments` and `damage` predicates; a `minecraft:attribute_modifiers` predicate is ignored, so the criterion matches too broadly
AttributeModifiersPredicate.EntryPredicate.test missing same as `matches`: no entry of an `attribute_modifiers` predicate is tested
BundlePredicate.matches missing ignored by `advancements.rs::item`, so a bundle contents criterion matches any bundle
1 Registry.byNameCodec skip:java codec over the registry by name
23 RegistrySetBuilder.BuildState.createBootstrap skip:data bootstrap and datagen registry-set building; Horizon reads registry data from tables
24 RegistrySynchronization.packRegistries inline crates/horizon-client/src/lan/data.rs::SyncData::registry_packets gap: minor: packs every registry SyncData holds; the synchronized-registry list is not checked here
25 RegistrySynchronization.packRegistry port crates/horizon-client/src/lan/data.rs::SyncData::registry_packets gap: minor: known_pack leaves out contents for every entry; vanilla checks each entry's known pack
26 RegistrySynchronization.networkSafeRegistries inline crates/horizon-client/src/lan/data.rs::SyncData::registry_packets gap: minor: unsure which registries count as networkable; decided when SyncData is built the networkable set is `SYNCHRONIZED` (lan/data.rs:18: 32 registries, `RegistryDataLoader.SYNCHRONIZED_REGISTRIES` in vanilla's order) and registry_packets packs every one of them
27 SectionPos.offset inline crates/horizon-client-render/src/light.rs::Region::slot gap: minor: the light engine reaches neighbour sections by block coordinate; the SectionTracker walk is not checked
28 SectionPos.sectionRelativePos port crates/horizon-core/src/position.rs::BlockPos::section_relative
29 SectionPos.relativeToBlockPos port crates/horizon-core/src/position.rs::SectionPos::relative_to_block
108 PatchedDataComponentMap.asPatch skip:java generic component map; Horizon sets typed StackData fields and ItemStack::tidy drops default-equal data
109 PatchedDataComponentMap.copy skip:java generic component map; Horizon sets typed StackData fields and ItemStack::tidy drops default-equal data
110 PatchedDataComponentMap.equals skip:java generic component map; Horizon sets typed StackData fields and ItemStack::tidy drops default-equal data
111 TypedDataComponent.encodeValue skip:java port crates/horizon-protocol/src/item/hash.rs::Component::code codec the component's own codec form, hashed; one arm per component kind, with the kinds that have no persistent codec (map_post_processing, creative_slot_lock) as the None
112 AttributeModifiersPredicate.matches missing `advancements.rs::item` reads `items`, `count`, the `potion_contents` and `damage` components and the `enchantments` and `damage` predicates; a `minecraft:attribute_modifiers` predicate is ignored, so the criterion matches too broadly
113 AttributeModifiersPredicate.EntryPredicate.test missing same as `matches`: no entry of an `attribute_modifiers` predicate is tested
114 BundlePredicate.matches missing ignored by `advancements.rs::item`, so a bundle contents criterion matches any bundle
+6 -6
View File
@@ -93,16 +93,16 @@ ClientboundTakeItemEntityPacket.write port crates/horizon-protocol/src/buf.rs::w
DebugEntityNameGenerator.getEntityName skip:debug debug name for entities
ServerboundBlockEntityTagQueryPacket.write port crates/horizon-protocol/src/buf.rs::write packet! BlockEntityTagQuery {transaction VarInt, pos Pos}
ServerboundEntityTagQueryPacket.write port crates/horizon-protocol/src/buf.rs::write packet! EntityTagQuery {transaction VarInt, entity VarInt}
ServerboundJigsawGeneratePacket.write missing minor: no jigsaw generate packet; the client has no jigsaw screen (only block_entity_nbt reads jigsaw data)
ServerboundJigsawGeneratePacket.write port crates/horizon-protocol/src/buf.rs::write packet! JigsawGenerate {pos Pos, levels VarInt, keep_jigsaws Bool}; the jigsaw screen's Generate button builds it (jigsaw_edit.rs::JigsawEditor::generate_packet)
ServerboundMovePlayerPacket.packFlags inline crates/horizon-client/src/player_sync.rs::PositionSender::update on-ground and horizontal-collision bits set when the move is sent
ServerboundPlayerActionPacket.write port crates/horizon-protocol/src/buf.rs::write packet! PlayerAction {action VarInt, pos, direction UByte, sequence VarInt}
ServerboundPlayerCommandPacket.write port crates/horizon-protocol/src/buf.rs::write packet! PlayerCommand {id, action, data VarInt}
ServerboundSeenAdvancementsPacket.write missing minor: the advancement tab the player opened is not sent; Horizon has no seen_advancements packet (ids.rs only)
ServerboundSetCommandMinecartPacket.write missing minor: minecart command block edits are not sent; Horizon has no such packet (ids.rs only)
ServerboundSetCommandMinecartPacket.getCommandBlock missing minor: minecart command block edits are not sent; Horizon has no such packet (ids.rs only)
ServerboundSetJigsawBlockPacket.write missing minor: jigsaw block edits are not sent; no packet and no jigsaw screen in the client
ServerboundSetStructureBlockPacket.<init> missing minor: structure block settings are not sent; no packet and no structure block screen in the client
ServerboundSetStructureBlockPacket.write missing minor: structure block settings are not sent; no packet and no structure block screen in the client
ServerboundSetCommandMinecartPacket.write port crates/horizon-protocol/src/buf.rs::write packet! SetCommandMinecart {entity VarInt, command Str, track_output Bool}; the cart's edit screen sends it (command_minecarts.rs::Game::set_command_minecart)
ServerboundSetCommandMinecartPacket.getCommandBlock inline crates/horizon-client/src/game/command_minecarts.rs::Game::apply_cart_command the packet's command and track-output flag go into the cart, with handleSetCommandMinecart's onUpdated and chat note, in one fn
ServerboundSetJigsawBlockPacket.write port crates/horizon-protocol/src/buf.rs::write packet! SetJigsawBlock {pos Pos, name/target/pool Ident, final_state Str, joint Str, selection and placement priority VarInt}; the jigsaw screen sends it (jigsaw_edit.rs::JigsawEditor::set_packet)
ServerboundSetStructureBlockPacket.<init> port crates/horizon-client/src/game/structure_edit.rs::StructureEditor::packet the constructor takes the block and the update type: the edit boxes supply the typed fields, the block its mode, mirror, rotation and flags
ServerboundSetStructureBlockPacket.write port crates/horizon-protocol/src/packets/play.rs::SetStructureBlock::write pos, update type and mode, the name, each offset and size as a byte, mirror and rotation, data, integrity, seed, then the four flags in one byte
VecDelta.Stepped.hasDeltaX skip:other no callers in vanilla
VecDelta.Stepped.hasDeltaZ skip:other no callers in vanilla
VecDelta.Stepped.tryEncode missing minor: the LAN host sends only linear deltas (send_changes); no stepped path is encoded
1 IdDispatchCodec.decode skip:java codec dispatch; packet_enum! matches the id in packets/mod.rs
93 DebugEntityNameGenerator.getEntityName skip:debug debug name for entities
94 ServerboundBlockEntityTagQueryPacket.write port crates/horizon-protocol/src/buf.rs::write packet! BlockEntityTagQuery {transaction VarInt, pos Pos}
95 ServerboundEntityTagQueryPacket.write port crates/horizon-protocol/src/buf.rs::write packet! EntityTagQuery {transaction VarInt, entity VarInt}
96 ServerboundJigsawGeneratePacket.write missing port crates/horizon-protocol/src/buf.rs::write minor: no jigsaw generate packet; the client has no jigsaw screen (only block_entity_nbt reads jigsaw data) packet! JigsawGenerate {pos Pos, levels VarInt, keep_jigsaws Bool}; the jigsaw screen's Generate button builds it (jigsaw_edit.rs::JigsawEditor::generate_packet)
97 ServerboundMovePlayerPacket.packFlags inline crates/horizon-client/src/player_sync.rs::PositionSender::update on-ground and horizontal-collision bits set when the move is sent
98 ServerboundPlayerActionPacket.write port crates/horizon-protocol/src/buf.rs::write packet! PlayerAction {action VarInt, pos, direction UByte, sequence VarInt}
99 ServerboundPlayerCommandPacket.write port crates/horizon-protocol/src/buf.rs::write packet! PlayerCommand {id, action, data VarInt}
100 ServerboundSeenAdvancementsPacket.write missing minor: the advancement tab the player opened is not sent; Horizon has no seen_advancements packet (ids.rs only)
101 ServerboundSetCommandMinecartPacket.write missing port crates/horizon-protocol/src/buf.rs::write minor: minecart command block edits are not sent; Horizon has no such packet (ids.rs only) packet! SetCommandMinecart {entity VarInt, command Str, track_output Bool}; the cart's edit screen sends it (command_minecarts.rs::Game::set_command_minecart)
102 ServerboundSetCommandMinecartPacket.getCommandBlock missing inline crates/horizon-client/src/game/command_minecarts.rs::Game::apply_cart_command minor: minecart command block edits are not sent; Horizon has no such packet (ids.rs only) the packet's command and track-output flag go into the cart, with handleSetCommandMinecart's onUpdated and chat note, in one fn
103 ServerboundSetJigsawBlockPacket.write missing port crates/horizon-protocol/src/buf.rs::write minor: jigsaw block edits are not sent; no packet and no jigsaw screen in the client packet! SetJigsawBlock {pos Pos, name/target/pool Ident, final_state Str, joint Str, selection and placement priority VarInt}; the jigsaw screen sends it (jigsaw_edit.rs::JigsawEditor::set_packet)
104 ServerboundSetStructureBlockPacket.<init> missing port crates/horizon-client/src/game/structure_edit.rs::StructureEditor::packet minor: structure block settings are not sent; no packet and no structure block screen in the client the constructor takes the block and the update type: the edit boxes supply the typed fields, the block its mode, mirror, rotation and flags
105 ServerboundSetStructureBlockPacket.write missing port crates/horizon-protocol/src/packets/play.rs::SetStructureBlock::write minor: structure block settings are not sent; no packet and no structure block screen in the client pos, update type and mode, the name, each offset and size as a byte, mirror and rotation, data, integrity, seed, then the four flags in one byte
106 VecDelta.Stepped.hasDeltaX skip:other no callers in vanilla
107 VecDelta.Stepped.hasDeltaZ skip:other no callers in vanilla
108 VecDelta.Stepped.tryEncode missing minor: the LAN host sends only linear deltas (send_changes); no stepped path is encoded
+9 -9
View File
@@ -10,8 +10,8 @@ Identifier.tryBySeparator port crates/horizon-world/src/identifier.rs::Identifie
Identifier.withPath port crates/horizon-world/src/identifier.rs::Identifier::from_parts no helper; callers rebuild the id with from_parts(namespace, path)
Identifier.equals skip:java derived PartialEq on namespace and path
Identifier.compareTo port crates/horizon-world/src/identifier.rs::Identifier::cmp path first, then namespace
Identifier.resolveAgainst missing unsure: no identifier-to-file-path guard found; callers join paths directly
Identifier.toLanguageKey skip:other string helper; translation keys are built where a Text is made
Identifier.resolveAgainst missing vanilla resolves namespace/path under a root and throws when the joined path normalises outside it; the resource and pack readers join the two onto their folder with no such check
Identifier.toLanguageKey inline crates/horizon-client/src/title/buffet.rs::display_name spelled out behind a 'biome.' prefix there (toLanguageKey("biome")); the namespace and path are joined by hand, and '/' is turned into '.' as well
Identifier.toShortLanguageKey skip:other string helper; translation keys are built where a Text is made
Identifier.toShortString skip:other string helper; the minecraft: prefix is stripped at each display site
Identifier.readGreedy port crates/horizon-command/src/args.rs::read_identifier reads id characters while they are allowed
@@ -44,7 +44,7 @@ RegistryOps.HolderLookupAdapter.lookup skip:data registry lookup adapter for da
RegistryOps.HolderLookupAdapter.equals skip:data registry lookup adapter for data decoding
ResourceKey.create skip:java interned key; Horizon uses plain identifiers
ResourceKey.dependent skip:data derived registry key; the ids come from Horizon's tables
ResourceManagerRegistryLoadTask.load skip:data registry loading from the resource manager
ResourceManagerRegistryLoadTask.load inline crates/horizon-client/src/lan/data.rs::file_order the registry's files are read from the prepared folder (walk) with a world's pack files over them (pack_entries), registered in the file order listMatchingResources keys them by (file_order); the element codec read is a serde_json parse
Bootstrap.bootStrap skip:data startup bootstrap of the registries and data
Bootstrap.checkTranslations skip:debug IDE-only translation check
Bootstrap.checkGameruleTranslations skip:debug IDE-only game rule translation check
@@ -90,9 +90,9 @@ MinecraftServer.sendDifficultyUpdate inline crates/horizon-client/src/lan/hub.rs
MinecraftServer.enforceGameTypeForPlayers inline crates/horizon-client/src/game/lan.rs::enforce_guest_game_mode applies the default mode to online players when force game mode is enabled
MinecraftServer.executeIfPossible skip:java rejects tasks during shutdown
MinecraftServer.reloadResources port crates/horizon-client/src/game/functions.rs::Game::cmd_reload gap: minor: data packs only, via reload_datapacks
MinecraftServer.configurePackRepository port crates/horizon-client/src/game/functions.rs::Game::reload_datapacks gap: minor: picks the enabled packs and adds new ones; no feature-flag check and no safe mode
MinecraftServer.configureRepositoryWithSelection inline crates/horizon-client/src/game/functions.rs::Game::reload_datapacks gap: minor: sets the enabled list only; no forced features
MinecraftServer.enableForcedFeaturePacks missing minor: Horizon's datapacks have no feature flags; a pack for a required feature is never forced on
MinecraftServer.configurePackRepository port crates/horizon-client/src/game/functions.rs::Game::reload_datapacks gap: minor: the enabled packs are picked and new ones added through datapack::enabled_ids, feature flags and all; no safe mode
MinecraftServer.configureRepositoryWithSelection inline crates/horizon-client/src/game/functions.rs::Game::reload_datapacks gap: minor: sets the enabled list only; the forced feature packs come from datapack::enabled_ids
MinecraftServer.enableForcedFeaturePacks inline crates/horizon-client-render/src/datapack.rs::enabled_ids the last loop forces on every feature pack whose flags the world enables, whatever the world's selection says; a pack's flags are its pack.mcmeta features.enabled
MinecraftServer.getSelectedPacks port crates/horizon-client/src/game/functions.rs::Game::save_datapack_settings gap: minor: writes the enabled and disabled lists; no feature check
MinecraftServer.findRespawnDimension inline crates/horizon-client/src/game/server_travel.rs::Game::world_spawn_trip gap: minor: always the Overworld; /setworldspawn keeps no dimension
MinecraftServer.setRespawnData port crates/horizon-client/src/game/commands.rs::Game::cmd_setworldspawn gap: minor: the new spawn is not sent to guests here; they get it on join
@@ -113,8 +113,8 @@ MinecraftServer.stopTimeProfiler skip:debug debug profiler stop
MinecraftServer.setAutoSave port crates/horizon-client/src/game/dedicated.rs::set_auto_save changes the shared server-level save flag; the tick skips periodic saves while it is disabled
MinecraftServer.isAutoSave port crates/horizon-client/src/game/dedicated.rs::auto_save reads the shared server-level save flag
MinecraftServer.storeChunkIoError skip:debug writes a chunk IO crash report
MinecraftServer.reportChunkLoadFailure missing unsure: no chunk-load failure log or report found in save.rs or streaming.rs
MinecraftServer.reportChunkSaveFailure port crates/horizon-client/src/save.rs::Persistent::save_finished gap: minor: logs only; no debug report and no low-disk warning
MinecraftServer.reportChunkLoadFailure port crates/horizon-client/src/chunk_io.rs::report_load_failure logs, warns on low disk space and records the failed chunk for the toast; gap: no suppressed-exception store and no debug crash report
MinecraftServer.reportChunkSaveFailure port crates/horizon-client/src/chunk_io.rs::report_save_failure logs, warns on low disk space and records the failed chunk; gap: no suppressed-exception store and no debug crash report
PlayerAdvancements.reload port crates/horizon-client/src/lan/advancements.rs::ServerAdvancements::reload on /reload every guest loaded so far has its visible set cleared and its progress re-read against the new tree, then the tick advancements are granted and the reward recipes added; gap: the host's own progress is not reloaded
PlayerAdvancements.registerListeners skip:other no per-criterion listener map; trigger() tests every criterion when an event fires
PlayerAdvancements.checkForAutomaticTriggers missing minor: criteria-less advancements from data packs are not granted when a player loads
@@ -122,7 +122,7 @@ PlayerAdvancements.asData inline crates/horizon-client/src/game/advancements.rs:
PlayerAdvancements.revoke port crates/horizon-client/src/game/advancements.rs::Game::revoke_advancement revokes one criterion or all of them
PlayerAdvancements.markForVisibilityUpdate inline crates/horizon-client/src/lan/advancements.rs::ServerAdvancements::update visibility is recomputed for the whole tree on each update
PlayerAdvancements.unregisterListeners skip:other no per-criterion listener map; trigger() tests every criterion when an event fires
PlayerAdvancements.setSelectedTab missing unsure: the server keeps no selected advancement tab and sends no select-tab packet (only the SELECT_ADVANCEMENTS_TAB id exists)
PlayerAdvancements.setSelectedTab missing the host keeps no selected advancement tab, and vanilla's only effect here is the ClientboundSelectAdvancementsTabPacket it sends when the tab changes; only the SELECT_ADVANCEMENTS_TAB id exists
PlayerAdvancements.getOrStartProgress skip:java map get-or-create
PlayerAdvancements.startProgress skip:java initialises progress for an advancement
PlayerAdvancements.updateTreeVisibility port crates/horizon-client/src/advancements.rs::Tree::visible gap: minor: returns the visible flags; the added and removed lists are built in lan/advancements.rs
Can't render this file because it contains an unexpected character in line 14 and column 147.
+8
View File
@@ -5,11 +5,13 @@ ExperienceCommand.Type.queryResponse inline crates/horizon-client/src/game/comma
FetchProfileCommand.resolveName port crates/horizon-client/src/game/commands/extra.rs::Game::cmd_fetchprofile the name lookup is queued on the profile resolver and answered on a later tick with commands.fetchprofile.name.success or .failure
FetchProfileCommand.resolveId port crates/horizon-client/src/game/commands/extra.rs::Game::cmd_fetchprofile the id is parsed with the selector's UUID reader and queued the same way, answered with commands.fetchprofile.id.success or .failure
FetchProfileCommand.printForEntity port crates/horizon-client/src/game/commands/extra.rs::Game::cmd_fetchprofile entity branch: player profile links; other entities get no_profile error
FetchProfileCommand.reportResolvedProfile port crates/horizon-client/src/game/commands/extra.rs::Game::report_resolved_profile the profile as ResolvableProfile.CODEC encodes it, four clickable actions and the head object, sent successfully without broadcasting
FillBiomeCommand.register port crates/horizon-command/src/tree_world.rs::fillbiome syntax of /fillbiome incl. replace filter
FillBiomeCommand.quantize inline crates/horizon-client/src/game/commands/terrain_cmds.rs::Game::cmd_fillbiome quart snap (v >> 2) << 2 done in cmd_fillbiome
FillBiomeCommand.makeResolver inline crates/horizon-client/src/game/commands/terrain_cmds.rs::Game::cmd_fillbiome biome cells inside the box set in cmd_fillbiome, filter honoured
FillCommand.register port crates/horizon-command/src/vanilla.rs::registered fill literal with replace/keep/filter/mode branches
FillCommand.fillBlocks port crates/horizon-client/src/game/commands.rs::Game::cmd_fill fill with area limit, hollow/outline/destroy/strict/keep modes
FillCommand.wrapWithMode port crates/horizon-command/src/vanilla.rs::fill_modes the mode literals outline, hollow, destroy and strict laid over the fill node
ForceLoadCommand.register port crates/horizon-command/src/tree_world.rs::forceload syntax of /forceload add|remove|query
ForceLoadCommand.queryForceLoad port crates/horizon-client/src/game/commands/terrain_cmds.rs::Game::cmd_forceload query branch of cmd_forceload
ForceLoadCommand.removeAll port crates/horizon-client/src/game/commands/terrain_cmds.rs::Game::cmd_forceload remove.all branch of cmd_forceload
@@ -38,6 +40,7 @@ ListPlayersCommand.format inline crates/horizon-client/src/game/commands.rs::Gam
LocateCommand.getHolders port crates/horizon-client/src/game/commands.rs::Game::structure_tag gap: minor: resolves a #tag to its structures; an unknown id is not validated here
LocateCommand.showLocateResult inline crates/horizon-client/src/game/commands.rs::Game::cmd_locate distance (2D for structures, 3D for biomes) and the success message
LocateCommand.dist inline crates/horizon-client/src/game/commands.rs::Game::cmd_locate floored horizontal distance computed in cmd_locate
LocateCommand.locatePoi port crates/horizon-client/src/game/commands.rs::Game::cmd_locate_poi PoiManager.findClosestWithType within 256 blocks of the source, over the loaded chunks
LookAt.LookAtEntity.perform port crates/horizon-client/src/game/commands/world_cmds.rs::Game::cmd_rotate facing-entity branch of cmd_rotate; look_at does the yaw and pitch math
LootCommand.register port crates/horizon-command/src/tree_world.rs::loot loot literal with loot/mine/kill/fish/replace/insert/give/spawn
LootCommand.addTargets port crates/horizon-command/src/tree_world.rs::loot replace entity|block, insert, give, spawn subcommands
@@ -58,6 +61,7 @@ LootCommand.drop inline crates/horizon-client/src/game/commands/terrain_cmds.rs:
LootContextSources.ContextDecorator.createParams port crates/horizon-client/src/game/commands/compute.rs::Game::compute_context gap: minor: keeps only the block and entity picks; no origin or this-entity loot params
LootContextSources.ContextDecorator.createContext inline crates/horizon-client/src/game/commands/compute.rs::Game::compute_context gap: minor: no LootContext is built
MsgCommand.register port crates/horizon-command/src/vanilla.rs::registered msg literal with tell and w aliases
MsgCommand.sendMessage port crates/horizon-client/src/lan/hub/chat.rs::Hub::whisper the outgoing line for the sender and the incoming one for each target
OpCommand.register inline crates/horizon-client/src/game/commands.rs::Game::execute_command "op" reaches the op arm of cmd_profiles on a dedicated server: the entry joins the op list, the lists are synced and commands.op.success is said
OpCommand.opPlayers port crates/horizon-server/src/lists.rs::run gap: minor: CLI op only (ops.json); no in-game /op
PardonCommand.register inline crates/horizon-client/src/game/commands.rs::Game::execute_command "pardon" reaches the pardon arm of cmd_profiles on a dedicated server
@@ -73,6 +77,8 @@ PerfCommand.whenStopped skip:server profiler for the dedicated server; not a ga
PlaceCommand.placeJigsaw port crates/horizon-client/src/game/commands/place.rs::Game::cmd_place_generated place.jigsaw branch: pool check, generation, success message
PlaceCommand.placeStructure port crates/horizon-client/src/game/commands/place.rs::Game::cmd_place_generated place.structure branch: id check, Structure.generate with the biome -> true predicate, checkLoaded over the start's box, per-chunk placeInChunk, success/failed messages
PlaceCommand.placeTemplate port crates/horizon-client/src/game/commands/place.rs::Game::cmd_place template placement with rotation, mirror, integrity and strict
PlaceCommand.checkLoaded port crates/horizon-client/src/game/commands/place.rs::Game::place_check_loaded every chunk of the inclusive range must be in the dimension and loaded
PlaceCommand.placeFeature port crates/horizon-client/src/game/commands/place.rs::Game::cmd_place_generated the feature, jigsaw and structure branches share the generation path
PlaySoundCommand.register port crates/horizon-command/src/tree_world.rs::sounds playsound literal with a sound-source literal per source
PlaySoundCommand.source port crates/horizon-command/src/tree_world.rs::sounds per-source literal with targets, pos, volume, pitch, minVolume
PlaySoundCommand.playSound port crates/horizon-client/src/game/commands/world_cmds.rs::Game::cmd_playsound range 16 (16 x volume above 1), minVolume fallback, one seed for all players
@@ -80,6 +86,7 @@ PostEffectCommand.addPostEffect port crates/horizon-client/src/game/commands/ext
PostEffectCommand.clearPostEffect port crates/horizon-client/src/game/commands/extra.rs::Game::cmd_posteffect gap: player: the cleared list is never drawn anyway
PostEffectCommand.removePostEffect port crates/horizon-client/src/game/commands/extra.rs::Game::cmd_posteffect gap: player: the removed effect was never drawn
PublishCommand.getSuccessMessage port crates/horizon-client/src/game/commands/extra.rs::Game::cmd_publish published-port message with a copy-on-click port
PublishCommand.publish port crates/horizon-client/src/game/commands/extra.rs::Game::cmd_publish with IntegratedServer.publishServer: the world is opened to the LAN and the port reported
RaidCommand.glow port crates/horizon-client/src/game/raids.rs::Game::raid_glow glowing for 1000 ticks, amplifier 1, on the raid's raiders (via cmd_raid raid.glow)
RaidCommand.setRaidOmenLevel port crates/horizon-client/src/game/commands/raid.rs::Game::cmd_raid raid.setomen branch
RaidCommand.spawnLeader port crates/horizon-client/src/game/commands/raid.rs::Game::cmd_raid raid.spawnleader branch: patrol leader and banner
@@ -98,6 +105,7 @@ RecipeCommand.giveRecipes port crates/horizon-client/src/game/commands/world_cmd
RecipeCommand.takeRecipes port crates/horizon-client/src/game/commands/world_cmds.rs::Game::cmd_recipe take branch; recipes removed through change_recipes
ReloadCommand.discoverNewPacks port crates/horizon-client/src/game/functions.rs::Game::reload_datapacks gap: minor: enables newly found packs that are not disabled; no NeoForge reorder of new packs
ReloadCommand.register port crates/horizon-command/src/tree_misc.rs::register reload literal
ReloadCommand.reloadPacks port crates/horizon-client/src/game/functions.rs::Game::apply_pack_change no message of its own; the content tables are reloaded with the functions
ReturnCommand.ReturnFailCustomExecutor.run port crates/horizon-client/src/game/functions.rs::Game::cmd_return return fail branch: finish_return with no value
ReturnCommand.ReturnFromCommandCustomModifier.apply port crates/horizon-client/src/game/commands/execute.rs::Game::run_return_run return run: the command's result becomes the frame's return
ReturnCommand.ReturnValueCustomExecutor.run port crates/horizon-client/src/game/functions.rs::Game::cmd_return return value branch: finish_return with the value
Can't render this file because it contains an unexpected character in line 65 and column 96.
+4
View File
@@ -4,12 +4,16 @@ SpreadPlayersCommand.Position.getLength inline crates/horizon-client/src/game/co
SpreadPlayersCommand.Position.moveAway inline crates/horizon-client/src/game/commands/world_cmds.rs::Game::cmd_spreadplayers helper folded into the spread loop
SpreadPlayersCommand.Position.clamp inline crates/horizon-client/src/game/commands/world_cmds.rs::Game::cmd_spreadplayers helper folded into the spread loop
SpreadPlayersCommand.Position.randomize inline crates/horizon-client/src/game/commands/world_cmds.rs::Game::cmd_spreadplayers helper folded into the spread loop
SpreadPlayersCommand.Position.getSpawnY port crates/horizon-client/src/game/commands/world_cmds.rs::Game::spread_spawn_y the first solid block under two air blocks, searched down from max_y; max_y + 1 when there is none
SpreadPlayersCommand.Position.isSafe port crates/horizon-client/src/game/commands/world_cmds.rs::Game::spread_safe spawn y minus one under max_y, not a liquid and in the entities_can_teleport_to tag
StopCommand.register port crates/horizon-client/src/game/dedicated.rs::Game::console_command gap: minor: /stop only from the dedicated console; no in-game /stop node
StopSoundCommand.register port crates/horizon-command/src/tree_world.rs::sounds /stopsound command tree
StopSoundCommand.stopSound inline crates/horizon-client/src/game/commands/world_cmds.rs::Game::cmd_stopsound the StopSound packet is built once and sent to each target's connection there; the local player's own sounds stop through Audio::stop_sounds
StopwatchCommand.createStopwatch port crates/horizon-client/src/game/commands/misc.rs::Game::cmd_stopwatch stopwatch.create arm
StopwatchCommand.queryStopwatch port crates/horizon-client/src/game/commands/misc.rs::Game::cmd_stopwatch stopwatch.query arm
StopwatchCommand.restartStopwatch port crates/horizon-client/src/game/commands/misc.rs::Game::cmd_stopwatch stopwatch.restart arm
StopwatchCommand.removeStopwatch port crates/horizon-client/src/game/commands/misc.rs::Game::cmd_stopwatch default arm: remove
StopwatchCommand.register port crates/horizon-command/src/tree_misc.rs::stopwatch create, query, restart and remove under the stopwatch literal
SummonCommand.register port crates/horizon-command/src/vanilla.rs::registered root summon node
SummonCommand.spawnEntity port crates/horizon-client/src/game/commands.rs::Game::cmd_summon commands.summon.success
SwingCommand.register port crates/horizon-command/src/tree_world.rs::rotate_swing /swing command tree
1 SpreadPlayersCommand.Position.dist inline crates/horizon-client/src/game/commands/world_cmds.rs::Game::cmd_spreadplayers helper folded into the spread loop
4 SpreadPlayersCommand.Position.moveAway inline crates/horizon-client/src/game/commands/world_cmds.rs::Game::cmd_spreadplayers helper folded into the spread loop
5 SpreadPlayersCommand.Position.clamp inline crates/horizon-client/src/game/commands/world_cmds.rs::Game::cmd_spreadplayers helper folded into the spread loop
6 SpreadPlayersCommand.Position.randomize inline crates/horizon-client/src/game/commands/world_cmds.rs::Game::cmd_spreadplayers helper folded into the spread loop
7 SpreadPlayersCommand.Position.getSpawnY port crates/horizon-client/src/game/commands/world_cmds.rs::Game::spread_spawn_y the first solid block under two air blocks, searched down from max_y; max_y + 1 when there is none
8 SpreadPlayersCommand.Position.isSafe port crates/horizon-client/src/game/commands/world_cmds.rs::Game::spread_safe spawn y minus one under max_y, not a liquid and in the entities_can_teleport_to tag
9 StopCommand.register port crates/horizon-client/src/game/dedicated.rs::Game::console_command gap: minor: /stop only from the dedicated console; no in-game /stop node
10 StopSoundCommand.register port crates/horizon-command/src/tree_world.rs::sounds /stopsound command tree
11 StopSoundCommand.stopSound inline crates/horizon-client/src/game/commands/world_cmds.rs::Game::cmd_stopsound the StopSound packet is built once and sent to each target's connection there; the local player's own sounds stop through Audio::stop_sounds
12 StopwatchCommand.createStopwatch port crates/horizon-client/src/game/commands/misc.rs::Game::cmd_stopwatch stopwatch.create arm
13 StopwatchCommand.queryStopwatch port crates/horizon-client/src/game/commands/misc.rs::Game::cmd_stopwatch stopwatch.query arm
14 StopwatchCommand.restartStopwatch port crates/horizon-client/src/game/commands/misc.rs::Game::cmd_stopwatch stopwatch.restart arm
15 StopwatchCommand.removeStopwatch port crates/horizon-client/src/game/commands/misc.rs::Game::cmd_stopwatch default arm: remove
16 StopwatchCommand.register port crates/horizon-command/src/tree_misc.rs::stopwatch create, query, restart and remove under the stopwatch literal
17 SummonCommand.register port crates/horizon-command/src/vanilla.rs::registered root summon node
18 SummonCommand.spawnEntity port crates/horizon-client/src/game/commands.rs::Game::cmd_summon commands.summon.success
19 SwingCommand.register port crates/horizon-command/src/tree_world.rs::rotate_swing /swing command tree
+3 -2
View File
@@ -2,9 +2,9 @@ LinkFSPath skip:other Java path of the in-memory link filesystem; Horizon links
LinkFSProvider skip:other Java NIO provider of the link filesystem; Horizon reads host folders and zips directly
LinkFileSystem skip:other link filesystem and its getPath; Horizon builds overlay folders instead (packs.rs::build_overlay)
LinkFileSystem.Builder skip:other builder of the link filesystem tree; no equivalent, see packs.rs::build_overlay
PackFormat.fullCodec skip:java codec for supported_formats; packs.rs::compatible reads the same array
PackFormat.fullCodec port crates/horizon-client-render/src/packs.rs::format_codec supported_formats / pack_format as an int, or a list of 1 to 256 non-negative ints whose second is the minor
PackFormat.validateHolderList inline crates/horizon-client-render/src/packs.rs::overlays_of gap: minor: a broken overlay entry is dropped alone; vanilla rejects the whole overlays list
PackFormat.lastPreMinorVersion inline crates/horizon-client-render/src/packs.rs::last_pre_minor the cut-off the old-format validator compares against
PackFormat.lastPreMinorVersion port crates/horizon-client-render/src/packs.rs::last_pre_minor the last major without a minor version, 81 for data packs and 64 for resource packs, which the old-format validator compares against
PackFormat.packCodec skip:java codec wiring; the validation it runs is missing (IntermediaryFormat.validate)
PackFormat.of skip:java constructor; Horizon keeps (major, minor) tuples (packs.rs::RESOURCE_FORMAT)
PackFormat.compareTo skip:java ordering; Horizon compares the (major, minor) tuples directly (packs.rs::compatibility)
@@ -91,6 +91,7 @@ PermissionSetUnion.hasPermission skip:other no unions; a sender has one level
PermissionSetUnion.union skip:other no unions; a sender has one level
PermissionSetUnion.ensureNoUnionsWithinUnions skip:java constructor guard against nested unions
PermissionTypes.bootstrap skip:data registry bootstrap of the permission codecs
BanListEntry.<init> port crates/horizon-server-files/src/user_lists.rs::BanEntry::new both vanilla constructors land here: a missing source becomes (Unknown); gap: minor: created is stamped with now instead of being passed, and a file's stored created date is kept by fields() through from_json
BanListEntry.equals port crates/horizon-server-files/src/user_lists.rs::BanEntry::eq same source, expiry, reason and user
BanListEntry.hashCode skip:java hash for map keys; bans are keyed by uuid or ip string
CachedUserNameToIdResolver.lookupGameProfile port crates/horizon-server-files/src/user_cache.rs::UserCache::get_by_name cache first, then the profile lookup, then offline if allowed
1 LinkFSPath skip:other Java path of the in-memory link filesystem; Horizon links host paths (packs.rs::link)
2 LinkFSProvider skip:other Java NIO provider of the link filesystem; Horizon reads host folders and zips directly
3 LinkFileSystem skip:other link filesystem and its getPath; Horizon builds overlay folders instead (packs.rs::build_overlay)
4 LinkFileSystem.Builder skip:other builder of the link filesystem tree; no equivalent, see packs.rs::build_overlay
5 PackFormat.fullCodec skip:java port crates/horizon-client-render/src/packs.rs::format_codec codec for supported_formats; packs.rs::compatible reads the same array supported_formats / pack_format as an int, or a list of 1 to 256 non-negative ints whose second is the minor
6 PackFormat.validateHolderList inline crates/horizon-client-render/src/packs.rs::overlays_of gap: minor: a broken overlay entry is dropped alone; vanilla rejects the whole overlays list
7 PackFormat.lastPreMinorVersion inline port crates/horizon-client-render/src/packs.rs::last_pre_minor the cut-off the old-format validator compares against the last major without a minor version, 81 for data packs and 64 for resource packs, which the old-format validator compares against
8 PackFormat.packCodec skip:java codec wiring; the validation it runs is missing (IntermediaryFormat.validate)
9 PackFormat.of skip:java constructor; Horizon keeps (major, minor) tuples (packs.rs::RESOURCE_FORMAT)
10 PackFormat.compareTo skip:java ordering; Horizon compares the (major, minor) tuples directly (packs.rs::compatibility)
91 PermissionSetUnion.union skip:other no unions; a sender has one level
92 PermissionSetUnion.ensureNoUnionsWithinUnions skip:java constructor guard against nested unions
93 PermissionTypes.bootstrap skip:data registry bootstrap of the permission codecs
94 BanListEntry.<init> port crates/horizon-server-files/src/user_lists.rs::BanEntry::new both vanilla constructors land here: a missing source becomes (Unknown); gap: minor: created is stamped with now instead of being passed, and a file's stored created date is kept by fields() through from_json
95 BanListEntry.equals port crates/horizon-server-files/src/user_lists.rs::BanEntry::eq same source, expiry, reason and user
96 BanListEntry.hashCode skip:java hash for map keys; bans are keyed by uuid or ip string
97 CachedUserNameToIdResolver.lookupGameProfile port crates/horizon-server-files/src/user_cache.rs::UserCache::get_by_name cache first, then the profile lookup, then offline if allowed
+17 -17
View File
@@ -28,8 +28,8 @@ CrudeIncrementalIntIdentityHashBiMap.copy skip:java identity bimap over registr
Crypt.generateKeyPair port crates/horizon-protocol/src/rsa.rs::RsaPrivateKey::generate RSA key pair with the bit size passed in; vanilla passes 1024
Crypt.digestData port crates/horizon-protocol/src/crypto.rs::server_hash returns the signed hex string; vanilla returns bytes and the caller converts them
Crypt.rsaStringToKey port crates/horizon-client-render/src/auth.rs::pem_der the PEM armour around the services' keys
Crypt.rsaPublicKeyToString missing minor: PEM writer for the account chat public key; Horizon has no PEM writer
Crypt.pemRsaPrivateKeyToString missing minor: PEM writer for the account chat private key; Horizon has no PEM writer
Crypt.rsaPublicKeyToString missing the writer half of the key-pair codec Crypt's static initialiser builds (its only caller); Horizon reads a PEM key pair (auth.rs::pem_der, parse_certificates) and writes none
Crypt.pemRsaPrivateKeyToString missing the writer half of the key-pair codec Crypt's static initialiser builds (its only caller); Horizon reads a PEM key pair (auth.rs::pem_der, parse_certificates) and writes none
Crypt.byteToPrivateKey port crates/horizon-protocol/src/rsa.rs::RsaPrivateKey::from_der the account chat key pair's PEM key is decoded and read as PKCS8EncodedKeySpec (a bare PKCS#1 key too) in auth.rs::parse_certificates
Crypt.byteToPublicKey port crates/horizon-protocol/src/rsa.rs::RsaPublicKey::from_der X.509 SubjectPublicKeyInfo decode
Crypt.decryptByteToSecretKey port crates/horizon-protocol/src/rsa.rs::RsaPrivateKey::decrypt RSA PKCS#1 v1.5 decrypt of the shared secret; the bytes are used as the AES key as given
@@ -63,17 +63,17 @@ DependencySorter.addDependencyIfNotCyclic inline crates/horizon-world/src/worldg
DependencySorter.orderByDependencies inline crates/horizon-world/src/worldgen/tags.rs::TagDefinitions::build required edges first, then optional ones, then the visit order
DirectoryLock.create port crates/horizon-client/src/save.rs::SessionLock::acquire exclusive file lock on session.lock with the two-byte snowman content
DirectoryLock.isLocked port crates/horizon-client/src/save.rs::SessionLock::is_locked trying for the world's session.lock tells a locked world; title.rs uses it to flag the entry
Ease.inBack missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only
Ease.inElastic missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only
Ease.inExpo missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only
Ease.inOutBounce missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only
Ease.inOutCirc missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only
Ease.inOutQuad missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only
Ease.inOutQuart missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only
Ease.inOutQuint missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only
Ease.outBounce missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only
Ease.outElastic missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only
Ease.outExpo missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only
Ease.inBack missing a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
Ease.inElastic missing a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
Ease.inExpo missing a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
Ease.inOutBounce missing a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
Ease.inOutCirc missing a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
Ease.inOutQuad missing a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
Ease.inOutQuart missing a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
Ease.inOutQuint missing a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
Ease.outBounce missing a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
Ease.outElastic missing a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
Ease.outExpo missing a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
EasingType.cubicBezier inline crates/horizon-client-render/src/environment.rs::Environment::load_dimension reads the four control points of a cubic_bezier ease; gap: minor: no range check, see CubicBezierControls.validate
EasingType.CubicBezier.curveFromControls inline crates/horizon-client-render/src/environment.rs::Easing::apply the curve coefficients closure
EasingType.CubicBezier.solveT inline crates/horizon-client-render/src/environment.rs::Easing::apply Newton steps with the 1e-5 tolerance
@@ -82,7 +82,7 @@ EasingType.CubicBezier.equals skip:java value equality
EasingType.CubicBezier.toString skip:debug debug string
EasingType.CubicBezier.CubicCurve.sample inline crates/horizon-client-render/src/environment.rs::Easing::apply the sample closure
EasingType.CubicBezier.CubicCurve.sampleGradient inline crates/horizon-client-render/src/environment.rs::Easing::apply the gradient closure
EasingType.CubicBezierControls.validate missing minor: vanilla rejects control points outside [0;1] when a timeline loads; Horizon takes any value
EasingType.CubicBezierControls.validate missing vanilla rejects control points outside [0;1] when a timeline loads; environment.rs::Environment::load_dimension takes the four numbers as given
EncoderCache.Key.equals skip:java cache key equality
EncoderCache.Key.hashCode skip:java cache key hashing
ExceptionCollector.add skip:java suppressed exception collector
@@ -117,7 +117,7 @@ FastBufferedInputStream.skip skip:java buffered stream helper; Rust readers do
FastBufferedInputStream.fill skip:java buffered stream helper; Rust readers do their own buffering
FileSystemUtil.safeGetPath skip:other opens a jar or zip filesystem for a URI; Horizon reads archives with the zip crate
FileUtil.sanitizeName port crates/horizon-client/src/title.rs::Title::target_folder gap: minor: replaces every non-alphanumeric character with _; vanilla only replaces illegal file characters, dots, slashes and quotes
FileUtil.isPathPortable skip:other portable path check; no caller in the 26.3 source
FileUtil.isPathPortable port crates/horizon-client/src/game/structure_blocks.rs::portable_path the structure path's check: no empty, `.` or `..` segment; vanilla also tests the part's characters (isPathPartPortable)
FileUtil.decomposePath skip:data resource path handling; Horizon reads its data from tables and files
FileUtil.resolvePath skip:data resource path handling; Horizon reads its data from tables and files
FileUtil.isValidPathSegment skip:data resource path handling; Horizon reads its data from tables and files
@@ -127,8 +127,8 @@ FileUtil.isEmptyPath skip:data resource path handling; Horizon reads its data f
FileZipper.add skip:debug profiling report archive (Minecraft.archiveProfilingReport, PerfCommand); Horizon writes no zip report
FileZipper.close skip:debug profiling report archive (Minecraft.archiveProfilingReport, PerfCommand); Horizon writes no zip report
FormattedCharSequence.forward port crates/horizon-client-core/src/text.rs::Text::styled styled text object; the per-character walk is Text::chars
FormattedCharSequence.backward skip:other two-argument reverse walk; no caller in the 26.3 source (SubStringSource uses the three-argument form)
FormattedCharSequence.decorateOutput missing minor: char-code modifier for reversed text, used only for right-to-left languages; Horizon has no right-to-left layout
FormattedCharSequence.backward skip:other the reverse walk SubStringSource.substring does, which is skip:ui (the HUD draws each span with its own style)
FormattedCharSequence.decorateOutput inline crates/horizon-client-render/src/bidi.rs::reorder the mirror a right-to-left character is drawn as; vanilla's sink mirrors every code point when the language reads right to left, here only the code points in a right-to-left run
FormattedCharSequence.composite port crates/horizon-client-core/src/text.rs::Text::empty zero parts give the empty sequence
FormattedCharSequence.fromPair port crates/horizon-client-core/src/text.rs::Text::append concatenation of two sequences
FutureChain.append skip:other ordering helper for the async chat-signature checks in ServerGamePacketListenerImpl; a scheduling wrapper, no game behaviour of its own
1 BinaryAnimator.tick port crates/horizon-client/src/mobs/axolotl.rs::Animation::tick one axolotl Animation holds the four animators; old/value pair and the clamp to 10 as vanilla
28 Crypt.generateKeyPair port crates/horizon-protocol/src/rsa.rs::RsaPrivateKey::generate RSA key pair with the bit size passed in; vanilla passes 1024
29 Crypt.digestData port crates/horizon-protocol/src/crypto.rs::server_hash returns the signed hex string; vanilla returns bytes and the caller converts them
30 Crypt.rsaStringToKey port crates/horizon-client-render/src/auth.rs::pem_der the PEM armour around the services' keys
31 Crypt.rsaPublicKeyToString missing minor: PEM writer for the account chat public key; Horizon has no PEM writer the writer half of the key-pair codec Crypt's static initialiser builds (its only caller); Horizon reads a PEM key pair (auth.rs::pem_der, parse_certificates) and writes none
32 Crypt.pemRsaPrivateKeyToString missing minor: PEM writer for the account chat private key; Horizon has no PEM writer the writer half of the key-pair codec Crypt's static initialiser builds (its only caller); Horizon reads a PEM key pair (auth.rs::pem_der, parse_certificates) and writes none
33 Crypt.byteToPrivateKey port crates/horizon-protocol/src/rsa.rs::RsaPrivateKey::from_der the account chat key pair's PEM key is decoded and read as PKCS8EncodedKeySpec (a bare PKCS#1 key too) in auth.rs::parse_certificates
34 Crypt.byteToPublicKey port crates/horizon-protocol/src/rsa.rs::RsaPublicKey::from_der X.509 SubjectPublicKeyInfo decode
35 Crypt.decryptByteToSecretKey port crates/horizon-protocol/src/rsa.rs::RsaPrivateKey::decrypt RSA PKCS#1 v1.5 decrypt of the shared secret; the bytes are used as the AES key as given
63 DependencySorter.orderByDependencies inline crates/horizon-world/src/worldgen/tags.rs::TagDefinitions::build required edges first, then optional ones, then the visit order
64 DirectoryLock.create port crates/horizon-client/src/save.rs::SessionLock::acquire exclusive file lock on session.lock with the two-byte snowman content
65 DirectoryLock.isLocked port crates/horizon-client/src/save.rs::SessionLock::is_locked trying for the world's session.lock tells a locked world; title.rs uses it to flag the entry
66 Ease.inBack missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
67 Ease.inElastic missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
68 Ease.inExpo missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
69 Ease.inOutBounce missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
70 Ease.inOutCirc missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
71 Ease.inOutQuad missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
72 Ease.inOutQuart missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
73 Ease.inOutQuint missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
74 Ease.outBounce missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
75 Ease.outElastic missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
76 Ease.outExpo missing minor: named easing reached only through the EasingType registry; vanilla timeline data uses cubic_bezier and constant only a named easing only a datapack's timeline can reach: the four shipped timelines ask for constant, cubic_bezier or no ease, and environment.rs::Environment::load_dimension reads those two
77 EasingType.cubicBezier inline crates/horizon-client-render/src/environment.rs::Environment::load_dimension reads the four control points of a cubic_bezier ease; gap: minor: no range check, see CubicBezierControls.validate
78 EasingType.CubicBezier.curveFromControls inline crates/horizon-client-render/src/environment.rs::Easing::apply the curve coefficients closure
79 EasingType.CubicBezier.solveT inline crates/horizon-client-render/src/environment.rs::Easing::apply Newton steps with the 1e-5 tolerance
82 EasingType.CubicBezier.toString skip:debug debug string
83 EasingType.CubicBezier.CubicCurve.sample inline crates/horizon-client-render/src/environment.rs::Easing::apply the sample closure
84 EasingType.CubicBezier.CubicCurve.sampleGradient inline crates/horizon-client-render/src/environment.rs::Easing::apply the gradient closure
85 EasingType.CubicBezierControls.validate missing minor: vanilla rejects control points outside [0;1] when a timeline loads; Horizon takes any value vanilla rejects control points outside [0;1] when a timeline loads; environment.rs::Environment::load_dimension takes the four numbers as given
86 EncoderCache.Key.equals skip:java cache key equality
87 EncoderCache.Key.hashCode skip:java cache key hashing
88 ExceptionCollector.add skip:java suppressed exception collector
117 FastBufferedInputStream.fill skip:java buffered stream helper; Rust readers do their own buffering
118 FileSystemUtil.safeGetPath skip:other opens a jar or zip filesystem for a URI; Horizon reads archives with the zip crate
119 FileUtil.sanitizeName port crates/horizon-client/src/title.rs::Title::target_folder gap: minor: replaces every non-alphanumeric character with _; vanilla only replaces illegal file characters, dots, slashes and quotes
120 FileUtil.isPathPortable skip:other port crates/horizon-client/src/game/structure_blocks.rs::portable_path portable path check; no caller in the 26.3 source the structure path's check: no empty, `.` or `..` segment; vanilla also tests the part's characters (isPathPartPortable)
121 FileUtil.decomposePath skip:data resource path handling; Horizon reads its data from tables and files
122 FileUtil.resolvePath skip:data resource path handling; Horizon reads its data from tables and files
123 FileUtil.isValidPathSegment skip:data resource path handling; Horizon reads its data from tables and files
127 FileZipper.add skip:debug profiling report archive (Minecraft.archiveProfilingReport, PerfCommand); Horizon writes no zip report
128 FileZipper.close skip:debug profiling report archive (Minecraft.archiveProfilingReport, PerfCommand); Horizon writes no zip report
129 FormattedCharSequence.forward port crates/horizon-client-core/src/text.rs::Text::styled styled text object; the per-character walk is Text::chars
130 FormattedCharSequence.backward skip:other two-argument reverse walk; no caller in the 26.3 source (SubStringSource uses the three-argument form) the reverse walk SubStringSource.substring does, which is skip:ui (the HUD draws each span with its own style)
131 FormattedCharSequence.decorateOutput missing inline crates/horizon-client-render/src/bidi.rs::reorder minor: char-code modifier for reversed text, used only for right-to-left languages; Horizon has no right-to-left layout the mirror a right-to-left character is drawn as; vanilla's sink mirrors every code point when the language reads right to left, here only the code points in a right-to-left run
132 FormattedCharSequence.composite port crates/horizon-client-core/src/text.rs::Text::empty zero parts give the empty sequence
133 FormattedCharSequence.fromPair port crates/horizon-client-core/src/text.rs::Text::append concatenation of two sequences
134 FutureChain.append skip:other ordering helper for the async chat-signature checks in ServerGamePacketListenerImpl; a scheduling wrapper, no game behaviour of its own
+6 -6
View File
@@ -106,9 +106,9 @@ BiasedToBottomInt.sample port crates/horizon-world/src/worldgen/value_provider.r
ClampedNormalFloat.sample port crates/horizon-world/src/worldgen/value_provider.rs::FloatProvider::sample ClampedNormal arm
ClampedNormalInt.sample port crates/horizon-world/src/worldgen/value_provider.rs::IntProvider::sample ClampedNormal arm
ConstantFloat.of port crates/horizon-world/src/worldgen/value_provider.rs::FloatProvider::from_json Constant arm normalises -0.0 to 0.0 as vanilla's ZERO does
FloatProviders.codec missing minor: unsure: min/max range check is per caller in Horizon (carver.rs:80-89 checks a few bounds); no generic check
IntProviders.validateCodec missing minor: unsure: min/max range check is per caller in Horizon; no generic check
IntProviders.validate missing minor: unsure: min/max range check is per caller in Horizon; no generic check
FloatProviders.codec missing no codec wrapper to carry a field's range: a float provider is read without one, and the sites that need a bound check it themselves (worldgen/carver.rs:89-95 rejects a floor_level outside -1..1)
IntProviders.validateCodec missing no codec wrapper to carry a field's range: the reading sites check what they need instead (worldgen/carver.rs:79-82 rejects a negative count)
IntProviders.validate missing no provider-level check: vanilla rejects minInclusive < minValue or maxInclusive > maxValue against the range each field's codec names, and here only the few sites that care check (worldgen/carver.rs:79-82)
MultipliedFloats.sample missing minor: unsure: SoundManager multiplies a wrapped sound's volume and pitch by the outer sound's; no nested product found in Horizon
TrapezoidFloat.sample port crates/horizon-world/src/worldgen/value_provider.rs::FloatProvider::sample Trapezoid arm
TrapezoidInt.sample port crates/horizon-world/src/worldgen/value_provider.rs::IntProvider::sample Trapezoid arm
@@ -130,11 +130,11 @@ UpgradeProgress.incrementFinishedOperationsBy skip:ui progress counters for an
UpgradeProgress.reset skip:ui progress counters for an upgrade progress bar Horizon does not show
UpgradeProgress.logProgress skip:other logs progress and sends JSON-RPC notifications; Horizon upgrades synchronously
UpgradeProgress.FileFixStats.getProgress skip:ui progress fraction for the upgrade screen
UpgradeStatusTranslator.translate skip:ui translatable status text for the upgrade screen
UpgradeStatusTranslator.Messages.forStatus skip:ui translatable status text for the upgrade screen
UpgradeStatusTranslator.translate port crates/horizon-world/src/datafix/world_upgrader.rs::status_key failed, counting, no data fix type, then upgrading/finished by type; the key comes back as a string and the screen makes the Text
UpgradeStatusTranslator.Messages.forStatus inline crates/horizon-world/src/datafix/world_upgrader.rs::status_key the upgrading/finished pick is the last match arm there
WorldUpgrader.getDataFixContextTag port crates/horizon-world/src/datafix/files.rs::generators gap: minor: returns (dimension, generator type) pairs, not the context compound
WorldUpgrader.verifyChunkPosAndEraseCache port crates/horizon-world/src/datafix/world_upgrader.rs::verify_chunk_pos_and_erase_cache the chunk's position is checked and its Heightmaps, isLightOn, BlockLight and SkyLight are dropped, in the per-chunk upgrade step
WorldUpgrader.verifyChunkPos skip:other only logs a warning for a chunk with a wrong stored position
WorldUpgrader.verifyChunkPos port crates/horizon-world/src/datafix/world_upgrader.rs::verify_chunk_pos warns when the stored xPos/zPos differ from the chunk's own position and reports no change
WorldUpgrader.work port crates/horizon-world/src/datafix/files.rs::upgrade_world gap: minor: no Optimize World pass and no progress; upgrade_world runs the file fixes then the data upgrade
WorldUpgrader.upgradeLevels inline crates/horizon-world/src/datafix/files.rs::upgrade_world per-dimension loop folded into upgrade_world
LongJumpUtil.isClearTransition port crates/horizon-client/src/mobs/breeze.rs::clear_transition boxed steps of nine tenths the smaller side from one point to the next must all be clear, for the breeze's long jump
1 EventLogDirectory.open skip:service telemetry log directory (TelemetryLogManager); not gameplay
106 ClampedNormalFloat.sample port crates/horizon-world/src/worldgen/value_provider.rs::FloatProvider::sample ClampedNormal arm
107 ClampedNormalInt.sample port crates/horizon-world/src/worldgen/value_provider.rs::IntProvider::sample ClampedNormal arm
108 ConstantFloat.of port crates/horizon-world/src/worldgen/value_provider.rs::FloatProvider::from_json Constant arm normalises -0.0 to 0.0 as vanilla's ZERO does
109 FloatProviders.codec missing minor: unsure: min/max range check is per caller in Horizon (carver.rs:80-89 checks a few bounds); no generic check no codec wrapper to carry a field's range: a float provider is read without one, and the sites that need a bound check it themselves (worldgen/carver.rs:89-95 rejects a floor_level outside -1..1)
110 IntProviders.validateCodec missing minor: unsure: min/max range check is per caller in Horizon; no generic check no codec wrapper to carry a field's range: the reading sites check what they need instead (worldgen/carver.rs:79-82 rejects a negative count)
111 IntProviders.validate missing minor: unsure: min/max range check is per caller in Horizon; no generic check no provider-level check: vanilla rejects minInclusive < minValue or maxInclusive > maxValue against the range each field's codec names, and here only the few sites that care check (worldgen/carver.rs:79-82)
112 MultipliedFloats.sample missing minor: unsure: SoundManager multiplies a wrapped sound's volume and pitch by the outer sound's; no nested product found in Horizon
113 TrapezoidFloat.sample port crates/horizon-world/src/worldgen/value_provider.rs::FloatProvider::sample Trapezoid arm
114 TrapezoidInt.sample port crates/horizon-world/src/worldgen/value_provider.rs::IntProvider::sample Trapezoid arm
130 UpgradeProgress.reset skip:ui progress counters for an upgrade progress bar Horizon does not show
131 UpgradeProgress.logProgress skip:other logs progress and sends JSON-RPC notifications; Horizon upgrades synchronously
132 UpgradeProgress.FileFixStats.getProgress skip:ui progress fraction for the upgrade screen
133 UpgradeStatusTranslator.translate skip:ui port crates/horizon-world/src/datafix/world_upgrader.rs::status_key translatable status text for the upgrade screen failed, counting, no data fix type, then upgrading/finished by type; the key comes back as a string and the screen makes the Text
134 UpgradeStatusTranslator.Messages.forStatus skip:ui inline crates/horizon-world/src/datafix/world_upgrader.rs::status_key translatable status text for the upgrade screen the upgrading/finished pick is the last match arm there
135 WorldUpgrader.getDataFixContextTag port crates/horizon-world/src/datafix/files.rs::generators gap: minor: returns (dimension, generator type) pairs, not the context compound
136 WorldUpgrader.verifyChunkPosAndEraseCache port crates/horizon-world/src/datafix/world_upgrader.rs::verify_chunk_pos_and_erase_cache the chunk's position is checked and its Heightmaps, isLightOn, BlockLight and SkyLight are dropped, in the per-chunk upgrade step
137 WorldUpgrader.verifyChunkPos skip:other port crates/horizon-world/src/datafix/world_upgrader.rs::verify_chunk_pos only logs a warning for a chunk with a wrong stored position warns when the stored xPos/zPos differ from the chunk's own position and reports no change
138 WorldUpgrader.work port crates/horizon-world/src/datafix/files.rs::upgrade_world gap: minor: no Optimize World pass and no progress; upgrade_world runs the file fixes then the data upgrade
139 WorldUpgrader.upgradeLevels inline crates/horizon-world/src/datafix/files.rs::upgrade_world per-dimension loop folded into upgrade_world
140 LongJumpUtil.isClearTransition port crates/horizon-client/src/mobs/breeze.rs::clear_transition boxed steps of nine tenths the smaller side from one point to the next must all be clear, for the breeze's long jump
+10
View File
@@ -1,4 +1,14 @@
PistonMovingBlockEntity.moveCollidedEntities port crates/horizon-client/src/game/piston_push.rs::Game::push_for_piston push loop; per-box distance from crates/horizon-world/src/piston_push.rs::collided_amount
PistonMovingBlockEntity.tick port crates/horizon-world/src/level/pistons.rs::Sim::moving_piston_tick a moving piston reports the push moveCollidedEntities would do at its new progress, and the caller moves the entities because Ticks has none
PistonMovingBlockEntity.finalTick port crates/horizon-world/src/level/pistons.rs::Sim::moving_piston_final_tick the arrival: the moved state is placed and the block's own update runs
PistonMovingBlockEntity.getExtendedProgress port crates/horizon-world/src/piston_push.rs::extended_progress where along its travel the moved block is, in blocks
PistonMovingBlockEntity.getMovement port crates/horizon-world/src/piston_push.rs::get_movement how far an entity stands inside the block it must be pushed out of, along the movement's axis
PistonMovingBlockEntity.moveByPositionAndProgress port crates/horizon-world/src/piston_push.rs::move_by_position_and_progress a box moved to the block's position and on to where the moved block currently is
PistonMovingBlockEntity.matchesStickyCritera port crates/horizon-world/src/piston_push.rs::matches_sticky_criteria the sticky test on the block ahead of the piston
PistonMovingBlockEntity.getCollisionRelatedBlockState port crates/horizon-world/src/piston_push.rs::uses_moved_state the collision state is the moved block's, unless a retracting source piston shows its head
PistonMovingBlockEntity.fixEntityWithinPistonBase port crates/horizon-world/src/piston_push.rs::fix_within_piston_base a retracting source piston drags an entity still inside its own block out the opposite way
PistonMovingBlockEntity.saveAdditional port crates/horizon-client/src/game/persist.rs::Game::moving_piston_to_nbt the moved state, facing, progress, extending and source fields, into the compound the entity's id and position are written to as well
PistonMovingBlockEntity.loadAdditional port crates/horizon-client/src/game/persist.rs::Game::moving_piston_from_nbt the moved state, facing, progress, extending and source read back, for an entity whose block is still a moving piston
SideChainPart.whenConnectedToTheRight port crates/horizon-world/src/level/shelf.rs::Part::connected_to_the_right same arms as vanilla
SideChainPart.whenConnectedToTheLeft port crates/horizon-world/src/level/shelf.rs::Part::connected_to_the_left same arms as vanilla
ProtoChunk.getFluidState port crates/horizon-world/src/worldgen/feature_region.rs::TerrainRegion::fluid_state gap: minor: read through the region's block queries; no chunk-level accessor, fluid comes from the block state
1 PistonMovingBlockEntity.moveCollidedEntities port crates/horizon-client/src/game/piston_push.rs::Game::push_for_piston push loop; per-box distance from crates/horizon-world/src/piston_push.rs::collided_amount
2 PistonMovingBlockEntity.tick port crates/horizon-world/src/level/pistons.rs::Sim::moving_piston_tick a moving piston reports the push moveCollidedEntities would do at its new progress, and the caller moves the entities because Ticks has none
3 PistonMovingBlockEntity.finalTick port crates/horizon-world/src/level/pistons.rs::Sim::moving_piston_final_tick the arrival: the moved state is placed and the block's own update runs
4 PistonMovingBlockEntity.getExtendedProgress port crates/horizon-world/src/piston_push.rs::extended_progress where along its travel the moved block is, in blocks
5 PistonMovingBlockEntity.getMovement port crates/horizon-world/src/piston_push.rs::get_movement how far an entity stands inside the block it must be pushed out of, along the movement's axis
6 PistonMovingBlockEntity.moveByPositionAndProgress port crates/horizon-world/src/piston_push.rs::move_by_position_and_progress a box moved to the block's position and on to where the moved block currently is
7 PistonMovingBlockEntity.matchesStickyCritera port crates/horizon-world/src/piston_push.rs::matches_sticky_criteria the sticky test on the block ahead of the piston
8 PistonMovingBlockEntity.getCollisionRelatedBlockState port crates/horizon-world/src/piston_push.rs::uses_moved_state the collision state is the moved block's, unless a retracting source piston shows its head
9 PistonMovingBlockEntity.fixEntityWithinPistonBase port crates/horizon-world/src/piston_push.rs::fix_within_piston_base a retracting source piston drags an entity still inside its own block out the opposite way
10 PistonMovingBlockEntity.saveAdditional port crates/horizon-client/src/game/persist.rs::Game::moving_piston_to_nbt the moved state, facing, progress, extending and source fields, into the compound the entity's id and position are written to as well
11 PistonMovingBlockEntity.loadAdditional port crates/horizon-client/src/game/persist.rs::Game::moving_piston_from_nbt the moved state, facing, progress, extending and source read back, for an entity whose block is still a moving piston
12 SideChainPart.whenConnectedToTheRight port crates/horizon-world/src/level/shelf.rs::Part::connected_to_the_right same arms as vanilla
13 SideChainPart.whenConnectedToTheLeft port crates/horizon-world/src/level/shelf.rs::Part::connected_to_the_left same arms as vanilla
14 ProtoChunk.getFluidState port crates/horizon-world/src/worldgen/feature_region.rs::TerrainRegion::fluid_state gap: minor: read through the region's block queries; no chunk-level accessor, fluid comes from the block state
+15
View File
@@ -77,6 +77,20 @@ def mobs(ox, oz):
return out
def glowing(ox, oz):
"""1000 glowing mobs (the outline post effect): the mobs scene's kinds and grid."""
out, i = [], 0
for layer in range(5):
for row in range(10):
for col in range(20):
kind = MOBS[i % len(MOBS)]
x, y, z = ox + 0.5 - 14.25 + 1.5 * col, FLOOR_Y + 1 + 2.5 * layer, oz + 8 + 2 * row
out.append(f"summon {kind} {num(x)} {num(y)} {num(z)} "
"{NoAI:1b,NoGravity:1b,PersistenceRequired:1b,Silent:1b,Glowing:1b}")
i += 1
return out
def eyes(ox, oz):
"""1000 mobs with glowing eyes (spiders, cave spiders, endermen): the mobs scene's grid."""
out, i = [], 0
@@ -207,6 +221,7 @@ STRESS = [
("stress_particles", particles),
("stress_held", held),
("stress_eyes", eyes),
("stress_glowing", glowing),
]
NAMES = [name for name, _ in STRESS]
+13 -6
View File
@@ -1671,16 +1671,23 @@ mod tests {
"native_item_damaged: the let _ = (…); placeholder needs its dispatch call; run cargo run -p xtask -- mod-api"
);
assert_eq!(std::fs::read_to_string(&hooks).unwrap(), placeholder);
let fragment = root.join("tools/native-hook-fragments/native_item_damaged.json");
let shipped_fragment = std::fs::read_to_string(&fragment).unwrap();
let mut json: Value = serde_json::from_str(&shipped_fragment).unwrap();
json["hooks"][0].as_object_mut().unwrap().remove("mod_api");
std::fs::write(&fragment, json.to_string()).unwrap();
// A stamped hook lives in the catalog, not in a fragment.
let catalog_path = root.join("native-hooks.json");
let shipped_catalog = std::fs::read_to_string(&catalog_path).unwrap();
let mut json: Value = serde_json::from_str(&shipped_catalog).unwrap();
let hook = json["hooks"]
.as_array_mut()
.unwrap()
.iter_mut()
.find(|hook| hook["name"] == "item_damaged")
.unwrap();
hook.as_object_mut().unwrap().remove("mod_api").unwrap();
std::fs::write(&catalog_path, json.to_string()).unwrap();
assert_eq!(
super::generate(&root, false).unwrap_err(),
"hook item_damaged has no mod_api entry: add one"
);
std::fs::write(&fragment, shipped_fragment).unwrap();
std::fs::write(&catalog_path, shipped_catalog).unwrap();
super::generate(&root, false).unwrap();
// The generator writes LF; a Windows checkout (core.autocrlf) has CRLF.
let lf = |s: &str| s.replace("\r\n", "\n");