mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge upstream main into w1-bow
# Conflicts: # crates/horizon-client/src/mod_api/generated.rs
This commit is contained in:
commit
a09da57204
58 files changed
+1411
-470
No files matched your search
@@ -7011,7 +7011,11 @@ impl Renderer {
|
||||
let additive_quads = (frame.additive.len() / 4) as u32 - self.entity_batch.moved_additive;
|
||||
let see_through_quads = (frame.see_through.len() / 4) as u32;
|
||||
let shadow_only_quads = (frame.shadow_only.len() / 4) as u32;
|
||||
let entity_vertices = u64::from(posed) + all_entities.len() as u64;
|
||||
// Glowing entities' silhouettes posed on the GPU go last, past every
|
||||
// material (the outline pass draws them from there).
|
||||
let outline_base = posed + all_entities.len() as u32;
|
||||
let outlined = skin.map_or(0, |s| s.outline_vertices);
|
||||
let entity_vertices = u64::from(outline_base) + u64::from(outlined);
|
||||
if entity_vertices > self.entity_capacity {
|
||||
self.entity_capacity = entity_vertices.next_power_of_two();
|
||||
self.entity_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
@@ -7037,6 +7041,7 @@ impl Renderer {
|
||||
skin,
|
||||
&self.entity_buffer,
|
||||
0,
|
||||
outline_base,
|
||||
);
|
||||
}
|
||||
let static_sections = frame.static_block_entities;
|
||||
@@ -7163,6 +7168,7 @@ impl Renderer {
|
||||
&mut self.uploads,
|
||||
&mut encoder,
|
||||
frame.glowing,
|
||||
(outline_base, outlined / 4),
|
||||
[self.config.width, self.config.height],
|
||||
);
|
||||
if let Some((rotation, brightness)) = frame.stars {
|
||||
@@ -7194,7 +7200,8 @@ impl Renderer {
|
||||
lightning::QUADS
|
||||
})
|
||||
.max(weather_quads)
|
||||
.max(frame.glowing.len() as u32 / 4);
|
||||
.max(frame.glowing.len() as u32 / 4)
|
||||
.max(outlined / 4);
|
||||
if max_quads > self.index_quads {
|
||||
self.index_quads = max_quads.next_power_of_two();
|
||||
self.indices = Self::quad_indices(&self.device, self.index_quads);
|
||||
@@ -9083,6 +9090,7 @@ impl Renderer {
|
||||
view_target,
|
||||
&self.entity_group,
|
||||
&self.indices,
|
||||
&self.entity_buffer,
|
||||
timing.as_mut(),
|
||||
);
|
||||
// The world icon grabs here: after the level and the hand, before
|
||||
|
||||
@@ -29,6 +29,8 @@ pub(super) struct EntityOutline {
|
||||
vertices: wgpu::Buffer,
|
||||
capacity: u64,
|
||||
quads: u32,
|
||||
/// The silhouettes posed on the GPU: first vertex in the entity buffer, quads.
|
||||
posed: (u32, u32),
|
||||
targets: Option<Targets>,
|
||||
}
|
||||
|
||||
@@ -159,6 +161,7 @@ impl EntityOutline {
|
||||
vertices: Self::vertex_buffer(device, 256),
|
||||
capacity: 256,
|
||||
quads: 0,
|
||||
posed: (0, 0),
|
||||
targets: None,
|
||||
}
|
||||
}
|
||||
@@ -175,16 +178,20 @@ impl EntityOutline {
|
||||
/// Uploads this frame's silhouettes (`LevelRenderer.currentFrameRendersEntityOutline`
|
||||
/// when there are any) and sizes the targets like the window. A frame with
|
||||
/// none gives the targets' video memory back until an entity glows again.
|
||||
/// `posed`: where the silhouettes posed on the GPU start in the entity
|
||||
/// buffer (`entity_skin`'s outline jobs), and how many quads they are.
|
||||
pub(super) fn prepare(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
uploads: &mut super::upload::Uploads,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
silhouettes: &[Vertex],
|
||||
posed: (u32, u32),
|
||||
size: [u32; 2],
|
||||
) {
|
||||
self.quads = (silhouettes.len() / 4) as u32;
|
||||
if self.quads == 0 {
|
||||
self.posed = posed;
|
||||
if self.quads == 0 && posed.1 == 0 {
|
||||
self.targets = None;
|
||||
return;
|
||||
}
|
||||
@@ -193,12 +200,14 @@ impl EntityOutline {
|
||||
self.capacity = vertices.next_power_of_two();
|
||||
self.vertices = Self::vertex_buffer(device, self.capacity);
|
||||
}
|
||||
uploads.buffer(
|
||||
encoder,
|
||||
&self.vertices,
|
||||
0,
|
||||
bytemuck::cast_slice(silhouettes),
|
||||
);
|
||||
if !silhouettes.is_empty() {
|
||||
uploads.buffer(
|
||||
encoder,
|
||||
&self.vertices,
|
||||
0,
|
||||
bytemuck::cast_slice(silhouettes),
|
||||
);
|
||||
}
|
||||
if self.targets.as_ref().is_some_and(|t| t.size == size) {
|
||||
return;
|
||||
}
|
||||
@@ -260,9 +269,14 @@ impl EntityOutline {
|
||||
frame: &wgpu::TextureView,
|
||||
entity_group: &wgpu::BindGroup,
|
||||
indices: &wgpu::Buffer,
|
||||
entities: &wgpu::Buffer,
|
||||
mut timing: Option<&mut super::pack_timing::PassTiming>,
|
||||
) {
|
||||
let Some(t) = self.targets.as_ref().filter(|_| self.quads > 0) else {
|
||||
let Some(t) = self
|
||||
.targets
|
||||
.as_ref()
|
||||
.filter(|_| self.quads > 0 || self.posed.1 > 0)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let clear = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
|
||||
@@ -277,8 +291,15 @@ impl EntityOutline {
|
||||
pass.set_pipeline(&self.silhouette);
|
||||
pass.set_bind_group(0, entity_group, &[]);
|
||||
pass.set_index_buffer(indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
pass.set_vertex_buffer(0, self.vertices.slice(..));
|
||||
pass.draw_indexed(0..self.quads * 6, 0, 0..1);
|
||||
if self.quads > 0 {
|
||||
pass.set_vertex_buffer(0, self.vertices.slice(..));
|
||||
pass.draw_indexed(0..self.quads * 6, 0, 0..1);
|
||||
}
|
||||
let (first, quads) = self.posed;
|
||||
if quads > 0 {
|
||||
pass.set_vertex_buffer(0, entities.slice(..));
|
||||
pass.draw_indexed(0..quads * 6, first as i32, 0..1);
|
||||
}
|
||||
}
|
||||
// post_effect/entity_outline.json; its last pass only copies swap back to the
|
||||
// outline target, so the blit reads swap.
|
||||
|
||||
@@ -107,6 +107,11 @@ pub struct SkinFrame {
|
||||
pub jobs: Vec<SkinJob>,
|
||||
/// Vertices the jobs write: a whole number of quads.
|
||||
pub vertices: u32,
|
||||
/// Glowing entities' silhouettes posed here (`OutlineBufferSource`'s copy of
|
||||
/// their draws, [`SkinFrame::outline_since`]): `dst` counts from the first.
|
||||
pub outline_jobs: Vec<SkinJob>,
|
||||
/// Vertices the outline jobs write.
|
||||
pub outline_vertices: u32,
|
||||
}
|
||||
|
||||
impl SkinFrame {
|
||||
@@ -115,6 +120,24 @@ impl SkinFrame {
|
||||
self.parts.clear();
|
||||
self.jobs.clear();
|
||||
self.vertices = 0;
|
||||
self.outline_jobs.clear();
|
||||
self.outline_vertices = 0;
|
||||
}
|
||||
|
||||
/// Copies the draws since job `first` into the glowing entities' outline
|
||||
/// (`Frame::glowing`'s, on the GPU), as `glowing.rs`'s `mark_outline` copies
|
||||
/// vertices: the same ones, in `color` (`Vertex::color`) with no normal.
|
||||
pub fn outline_since(&mut self, first: usize, color: u32) {
|
||||
for i in first..self.jobs.len() {
|
||||
let job = self.jobs[i];
|
||||
self.outline_jobs.push(SkinJob {
|
||||
dst: self.outline_vertices,
|
||||
color,
|
||||
mode: SkinJob::FLAT,
|
||||
..job
|
||||
});
|
||||
self.outline_vertices += job.count;
|
||||
}
|
||||
}
|
||||
|
||||
/// The renderer took the new models.
|
||||
@@ -138,7 +161,10 @@ impl SkinFrame {
|
||||
|
||||
/// Whether the renderer has anything to take.
|
||||
pub fn pending(&self) -> bool {
|
||||
self.reset || !self.new_model.is_empty() || !self.jobs.is_empty()
|
||||
self.reset
|
||||
|| !self.new_model.is_empty()
|
||||
|| !self.jobs.is_empty()
|
||||
|| !self.outline_jobs.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +319,8 @@ impl EntitySkin {
|
||||
}
|
||||
|
||||
/// Records the frame's jobs: they write `frame.vertices` vertices into
|
||||
/// `out` from vertex `base` on. Call after the CPU vertices' upload.
|
||||
/// `out` from vertex `base` on, and the outline jobs `frame.outline_vertices`
|
||||
/// from vertex `outline_base` on. Call after the CPU vertices' upload.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn dispatch(
|
||||
&mut self,
|
||||
@@ -303,26 +330,32 @@ impl EntitySkin {
|
||||
frame: &SkinFrame,
|
||||
out: &wgpu::Buffer,
|
||||
base: u32,
|
||||
outline_base: u32,
|
||||
) {
|
||||
if frame.jobs.is_empty() {
|
||||
if frame.jobs.is_empty() && frame.outline_jobs.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Some(model) = &self.model_buffer else {
|
||||
return;
|
||||
};
|
||||
// Whole rows of workgroups past `ROW` jobs: the padding jobs write nothing.
|
||||
let count = frame.jobs.len() as u32;
|
||||
let count = (frame.jobs.len() + frame.outline_jobs.len()) as u32;
|
||||
let rows = count.div_ceil(ROW);
|
||||
let padded = if rows > 1 {
|
||||
(rows * ROW) as usize
|
||||
} else {
|
||||
frame.jobs.len()
|
||||
count as usize
|
||||
};
|
||||
self.staged.clear();
|
||||
self.staged.extend(frame.jobs.iter().map(|job| SkinJob {
|
||||
dst: job.dst + base,
|
||||
..*job
|
||||
}));
|
||||
let moved = |at: u32| {
|
||||
move |job: &SkinJob| SkinJob {
|
||||
dst: job.dst + at,
|
||||
..*job
|
||||
}
|
||||
};
|
||||
self.staged.extend(frame.jobs.iter().map(moved(base)));
|
||||
self.staged
|
||||
.extend(frame.outline_jobs.iter().map(moved(outline_base)));
|
||||
self.staged.resize(padded, SkinJob::default());
|
||||
self.parts.ensure(
|
||||
device,
|
||||
@@ -369,8 +402,22 @@ impl EntitySkin {
|
||||
/// the same way: the reference its tests hold the shader and the game's walk
|
||||
/// (`mobs::skin`) to. `model` is the whole store.
|
||||
pub fn expand(model: &[SkinVertex], frame: &SkinFrame) -> Vec<Vertex> {
|
||||
let mut out = vec![bytemuck::Zeroable::zeroed(); frame.vertices as usize];
|
||||
for job in &frame.jobs {
|
||||
expand_jobs(model, frame, &frame.jobs, frame.vertices)
|
||||
}
|
||||
|
||||
/// [`expand`] of the frame's outline jobs: the silhouettes they write.
|
||||
pub fn expand_outline(model: &[SkinVertex], frame: &SkinFrame) -> Vec<Vertex> {
|
||||
expand_jobs(model, frame, &frame.outline_jobs, frame.outline_vertices)
|
||||
}
|
||||
|
||||
fn expand_jobs(
|
||||
model: &[SkinVertex],
|
||||
frame: &SkinFrame,
|
||||
jobs: &[SkinJob],
|
||||
vertices: u32,
|
||||
) -> Vec<Vertex> {
|
||||
let mut out = vec![bytemuck::Zeroable::zeroed(); vertices as usize];
|
||||
for job in jobs {
|
||||
for k in 0..job.count {
|
||||
let v = model[(job.first + k) as usize];
|
||||
let part = &frame.parts[(job.part + v.part) as usize];
|
||||
@@ -557,13 +604,30 @@ mod tests {
|
||||
},
|
||||
],
|
||||
vertices: 120,
|
||||
outline_jobs: Vec::new(),
|
||||
outline_vertices: 0,
|
||||
};
|
||||
// A glowing entity's silhouette: the item job and the first job again,
|
||||
// in its team colour with no normal.
|
||||
let mut frame = frame;
|
||||
frame.jobs.push(frame.jobs[2]);
|
||||
frame.jobs.last_mut().expect("pushed").dst = 120;
|
||||
frame.jobs.push(SkinJob {
|
||||
dst: 144,
|
||||
..frame.jobs[0]
|
||||
});
|
||||
frame.vertices = 192;
|
||||
frame.outline_since(4, 0xff20_c0e0);
|
||||
assert_eq!(frame.outline_vertices, 72);
|
||||
let expected = expand(&model, &frame);
|
||||
// The posed vertices land after 5 others, which they leave alone.
|
||||
let outline = expand_outline(&model, &frame);
|
||||
// The posed vertices land after 5 others, which they leave alone, and the
|
||||
// silhouettes 3 past them.
|
||||
let before = 5u32;
|
||||
let outline_base = before + frame.vertices + 3;
|
||||
let out = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: None,
|
||||
size: u64::from(before + frame.vertices) * 44,
|
||||
size: u64::from(outline_base + frame.outline_vertices) * 44,
|
||||
usage: EntitySkin::usage(true) | wgpu::BufferUsages::COPY_SRC,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
@@ -572,7 +636,15 @@ mod tests {
|
||||
let mut uploads = Uploads::new(&device);
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
assert!(skin.register(&device, &mut uploads, &mut encoder, &frame));
|
||||
skin.dispatch(&device, &mut uploads, &mut encoder, &frame, &out, before);
|
||||
skin.dispatch(
|
||||
&device,
|
||||
&mut uploads,
|
||||
&mut encoder,
|
||||
&frame,
|
||||
&out,
|
||||
before,
|
||||
outline_base,
|
||||
);
|
||||
let read = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: None,
|
||||
size: out.size(),
|
||||
@@ -595,5 +667,24 @@ mod tests {
|
||||
"vertex {i}: {got:?} != {want:?}"
|
||||
);
|
||||
}
|
||||
let silhouettes = &got[(outline_base - before) as usize..];
|
||||
assert_eq!(silhouettes.len(), outline.len());
|
||||
for (i, (got, want)) in silhouettes.iter().zip(&outline).enumerate() {
|
||||
assert_eq!(
|
||||
bytemuck::bytes_of(got),
|
||||
bytemuck::bytes_of(want),
|
||||
"silhouette {i}: {got:?} != {want:?}"
|
||||
);
|
||||
}
|
||||
// A silhouette is its draw's vertex with the colour and no normal
|
||||
// (`mark_outline`).
|
||||
for (copy, of) in outline.iter().zip(&expected[120..]) {
|
||||
let marked = Vertex {
|
||||
color: 0xff20_c0e0,
|
||||
normal: [0; 4],
|
||||
..*of
|
||||
};
|
||||
assert_eq!(bytemuck::bytes_of(copy), bytemuck::bytes_of(&marked));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -753,6 +753,9 @@ fn print(out: &mut String, t: i32, i: usize, m: &Mob, player_down: bool, now: i6
|
||||
if let Some((acts, mem)) = brain {
|
||||
let _ = write!(out, "\tact {acts}\tmem {mem}");
|
||||
}
|
||||
if horizon_client::mobs::finalize::ageable(m.species()) {
|
||||
let _ = write!(out, "\tage {}", m.extra.age);
|
||||
}
|
||||
if m.species() == Species::Villager {
|
||||
let _ = write!(out, "\tvx {}", villager(m));
|
||||
}
|
||||
@@ -1026,6 +1029,7 @@ pub fn main() -> horizon_client::Result<()> {
|
||||
m.rider = Some(horizon_client::mobs::equine::Rider::default());
|
||||
}
|
||||
"noai" => m.no_ai = true,
|
||||
"agelocked" => m.extra.age_locked = true,
|
||||
// Warden.finalizeSpawn's own part.
|
||||
"digcooldown" | "emerge" => horizon_client::mobs::warden::finalize(
|
||||
&mut m,
|
||||
|
||||
@@ -170,6 +170,8 @@ pub struct ArrowState {
|
||||
/// The block it stuck in (`lastState`).
|
||||
pub last_state: Option<BlockStateId>,
|
||||
pub pickup: Pickup,
|
||||
/// Its entity type is `SpectralArrow` (the type is its own, not the pickup item's).
|
||||
pub spectral: bool,
|
||||
/// `pickupItemStack`: what picking the arrow up gives.
|
||||
pub pickup_item: crate::items::ItemStack,
|
||||
/// `AbstractArrow.firedFromWeapon`: the crossbow or bow it was shot from.
|
||||
@@ -227,6 +229,7 @@ impl ArrowState {
|
||||
shake: 0,
|
||||
last_state: None,
|
||||
pickup: Pickup::Disallowed,
|
||||
spectral: false,
|
||||
pickup_item: crate::items::ItemStack::new("minecraft:arrow", 1),
|
||||
fired_from_weapon: None,
|
||||
no_physics: false,
|
||||
|
||||
@@ -1483,6 +1483,7 @@ impl ArmorStand {
|
||||
facts: &BlockFacts,
|
||||
in_water: bool,
|
||||
push: &mut dyn FnMut(Aabb, &mut Vec3),
|
||||
border: Option<&BorderGeometry>,
|
||||
) {
|
||||
self.previous = self.position;
|
||||
if self.marker {
|
||||
@@ -1519,7 +1520,20 @@ impl ArmorStand {
|
||||
1.0
|
||||
};
|
||||
let mut velocity = wanted;
|
||||
if let Ok(moved) = scene.resolve(self.bounds(), wanted, 0.0, self.on_ground) {
|
||||
// Entity.collide: the world border's walls stop the stand too.
|
||||
let moved = SceneCollisions {
|
||||
world: scene.world,
|
||||
binding: scene.binding,
|
||||
subject: scene.subject,
|
||||
}
|
||||
.resolve_in_border(
|
||||
self.bounds(),
|
||||
wanted,
|
||||
0.0,
|
||||
self.on_ground,
|
||||
border.map(|b| (b, [self.position.x, self.position.z])),
|
||||
);
|
||||
if let Ok(moved) = moved {
|
||||
self.position = Vec3::new(
|
||||
self.position.x + moved.x,
|
||||
self.position.y + moved.y,
|
||||
@@ -2699,7 +2713,53 @@ mod armor_stand_motion_tests {
|
||||
binding: collision,
|
||||
subject: None,
|
||||
};
|
||||
stand.tick(&mut scene, facts, false, &mut |_, _| {});
|
||||
stand.tick(&mut scene, facts, false, &mut |_, _| {}, None);
|
||||
}
|
||||
|
||||
/// `Entity.collectColliders` (#7080): the world border's wall stops an
|
||||
/// armor stand as it stops the player. A border of size 10 around
|
||||
/// (20, 20) has its east wall at x = 25; the stand starts at x = 24 on the
|
||||
/// test scene's flat ground, moving 1.5 east.
|
||||
#[test]
|
||||
fn the_world_border_stops_an_armor_stand() {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../.local/horizon-client")
|
||||
.join(horizon_core::VANILLA_VERSION);
|
||||
if !root.join("movement-properties.nbt").exists() {
|
||||
return;
|
||||
}
|
||||
let assets = crate::assets::Assets::load(&root).unwrap();
|
||||
let render = assets.reload(&root).unwrap();
|
||||
let facts = BlockFacts::load(&root, assets.registry).unwrap();
|
||||
let collision = crate::collisions::load(&root, assets.registry).unwrap();
|
||||
let world = World::test_scene(&assets, &render, 3).unwrap();
|
||||
let border = BorderGeometry::new([20.0, 20.0], 10.0, 29_999_984).unwrap();
|
||||
for bordered in [false, true] {
|
||||
let mut stand = ArmorStand::new(1, Vec3::new(24.0, 4.0, 20.5), 0.0);
|
||||
stand.on_ground = true;
|
||||
stand.velocity = Vec3::new(1.5, 0.0, 0.0);
|
||||
let mut scene = SceneCollisions {
|
||||
world: &world,
|
||||
binding: &collision,
|
||||
subject: None,
|
||||
};
|
||||
stand.tick(
|
||||
&mut scene,
|
||||
&facts,
|
||||
false,
|
||||
&mut |_, _| {},
|
||||
bordered.then_some(&border),
|
||||
);
|
||||
let east = stand.bounds().max_position().x;
|
||||
if bordered {
|
||||
assert!(
|
||||
(east - 25.0).abs() < 1.0e-7,
|
||||
"stops on the wall, got {east}"
|
||||
);
|
||||
} else {
|
||||
assert!(east > 25.0, "no border: goes on, got {east}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -380,7 +380,7 @@ mod tests {
|
||||
binding: &collision,
|
||||
subject: None,
|
||||
};
|
||||
still.tick(&mut scene, &facts, in_water, &mut |_, _| {});
|
||||
still.tick(&mut scene, &facts, in_water, &mut |_, _| {}, None);
|
||||
assert_eq!(still.position.x, at.x);
|
||||
assert_eq!(still.velocity.x, 0.0);
|
||||
|
||||
@@ -394,7 +394,7 @@ mod tests {
|
||||
binding: &collision,
|
||||
subject: None,
|
||||
};
|
||||
marker.tick(&mut scene, &facts, in_water, &mut |_, _| {});
|
||||
marker.tick(&mut scene, &facts, in_water, &mut |_, _| {}, None);
|
||||
assert_eq!(marker.position, at);
|
||||
assert_eq!(marker.velocity, Vec3::ZERO);
|
||||
|
||||
@@ -406,7 +406,7 @@ mod tests {
|
||||
binding: &collision,
|
||||
subject: None,
|
||||
};
|
||||
carried.tick(&mut scene, &facts, in_water, &mut |_, _| {});
|
||||
carried.tick(&mut scene, &facts, in_water, &mut |_, _| {}, None);
|
||||
assert!((carried.position.x - (at.x + 0.014)).abs() < 1e-12);
|
||||
assert!((carried.velocity.x - 0.014 * f64::from(0.8f32)).abs() < 1e-12);
|
||||
for _ in 0..20 {
|
||||
@@ -416,7 +416,7 @@ mod tests {
|
||||
binding: &collision,
|
||||
subject: None,
|
||||
};
|
||||
carried.tick(&mut scene, &facts, in_water, &mut |_, _| {});
|
||||
carried.tick(&mut scene, &facts, in_water, &mut |_, _| {}, None);
|
||||
}
|
||||
assert!(carried.position.x > at.x + 0.5);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ fn string(s: &str) -> Tag {
|
||||
}
|
||||
/// Entity type ID (`id`).
|
||||
pub fn kind(tag: &Tag) -> Option<String> {
|
||||
text(tag.get("id"))
|
||||
horizon_client_core::identifier::normalize_id(&text(tag.get("id"))?)
|
||||
}
|
||||
pub fn position(tag: &Tag) -> Option<Vec3> {
|
||||
read_doubles::<3>(tag.get("Pos")).map(|p| Vec3::new(p[0], p[1], p[2]))
|
||||
@@ -2238,7 +2238,12 @@ pub fn arrow_to_nbt(
|
||||
return None;
|
||||
};
|
||||
let base = Base {
|
||||
id: "minecraft:arrow",
|
||||
// A spectral arrow is its own entity type.
|
||||
id: if a.spectral || &*a.pickup_item.item == "minecraft:spectral_arrow" {
|
||||
"minecraft:spectral_arrow"
|
||||
} else {
|
||||
"minecraft:arrow"
|
||||
},
|
||||
uuid,
|
||||
position: p.position,
|
||||
motion: p.velocity,
|
||||
@@ -2298,10 +2303,17 @@ pub fn arrow_from_nbt(
|
||||
let position = position(tag)?;
|
||||
let owner = uuid_from_tag(tag.get("Owner"));
|
||||
let from_player = player.is_some() && owner == player;
|
||||
// `getDefaultPickupItem`: a `SpectralArrow` is picked up as its own item.
|
||||
let spectral = kind(tag).as_deref() == Some("minecraft:spectral_arrow");
|
||||
let default_item = if spectral {
|
||||
"minecraft:spectral_arrow"
|
||||
} else {
|
||||
"minecraft:arrow"
|
||||
};
|
||||
let item = tag
|
||||
.get("item")
|
||||
.and_then(item_from_nbt)
|
||||
.unwrap_or_else(|| crate::items::ItemStack::new("minecraft:arrow", 1));
|
||||
.unwrap_or_else(|| crate::items::ItemStack::new(default_item, 1));
|
||||
let kind = ProjectileKind::Arrow {
|
||||
base_damage: float(tag.get("damage")).unwrap_or(2.0),
|
||||
critical: flag(tag, "crit"),
|
||||
@@ -2322,6 +2334,7 @@ pub fn arrow_from_nbt(
|
||||
a.has_been_shot = flag(tag, "HasBeenShot");
|
||||
a.fire_ticks = number(tag.get("Fire")).unwrap_or(0) as i16 as i32;
|
||||
a.fall_distance = float(tag.get("fall_distance")).unwrap_or(0.0);
|
||||
a.spectral = spectral;
|
||||
a.pickup_item = item;
|
||||
a.fired_from_weapon = tag.get("weapon").and_then(item_from_nbt);
|
||||
p.stuck = a.in_ground;
|
||||
@@ -2403,12 +2416,13 @@ fn put_projectile(tag: &mut Tag, p: &Projectile, owner: Option<[i32; 4]>) {
|
||||
|
||||
/// The thrown item projectiles Horizon saves: each entity type's id is also
|
||||
/// the item it saves when none was loaded (`getDefaultItem`).
|
||||
const THROWABLE_ITEMS: [&str; 5] = [
|
||||
const THROWABLE_ITEMS: [&str; 6] = [
|
||||
"minecraft:snowball",
|
||||
"minecraft:egg",
|
||||
"minecraft:ender_pearl",
|
||||
"minecraft:splash_potion",
|
||||
"minecraft:lingering_potion",
|
||||
"minecraft:experience_bottle",
|
||||
];
|
||||
/// The item a throwable flies as: its stack (a potion's), the mod type's
|
||||
/// item, or the item its entity type is named after.
|
||||
@@ -2432,6 +2446,7 @@ pub fn throwable_kind(id: &str) -> Option<ProjectileKind> {
|
||||
"minecraft:egg" => Some(ProjectileKind::Egg),
|
||||
"minecraft:ender_pearl" => Some(ProjectileKind::EnderPearl),
|
||||
// The entity type is the id; the `Item` read later may be another potion.
|
||||
"minecraft:experience_bottle" => Some(ProjectileKind::ExperienceBottle),
|
||||
_ => Some(ProjectileKind::Potion(
|
||||
Box::new(crate::items::ItemStack::new(id, 1)),
|
||||
id == "minecraft:lingering_potion",
|
||||
@@ -2454,12 +2469,17 @@ pub fn saved_with_chunk(p: &Projectile) -> bool {
|
||||
| ProjectileKind::Egg
|
||||
| ProjectileKind::EnderPearl
|
||||
| ProjectileKind::Potion(..)
|
||||
| ProjectileKind::ExperienceBottle
|
||||
| ProjectileKind::LlamaSpit
|
||||
| ProjectileKind::ShulkerBullet { target: None, .. }
|
||||
| ProjectileKind::Thrown(_) => true,
|
||||
ProjectileKind::Arrow { .. } => p.arrow.is_some(),
|
||||
// Not written yet: tridents save from their own list.
|
||||
// Not written yet: tridents save from their own list, and a bullet
|
||||
// with a target would lose it.
|
||||
ProjectileKind::Trident
|
||||
| ProjectileKind::LlamaSpit
|
||||
| ProjectileKind::ShulkerBullet { .. }
|
||||
| ProjectileKind::ShulkerBullet {
|
||||
target: Some(_), ..
|
||||
}
|
||||
| ProjectileKind::WitherSkull { .. } => false,
|
||||
}
|
||||
}
|
||||
@@ -2522,6 +2542,59 @@ pub fn throwable_from_nbt(tag: &Tag, player: Option<[i32; 4]>) -> Option<Project
|
||||
Some(p)
|
||||
}
|
||||
|
||||
/// `LlamaSpit` and `ShulkerBullet` as `/summon` or a chunk loads them:
|
||||
/// `Projectile`'s `Owner`, `LeftOwner`, `NoGravity` and the motion. A bullet
|
||||
/// loaded without a `Target` has none, so it falls (`ShulkerBullet.tick`).
|
||||
pub fn spit_or_bullet_from_nbt(tag: &Tag, player: Option<[i32; 4]>) -> Option<Projectile> {
|
||||
let kind = match kind(tag)?.as_str() {
|
||||
"minecraft:llama_spit" => ProjectileKind::LlamaSpit,
|
||||
"minecraft:shulker_bullet" => ProjectileKind::ShulkerBullet {
|
||||
target: None,
|
||||
dir: None,
|
||||
steps: 0,
|
||||
delta: [0.0; 3],
|
||||
seed: 0,
|
||||
},
|
||||
_ => return None,
|
||||
};
|
||||
let owner = owner_uuid(tag);
|
||||
let from_player = player.is_some() && owner == player;
|
||||
let mut p = Projectile::new(kind, position(tag)?, from_player.then_some(Who::Player));
|
||||
p.velocity = motion(tag);
|
||||
[p.yaw, p.pitch] = rotation(tag);
|
||||
p.left_owner = flag(tag, "LeftOwner");
|
||||
p.owner_uuid = owner;
|
||||
p.no_gravity = flag(tag, "NoGravity");
|
||||
Some(p)
|
||||
}
|
||||
|
||||
/// The save of [`spit_or_bullet_from_nbt`]'s kinds: `Projectile`'s keys.
|
||||
pub fn spit_or_bullet_to_nbt(
|
||||
p: &Projectile,
|
||||
uuid: [i32; 4],
|
||||
owner: Option<[i32; 4]>,
|
||||
raw: Option<&Tag>,
|
||||
) -> Option<Tag> {
|
||||
if !matches!(
|
||||
p.kind,
|
||||
ProjectileKind::LlamaSpit | ProjectileKind::ShulkerBullet { target: None, .. }
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
let base = Base {
|
||||
id: p.kind.id(),
|
||||
uuid,
|
||||
position: p.position,
|
||||
motion: p.velocity,
|
||||
rotation: [p.yaw, p.pitch],
|
||||
on_ground: false,
|
||||
fall_distance: 0.0,
|
||||
};
|
||||
let mut tag = base.tag(raw);
|
||||
put_projectile(&mut tag, p, owner);
|
||||
Some(tag)
|
||||
}
|
||||
|
||||
/// `Projectile`'s `Owner`.
|
||||
pub fn owner_uuid(tag: &Tag) -> Option<[i32; 4]> {
|
||||
uuid_from_tag(tag.get("Owner"))
|
||||
@@ -3311,6 +3384,31 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::items::ItemStack;
|
||||
|
||||
#[test]
|
||||
fn mob_loading_accepts_default_namespace_and_preserves_custom_namespaces() {
|
||||
let cow = crate::mobs::sensor_scan_tests::kind();
|
||||
let types = [
|
||||
("minecraft:cow".into(), cow.clone()),
|
||||
("test:cow".into(), cow),
|
||||
]
|
||||
.into();
|
||||
let pos = || {
|
||||
Tag::List(
|
||||
[4.5_f64, 220.0, 6.5]
|
||||
.map(|v| Tag::Double(v.to_bits()))
|
||||
.to_vec(),
|
||||
)
|
||||
};
|
||||
for id in ["cow", ":cow", "minecraft:cow", "test:cow"] {
|
||||
let tag = Tag::compound([("id", string(id)), ("Pos", pos()), ("NoAI", Tag::Byte(1))]);
|
||||
let mob = mob_from_nbt(&tag, &types, 7, None).expect(id);
|
||||
assert_eq!(mob.position, Vec3::new(4.5, 220.0, 6.5));
|
||||
assert!(mob.no_ai);
|
||||
}
|
||||
let unknown = Tag::compound([("id", string("other:cow")), ("Pos", pos())]);
|
||||
assert!(mob_from_nbt(&unknown, &types, 8, None).is_none());
|
||||
}
|
||||
|
||||
/// A saved rocket and trident come back with their own Life, LifeTime,
|
||||
/// flags and position (the keys vanilla's addAdditionalSaveData writes).
|
||||
#[test]
|
||||
@@ -5287,6 +5385,72 @@ mod mod_thrown_nbt_tests {
|
||||
// a type no mod declares now is not a thrown kind.
|
||||
assert!(throwable_kind("gone:bolt").is_none());
|
||||
}
|
||||
fn summoned(id: &str, extra: &[(&str, Tag)]) -> Tag {
|
||||
let mut c = vec![
|
||||
("id", Tag::String(id.into())),
|
||||
("Pos", doubles(&[0.5, 110.0, 0.5])),
|
||||
("Motion", doubles(&[0.0, 0.0, 0.0])),
|
||||
];
|
||||
c.extend(extra.iter().cloned());
|
||||
Tag::compound(c)
|
||||
}
|
||||
|
||||
/// Four summoned kinds had no loader and stayed inert (a spectral arrow,
|
||||
/// a shulker bullet, a llama's spit and an experience bottle never fell).
|
||||
#[test]
|
||||
fn summoned_projectiles_of_every_kind_load_and_honour_no_gravity() {
|
||||
use crate::entity_rules::EntityRules;
|
||||
let flag = ("NoGravity", Tag::Byte(1));
|
||||
let arrow = arrow_from_nbt(&summoned("minecraft:spectral_arrow", &[]), None, |_| None)
|
||||
.unwrap()
|
||||
.0;
|
||||
assert!(arrow.is_spectral_arrow() && !arrow.no_gravity);
|
||||
let held = arrow_from_nbt(
|
||||
&summoned("minecraft:spectral_arrow", std::slice::from_ref(&flag)),
|
||||
None,
|
||||
|_| None,
|
||||
)
|
||||
.unwrap()
|
||||
.0;
|
||||
assert!(held.no_gravity);
|
||||
// ...and it is saved as the entity type it is.
|
||||
let saved = arrow_to_nbt(&arrow, [1; 4], None, None, None).unwrap();
|
||||
assert_eq!(kind(&saved).as_deref(), Some("minecraft:spectral_arrow"));
|
||||
let plain = arrow_from_nbt(&summoned("minecraft:arrow", &[]), None, |_| None)
|
||||
.unwrap()
|
||||
.0;
|
||||
let saved = arrow_to_nbt(&plain, [1; 4], None, None, None).unwrap();
|
||||
assert_eq!(kind(&saved).as_deref(), Some("minecraft:arrow"));
|
||||
|
||||
for id in ["minecraft:llama_spit", "minecraft:shulker_bullet"] {
|
||||
let p = spit_or_bullet_from_nbt(&summoned(id, &[]), None).unwrap();
|
||||
assert_eq!(p.kind.id(), id);
|
||||
assert!(!p.no_gravity && p.position == Vec3::new(0.5, 110.0, 0.5));
|
||||
let held =
|
||||
spit_or_bullet_from_nbt(&summoned(id, std::slice::from_ref(&flag)), None).unwrap();
|
||||
assert!(held.no_gravity);
|
||||
}
|
||||
assert!(spit_or_bullet_from_nbt(&summoned("minecraft:arrow", &[]), None).is_none());
|
||||
// a spit and a target-less bullet are saved and selectable (`/data`, `@e`).
|
||||
let spit = spit_or_bullet_from_nbt(&summoned("minecraft:llama_spit", &[]), None).unwrap();
|
||||
assert!(saved_with_chunk(&spit));
|
||||
let saved = spit_or_bullet_to_nbt(&spit, [1; 4], None, None).unwrap();
|
||||
assert_eq!(kind(&saved).as_deref(), Some("minecraft:llama_spit"));
|
||||
|
||||
// A summoned experience bottle flies as the bottle item, with a bottle's gravity.
|
||||
let bottle =
|
||||
throwable_from_nbt(&summoned("minecraft:experience_bottle", &[]), None).unwrap();
|
||||
assert_eq!(bottle.kind.id(), "minecraft:experience_bottle");
|
||||
assert!(!bottle.no_gravity && (bottle.get_gravity() - 0.07).abs() < 1e-12);
|
||||
let held =
|
||||
throwable_from_nbt(&summoned("minecraft:experience_bottle", &[flag]), None).unwrap();
|
||||
assert!(held.no_gravity && held.get_gravity() == 0.0);
|
||||
let saved = throwable_to_nbt(&bottle, [1; 4], None, None).unwrap();
|
||||
assert_eq!(kind(&saved).as_deref(), Some("minecraft:experience_bottle"));
|
||||
let item = saved.get("Item").and_then(item_from_nbt).unwrap();
|
||||
assert_eq!(&*item.item, "minecraft:experience_bottle");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_gravity_survives_save_and_load_for_orbs() {
|
||||
// Entity.saveWithoutId / load: NoGravity round-trips on a kind that
|
||||
|
||||
@@ -3782,13 +3782,13 @@ impl Game<'_> {
|
||||
/// Item entity `i` leaves the level (`Entity.discard`); mods hear it.
|
||||
pub(super) fn remove_dropped(&mut self, i: usize) -> ItemEntity {
|
||||
let e = self.dropped.remove(i);
|
||||
self.native_entities_left("minecraft:item", &[e.id]);
|
||||
self.native_entity_left(e.id);
|
||||
e
|
||||
}
|
||||
/// Experience orb `i` leaves the level (`Entity.discard`); mods hear it.
|
||||
pub(super) fn remove_orb(&mut self, i: usize) -> ExperienceOrb {
|
||||
let o = self.orbs.remove(i);
|
||||
self.native_entities_left("minecraft:experience_orb", &[o.id]);
|
||||
self.native_entity_left(o.id);
|
||||
o
|
||||
}
|
||||
/// The item entity `spawn_item` just added, unless a native mod kept it
|
||||
@@ -7332,6 +7332,9 @@ impl Game<'_> {
|
||||
if running {
|
||||
horizon_client::spans::time("tick_lan_packets", || self.tick_lan_packets());
|
||||
horizon_client::spans::time("tick_blocks", || self.tick_blocks())?;
|
||||
// What the blocks spawned (a dispensed minecart, a lit TNT, a
|
||||
// placed boat) joins before the entities tick.
|
||||
self.native_joins_flush(false, 0);
|
||||
}
|
||||
if running {
|
||||
horizon_client::spans::time("tick_paintings", || self.tick_paintings());
|
||||
@@ -7363,34 +7366,35 @@ impl Game<'_> {
|
||||
self.handle_entity_portals();
|
||||
self.handle_end_gateway_portals();
|
||||
});
|
||||
let before = self.dropped_ids();
|
||||
horizon_client::spans::time("tick_items", || self.tick_items());
|
||||
if self.remote.is_none() {
|
||||
self.hoppers_catch_items();
|
||||
}
|
||||
// Merged, burnt, despawned or picked up.
|
||||
self.native_items_left_since(&before);
|
||||
self.native_leaves_flush();
|
||||
self.native_joins_flush(false, 0);
|
||||
horizon_client::spans::time("touch_arrows", || self.touch_arrows());
|
||||
}
|
||||
horizon_client::spans::time("spawn_loaded_entities", || self.spawn_loaded_entities());
|
||||
// The ride the player's file names, once the vehicle is in the world.
|
||||
self.restore_root_vehicle();
|
||||
if running {
|
||||
let mobs_before = self.mob_ids();
|
||||
horizon_client::spans::time("tick_mobs", || self.tick_mobs())?;
|
||||
// Dead, despawned or gone into a hive.
|
||||
self.native_mobs_left_since(&mobs_before);
|
||||
self.native_leaves_flush();
|
||||
horizon_client::spans::time("recheck_golem_openers", || self.recheck_golem_openers());
|
||||
self.tick_mod_container_viewers();
|
||||
let before = self.orb_ids();
|
||||
horizon_client::spans::time("tick_experience", || self.tick_experience());
|
||||
// Merged, despawned or picked up.
|
||||
self.native_orbs_left_since(&before);
|
||||
self.native_leaves_flush();
|
||||
horizon_client::spans::time("entity_portals", || {
|
||||
self.find_entity_portals();
|
||||
self.resolve_arrivals();
|
||||
});
|
||||
}
|
||||
// The rest of the tick's spawns join, and what left is told of.
|
||||
self.native_joins_flush(false, 0);
|
||||
self.native_leaves_flush();
|
||||
horizon_client::spans::time("tick_lan", || self.tick_lan(false));
|
||||
self.tick_friends();
|
||||
horizon_client::spans::time("tick_hand", || self.tick_hand());
|
||||
|
||||
@@ -810,17 +810,23 @@ impl Game<'_> {
|
||||
subject: None,
|
||||
};
|
||||
let mut tops: Vec<BlockPos> = Vec::new();
|
||||
a.tick(&mut scene, facts, in_water, &mut |bounds, velocity| {
|
||||
horizon_client::motion::bubble_column_push_over(
|
||||
world,
|
||||
registry,
|
||||
facts,
|
||||
collision,
|
||||
bounds,
|
||||
velocity,
|
||||
|pos| tops.push(pos),
|
||||
);
|
||||
});
|
||||
a.tick(
|
||||
&mut scene,
|
||||
facts,
|
||||
in_water,
|
||||
&mut |bounds, velocity| {
|
||||
horizon_client::motion::bubble_column_push_over(
|
||||
world,
|
||||
registry,
|
||||
facts,
|
||||
collision,
|
||||
bounds,
|
||||
velocity,
|
||||
|pos| tops.push(pos),
|
||||
);
|
||||
},
|
||||
Some(self.border.geometry()),
|
||||
);
|
||||
for pos in tops {
|
||||
self.bubble_column_particles(pos);
|
||||
}
|
||||
|
||||
@@ -2105,9 +2105,8 @@ impl Game<'_> {
|
||||
Target::Spawner(id) => self.ominous_spawners.retain(|s| s.id != id),
|
||||
}
|
||||
// The orbs marked for removal are gone (Entity.kill removes them).
|
||||
let before = self.orb_ids();
|
||||
self.orbs.retain(|o| o.age < u32::MAX / 2);
|
||||
self.native_orbs_left_since(&before);
|
||||
self.native_leaves_flush();
|
||||
}
|
||||
fn cmd_kill(&mut self, ctx: &Context) -> R<i32> {
|
||||
let targets = if ctx.get("targets").is_some() {
|
||||
|
||||
@@ -173,7 +173,8 @@ impl Game<'_> {
|
||||
let raw = self.persist.raw.get(&id);
|
||||
entity_nbt::arrow_to_nbt(p, random_uuid(), owner, in_block, raw)
|
||||
.or_else(|| entity_nbt::fireball_to_nbt(p, random_uuid(), owner, raw))
|
||||
.or_else(|| entity_nbt::throwable_to_nbt(p, random_uuid(), owner, raw))?
|
||||
.or_else(|| entity_nbt::throwable_to_nbt(p, random_uuid(), owner, raw))
|
||||
.or_else(|| entity_nbt::spit_or_bullet_to_nbt(p, random_uuid(), owner, raw))?
|
||||
}
|
||||
Target::Crystal(id) => {
|
||||
let c = self.crystals.iter().find(|c| c.id == id)?;
|
||||
@@ -879,7 +880,9 @@ impl Game<'_> {
|
||||
let target = self.spawn_entity_from_nbt(tag);
|
||||
self.native_mobs_joined(from, false);
|
||||
// A mod kept the entity out: tryAddFreshEntityWithPassengers is false.
|
||||
if target.is_none() && self.persist.joins_canceled != canceled {
|
||||
if self.persist.joins_canceled != canceled
|
||||
&& target.is_none_or(|t| self.target_position(t).is_none())
|
||||
{
|
||||
return Err(err("commands.summon.failed.uuid", &[]));
|
||||
}
|
||||
Ok(target)
|
||||
|
||||
@@ -1053,9 +1053,9 @@ impl Game<'_> {
|
||||
for e in &mut events[from..] {
|
||||
if let MobEvent::Projectile(p) = e
|
||||
&& let ProjectileKind::ShulkerBullet { target, .. } = &mut p.kind
|
||||
&& *target == Who::Player
|
||||
&& *target == Some(Who::Player)
|
||||
{
|
||||
*target = Who::Mob(id);
|
||||
*target = Some(Who::Mob(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4813,7 +4813,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,
|
||||
@@ -6528,12 +6537,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(),
|
||||
@@ -6612,14 +6624,18 @@ impl Game<'_> {
|
||||
}
|
||||
// CustomHeadLayer (a pumpkin, skull or head item) included.
|
||||
if layers {
|
||||
// Held items join the GPU path's draws unless an outline reads their vertices.
|
||||
let skin = (skinning && outline.is_none()).then_some(&mut skinned);
|
||||
// Held items join the GPU path's draws, their silhouette too.
|
||||
let skin = (skinning && silhouette_on_gpu).then_some(&mut skinned);
|
||||
self.mob_block_geometry(mob, camera, alpha, packed, skin, out);
|
||||
// `native_mob_layer`: a native mod's layer, over the vanilla ones.
|
||||
self.mod_mob_layer_geometry(mob, who, alpha as f32, camera, packed, out);
|
||||
}
|
||||
if let Some(rgb) = outline {
|
||||
self.mark_outline(&out[entity_start..], rgb);
|
||||
if let Some(((first, _, _), _)) = skinned {
|
||||
let color = horizon_client::mobs::render::abgr(rgb);
|
||||
self.skin_frame.borrow_mut().outline_since(first, color);
|
||||
}
|
||||
if mob.invisible {
|
||||
out.truncate(entity_start);
|
||||
continue;
|
||||
|
||||
@@ -1100,6 +1100,7 @@ impl Game<'_> {
|
||||
| K::EnderPearl
|
||||
| K::LlamaSpit
|
||||
| K::Potion { .. }
|
||||
| K::ExperienceBottle
|
||||
| K::Thrown(_) => (0.25, 0.2125),
|
||||
K::SmallFireball | K::ShulkerBullet { .. } | K::WitherSkull { .. } => {
|
||||
(0.3125, 0.265_625)
|
||||
|
||||
@@ -241,7 +241,7 @@ mod tests {
|
||||
// A shulker bullet takes a blow, with no pick radius round its box.
|
||||
let bullet = Projectile::new(
|
||||
ProjectileKind::ShulkerBullet {
|
||||
target: Who::Player,
|
||||
target: Some(Who::Player),
|
||||
dir: None,
|
||||
steps: 0,
|
||||
delta: [0.0; 3],
|
||||
|
||||
@@ -1456,6 +1456,7 @@ impl Game<'_> {
|
||||
// A gateway keeps the item in its level (teleportSameDimension
|
||||
// moves it): it doesn't leave, so no native_entity_leave_level.
|
||||
self.dropped.remove(i);
|
||||
self.native_leave_skip(id);
|
||||
self.detach_entity(id);
|
||||
tag
|
||||
} else if let Some(i) = self.mobs.iter().position(|m| m.id == id) {
|
||||
@@ -1479,11 +1480,13 @@ impl Game<'_> {
|
||||
}
|
||||
self.mobs.retain(|m| !gone.contains(&m.id));
|
||||
for g in gone {
|
||||
self.native_leave_skip(g);
|
||||
self.detach_entity(g);
|
||||
}
|
||||
tag
|
||||
} else if let Some(i) = self.creatures.projectiles.iter().position(|p| p.id == id) {
|
||||
let pearl = Box::new(self.creatures.projectiles.remove(i));
|
||||
self.native_leave_skip(id);
|
||||
let (job, target) = self.gateway_job([gateway.x, gateway.y, gateway.z]);
|
||||
let arrival = GatewayArrival {
|
||||
traveller: Arriving::Pearl(pearl),
|
||||
|
||||
@@ -623,7 +623,6 @@ impl Game<'_> {
|
||||
items_ignore: items_ignore_explosion(self.ui.rules.bool("mob_griefing"), interaction),
|
||||
spared: exploding.filter(|_| self.combat.explosion_source_named),
|
||||
};
|
||||
let (items_before, orbs_before) = (self.dropped_ids(), self.orb_ids());
|
||||
let (items_len, orbs_len) = (self.dropped.len(), self.orbs.len());
|
||||
let (mut items_kept, mut orbs_kept) = (0, 0);
|
||||
let (mut boats_gone, mut carts_gone) = (0, 0);
|
||||
@@ -723,8 +722,7 @@ impl Game<'_> {
|
||||
}
|
||||
self.dropped.drain(items_kept..items_len);
|
||||
self.orbs.drain(orbs_kept..orbs_len);
|
||||
self.native_items_left_since(&items_before);
|
||||
self.native_orbs_left_since(&orbs_before);
|
||||
self.native_leaves_flush();
|
||||
// Frames, paintings, cushions and armour stands last: vanilla lists
|
||||
// the entities in reach before hurting any, so the items they drop
|
||||
// here are neither destroyed nor pushed by the same blast.
|
||||
|
||||
@@ -618,8 +618,8 @@ impl Game<'_> {
|
||||
seed,
|
||||
);
|
||||
let glowing = self.entity_glowing(item.id);
|
||||
// Posed on the GPU when every layer can be (the outline reads the CPU's vertices).
|
||||
let skinned = (skinning && !glowing)
|
||||
// Posed on the GPU when every layer can be, its silhouette too when it glows.
|
||||
let skinned = skinning
|
||||
.then(|| self.submit_item_skinned(&state, &copies, vertex_light(packed), lights))
|
||||
.flatten();
|
||||
if skinned.is_none() {
|
||||
@@ -630,6 +630,10 @@ impl Game<'_> {
|
||||
if glowing {
|
||||
let rgb = super::glowing::team_color(self.mob_team(item.id));
|
||||
self.mark_outline(&out[entity_start..], rgb);
|
||||
if let Some(((first, _, _), _)) = skinned {
|
||||
let color = horizon_client::mobs::render::abgr(rgb);
|
||||
self.skin_frame.borrow_mut().outline_since(first, color);
|
||||
}
|
||||
} else {
|
||||
self.cull_skinned(out, entity_start, skinned);
|
||||
}
|
||||
|
||||
@@ -635,7 +635,6 @@ impl Game<'_> {
|
||||
// over the 40 a vehicle takes) destroys them. Lightning is no fire
|
||||
// source, so a TNT cart does not prime. `ExperienceOrb.hurtServer`:
|
||||
// 5 damage off 5 health. Last first, since each removes its entry.
|
||||
let orbs_before = self.orb_ids();
|
||||
let mut left = self.thunder_count(strike, ThunderHit::Vehicle)
|
||||
+ self.thunder_count(strike, ThunderHit::Orb);
|
||||
for h in self.entity_handles_rev() {
|
||||
@@ -659,7 +658,7 @@ impl Game<'_> {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self.native_orbs_left_since(&orbs_before);
|
||||
self.native_leaves_flush();
|
||||
// Every player in the box, the server's players too (0.6 x 1.8).
|
||||
let strike_players = move |g: &mut Self| {
|
||||
if let Some(p) = g.player.as_ref().map(|p| p.position)
|
||||
@@ -679,7 +678,6 @@ impl Game<'_> {
|
||||
strike_players(self);
|
||||
self.each_guest_hit(strike_players);
|
||||
// Dropped items burn up (ItemEntity health 5), save fire-resistant ones.
|
||||
let before = self.dropped_ids();
|
||||
let mut left = self.thunder_count(strike, ThunderHit::Item);
|
||||
for h in self.entity_handles_rev() {
|
||||
if left == 0 {
|
||||
@@ -696,7 +694,7 @@ impl Game<'_> {
|
||||
self.dropped.remove(i);
|
||||
}
|
||||
}
|
||||
self.native_items_left_since(&before);
|
||||
self.native_leaves_flush();
|
||||
struck
|
||||
}
|
||||
/// `LightningBolt.shouldRenderAtSqrDistance`: a bolt is drawn within 64
|
||||
|
||||
@@ -258,9 +258,38 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Entities joining the level (`native_entity_join_level`), for every kind in
|
||||
/// the entity walk. Kept out of `native_hooks.rs`, whose lines may not hold macros.
|
||||
/// The entities the walk does not hold: Horizon keeps these in lists of their own.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Apart {
|
||||
Crystal(usize),
|
||||
Cushion(usize),
|
||||
Painting(usize),
|
||||
Frame(usize),
|
||||
Dragon,
|
||||
}
|
||||
|
||||
/// One entity a mod is told of: in the walk, or apart from it.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Slot {
|
||||
Walk(super::entity::EntityHandle),
|
||||
Apart(Apart),
|
||||
}
|
||||
|
||||
/// Entities joining and leaving the level (`native_entity_join_level`,
|
||||
/// `native_entity_leave_level`), for every kind in the entity walk and the
|
||||
/// five kept apart from it. Kept out of `native_hooks.rs`, whose lines may not hold macros.
|
||||
impl Game<'_> {
|
||||
/// Whether an entity may be waiting to be told of: one with an id above
|
||||
/// the last announced, or one of the kinds that take their id late and
|
||||
/// have none. Reads a counter and five lists' ids; no walk.
|
||||
fn joins_pending(&self) -> bool {
|
||||
self.next_entity > self.persist.joined_mark
|
||||
|| self.tnts.iter().any(|t| t.id == 0)
|
||||
|| self.falling.iter().any(|f| f.id == 0)
|
||||
|| self.creatures.projectiles.iter().any(|p| p.id == 0)
|
||||
|| self.tridents.iter().any(|t| t.id == 0)
|
||||
|| self.rockets.iter().any(|r| r.id == 0)
|
||||
}
|
||||
/// Tells `native_entity_join_level` once for every entity added since the
|
||||
/// last call, of any kind, in the order they were created (ids count up
|
||||
/// from one counter). A mod that cancels takes the entity back out here,
|
||||
@@ -268,11 +297,10 @@ impl Game<'_> {
|
||||
/// `loaded_from_disk` is for the entities that came with a chunk. Returns
|
||||
/// whether `watch` (an id, or 0) is still in the level. Walks the lists
|
||||
/// with static dispatch and allocates nothing but the kind name of an
|
||||
/// entity it announces; nothing runs when no mod is loaded or on a client
|
||||
/// of a server (which tells nothing).
|
||||
/// entity it announces; nothing runs when no mod is loaded, on a client
|
||||
/// of a server (which tells nothing), or when nothing is new.
|
||||
pub(super) fn native_joins_flush(&mut self, loaded_from_disk: bool, watch: u32) -> bool {
|
||||
use super::entity::Entity;
|
||||
if self.remote.is_some() || crate::mod_api::mods().is_empty() {
|
||||
if self.remote.is_some() || crate::mod_api::mods().is_empty() || !self.joins_pending() {
|
||||
return true;
|
||||
}
|
||||
for t in &mut self.tnts {
|
||||
@@ -291,11 +319,9 @@ impl Game<'_> {
|
||||
super::lan_entities::assign_entity_id(&mut self.next_entity, &mut r.id);
|
||||
}
|
||||
let mut kept = true;
|
||||
while let Some((handle, id)) = self.next_unannounced() {
|
||||
while let Some((slot, id)) = self.next_unannounced() {
|
||||
self.persist.joined_mark = id;
|
||||
let (kind, at) = super::entity::with_entity!(self, handle, e => {
|
||||
(e.native_type(), e.position())
|
||||
});
|
||||
let (kind, at) = self.slot_kind_and_position(slot);
|
||||
let mut canceled = false;
|
||||
let dimension = self.dim.id();
|
||||
self.native_entity_join_level(
|
||||
@@ -311,32 +337,162 @@ impl Game<'_> {
|
||||
self.forget_entity(id);
|
||||
// Handles are indexes: the entity is taken out before any
|
||||
// other change to its list.
|
||||
self.discard_entity(handle);
|
||||
self.discard_slot(slot);
|
||||
kept &= id != watch;
|
||||
} else {
|
||||
self.persist.live.push((id, kind.into_owned(), true));
|
||||
}
|
||||
}
|
||||
kept
|
||||
}
|
||||
/// The entity with the lowest id above the last one announced, by handle.
|
||||
fn next_unannounced(&self) -> Option<(super::entity::EntityHandle, u32)> {
|
||||
/// The entity with the lowest id above the last one announced.
|
||||
fn next_unannounced(&self) -> Option<(Slot, u32)> {
|
||||
use super::entity::Entity;
|
||||
let mark = self.persist.joined_mark;
|
||||
let mut best: Option<(super::entity::EntityHandle, u32)> = None;
|
||||
for handle in self.entity_handles() {
|
||||
let id = {
|
||||
use super::entity::Entity;
|
||||
super::entity::with_entity!(self, handle, e => e.id())
|
||||
};
|
||||
let mut best: Option<(Slot, u32)> = None;
|
||||
let mut offer = |slot: Slot, id: u32| {
|
||||
if id > mark && best.is_none_or(|(_, b)| id < b) {
|
||||
best = Some((handle, id));
|
||||
best = Some((slot, id));
|
||||
}
|
||||
};
|
||||
for handle in self.entity_handles() {
|
||||
offer(
|
||||
Slot::Walk(handle),
|
||||
super::entity::with_entity!(self, handle, e => e.id()),
|
||||
);
|
||||
}
|
||||
for (i, c) in self.crystals.iter().enumerate() {
|
||||
offer(Slot::Apart(Apart::Crystal(i)), c.id);
|
||||
}
|
||||
for (i, c) in self.cushions.iter().enumerate() {
|
||||
offer(Slot::Apart(Apart::Cushion(i)), c.id);
|
||||
}
|
||||
for (i, p) in self.paintings.iter().enumerate() {
|
||||
offer(Slot::Apart(Apart::Painting(i)), p.id);
|
||||
}
|
||||
for (i, f) in self.item_frames.iter().enumerate() {
|
||||
offer(Slot::Apart(Apart::Frame(i)), f.id);
|
||||
}
|
||||
if let Some(d) = &self.dragon {
|
||||
offer(Slot::Apart(Apart::Dragon), d.id);
|
||||
}
|
||||
best
|
||||
}
|
||||
/// The type id and position a mod is told of.
|
||||
fn slot_kind_and_position(&self, slot: Slot) -> (std::borrow::Cow<'static, str>, Vec3) {
|
||||
use super::entity::Entity;
|
||||
let block = |p: [i32; 3]| {
|
||||
Vec3::new(
|
||||
f64::from(p[0]) + 0.5,
|
||||
f64::from(p[1]) + 0.5,
|
||||
f64::from(p[2]) + 0.5,
|
||||
)
|
||||
};
|
||||
match slot {
|
||||
Slot::Walk(h) => {
|
||||
super::entity::with_entity!(self, h, e => (e.native_type(), e.position()))
|
||||
}
|
||||
Slot::Apart(Apart::Crystal(i)) => {
|
||||
("minecraft:end_crystal".into(), self.crystals[i].position)
|
||||
}
|
||||
Slot::Apart(Apart::Cushion(i)) => {
|
||||
("minecraft:cushion".into(), self.cushions[i].position)
|
||||
}
|
||||
Slot::Apart(Apart::Painting(i)) => {
|
||||
("minecraft:painting".into(), block(self.paintings[i].pos))
|
||||
}
|
||||
Slot::Apart(Apart::Frame(i)) => {
|
||||
let f = &self.item_frames[i];
|
||||
let kind = if f.glow {
|
||||
"minecraft:glow_item_frame"
|
||||
} else {
|
||||
"minecraft:item_frame"
|
||||
};
|
||||
(kind.into(), block(f.pos))
|
||||
}
|
||||
Slot::Apart(Apart::Dragon) => (
|
||||
"minecraft:ender_dragon".into(),
|
||||
self.dragon.as_ref().map_or(Vec3::ZERO, |d| d.position),
|
||||
),
|
||||
}
|
||||
}
|
||||
/// Takes the entity out of the level, before it ever ticked.
|
||||
fn discard_slot(&mut self, slot: Slot) {
|
||||
match slot {
|
||||
Slot::Walk(h) => self.discard_entity(h),
|
||||
Slot::Apart(Apart::Crystal(i)) => drop(self.crystals.remove(i)),
|
||||
Slot::Apart(Apart::Cushion(i)) => drop(self.cushions.remove(i)),
|
||||
Slot::Apart(Apart::Painting(i)) => drop(self.paintings.remove(i)),
|
||||
Slot::Apart(Apart::Frame(i)) => drop(self.item_frames.remove(i)),
|
||||
Slot::Apart(Apart::Dragon) => self.dragon = None,
|
||||
}
|
||||
}
|
||||
/// Entities that were moved, not added (a portal's arrival keeps being
|
||||
/// silent): everything now in the level counts as announced.
|
||||
/// silent): everything now in the level counts as announced, and will
|
||||
/// be told when it leaves.
|
||||
pub(super) fn native_joins_skip(&mut self) {
|
||||
if self.remote.is_none() && !crate::mod_api::mods().is_empty() {
|
||||
while let Some((slot, id)) = self.next_unannounced() {
|
||||
self.persist.joined_mark = id;
|
||||
let kind = self.slot_kind_and_position(slot).0.into_owned();
|
||||
self.persist.live.push((id, kind, true));
|
||||
}
|
||||
}
|
||||
self.persist.joined_mark = self.persist.joined_mark.max(self.next_entity);
|
||||
}
|
||||
/// An entity taken out of the level without leaving it (an item through
|
||||
/// an End gateway): forgotten, not told of.
|
||||
pub(super) fn native_leave_skip(&mut self, id: u32) {
|
||||
if let Ok(i) = self.persist.live.binary_search_by_key(&id, |l| l.0) {
|
||||
self.persist.live.remove(i);
|
||||
}
|
||||
}
|
||||
/// Entity `id` has left the level: told once, if mods were told it joined.
|
||||
pub(super) fn native_entity_left(&mut self, id: u32) {
|
||||
if self.remote.is_some() {
|
||||
return;
|
||||
}
|
||||
if let Ok(i) = self.persist.live.binary_search_by_key(&id, |l| l.0) {
|
||||
let (id, kind, _) = self.persist.live.remove(i);
|
||||
let dimension = self.dim.id();
|
||||
self.native_entity_leave_level(dimension, id, &kind);
|
||||
}
|
||||
}
|
||||
/// Tells `native_entity_leave_level` for every announced entity that is
|
||||
/// no longer in any list (died, despawned, picked up, burnt, blown up, gone
|
||||
/// through a portal, unloaded), lowest id first. Marks the entities still
|
||||
/// there in the registry and tells the rest: no allocation, and nothing
|
||||
/// runs when no mod is loaded or none was announced.
|
||||
pub(super) fn native_leaves_flush(&mut self) {
|
||||
use super::entity::Entity;
|
||||
if self.persist.live.is_empty() || self.remote.is_some() {
|
||||
return;
|
||||
}
|
||||
for l in &mut self.persist.live {
|
||||
l.2 = false;
|
||||
}
|
||||
let live = &mut self.persist.live;
|
||||
let mut seen = |id: u32| {
|
||||
if let Ok(i) = live.binary_search_by_key(&id, |l| l.0) {
|
||||
live[i].2 = true;
|
||||
}
|
||||
};
|
||||
super::entity::for_each_entity!(super::entity::entity_lists_of!(self), e => {
|
||||
seen(e.id());
|
||||
});
|
||||
self.crystals.iter().for_each(|c| seen(c.id));
|
||||
self.cushions.iter().for_each(|c| seen(c.id));
|
||||
self.paintings.iter().for_each(|p| seen(p.id));
|
||||
self.item_frames.iter().for_each(|f| seen(f.id));
|
||||
if let Some(d) = &self.dragon {
|
||||
seen(d.id);
|
||||
}
|
||||
while let Some(i) = self.persist.live.iter().position(|l| !l.2) {
|
||||
let (id, kind, _) = self.persist.live.remove(i);
|
||||
let dimension = self.dim.id();
|
||||
self.native_entity_leave_level(dimension, id, &kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -697,7 +697,7 @@
|
||||
//! change a cauldron.
|
||||
//!
|
||||
//! API 53 adds entities leaving a level (`native_entity_leave_level`), for
|
||||
//! item entities, experience orbs and mobs so far.
|
||||
//! every kind the join hook tells (see API 47).
|
||||
//!
|
||||
//! API 52 adds `native_block_entity_sync`: a mod block entity's block and update tag
|
||||
//! sent to clients again (`Level.sendBlockUpdated`).
|
||||
@@ -718,7 +718,7 @@
|
||||
//! API 47 adds entities joining a level (`native_entity_join_level`); it tells
|
||||
//! every kind the game keeps in its entity lists (items, orbs, mobs, boats,
|
||||
//! minecarts, armor stands, TNT, falling blocks, projectiles, rockets,
|
||||
//! tridents).
|
||||
//! tridents, end crystals, the dragon, cushions, paintings, item frames).
|
||||
//!
|
||||
//! API 46 adds bounded world-space rim and colored-light requests
|
||||
//! (`native_visuals`) and opt-in texture-matched emissive material
|
||||
@@ -2309,6 +2309,16 @@ impl Game<'_> {
|
||||
pub(super) fn native_commands(&mut self, commands: &mut Vec<&'static str>) {
|
||||
mod_api::dispatch_commands(self, mod_api::mods(), commands);
|
||||
}
|
||||
/// Local player movement input, after reading it and before applying it.
|
||||
pub(super) fn native_movement_input(
|
||||
&self,
|
||||
forward: &mut f32,
|
||||
strafe: &mut f32,
|
||||
jump: &mut bool,
|
||||
sneak: &mut bool,
|
||||
) {
|
||||
mod_api::dispatch_movement_input(self, mod_api::mods(), forward, strafe, jump, sneak);
|
||||
}
|
||||
/// World field of view, after normal calculation and zoom and before projection/Nausea.
|
||||
/// Called for world rendering and near-plane overlays; hand/HUD FOV uses its own path.
|
||||
/// Only finite values in 1..=179 degrees replace the computed FOV.
|
||||
@@ -6369,8 +6379,10 @@ impl Game<'_> {
|
||||
/// a level closing with nobody in it included. Not for the entities still
|
||||
/// loaded when the server stops, as in vanilla. `entity` is its ID and
|
||||
/// `kind` its type (`minecraft:item`); it is no longer in the level's lists.
|
||||
/// It only hears. 53 tells item entities, experience orbs and mobs (`kind`
|
||||
/// the mob type); later APIs add other kinds, so check `kind`. Server-side.
|
||||
/// It only hears. 53 told item entities, experience orbs and mobs; it now
|
||||
/// tells every kind `native_entity_join_level` does (`kind` is the type), each
|
||||
/// once, when the game next notices it gone (the tick or action that removed
|
||||
/// it). Check `kind`. Server-side.
|
||||
pub(super) fn native_entity_leave_level(&mut self, dimension: &str, entity: u32, kind: &str) {
|
||||
mod_api::dispatch_entity_leave_level(self, mod_api::mods(), dimension, entity, kind);
|
||||
}
|
||||
@@ -6384,12 +6396,14 @@ impl Game<'_> {
|
||||
/// back out as if never added, and a `/summon` of it fails. Tells every
|
||||
/// kind in the game's entity lists (items, orbs, mobs, boats, minecarts,
|
||||
/// armor stands, primed TNT, falling blocks, projectiles, rockets,
|
||||
/// tridents), once each, in the order they were created; check `kind`. A
|
||||
/// gameplay spawn is told when the next tick starts (a drop, an orb, a mob, a
|
||||
/// `/summon` and a chunk's entities are told at once). A mod that
|
||||
/// cancels one of the others sees it removed before it ticks again.
|
||||
/// Not told: end crystals, the dragon, cushions, paintings, item frames and
|
||||
/// the entities Horizon keeps without simulating. Server-side.
|
||||
/// tridents, end crystals, the dragon, cushions, paintings, item frames),
|
||||
/// once each, in the order they were created; check `kind`. A drop, an
|
||||
/// orb, a mob, a `/summon` and a chunk's entities are told at once, any
|
||||
/// other spawn between the tick's phases (it is checked after the blocks,
|
||||
/// the vehicles, the projectiles, the items and the mobs, and at the
|
||||
/// tick's end). A mod that cancels one of the others sees it removed
|
||||
/// before it ticks again. Not told: the entities Horizon keeps without
|
||||
/// simulating (displays, markers, interactions). Server-side.
|
||||
pub(super) fn native_entity_join_level(
|
||||
&mut self,
|
||||
dimension: &str,
|
||||
@@ -8310,22 +8324,6 @@ pub(super) fn mob_kind(mob: &horizon_client::mobs::Mob) -> String {
|
||||
["minecraft:", mob.kind.id.trim_start_matches("minecraft:")].concat()
|
||||
}
|
||||
|
||||
/// The ids of `before` (id, kind) that are not in `now`, grouped by kind in
|
||||
/// first-seen order: the entities that left the level.
|
||||
fn vanished_by_kind(
|
||||
before: &[(u32, String)],
|
||||
now: &std::collections::HashSet<u32>,
|
||||
) -> Vec<(String, Vec<u32>)> {
|
||||
let mut out: Vec<(String, Vec<u32>)> = Vec::new();
|
||||
for (id, kind) in before.iter().filter(|(id, _)| !now.contains(id)) {
|
||||
match out.iter_mut().find(|(k, _)| k == kind) {
|
||||
Some((_, ids)) => ids.push(*id),
|
||||
None => out.push((kind.clone(), Vec::from([*id]))),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Where the game calls the hook points.
|
||||
impl Game<'_> {
|
||||
/// The acting player as the hook points name one: the guest whose turn
|
||||
@@ -8447,80 +8445,6 @@ impl Game<'_> {
|
||||
self.native_mob_spawned(self.dim.id(), &kind, pos, natural);
|
||||
}
|
||||
}
|
||||
/// `native_entity_leave_level` for each of `entities`, of `kind`, gone
|
||||
/// from the level in place. A client of a server tells nothing.
|
||||
pub(super) fn native_entities_left(&mut self, kind: &str, entities: &[u32]) {
|
||||
if self.remote.is_some() {
|
||||
return;
|
||||
}
|
||||
let dimension = self.dim.id();
|
||||
for &entity in entities {
|
||||
self.native_entity_leave_level(dimension, entity, kind);
|
||||
}
|
||||
}
|
||||
/// The IDs of the level's item entities, for `native_items_left_since`
|
||||
/// (none on a client of a server, which tells nothing).
|
||||
pub(super) fn dropped_ids(&self) -> Vec<u32> {
|
||||
if self.remote.is_some() || mod_api::mods().is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
self.dropped.iter().map(|e| e.id).collect()
|
||||
}
|
||||
/// The IDs of the level's experience orbs, for `native_orbs_left_since`.
|
||||
pub(super) fn orb_ids(&self) -> Vec<u32> {
|
||||
if self.remote.is_some() || mod_api::mods().is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
self.orbs.iter().map(|o| o.id).collect()
|
||||
}
|
||||
/// The (id, kind) of the level's mobs, for `native_mobs_left_since` (none
|
||||
/// on a client of a server, which tells nothing).
|
||||
pub(super) fn mob_ids(&self) -> Vec<(u32, String)> {
|
||||
if self.remote.is_some() || mod_api::mods().is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
self.mobs.iter().map(|m| (m.id, mob_kind(m))).collect()
|
||||
}
|
||||
/// The mobs of `before` gone since have left the level (died, despawned,
|
||||
/// entered a hive, unloaded). Mobs spawned meanwhile are not counted.
|
||||
pub(super) fn native_mobs_left_since(&mut self, before: &[(u32, String)]) {
|
||||
// Nothing was taken: no mod listens, or a client of a server.
|
||||
if before.is_empty() {
|
||||
return;
|
||||
}
|
||||
let now: std::collections::HashSet<u32> = self.mobs.iter().map(|m| m.id).collect();
|
||||
for (kind, ids) in vanished_by_kind(before, &now) {
|
||||
self.native_entities_left(&kind, &ids);
|
||||
}
|
||||
}
|
||||
/// The item entities of `before` gone since have left the level.
|
||||
pub(super) fn native_items_left_since(&mut self, before: &[u32]) {
|
||||
// Nothing was taken: no mod listens, or a client of a server.
|
||||
if before.is_empty() {
|
||||
return;
|
||||
}
|
||||
let now: std::collections::HashSet<u32> = self.dropped.iter().map(|e| e.id).collect();
|
||||
let gone: Vec<u32> = before
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|id| !now.contains(id))
|
||||
.collect();
|
||||
self.native_entities_left("minecraft:item", &gone);
|
||||
}
|
||||
/// The experience orbs of `before` gone since have left the level.
|
||||
pub(super) fn native_orbs_left_since(&mut self, before: &[u32]) {
|
||||
// Nothing was taken: no mod listens, or a client of a server.
|
||||
if before.is_empty() {
|
||||
return;
|
||||
}
|
||||
let now: std::collections::HashSet<u32> = self.orbs.iter().map(|o| o.id).collect();
|
||||
let gone: Vec<u32> = before
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|id| !now.contains(id))
|
||||
.collect();
|
||||
self.native_entities_left("minecraft:experience_orb", &gone);
|
||||
}
|
||||
/// The acting player's blow killed mob `index`.
|
||||
pub(super) fn native_kill_event(&mut self, index: usize) {
|
||||
let (entity, kind) = (self.mobs[index].id, mob_kind(&self.mobs[index]));
|
||||
@@ -9371,26 +9295,6 @@ fn mod_screen_opens(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn vanished_mobs_are_reported_by_kind() {
|
||||
use std::collections::HashSet;
|
||||
let z = || "minecraft:zombie".to_string();
|
||||
let b = || "minecraft:bee".to_string();
|
||||
let before = vec![(1, z()), (2, b()), (3, z()), (4, b())];
|
||||
let now: HashSet<u32> = [1, 4, 99].into_iter().collect(); // 99 spawned meanwhile
|
||||
assert_eq!(
|
||||
super::vanished_by_kind(&before, &now),
|
||||
vec![(b(), vec![2]), (z(), vec![3])]
|
||||
);
|
||||
let all: HashSet<u32> = [1, 2, 3, 4, 5].into_iter().collect();
|
||||
assert!(super::vanished_by_kind(&before, &all).is_empty());
|
||||
assert_eq!(
|
||||
super::vanished_by_kind(&before, &HashSet::new()),
|
||||
vec![(z(), vec![1, 3]), (b(), vec![2, 4])]
|
||||
);
|
||||
assert!(super::vanished_by_kind(&[], &now).is_empty());
|
||||
}
|
||||
|
||||
/// Exercise the gameplay constructor, where name-based entities compete
|
||||
/// with a mod block's sign template.
|
||||
#[test]
|
||||
|
||||
@@ -45,6 +45,10 @@ pub struct EntityMemory {
|
||||
pub(super) joins_canceled: u32,
|
||||
/// The highest entity id `native_entity_join_level` has been told of.
|
||||
pub(super) joined_mark: u32,
|
||||
/// The entities mods were told of and that have not left since: id (in
|
||||
/// increasing order), type, and whether the last leave sweep saw it.
|
||||
/// Only filled while a mod is loaded.
|
||||
pub(super) live: Vec<(u32, String, bool)>,
|
||||
}
|
||||
|
||||
/// The key that marks a compound queued with the chunk loads as a fresh add
|
||||
@@ -355,6 +359,10 @@ impl Game<'_> {
|
||||
kind.as_str(),
|
||||
"minecraft:experience_orb"
|
||||
| "minecraft:arrow"
|
||||
| "minecraft:spectral_arrow"
|
||||
| "minecraft:llama_spit"
|
||||
| "minecraft:shulker_bullet"
|
||||
| "minecraft:experience_bottle"
|
||||
| "minecraft:fireball"
|
||||
| "minecraft:small_fireball"
|
||||
| "minecraft:wind_charge"
|
||||
@@ -398,7 +406,7 @@ impl Game<'_> {
|
||||
self.remember_tags(id, &tag);
|
||||
}
|
||||
}
|
||||
"minecraft:arrow" => {
|
||||
"minecraft:arrow" | "minecraft:spectral_arrow" => {
|
||||
if let Some((mut p, in_block)) = entity_nbt::arrow_from_nbt(
|
||||
&tag,
|
||||
player_uuid,
|
||||
@@ -431,6 +439,17 @@ impl Game<'_> {
|
||||
self.remember_last_projectile_tags(&tag);
|
||||
}
|
||||
}
|
||||
"minecraft:llama_spit" | "minecraft:shulker_bullet" => {
|
||||
if let Some(mut p) = entity_nbt::spit_or_bullet_from_nbt(&tag, player_uuid)
|
||||
{
|
||||
self.claim_owner(&mut p);
|
||||
self.next_entity += 1;
|
||||
p.id = self.next_entity;
|
||||
self.restore_portal_cooldown(p.id, &tag);
|
||||
self.add_projectile(p);
|
||||
self.remember_last_projectile_tags(&tag);
|
||||
}
|
||||
}
|
||||
id if entity_nbt::throwable_kind(id).is_some() => {
|
||||
if let Some(mut p) = entity_nbt::throwable_from_nbt(&tag, player_uuid) {
|
||||
self.claim_owner(&mut p);
|
||||
@@ -1326,7 +1345,6 @@ impl Game<'_> {
|
||||
})
|
||||
.map(|i| self.mobs[i].id)
|
||||
.collect();
|
||||
let mobs_before = self.mob_ids();
|
||||
self.mobs.retain(|m| {
|
||||
let keep = !leaving.contains(&m.id);
|
||||
if !keep {
|
||||
@@ -1334,7 +1352,6 @@ impl Game<'_> {
|
||||
}
|
||||
keep
|
||||
});
|
||||
let (items_before, orbs_before) = (self.dropped_ids(), self.orb_ids());
|
||||
self.dropped.retain(|e| {
|
||||
let keep = !gone(e.position);
|
||||
if !keep {
|
||||
@@ -1344,9 +1361,7 @@ impl Game<'_> {
|
||||
});
|
||||
self.orbs.retain(|o| !gone(o.position));
|
||||
// Unloaded with their chunk: PersistentEntitySectionManager.stopTracking.
|
||||
self.native_mobs_left_since(&mobs_before);
|
||||
self.native_items_left_since(&items_before);
|
||||
self.native_orbs_left_since(&orbs_before);
|
||||
self.native_leaves_flush();
|
||||
// the ones saved with the chunk go with it (a trident too, unsaved).
|
||||
self.creatures.projectiles.retain(|p| {
|
||||
let leaves = entity_nbt::saved_with_chunk(p) || p.arrow.is_some();
|
||||
@@ -1512,7 +1527,8 @@ impl Game<'_> {
|
||||
.map(|s| self.state_to_nbt(s));
|
||||
let tag = entity_nbt::arrow_to_nbt(p, uuid, owner, in_block, raw.as_ref())
|
||||
.or_else(|| entity_nbt::fireball_to_nbt(p, uuid, owner, raw.as_ref()))
|
||||
.or_else(|| entity_nbt::throwable_to_nbt(p, uuid, owner, raw.as_ref()))?;
|
||||
.or_else(|| entity_nbt::throwable_to_nbt(p, uuid, owner, raw.as_ref()))
|
||||
.or_else(|| entity_nbt::spit_or_bullet_to_nbt(p, uuid, owner, raw.as_ref()))?;
|
||||
Some((
|
||||
chunk_of(p.position),
|
||||
with_raw(tag, self.persist.raw.get(&p.id)),
|
||||
|
||||
@@ -5781,6 +5781,20 @@ impl Game<'_> {
|
||||
if self.integrated.is_none() && self.server_address().is_some() {
|
||||
keys.analog = None;
|
||||
}
|
||||
let [mut strafe, mut forward] = keys.direction();
|
||||
let original = [strafe, forward];
|
||||
self.native_movement_input(&mut forward, &mut strafe, &mut keys.jump, &mut keys.shift);
|
||||
if [strafe, forward] != original {
|
||||
if forward != original[1] {
|
||||
keys.forward = forward > 0.0;
|
||||
keys.backward = forward < 0.0;
|
||||
}
|
||||
if strafe != original[0] {
|
||||
keys.left = strafe > 0.0;
|
||||
keys.right = strafe < 0.0;
|
||||
}
|
||||
keys.analog = Some([strafe, forward]);
|
||||
}
|
||||
let input = u8::from(keys.forward)
|
||||
| u8::from(keys.backward) << 1
|
||||
| u8::from(keys.left) << 2
|
||||
|
||||
@@ -98,7 +98,7 @@ fn projectile_kind(kind: &str) -> Option<ProjectileKind> {
|
||||
"minecraft:breeze_wind_charge" => ProjectileKind::BreezeWindCharge,
|
||||
// Homing state stays on the server; the client interpolates its positions.
|
||||
"minecraft:shulker_bullet" => ProjectileKind::ShulkerBullet {
|
||||
target: horizon_client::mobs::Who::Player,
|
||||
target: Some(horizon_client::mobs::Who::Player),
|
||||
dir: None,
|
||||
steps: 0,
|
||||
delta: [0.0; 3],
|
||||
|
||||
@@ -222,7 +222,10 @@ impl Game<'_> {
|
||||
if r.trial {
|
||||
tag.insert("PersistenceRequired".into(), Tag::Byte(1));
|
||||
}
|
||||
let tag = Tag::Compound(tag);
|
||||
let mut tag = Tag::Compound(tag);
|
||||
// EntityType.loadEntityRecursive applies the spawner's placement
|
||||
// callback to every passenger as well as the root.
|
||||
super::commands::place_summoned(&mut tag, r.position);
|
||||
let mob = horizon_client::entity_nbt::mob_from_nbt(
|
||||
&tag,
|
||||
self.res.mob_types(),
|
||||
|
||||
@@ -636,6 +636,7 @@ pub fn tick(
|
||||
if m.species() == Species::Endermite {
|
||||
endermite_lifetime(m);
|
||||
}
|
||||
ageable_step(m, w, level_random, events);
|
||||
// AbstractHorse.tick runs the stand counter down with or without AI.
|
||||
if super::equine::is_equine(m.species())
|
||||
&& !super::camel::is_camel(m.species())
|
||||
@@ -1263,6 +1264,68 @@ fn play_ambient(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
fn turtle_ambient_silent(m: &Mob) -> bool {
|
||||
m.fluids.in_water || !m.on_ground || m.is_baby()
|
||||
}
|
||||
/// `AgeableMob.aiStep` runs even when `Mob.serverAiStep` is disabled.
|
||||
fn ageable_step(
|
||||
m: &mut Mob,
|
||||
w: &MobWorld,
|
||||
level_random: &mut LegacyRandom,
|
||||
events: &mut Vec<MobEvent>,
|
||||
) {
|
||||
// AgeableMob.aiStep: a forced age-up's particles (`sendParticles`, so the
|
||||
// players are sent them), then the age.
|
||||
if m.extra.forced_age_timer > 0 {
|
||||
if m.extra.forced_age_timer % 4 == 0 {
|
||||
let (w, h) = (f64::from(m.width()), f64::from(m.height()));
|
||||
let r = &mut m.random;
|
||||
let x = m.position.x + w * (2.0 * r.next_double() - 1.0);
|
||||
let y = m.position.y + h * r.next_double() + 0.5;
|
||||
let z = m.position.z + w * (2.0 * r.next_double() - 1.0);
|
||||
// sendParticles(HAPPY_VILLAGER, x, y, z, 1, 0, 0, 0, 0): exactly there, at rest.
|
||||
events.push(MobEvent::LevelParticles {
|
||||
kind: "happy_villager",
|
||||
at: [x, y, z],
|
||||
spread: [0.0; 3],
|
||||
speed: 0.0,
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
m.extra.forced_age_timer -= 1;
|
||||
}
|
||||
// AgeableMob.aiStep: an age counts only for its subclasses (a tadpole
|
||||
// counts its own age up, a zombie's baby age never moves). `canAgeUp`:
|
||||
// a baby grows unless an age lock holds it; a breeding pause counts down.
|
||||
if m.alive() && super::finalize::ageable(m.species()) {
|
||||
if can_age_up(m) {
|
||||
let (bw, bh, _) = m.dims();
|
||||
// AgeableMob.setAge(getAge() + 1): the crossing is where a species'
|
||||
// ageBoundaryReached runs (a goat's and a hoglin's attack damage, a
|
||||
// sulfur cube's size), which the age write used to skip.
|
||||
let next = m.extra.age + 1;
|
||||
super::finalize::set_age(m, next);
|
||||
// setAge at the age boundary: refreshDimensions.
|
||||
if m.extra.age >= 0 {
|
||||
m.fudge_position_after_size_change(w, bw, bh);
|
||||
if m.species() == Species::Villager {
|
||||
super::villager_brain::age_boundary_reached(m, w, level_random, events);
|
||||
}
|
||||
// Turtle.ageBoundaryReached: a grown turtle drops a scute.
|
||||
if m.species() == Species::Turtle {
|
||||
events.push(MobEvent::GiftLoot {
|
||||
mob: m.id,
|
||||
table: "minecraft:gameplay/turtle_grow",
|
||||
at: [m.position.x, m.position.y, m.position.z],
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if m.extra.age > 0 {
|
||||
m.extra.age -= 1;
|
||||
}
|
||||
}
|
||||
if super::finalize::ageable(m.species()) {
|
||||
make_age_locked_particle(m, events);
|
||||
}
|
||||
}
|
||||
|
||||
/// LivingEntity.aiStep (and Mob/AgeableMob/Animal additions).
|
||||
fn ai_step(m: &mut Mob, w: &MobWorld, level_random: &mut LegacyRandom, events: &mut Vec<MobEvent>) {
|
||||
if m.species() == Species::Parrot {
|
||||
@@ -1337,59 +1400,7 @@ fn ai_step(m: &mut Mob, w: &MobWorld, level_random: &mut LegacyRandom, events: &
|
||||
if m.kind.flags.climber {
|
||||
m.climbing = m.horizontal_collision;
|
||||
}
|
||||
// AgeableMob.aiStep: a forced age-up's particles (`sendParticles`, so the
|
||||
// players are sent them), then the age.
|
||||
if m.extra.forced_age_timer > 0 {
|
||||
if m.extra.forced_age_timer % 4 == 0 {
|
||||
let (w, h) = (f64::from(m.width()), f64::from(m.height()));
|
||||
let r = &mut m.random;
|
||||
let x = m.position.x + w * (2.0 * r.next_double() - 1.0);
|
||||
let y = m.position.y + h * r.next_double() + 0.5;
|
||||
let z = m.position.z + w * (2.0 * r.next_double() - 1.0);
|
||||
// sendParticles(HAPPY_VILLAGER, x, y, z, 1, 0, 0, 0, 0): exactly there, at rest.
|
||||
events.push(MobEvent::LevelParticles {
|
||||
kind: "happy_villager",
|
||||
at: [x, y, z],
|
||||
spread: [0.0; 3],
|
||||
speed: 0.0,
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
m.extra.forced_age_timer -= 1;
|
||||
}
|
||||
// AgeableMob.aiStep: an age counts only for its subclasses (a tadpole
|
||||
// counts its own age up, a zombie's baby age never moves). `canAgeUp`:
|
||||
// a baby grows unless an age lock holds it; a breeding pause counts down.
|
||||
if m.alive() && super::finalize::ageable(m.species()) {
|
||||
if can_age_up(m) {
|
||||
let (bw, bh, _) = m.dims();
|
||||
// AgeableMob.setAge(getAge() + 1): the crossing is where a species'
|
||||
// ageBoundaryReached runs (a goat's and a hoglin's attack damage, a
|
||||
// sulfur cube's size), which the age write used to skip.
|
||||
let next = m.extra.age + 1;
|
||||
super::finalize::set_age(m, next);
|
||||
// setAge at the age boundary: refreshDimensions.
|
||||
if m.extra.age >= 0 {
|
||||
m.fudge_position_after_size_change(w, bw, bh);
|
||||
if m.species() == Species::Villager {
|
||||
super::villager_brain::age_boundary_reached(m, w, level_random, events);
|
||||
}
|
||||
// Turtle.ageBoundaryReached: a grown turtle drops a scute.
|
||||
if m.species() == Species::Turtle {
|
||||
events.push(MobEvent::GiftLoot {
|
||||
mob: m.id,
|
||||
table: "minecraft:gameplay/turtle_grow",
|
||||
at: [m.position.x, m.position.y, m.position.z],
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if m.extra.age > 0 {
|
||||
m.extra.age -= 1;
|
||||
}
|
||||
}
|
||||
if super::finalize::ageable(m.species()) {
|
||||
make_age_locked_particle(m, events);
|
||||
}
|
||||
ageable_step(m, w, level_random, events);
|
||||
if m.alive() {
|
||||
// Animal.aiStep
|
||||
if m.extra.age != 0 {
|
||||
@@ -3431,6 +3442,75 @@ mod age_up_tests {
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Official Java 26.3: NoAI leaves both age clocks running. AgeLocked
|
||||
/// holds a baby's age, but a positive breeding cooldown still expires.
|
||||
#[test]
|
||||
fn age_clocks_tick_with_no_ai_and_respect_baby_age_lock() {
|
||||
let ran = crate::mobs::world::with_test_world(&[], |w, types| {
|
||||
for species in ["pig", "cow", "horse", "turtle"] {
|
||||
for no_ai in [true, false] {
|
||||
for locked in [false, true] {
|
||||
for age in [-2, 2] {
|
||||
let kind =
|
||||
Arc::new(types.types[&format!("minecraft:{species}")].clone());
|
||||
let mut m = Mob::new(1, kind, Vec3::new(4.0, 220.0, 4.0), 0.0);
|
||||
m.no_ai = no_ai;
|
||||
m.no_gravity = true;
|
||||
m.persistent = true;
|
||||
m.extra.age_locked = locked;
|
||||
super::super::finalize::set_age(&mut m, age);
|
||||
let (mut random, mut events) = (LegacyRandom::new(0), Vec::new());
|
||||
let expected = if age > 0 {
|
||||
[1, 0, 0, 0]
|
||||
} else if locked {
|
||||
[-2, -2, -2, -2]
|
||||
} else {
|
||||
[-1, 0, 0, 0]
|
||||
};
|
||||
for (elapsed, expected_age) in expected.into_iter().enumerate() {
|
||||
let previous_age = m.extra.age;
|
||||
let previous_position = m.position;
|
||||
tick(&mut m, w, &mut random, &mut events);
|
||||
assert_eq!(
|
||||
m.extra.age,
|
||||
expected_age,
|
||||
"{species}: NoAI={no_ai}, AgeLocked={locked}, initial Age={age}, tick={}",
|
||||
elapsed + 1
|
||||
);
|
||||
if no_ai {
|
||||
assert_eq!(m.velocity, Vec3::ZERO);
|
||||
// Growing dimensions can reposition the mob at adulthood.
|
||||
if previous_age != -1 || expected_age != 0 {
|
||||
assert_eq!(m.position, previous_position);
|
||||
}
|
||||
}
|
||||
}
|
||||
if species == "turtle" && age < 0 && !locked {
|
||||
assert_eq!(
|
||||
events
|
||||
.iter()
|
||||
.filter(|event| matches!(
|
||||
event,
|
||||
MobEvent::GiftLoot {
|
||||
table: "minecraft:gameplay/turtle_grow",
|
||||
..
|
||||
}
|
||||
))
|
||||
.count(),
|
||||
1,
|
||||
"growing up drops one scute"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if !ran {
|
||||
eprintln!("age clock regression needs prepared vanilla resources");
|
||||
}
|
||||
}
|
||||
|
||||
/// A baby goat `age` ticks from adulthood, from no vanilla data.
|
||||
fn baby_goat(age: i32) -> Mob {
|
||||
let mut kind = crate::mobs::sensor_scan_tests::kind();
|
||||
|
||||
+47
-8
@@ -714,12 +714,14 @@ pub trait Hooks: Send + Sync {
|
||||
/// back out as if never added, and a `/summon` of it fails. Tells every
|
||||
/// kind in the game's entity lists (items, orbs, mobs, boats, minecarts,
|
||||
/// armor stands, primed TNT, falling blocks, projectiles, rockets,
|
||||
/// tridents), once each, in the order they were created; check `kind`. A
|
||||
/// gameplay spawn is told when the next tick starts (a drop, an orb, a mob, a
|
||||
/// `/summon` and a chunk's entities are told at once). A mod that
|
||||
/// cancels one of the others sees it removed before it ticks again.
|
||||
/// Not told: end crystals, the dragon, cushions, paintings, item frames and
|
||||
/// the entities Horizon keeps without simulating. Server-side.
|
||||
/// tridents, end crystals, the dragon, cushions, paintings, item frames),
|
||||
/// once each, in the order they were created; check `kind`. A drop, an
|
||||
/// orb, a mob, a `/summon` and a chunk's entities are told at once, any
|
||||
/// other spawn between the tick's phases (it is checked after the blocks,
|
||||
/// the vehicles, the projectiles, the items and the mobs, and at the
|
||||
/// tick's end). A mod that cancels one of the others sees it removed
|
||||
/// before it ticks again. Not told: the entities Horizon keeps without
|
||||
/// simulating (displays, markers, interactions). Server-side.
|
||||
///
|
||||
/// Return `Flow::Cancel` to take it: this sets `canceled`, and later mods are skipped.
|
||||
fn entity_join_level(&self, host: &mut dyn Host, event: &mut EntityJoinLevelEvent<'_>) -> Flow {
|
||||
@@ -763,8 +765,10 @@ pub trait Hooks: Send + Sync {
|
||||
/// a level closing with nobody in it included. Not for the entities still
|
||||
/// loaded when the server stops, as in vanilla. `entity` is its ID and
|
||||
/// `kind` its type (`minecraft:item`); it is no longer in the level's lists.
|
||||
/// It only hears. 53 tells item entities, experience orbs and mobs (`kind`
|
||||
/// the mob type); later APIs add other kinds, so check `kind`. Server-side.
|
||||
/// It only hears. 53 told item entities, experience orbs and mobs; it now
|
||||
/// tells every kind `native_entity_join_level` does (`kind` is the type), each
|
||||
/// once, when the game next notices it gone (the tick or action that removed
|
||||
/// it). Check `kind`. Server-side.
|
||||
fn entity_leave_level(&self, host: &mut dyn Host, event: &mut EntityLeaveLevelEvent<'_>) {
|
||||
let _ = host;
|
||||
let _ = event;
|
||||
@@ -4531,6 +4535,11 @@ pub trait Hooks: Send + Sync {
|
||||
let _ = host;
|
||||
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]
|
||||
@@ -13886,3 +13895,33 @@ pub fn dispatch_living_get_projectile(
|
||||
mod_hook.living_get_projectile(host, &mut event);
|
||||
}
|
||||
}
|
||||
/// What [`Hooks::movement_input`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
|
||||
#[non_exhaustive]
|
||||
pub struct MovementInputEvent {
|
||||
pub forward: f32,
|
||||
pub strafe: f32,
|
||||
pub jump: bool,
|
||||
pub sneak: bool,
|
||||
}
|
||||
pub fn dispatch_movement_input(
|
||||
host: &dyn Host,
|
||||
mods: &[Box<dyn Hooks>],
|
||||
forward: &mut f32,
|
||||
strafe: &mut f32,
|
||||
jump: &mut bool,
|
||||
sneak: &mut bool,
|
||||
) {
|
||||
let mut event = MovementInputEvent {
|
||||
forward: *forward,
|
||||
strafe: *strafe,
|
||||
jump: *jump,
|
||||
sneak: *sneak,
|
||||
};
|
||||
for mod_hook in super::entered(mods, "movement_input") {
|
||||
mod_hook.movement_input(host, &mut event);
|
||||
}
|
||||
*forward = event.forward;
|
||||
*strafe = event.strafe;
|
||||
*jump = event.jump;
|
||||
*sneak = event.sneak;
|
||||
}
|
||||
@@ -2033,9 +2033,26 @@ fn link(target: &Path, at: &Path) -> std::io::Result<()> {
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
fs::copy(target, at).map(|_| ())
|
||||
link_file(target, at, |target, at| fs::hard_link(target, at))
|
||||
}
|
||||
}
|
||||
/// A file of the overlay where symlinks need a privilege: a hard link to the base's file, which costs a name where
|
||||
/// a copy costs the file's bytes (the base is ~28,000 files, 0.6 GiB), or a copy when no link can be made (another
|
||||
/// volume, a filesystem without links, the NTFS limit on links to one file). A link shares the base file's data,
|
||||
/// which is safe because the overlay is only ever replaced name by name (`Overlay::write` removes the name before it
|
||||
/// writes), never written in place. A symlink is copied through, as before: linked, it would stay a symlink.
|
||||
#[cfg(any(not(unix), test))]
|
||||
fn link_file(
|
||||
target: &Path,
|
||||
at: &Path,
|
||||
hard_link: impl FnOnce(&Path, &Path) -> std::io::Result<()>,
|
||||
) -> std::io::Result<()> {
|
||||
let symlink = fs::symlink_metadata(target).is_ok_and(|m| m.file_type().is_symlink());
|
||||
if !symlink && hard_link(target, at).is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
fs::copy(target, at).map(|_| ())
|
||||
}
|
||||
|
||||
/// A tag file's JSON text with `more` appended to its values (no repeats).
|
||||
fn merge_tag(text: Option<&str>, more: &[J], what: &str) -> Result<String> {
|
||||
|
||||
@@ -3774,3 +3774,168 @@ fn a_feature_gives_a_mods_sign_its_block_entity() {
|
||||
assert_eq!(generate(true), (placed, placed));
|
||||
let _ = fs::remove_dir_all(&scratch);
|
||||
}
|
||||
|
||||
/// A small stand-in for the base root: nested folders, an empty file, an empty folder.
|
||||
fn small_base(base: &Path) {
|
||||
for (name, bytes) in [
|
||||
("items.json", &b"{}"[..]),
|
||||
("empty.log", &b""[..]),
|
||||
("assets/minecraft/lang/en_us.json", &b"{\"a\": 1}"[..]),
|
||||
(
|
||||
"data/minecraft/tags/block/logs.json",
|
||||
&b"{\"values\": []}"[..],
|
||||
),
|
||||
] {
|
||||
let path = base.join(name);
|
||||
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
fs::write(path, bytes).unwrap();
|
||||
}
|
||||
fs::create_dir_all(base.join("worldgen/empty-folder")).unwrap();
|
||||
}
|
||||
|
||||
/// Every folder (`None`) and file (`Some(bytes)`) under `root`, by relative path.
|
||||
fn tree(root: &Path) -> BTreeMap<String, Option<Vec<u8>>> {
|
||||
fn walk(root: &Path, dir: &Path, out: &mut BTreeMap<String, Option<Vec<u8>>>) {
|
||||
for entry in fs::read_dir(dir).unwrap().flatten() {
|
||||
let path = entry.path();
|
||||
let rel = path
|
||||
.strip_prefix(root)
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
if path.is_dir() {
|
||||
out.insert(rel, None);
|
||||
walk(root, &path, out);
|
||||
} else {
|
||||
out.insert(rel, Some(fs::read(&path).unwrap()));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = BTreeMap::new();
|
||||
walk(root, root, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
/// Writes into a file where it is (same data), not by replacing it: what a link shares and a copy does not.
|
||||
fn edit_in_place(path: &Path, bytes: &[u8]) {
|
||||
use std::io::Write;
|
||||
let mut file = fs::File::options()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(path)
|
||||
.unwrap();
|
||||
file.write_all(bytes).unwrap();
|
||||
}
|
||||
|
||||
/// The overlay looks like the base, and shares its files (a hard link on Windows, a symlink elsewhere) rather than
|
||||
/// holding copies of them.
|
||||
#[test]
|
||||
fn the_overlay_shares_the_base_files() {
|
||||
let scratch = std::env::temp_dir().join(format!("horizon-mod-share-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&scratch);
|
||||
let (base, out) = (scratch.join("base"), scratch.join("root"));
|
||||
small_base(&base);
|
||||
Overlay::new(&base, &out).unwrap();
|
||||
assert_eq!(tree(&out), tree(&base));
|
||||
edit_in_place(
|
||||
&base.join("assets/minecraft/lang/en_us.json"),
|
||||
b"{\"a\": 2}",
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(out.join("assets/minecraft/lang/en_us.json")).unwrap(),
|
||||
b"{\"a\": 2}",
|
||||
"the overlay copied the base file instead of linking it"
|
||||
);
|
||||
let _ = fs::remove_dir_all(&scratch);
|
||||
}
|
||||
|
||||
/// Writing a file of the overlay replaces the name, so the base file it was linked to keeps its bytes and the other
|
||||
/// files of the overlay still follow the base.
|
||||
#[test]
|
||||
fn writing_into_the_overlay_leaves_the_base_alone() {
|
||||
let scratch = std::env::temp_dir().join(format!("horizon-mod-write-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&scratch);
|
||||
let (base, out) = (scratch.join("base"), scratch.join("root"));
|
||||
small_base(&base);
|
||||
let overlay = Overlay::new(&base, &out).unwrap();
|
||||
let before = tree(&base);
|
||||
overlay
|
||||
.write(
|
||||
"data/minecraft/tags/block/logs.json",
|
||||
b"{\"values\": [\"mod:log\"]}",
|
||||
)
|
||||
.unwrap();
|
||||
overlay.write("items.json", b"{\"mod\": 1}").unwrap();
|
||||
overlay.write("assets/mod/new.json", b"{}").unwrap();
|
||||
assert_eq!(tree(&base), before, "a write reached the base");
|
||||
assert_eq!(
|
||||
fs::read(out.join("data/minecraft/tags/block/logs.json")).unwrap(),
|
||||
b"{\"values\": [\"mod:log\"]}"
|
||||
);
|
||||
assert_eq!(fs::read(out.join("items.json")).unwrap(), b"{\"mod\": 1}");
|
||||
edit_in_place(
|
||||
&base.join("assets/minecraft/lang/en_us.json"),
|
||||
b"{\"a\": 3}",
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(out.join("assets/minecraft/lang/en_us.json")).unwrap(),
|
||||
b"{\"a\": 3}"
|
||||
);
|
||||
// A rebuild starts by removing the overlay: that removes names, not the base's data.
|
||||
fs::remove_dir_all(&out).unwrap();
|
||||
assert_eq!(fs::read(base.join("items.json")).unwrap(), b"{}");
|
||||
let _ = fs::remove_dir_all(&scratch);
|
||||
}
|
||||
|
||||
/// Where a hard link cannot be made (another volume, a filesystem without links) a file is copied, as it always was
|
||||
/// on Windows; where it can, the file is linked.
|
||||
#[test]
|
||||
fn a_file_is_linked_or_else_copied() {
|
||||
let scratch = std::env::temp_dir().join(format!("horizon-mod-linkfile-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&scratch);
|
||||
fs::create_dir_all(&scratch).unwrap();
|
||||
let target = scratch.join("base.json");
|
||||
fs::write(&target, b"original").unwrap();
|
||||
|
||||
let linked = scratch.join("linked.json");
|
||||
link_file(&target, &linked, |from, to| fs::hard_link(from, to)).unwrap();
|
||||
edit_in_place(&target, b"edited");
|
||||
assert_eq!(fs::read(&linked).unwrap(), b"edited", "not a link");
|
||||
|
||||
let copied = scratch.join("copied.json");
|
||||
fs::write(&target, b"original").unwrap();
|
||||
link_file(&target, &copied, |_, _| {
|
||||
Err(std::io::Error::other("no links here"))
|
||||
})
|
||||
.unwrap();
|
||||
edit_in_place(&target, b"edited");
|
||||
assert_eq!(fs::read(&copied).unwrap(), b"original", "not a copy");
|
||||
|
||||
// A target that is not there is an error whichever way the file is made.
|
||||
let missing = link_file(
|
||||
&scratch.join("missing.json"),
|
||||
&scratch.join("x.json"),
|
||||
|from, to| fs::hard_link(from, to),
|
||||
);
|
||||
assert!(missing.is_err());
|
||||
let _ = fs::remove_dir_all(&scratch);
|
||||
}
|
||||
|
||||
/// A symlink in the base is copied through, as before; linked, the overlay's file would be a symlink too.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn a_symlink_file_is_copied_through() {
|
||||
let scratch = std::env::temp_dir().join(format!("horizon-mod-symfile-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&scratch);
|
||||
fs::create_dir_all(&scratch).unwrap();
|
||||
fs::write(scratch.join("real.json"), b"real").unwrap();
|
||||
std::os::unix::fs::symlink("real.json", scratch.join("alias.json")).unwrap();
|
||||
let at = scratch.join("copy.json");
|
||||
link_file(&scratch.join("alias.json"), &at, |from, to| {
|
||||
fs::hard_link(from, to)
|
||||
})
|
||||
.unwrap();
|
||||
assert!(!fs::symlink_metadata(&at).unwrap().file_type().is_symlink());
|
||||
assert_eq!(fs::read(&at).unwrap(), b"real");
|
||||
let _ = fs::remove_dir_all(&scratch);
|
||||
}
|
||||
@@ -94,9 +94,10 @@ pub enum ProjectileKind {
|
||||
/// flight that bursts on whatever it meets.
|
||||
WindCharge,
|
||||
BreezeWindCharge,
|
||||
/// `ShulkerBullet`: homing on `target` in axis-aligned steps.
|
||||
/// `ShulkerBullet`: homing on `target` in axis-aligned steps; one with
|
||||
/// no target (a summoned one) falls like any projectile.
|
||||
ShulkerBullet {
|
||||
target: Who,
|
||||
target: Option<Who>,
|
||||
/// The current step direction (3D data value), if any.
|
||||
dir: Option<u8>,
|
||||
steps: i32,
|
||||
@@ -116,6 +117,8 @@ pub enum ProjectileKind {
|
||||
/// type is the entity's own and `/summon` can pair either with any `Item`,
|
||||
/// so it is kept next to the stack (a thrown potion's follows its item).
|
||||
Potion(Box<crate::items::ItemStack>, bool),
|
||||
/// `ThrownExperienceBottle`: its own entity type, gravity 0.07.
|
||||
ExperienceBottle,
|
||||
/// a content mod's type.
|
||||
Thrown(&'static ThrownType),
|
||||
}
|
||||
@@ -148,6 +151,7 @@ impl ProjectileKind {
|
||||
| Self::EnderPearl
|
||||
| Self::LlamaSpit
|
||||
| Self::Potion(..)
|
||||
| Self::ExperienceBottle
|
||||
| Self::Thrown(_) => 0.25,
|
||||
Self::SmallFireball
|
||||
| Self::WindCharge
|
||||
@@ -175,6 +179,7 @@ impl ProjectileKind {
|
||||
// `ThrownSplashPotion`/`ThrownLingeringPotion`: the item names it.
|
||||
Self::Potion(_, true) => "minecraft:lingering_potion",
|
||||
Self::Potion(_, false) => "minecraft:splash_potion",
|
||||
Self::ExperienceBottle => "minecraft:experience_bottle",
|
||||
Self::Thrown(t) => t.id,
|
||||
}
|
||||
}
|
||||
@@ -314,13 +319,16 @@ impl crate::entity_rules::EntityRules for Projectile {
|
||||
}
|
||||
/// Each projectile class's `getDefaultGravity`.
|
||||
fn get_default_gravity(&self) -> f64 {
|
||||
match self.kind {
|
||||
match &self.kind {
|
||||
ProjectileKind::Arrow { .. } | ProjectileKind::Trident => 0.05,
|
||||
ProjectileKind::SmallFireball
|
||||
| ProjectileKind::LargeFireball { .. }
|
||||
| ProjectileKind::WitherSkull { .. } => 0.0,
|
||||
ProjectileKind::LlamaSpit => 0.06,
|
||||
// `ThrownExperienceBottle.getDefaultGravity` is 0.07.
|
||||
ProjectileKind::ExperienceBottle => 0.07,
|
||||
ProjectileKind::Potion { .. } => 0.05,
|
||||
ProjectileKind::ShulkerBullet { .. } => 0.04,
|
||||
_ => 0.03,
|
||||
}
|
||||
}
|
||||
@@ -465,7 +473,7 @@ impl Projectile {
|
||||
|| self
|
||||
.arrow
|
||||
.as_ref()
|
||||
.is_some_and(|a| &*a.pickup_item.item == "minecraft:spectral_arrow")
|
||||
.is_some_and(|a| a.spectral || &*a.pickup_item.item == "minecraft:spectral_arrow")
|
||||
}
|
||||
/// `Arrow.ID_EFFECT_COLOR` (`updateColor`): the opaque colour of the
|
||||
/// potion the arrow carries, from its tipped pickup item or the effect a
|
||||
@@ -598,7 +606,7 @@ impl Projectile {
|
||||
);
|
||||
let mut p = Self::new(
|
||||
ProjectileKind::ShulkerBullet {
|
||||
target,
|
||||
target: Some(target),
|
||||
dir: Some(1),
|
||||
steps: 0,
|
||||
delta: [0.0; 3],
|
||||
@@ -629,7 +637,11 @@ impl Projectile {
|
||||
]
|
||||
}
|
||||
fn target_box(&self, w: &impl ProjectileWorld) -> Option<Aabb> {
|
||||
let ProjectileKind::ShulkerBullet { target, .. } = self.kind else {
|
||||
let ProjectileKind::ShulkerBullet {
|
||||
target: Some(target),
|
||||
..
|
||||
} = self.kind
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
w.targets()
|
||||
@@ -743,7 +755,8 @@ impl Projectile {
|
||||
v.z + (dl[2] - v.z) * 0.2,
|
||||
);
|
||||
} else {
|
||||
self.velocity.y -= 0.04;
|
||||
// `Entity.applyGravity`: `getGravity` is 0 under NoGravity.
|
||||
self.velocity.y -= crate::entity_rules::EntityRules::get_gravity(self);
|
||||
}
|
||||
let from = self.position;
|
||||
let to = from.add_vector(self.velocity);
|
||||
@@ -1004,6 +1017,10 @@ impl Projectile {
|
||||
pub fn drawn_item(&self) -> Option<crate::items::ItemStack> {
|
||||
match &self.kind {
|
||||
ProjectileKind::Potion(stack, _) => Some((**stack).clone()),
|
||||
ProjectileKind::ExperienceBottle => Some(crate::items::ItemStack::new(
|
||||
"minecraft:experience_bottle",
|
||||
1,
|
||||
)),
|
||||
ProjectileKind::Thrown(t) => Some(crate::items::ItemStack::new(t.item, 1)),
|
||||
_ => None,
|
||||
}
|
||||
@@ -1018,6 +1035,7 @@ impl Projectile {
|
||||
"item/fire_charge"
|
||||
}
|
||||
ProjectileKind::Potion(stack, _) => potion_sprite(&stack.item),
|
||||
ProjectileKind::ExperienceBottle => "item/experience_bottle",
|
||||
ProjectileKind::LlamaSpit => "entity/llama/llama_spit",
|
||||
// Drawn as `WindChargeModel` and `ShulkerBulletModel` by the game.
|
||||
ProjectileKind::WindCharge
|
||||
@@ -1298,6 +1316,38 @@ mod tests {
|
||||
assert_eq!(settings("ruby:late"), (5, 3, true));
|
||||
}
|
||||
|
||||
/// `ShulkerBullet.tick` with no target: `applyGravity` (0.04, none under
|
||||
/// NoGravity), then the move, no drag. 20 ticks from y 110 fall 8.4.
|
||||
#[test]
|
||||
fn a_shulker_bullet_without_a_target_falls_unless_no_gravity() {
|
||||
let bullet = || {
|
||||
Projectile::new(
|
||||
ProjectileKind::ShulkerBullet {
|
||||
target: None,
|
||||
dir: None,
|
||||
steps: 0,
|
||||
delta: [0.0; 3],
|
||||
seed: 0,
|
||||
},
|
||||
Vec3::new(0.5, 110.0, 0.5),
|
||||
None,
|
||||
)
|
||||
};
|
||||
let w = Empty(vec![]);
|
||||
let mut falling = bullet();
|
||||
let mut held = bullet();
|
||||
held.no_gravity = true;
|
||||
for _ in 0..20 {
|
||||
falling.tick(&w);
|
||||
held.tick(&w);
|
||||
}
|
||||
assert!(
|
||||
(falling.position.y - 101.6).abs() < 1e-6,
|
||||
"{}",
|
||||
falling.position.y
|
||||
);
|
||||
assert_eq!(held.position, Vec3::new(0.5, 110.0, 0.5));
|
||||
}
|
||||
#[test]
|
||||
fn llama_spit_moves_first_then_drags_and_falls() {
|
||||
let mut p = Projectile::new(ProjectileKind::LlamaSpit, Vec3::new(0.0, 100.0, 0.0), None);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! A mod crate's view of the mod API: `Hooks` is implemented with the names `mod_api` exports.
|
||||
//! The test is that this file builds; #7044's mod could not name `key`'s event.
|
||||
use horizon_client::mod_api::{Hooks, Host, InputKeyEvent, KeyEvent, winit};
|
||||
use horizon_client::mod_api::{
|
||||
Hooks, Host, InputKeyEvent, KeyEvent, MovementInputEvent, dispatch_movement_input, winit,
|
||||
};
|
||||
|
||||
struct Keys;
|
||||
|
||||
@@ -18,3 +20,29 @@ impl Hooks for Keys {
|
||||
fn a_mod_implements_the_key_hooks() {
|
||||
let _: Box<dyn Hooks> = Box::new(Keys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn movement_input_writes_back_all_fields() {
|
||||
struct TestHost;
|
||||
impl Host for TestHost {}
|
||||
struct Movement;
|
||||
impl Hooks for Movement {
|
||||
fn movement_input(&self, _host: &dyn Host, event: &mut MovementInputEvent) {
|
||||
event.forward = -0.5;
|
||||
event.strafe = 0.25;
|
||||
event.jump = true;
|
||||
event.sneak = true;
|
||||
}
|
||||
}
|
||||
let mods: Vec<Box<dyn Hooks>> = vec![Box::new(Movement)];
|
||||
let (mut forward, mut strafe, mut jump, mut sneak) = (1.0, 0.0, false, false);
|
||||
dispatch_movement_input(
|
||||
&TestHost,
|
||||
&mods,
|
||||
&mut forward,
|
||||
&mut strafe,
|
||||
&mut jump,
|
||||
&mut sneak,
|
||||
);
|
||||
assert_eq!((forward, strafe, jump, sneak), (-0.5, 0.25, true, true));
|
||||
}
|
||||
@@ -14,7 +14,7 @@ use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
Mutex, OnceLock,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
atomic::{AtomicU32, Ordering},
|
||||
},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
@@ -24,7 +24,7 @@ const VERSION: u32 = 2;
|
||||
const FILE_NAME: &str = "behaviours-v2.bin";
|
||||
const MAX_FILE_BYTES: usize = 128 * 1024 * 1024;
|
||||
const MAX_DECODED_BYTES: usize = 256 * 1024 * 1024;
|
||||
static NEXT_TEMP: AtomicU64 = AtomicU64::new(0);
|
||||
static NEXT_TEMP: AtomicU32 = AtomicU32::new(0);
|
||||
static DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||
|
||||
/// Keep snapshots in `dir` (the client passes `<game folder>/.horizon-cache`).
|
||||
@@ -776,9 +776,9 @@ fn write(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
|
||||
static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0);
|
||||
static NEXT_FIXTURE: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
struct FixtureDir(PathBuf);
|
||||
impl Drop for FixtureDir {
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::{
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{self, Read, Seek, SeekFrom, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
sync::atomic::{AtomicU32, Ordering},
|
||||
};
|
||||
|
||||
const SECTOR: usize = 4096;
|
||||
@@ -387,7 +387,7 @@ impl RegionFile {
|
||||
}
|
||||
type BTreeChanges<'a> = std::collections::BTreeMap<usize, Option<&'a ChunkRecord>>;
|
||||
|
||||
static NEXT_TEMP: AtomicU64 = AtomicU64::new(0);
|
||||
static NEXT_TEMP: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
/// Writes a complete new region to a temporary sibling, then publishes it with
|
||||
/// an atomic, non-overwriting hard link. Drop rolls back unpublished files.
|
||||
|
||||
@@ -10,10 +10,10 @@ use std::{
|
||||
fs,
|
||||
io::{Seek, SeekFrom, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
sync::atomic::{AtomicU32, Ordering},
|
||||
};
|
||||
|
||||
static NEXT: AtomicU64 = AtomicU64::new(0);
|
||||
static NEXT: AtomicU32 = AtomicU32::new(0);
|
||||
struct Directory(PathBuf);
|
||||
impl Directory {
|
||||
fn new() -> Self {
|
||||
|
||||
@@ -841,6 +841,8 @@ API 108 adds `native_server_data_loaded(reload)` fires once the server's data ta
|
||||
|
||||
API 109 adds `native_compute_fov(partial, camera, degrees)` receives the computed world FOV after settings, ordinary FOV effects and zoom, before projection/Nausea. `camera` is its world position and `partial` the interpolation fraction. Mods can narrow or widen the angle; only finite values in 1..=179 degrees are accepted, otherwise the original computed angle is used. It runs for world rendering and near-plane overlays, not just once per frame. Hand/HUD FOV and the title panorama keep their existing paths. Bundles need "hooks": 109.
|
||||
|
||||
`native_movement_input(forward, strafe, jump, sneak)` runs on the client each local player tick after input is read and before it is applied. Mods may change all four values; positive strafe is left. The server receives direction flags, not scalar magnitudes, so remote servers may correct modified movement. Bundles need "hooks": @API:movement_input@.
|
||||
|
||||
API 110 adds mobs drawn by client mods: `native_mob_submit(mob, draw, drawn)` is asked each frame for every mob in view but players,
|
||||
with its id, kind, custom name and the frame's body and head turns and walk cycle; a mod draws a model of its own with the
|
||||
block entity drawing helpers under `draw`'s pose (at the feet, turned with the body) and sets `drawn` to leave out the vanilla
|
||||
|
||||
@@ -585,6 +585,7 @@ public final class VanillaMobAiOracle {
|
||||
new net.minecraft.world.item.ItemStack(BuiltInRegistries.ITEM.getValue(
|
||||
net.minecraft.resources.Identifier.parse("minecraft:red_harness"))));
|
||||
case "noai" -> mob.setNoAi(true);
|
||||
case "agelocked" -> method(AgeableMob.class, "setAgeLocked", boolean.class).invoke(mob, true);
|
||||
case "profession" -> {
|
||||
Villager villager = (Villager) mob;
|
||||
villager.setVillagerData(villager.getVillagerData().withProfession(level.registryAccess(),
|
||||
@@ -1109,6 +1110,7 @@ public final class VanillaMobAiOracle {
|
||||
mem.sort(null);
|
||||
sb.append("\tmem ").append(String.join(" ", mem));
|
||||
}
|
||||
if (m instanceof AgeableMob ageable) sb.append("\tage ").append(ageable.getAge());
|
||||
if (m instanceof Villager villager) sb.append("\tvx ").append(villager(villager));
|
||||
if (m instanceof net.minecraft.world.entity.animal.golem.CopperGolem golem) {
|
||||
Object opened = get(golem, "openedChestPos");
|
||||
|
||||
@@ -350,6 +350,7 @@ class StressSceneTests(unittest.TestCase):
|
||||
self.assertEqual(summons("stress_items", "item"), 2000)
|
||||
self.assertEqual(sum(summons("stress_held", k) for k in stress_scenes.HOLDERS), 1000)
|
||||
self.assertEqual(sum(summons("stress_eyes", k) for k in stress_scenes.EYED), 1000)
|
||||
self.assertEqual(sum(summons("stress_glowing", k) for k in stress_scenes.MOBS), 1000)
|
||||
self.assertEqual(summons("stress_frames", "item_frame"), 512)
|
||||
self.assertEqual(summons("stress_armor_stands", "armor_stand"), 500)
|
||||
self.assertEqual(stress_scenes.setup("stress_empty"), stress_scenes.SETTINGS + stress_scenes.floor(
|
||||
|
||||
@@ -1686,6 +1686,38 @@ def summoned_potions():
|
||||
return ops
|
||||
|
||||
|
||||
def trial_spawner_passengers():
|
||||
"""Configured passengers must exist as entities with a vehicle (#7254)."""
|
||||
ops = [*setup('/fill 1 219 1 13 219 8 stone',
|
||||
'/fill 1 220 1 13 224 8 air', '/gamerule spawn_mobs true')]
|
||||
# Exercise namespace and position independently, plus the explicit control.
|
||||
for i, (kind, pos) in enumerate([
|
||||
('chicken', ''), ('minecraft:chicken', ''),
|
||||
('chicken', ',Pos:[4.5d,220.0d,6.5d]'),
|
||||
('minecraft:chicken', ',Pos:[4.5d,220.0d,6.5d]'),
|
||||
]):
|
||||
parent, rider = f'trial_parent_{i}', f'trial_rider_{i}'
|
||||
ops += setup(
|
||||
'/setblock 4 220 4 trial_spawner[trial_spawner_state=active]'
|
||||
'{normal_config:{spawn_range:3,total_mobs:1.0f,simultaneous_mobs:1.0f,'
|
||||
'total_mobs_added_per_player:0.0f,simultaneous_mobs_added_per_player:0.0f,'
|
||||
'ticks_between_spawn:1000},spawn_data:{entity:{id:"zombie",'
|
||||
f'Tags:["{parent}"],Pos:[4.5d,220.0d,6.5d],NoAI:1b,'
|
||||
f'Passengers:[{{id:"{kind}",NoAI:1b,Tags:["{rider}"]{pos}}}]'
|
||||
'}},next_mob_spawns_at:0L}',
|
||||
'/data modify block 4 220 4 registered_players append from entity @s UUID')
|
||||
ops += [
|
||||
{'op': 'tick', 'n': 5},
|
||||
cmd(f'/execute if entity @e[tag={parent}]'),
|
||||
cmd(f'/execute if entity @e[tag={rider}]'),
|
||||
cmd(f'/execute as @e[tag={rider}] on vehicle if entity @s[tag={parent}]'),
|
||||
cmd(f'/data get entity @e[tag={parent},limit=1] Passengers[0].Tags'),
|
||||
*setup(f'/kill @e[tag={parent}]', f'/kill @e[tag={rider}]',
|
||||
'/setblock 4 220 4 air'),
|
||||
]
|
||||
return ops + setup('/fill 1 219 1 13 224 8 air', '/gamerule spawn_mobs false')
|
||||
|
||||
|
||||
def groups():
|
||||
return [
|
||||
('setup', group_setup()),
|
||||
@@ -1698,6 +1730,7 @@ def groups():
|
||||
('wither spawn', wither_spawn()),
|
||||
('riding teleport', riding_teleport()),
|
||||
('mob riding', mob_riding()),
|
||||
('trial spawner passengers', trial_spawner_passengers()),
|
||||
('camel riders', camel_riders()),
|
||||
('scores, teams and bossbars', scores()),
|
||||
('execute', execute()),
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"name": "movement_input",
|
||||
"target": "horizon_client::game::native_hooks::Game<'_>::native_movement_input",
|
||||
"neoforge": "MovementInputUpdateEvent",
|
||||
"side": "client",
|
||||
"cancellable": false,
|
||||
"mod_api": {
|
||||
"contract": "listen"
|
||||
}
|
||||
}
|
||||
],
|
||||
"rust_summary": "Local player movement input before application (`native_movement_input`).",
|
||||
"doc_summary": "`native_movement_input(forward, strafe, jump, sneak)` lets client mods change local movement after input is read and before it is applied. Positive strafe is left; servers receive direction flags rather than scalar magnitudes. Bundles need hooks API @API:movement_input@."
|
||||
}
|
||||
@@ -61,7 +61,7 @@ StandingSignBlock.getStateForPlacement port crates/horizon-world/src/level/place
|
||||
StandingSignBlock.mirror port crates/horizon-world/src/worldgen/structure.rs::StateTransforms::mirror
|
||||
StandingSignBlock.rotate port crates/horizon-world/src/worldgen/structure.rs::StateTransforms::rotate
|
||||
StandingSignBlock.updateShape port crates/horizon-world/src/level/shapes.rs::Sim::update_shape_class
|
||||
StemBlock.getCloneItemStack missing unsure: minor: middle-click on a stem may not return its seed item; no stem case found in the pick code
|
||||
StemBlock.getCloneItemStack port crates/horizon-client/src/game/block_items.rs::clone_item a pumpkin or melon stem, attached or not, picks the seed it grew from; any other block picks its own item
|
||||
StemBlock.isValidBonemealTarget port crates/horizon-world/src/level/bonemeal.rs::Sim::bonemeal_valid
|
||||
StemBlock.performBonemeal port crates/horizon-world/src/level/bonemeal.rs::Sim::perform_bonemeal
|
||||
StemBlock.randomTick port crates/horizon-world/src/level/tick.rs::Sim::stem_random_tick
|
||||
@@ -75,8 +75,8 @@ SuspiciousEffectHolder.tryGet port crates/horizon-items/src/flowers.rs::stew_eff
|
||||
TallSeagrassBlock.canSurvive port crates/horizon-world/src/level/survive.rs::Sim::can_survive_class
|
||||
TallSeagrassBlock.getStateForPlacement port crates/horizon-world/src/level/place.rs::Sim::placement_class
|
||||
TallSeagrassBlock.mayPlaceOn port crates/horizon-world/src/level/survive.rs::Sim::may_place_on
|
||||
TestBlock missing minor: game-test block for operators; Horizon keeps only its NBT
|
||||
TestInstanceBlock missing minor: game-test instance block; Horizon keeps only its NBT
|
||||
TestBlock skip:debug game-test tooling: an operator's test block runs a structure when triggered and Horizon keeps only its block entity NBT
|
||||
TestInstanceBlock skip:debug game-test tooling: the instance block runs and exports a test structure; Horizon keeps its NBT
|
||||
TintedParticleLeavesBlock.spawnFallingLeavesParticle port crates/horizon-client/src/animate_tick.rs::block_animate_tick falling tinted leaf particle
|
||||
TorchBlock.animateTick port crates/horizon-client/src/animate_tick.rs::block_animate_tick torch flame
|
||||
TorchflowerCropBlock.randomTick port crates/horizon-world/src/level/tick.rs::Sim::random_tick_block growth arm
|
||||
@@ -100,9 +100,9 @@ WallHangingSignBlock.canAttachTo inline crates/horizon-world/src/level/place.rs:
|
||||
WallHangingSignBlock.canPlace port crates/horizon-world/src/level/place.rs::Sim::wall_hanging_sign_can_place
|
||||
WallHangingSignBlock.createBlockStateDefinition skip:data states come from the generated block state table
|
||||
WallHangingSignBlock.getStateForPlacement port crates/horizon-world/src/level/place.rs::Sim::placement_class
|
||||
WallHangingSignBlock.isHittingEditableSide missing unsure: player: a hanging sign clicked on its side may not chain onto a wall hanging sign
|
||||
WallHangingSignBlock.isHittingEditableSide inline crates/horizon-world/src/level/interact.rs::Behaviours::block_use_item_on the faces of the sign it faces and its opposite are the editable ones; a hanging sign aimed at any other face passes
|
||||
WallHangingSignBlock.rotate port crates/horizon-world/src/worldgen/structure.rs::StateTransforms::rotate
|
||||
WallHangingSignBlock.shouldTryToChainAnotherHangingSign missing unsure: player: a second hanging sign used on a wall sign may not chain on
|
||||
WallHangingSignBlock.shouldTryToChainAnotherHangingSign inline crates/horizon-world/src/level/interact.rs::Behaviours::block_use_item_on passing on a non-editable face lets the held hanging sign place its own block, which chains onto the wall sign
|
||||
WallHangingSignBlock.updateShape port crates/horizon-world/src/level/shapes.rs::Sim::update_shape_class
|
||||
WallHangingSignBlock.useItemOn port crates/horizon-world/src/level/interact.rs::Behaviours::use_item_on gap: player: chaining a hanging sign onto a wall hanging sign is not ported here
|
||||
WallSignBlock.canSurvive port crates/horizon-world/src/level/survive.rs::Sim::can_survive_class
|
||||
@@ -132,7 +132,7 @@ BannerPatternLayers.removeLast inline crates/horizon-client/src/cauldron.rs::int
|
||||
BannerPatternLayers.Builder skip:java builder for banner pattern layers
|
||||
BannerPatterns skip:data registry bootstrap
|
||||
BarrelBlockEntity.loadAdditional port crates/horizon-client-core/src/block_entity_nbt.rs::canonical gap: minor: canonical NBT form only, no load step of its own
|
||||
BarrelBlockEntity.recheckOpen missing unsure: player: a barrel lid may stay open after a player walks away; only golem openers are rechecked on a timer
|
||||
BarrelBlockEntity.recheckOpen missing the lid's periodic recheck of whether its viewers are still there; Horizon draws no barrel lid at all (containers.rs holds its 27 slots), so nothing rechecks one, where a golem's openers are rechecked on a timer
|
||||
BarrelBlockEntity.saveAdditional port crates/horizon-client-core/src/block_entity_nbt.rs::canonical gap: minor: canonical NBT form only
|
||||
BarrelBlockEntity.startOpen port crates/horizon-client/src/game/block_entity_anims.rs::Game::barrel_openers_changed
|
||||
BarrelBlockEntity.stopOpen port crates/horizon-client/src/game/block_entity_anims.rs::Game::barrel_openers_changed
|
||||
@@ -162,15 +162,15 @@ PotentSulfurBlockEntity.setLevel inline crates/horizon-client/src/game/potent_su
|
||||
PotentSulfurBlockEntity.spawnGeyserParticle port crates/horizon-client/src/game/potent_sulfur.rs::Game::spawn_geyser_particle ticked in #3516; `PotentSulfurBlockEntity.spawnGeyserParticle` is cited at crates/horizon-client/src/game/potent_sulfur.rs:380
|
||||
PotentSulfurBlockEntity.spawnNoxiousGasCloudParticle port crates/horizon-client/src/game/potent_sulfur.rs::Game::spawn_noxious_gas_cloud_particle ticked in #3516; `PotentSulfurBlockEntity.spawnNoxiousGasCloudParticle` is cited at crates/horizon-client/src/game/potent_sulfur.rs:395
|
||||
RandomizableContainerBlockEntity.applyImplicitComponents port crates/horizon-client/src/block_entity_item.rs::place the item's minecraft:container_loot hands the block its LootTable and LootTableSeed
|
||||
RandomizableContainerBlockEntity.canOpen missing unsure: minor: the spectator and loot-table open checks were not found
|
||||
RandomizableContainerBlockEntity.canOpen inline crates/horizon-client/src/game/advancements.rs::Game::container_locked_for the negative of it: a spectator opens any container, and otherwise the lock decides (LockCode.canUnlock)
|
||||
RandomizableContainerBlockEntity.collectImplicitComponents port crates/horizon-client/src/block_entity_item.rs::collect the block's LootTable and LootTableSeed leave on the item as minecraft:container_loot
|
||||
RandomizableContainerBlockEntity.createMenu port crates/horizon-client/src/game.rs::Game::open_menu gap: minor: the spectator and loot-table checks are not ported here
|
||||
SculkSensorBlockEntity.loadAdditional port crates/horizon-client-render/src/sculk.rs::SculkEntity::load
|
||||
SculkSensorBlockEntity.saveAdditional port crates/horizon-client-render/src/sculk.rs::SculkEntity::to_nbt
|
||||
SculkSensorBlockEntity.VibrationUser.canReceiveVibration port crates/horizon-client-render/src/sculk.rs::valid_vibration
|
||||
SculkSensorBlockEntity.VibrationUser.onReceiveVibration port crates/horizon-client-render/src/sculk.rs::Data::schedule
|
||||
TestBlockEntity missing minor: game-test block entity; reset, trigger and logging are not ported, NBT only
|
||||
TestInstanceBlockEntity missing minor: game-test structure tooling; export and chunk forcing are not ported
|
||||
TestBlockEntity skip:debug game-test tooling: reset, trigger and logging belong to the test runner, which Horizon does not have; its NBT is kept
|
||||
TestInstanceBlockEntity skip:debug game-test tooling: export and chunk forcing belong to the test runner, which Horizon does not have
|
||||
TrialSpawnerConfig.calculateTargetSimultaneousMobs port crates/horizon-client/src/mobs/trial_spawner.rs::TrialConfig::target_simultaneous_mobs
|
||||
TrialSpawnerConfig.calculateTargetTotalMobs port crates/horizon-client/src/mobs/trial_spawner.rs::TrialConfig::target_total_mobs
|
||||
TrialSpawnerConfig.withSpawning skip:java builder-style setter on the config record
|
||||
@@ -204,7 +204,7 @@ ImposterProtoChunk skip:other wrapper so the vanilla status pipeline can read a
|
||||
LinearPalette skip:java palette storage; Horizon's chunk.rs keeps block ids in plain arrays
|
||||
PalettedContainerFactory skip:java factory for palette storage; Horizon's chunk.rs keeps block ids in plain arrays
|
||||
SingleValuePalette skip:java palette storage; Horizon's chunk.rs keeps block ids in plain arrays
|
||||
UpgradeData missing unsure: minor: block upgrades of old chunks on load are not ported
|
||||
UpgradeData missing the block upgrades an old chunk takes when it loads (the Sides bits a save carries); the datafix writes and keeps UpgradeData (datafix/chunk_leaves.rs) but no load path applies the upgrades
|
||||
ChunkDependencies.getRadiusOf inline crates/horizon-client-render/src/streaming.rs::Ranges::margins fixed margins instead of a per-status radius
|
||||
ChunkPyramid.Builder skip:java builder for the status pyramid; Horizon uses fixed margins
|
||||
ChunkStatusTasks.buildTerrain inline crates/horizon-world/src/worldgen/generator.rs::Pipeline::generate_with_context terrain pass
|
||||
|
||||
|
@@ -1,9 +1,10 @@
|
||||
SetStewEffectFunction.Builder skip:java builder for the stew effect list; Horizon keeps the effects in loot.rs Context::run (set_stew_effect)
|
||||
SetWritableBookPagesFunction missing minor: no vanilla loot table uses set_writable_book_pages; Horizon's loot run has no arm for it
|
||||
SetWrittenBookPagesFunction missing minor: no vanilla loot table uses set_written_book_pages; Horizon's loot run has no arm for it
|
||||
SetWritableBookPagesFunction missing `horizon-world/src/loot.rs::modify` has no `set_writable_book_pages` arm, and no vanilla loot table asks for one
|
||||
SetWrittenBookPagesFunction missing no `set_written_book_pages` arm in `loot.rs::modify` (the arms run from `sequence` and `set_count` through `set_potion` to `set_stew_effect`); no vanilla loot table asks for one
|
||||
SmeltItemFunction.run port crates/horizon-world/src/loot.rs::Context::run furnace_smelt arm: smelting recipe, use_input_count and stack cap
|
||||
SmeltItemFunction.smelted skip:java static builder factory
|
||||
ToggleTooltips home crates/horizon-client/src/game/commands/items.rs the `/item modify` arm toggle_tooltips writes the tooltip_display component's hide_tooltip and hidden_components as run does; the loot run has no arm, and no vanilla loot table uses it
|
||||
ToggleTooltips home crates/horizon-client/src/game/commands/items.rs the `/item modify` arm of cmd_item_modify writes the tooltip_display component's hide_tooltip and hidden_components as run does; the loot run has no arm, and no vanilla loot table uses it
|
||||
ToggleTooltips.run inline crates/horizon-client/src/game/commands/items.rs::Game::cmd_item_modify the `minecraft:toggle_tooltips` arm of the `/item modify` command adds each false toggle to `tooltip_display`'s hidden_components and keeps hide_tooltip; the loot run has no arm, so a loot table's toggle_tooltips is ignored (no vanilla loot table uses one)
|
||||
LootContextParamSets skip:data loot parameter set registry and validation; Horizon reads its loot tables
|
||||
AllOfCondition.allOf skip:java static builder
|
||||
AllOfCondition.combine inline crates/horizon-items/src/items.rs::LootContext::condition all_of arm: every term must hold
|
||||
@@ -16,11 +17,11 @@ CompositeLootItemCondition.Builder skip:java builder (addTerm)
|
||||
ConditionUserBuilder skip:java builder interface for loot conditions
|
||||
DamageSourceCondition.test port crates/horizon-world/src/loot.rs::Context::condition damage_source_properties arm (mob death loot)
|
||||
EnchantmentActiveCheck.test inline crates/horizon-client/src/game/enchant_effects.rs::Game::tick_soul_speed the soul speed modifiers switch on and off here; the ENCHANTMENT_ACTIVE flag itself is not read as a condition
|
||||
EntityHasScoreCondition.hasScore missing minor: no vanilla loot table uses entity_scores
|
||||
EntityHasScoreCondition.test missing minor: no vanilla loot table uses entity_scores
|
||||
EntityHasScoreCondition.hasScore missing `loot.rs::condition` has no `entity_scores` arm, and no vanilla loot table asks for one
|
||||
EntityHasScoreCondition.test missing same as `hasScore`: the condition dispatch (`inverted`, `all_of`, `any_of`, `entity_properties`, `killed_by_player`, the chance and location checks) has no scoreboard arm
|
||||
EntityHasScoreCondition.validate skip:data datapack validation
|
||||
EnvironmentAttributeCheck.createCodec skip:java codec
|
||||
EnvironmentAttributeCheck.test missing minor: no vanilla data uses environment_attribute_check
|
||||
EnvironmentAttributeCheck.test missing no `environment_attribute_check` arm in `loot.rs::condition`; no vanilla data uses it
|
||||
ExplosionCondition.test port crates/horizon-items/src/items.rs::LootContext::condition survives_explosion arm: 1 / radius under an explosion (block loot; only block tables use it)
|
||||
FloatValueCheck.validate skip:data datapack validation
|
||||
IntValueCheck.validate skip:data datapack validation
|
||||
@@ -39,7 +40,7 @@ MatchBlock.blockMatches skip:java static builder
|
||||
MatchBlock.getReferencedContextParams skip:data validation metadata for context params
|
||||
MatchBlock.test port crates/horizon-items/src/items.rs::LootContext::condition match_block arm: block id, state and property ranges; gap: minor: the block-entity part of the predicate is not read
|
||||
MatchTool.test port crates/horizon-items/src/items.rs::LootContext::condition match_tool arm; gap: minor: item ids, tags and enchantment levels only, other ItemPredicate parts are not read
|
||||
TimeCheck missing minor: time_check is used by no vanilla data file (datapacks only)
|
||||
TimeCheck missing no `time_check` arm in `loot.rs::condition`; no vanilla data file uses it, so only a datapack reaches it
|
||||
WeatherCheck.test port crates/horizon-client/src/game/trident.rs::Game::channel_lightning channeling's thunder check (thundering only); gap: minor: raining is not read, channeling only uses thundering
|
||||
NbtProviders skip:data registry bootstrap
|
||||
AggregateProvider skip:java codec only
|
||||
@@ -58,13 +59,13 @@ PowerProvider.mapCodec skip:java codec
|
||||
PowerProvider.validate skip:data datapack validation
|
||||
RangeProvider.mapCodec skip:java codec
|
||||
RangeProvider.validate skip:data datapack validation
|
||||
StoredNumberAccess missing minor: storage-backed numbers; no vanilla data uses storage providers
|
||||
StoredNumberAccess missing no storage-backed number provider: `loot.rs::level_value` and the int and float readers know constant, uniform, binomial, linear, clamped, fraction, levels_squared and lookup only
|
||||
UnaryProvider skip:java codec only
|
||||
floats/Average.getFloatUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_float avg arm
|
||||
floats/ContextFloatProviders skip:data registry bootstrap
|
||||
floats/Difference.getFloatUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_float sub arm
|
||||
floats/EnchantmentLevelProvider.getFloatUnsafe port crates/horizon-items/src/enchant.rs::Enchantments::condition random_chance arm: chance at the enchantment level; gap: minor: only the random_chance use is read
|
||||
floats/EnvironmentAttributeValue missing minor: no vanilla data uses environment_attribute providers
|
||||
floats/EnvironmentAttributeValue missing no `environment_attribute` provider in `loot.rs::level_value`
|
||||
floats/Length.getFloatUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_float length arm
|
||||
floats/Maximum.getFloatUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_float max arm
|
||||
floats/Minimum.getFloatUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_float min arm
|
||||
@@ -73,7 +74,7 @@ floats/Product.getFloatUnsafe port crates/horizon-client/src/game/commands/compu
|
||||
floats/Quotient.getFloatUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_float div arm
|
||||
ResolvableFloat.getFromItem port crates/horizon-client-render/src/providers.rs::Providers::speed furnace fuel speed multiplier (COOKING_FUEL); gap: minor: only the furnace fuel component is read
|
||||
ResolvableFloat.wrap skip:java codec helper
|
||||
floats/StorageValue missing minor: no vanilla data uses storage providers
|
||||
floats/StorageValue missing no `storage` provider in `loot.rs::level_value`
|
||||
floats/Sum.getFloatUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_float add arm
|
||||
floats/Truncate.getFloatUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_float truncate arm
|
||||
floats/UniformGenerator.getFloatUnsafe port crates/horizon-world/src/loot.rs::Context::float uniform arm: min + random * (max - min)
|
||||
@@ -85,7 +86,7 @@ ints/ContextIntProviders.bootstrap skip:data registry bootstrap
|
||||
ints/ContextIntProviders.compostable port crates/horizon-client/src/game/composter.rs::Game::compostable the item's compostable layer provider; evaluated when the composter takes it (Game::composter_add)
|
||||
ints/ContextIntProviders.cooking port crates/horizon-client-render/src/providers.rs::Providers::burn_time fuel burn times (the cooking/time_* providers)
|
||||
ints/Difference.getIntUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_int sub arm
|
||||
ints/EnvironmentAttributeValue missing minor: no vanilla data uses environment_attribute providers
|
||||
ints/EnvironmentAttributeValue missing no `environment_attribute` provider in the int reader (`loot.rs::int`)
|
||||
ints/FloorModulus.getIntUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_int floor_mod arm
|
||||
ints/FloorQuotient.getIntUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_int floor_div arm
|
||||
ints/Maximum.getIntUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_int max arm
|
||||
@@ -95,10 +96,10 @@ ints/Product.getIntUnsafe port crates/horizon-client/src/game/commands/compute.r
|
||||
ints/Quotient.getIntUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_int div arm
|
||||
ResolvableInt.getFromItem port crates/horizon-client-render/src/providers.rs::Providers::burn_time furnace fuel burn time (COOKING_FUEL); gap: minor: only the furnace fuel component is read
|
||||
ResolvableInt.wrap skip:java codec helper
|
||||
ints/ScoreboardValue.getIntUnsafe missing minor: no vanilla data uses score providers
|
||||
ints/ScoreboardValue.getScoreInfo missing minor: no vanilla data uses score providers
|
||||
ints/ScoreboardValue.getIntUnsafe missing no `score` provider in `loot.rs::int`
|
||||
ints/ScoreboardValue.getScoreInfo missing no `score` provider in `loot.rs::int`
|
||||
ints/ScoreboardValue.validate skip:data datapack validation
|
||||
ints/StorageValue missing minor: no vanilla data uses storage providers
|
||||
ints/StorageValue missing no `storage` provider in `loot.rs::int`
|
||||
ints/Sum.getIntUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_int add arm
|
||||
ints/UniformGenerator.getIntUnsafe port crates/horizon-world/src/loot.rs::Context::int uniform arm: inclusive random int between min and max
|
||||
ints/WeightedListValue.getIntUnsafe port crates/horizon-client/src/game/commands/compute.rs::Game::provider_select weighted list pick (the default branch)
|
||||
@@ -118,7 +119,7 @@ DirectoryValidator.validateSymlink skip:server server check of symlinks in worl
|
||||
PathAllowList.getForFileSystem skip:server server file allow-list
|
||||
PathAllowList.readPlain skip:server server file allow-list
|
||||
PathAllowList.ConfigEntry.parse skip:server server file allow-list
|
||||
BlockHitResult.hitBorder missing minor: BlockHit has no world-border flag; vanilla projectiles check it on a border hit
|
||||
BlockHitResult.hitBorder missing `BlockHit` (`horizon-core/src/geometry/mod.rs`) carries the location, the direction, the block and `inside`, no world-border flag, so a projectile's border hit is not told apart from any other
|
||||
BlockHitResult.withDirection skip:java copy helper (Rust struct update)
|
||||
BlockHitResult.withPosition skip:java copy helper (Rust struct update)
|
||||
HitResult.distanceTo port crates/horizon-client/src/game/fishing.rs::rebound the bobber's block-hit bounce: the squared distance to the hook scales the normalised velocity (`FishingHook.onHitBlock`)
|
||||
@@ -149,7 +150,7 @@ IdenticalMerger.forMergedIndexes port crates/horizon-core/src/geometry/shape/joi
|
||||
IndirectMerger.<init> skip:java list wrapper constructor
|
||||
IndirectMerger.forMergedIndexes port crates/horizon-core/src/geometry/shape/join.rs::merge
|
||||
IndirectMerger.getList skip:java list accessor
|
||||
MinecartCollisionContext missing player: vanilla lets a rail cart pass through the block under its rail and the slope block of an ascending rail (MinecartCollisionContext); minecarts.rs move_cart has no such exclusion, so carts may bump on those blocks (read from code, not checked in game)
|
||||
MinecartCollisionContext missing `game/minecarts.rs::move_cart` has no exclusion for the block a rail rests on or the slope block of an ascending rail, so a cart's move collides with them as with any other block
|
||||
NonOverlappingMerger.forMergedIndexes port crates/horizon-core/src/geometry/shape/join.rs::disjoint
|
||||
NonOverlappingMerger.forNonSwappedIndexes port crates/horizon-core/src/geometry/shape/join.rs::disjoint
|
||||
NonOverlappingMerger.getDouble skip:java list accessor
|
||||
|
||||
|
@@ -9,7 +9,7 @@ EnderDragonRenderer.submit port crates/horizon-client/src/game/dragon.rs::Game::
|
||||
EnderDragonRenderer.submitRays port crates/horizon-client/src/game/dragon.rs::Game::dragon_rays
|
||||
EnderDragonRenderer.submitCrystalBeams port crates/horizon-client/src/game/dragon.rs::Game::crystal_beam
|
||||
EnderDragonRenderer.extractRenderState inline crates/horizon-client/src/game/dragon.rs::Game::dragon_geometry flap, death fade, hurt overlay, nearest-crystal beam and egg distance read there
|
||||
EndermanRenderer.getRenderOffset port crates/horizon-client/src/mobs/render.rs EndermanRenderer.getRenderOffset's random side offset for a creepy enderman
|
||||
EndermanRenderer.getRenderOffset port crates/horizon-client/src/mobs/render.rs::enderman_render_offset EndermanRenderer.getRenderOffset's random side offset for a creepy enderman
|
||||
EndermanRenderer.extractRenderState inline crates/horizon-client/src/mobs/enderman.rs::anim gap: minor: carried block is drawn by game/mob_blocks.rs, not from this pose
|
||||
EntityRenderDispatcher.getRenderer skip:other renderer lookup; Horizon dispatches by mob kind in game.rs::Game::entity_geometry and game/creatures.rs::Game::mob_geometry
|
||||
EntityRenderDispatcher.getAvatarRenderer port crates/horizon-client/src/mobs/render.rs::body slim or wide arm model by skin
|
||||
@@ -65,7 +65,7 @@ HumanoidMobRenderer.getEquipmentIfRenderable port crates/horizon-client/src/mobs
|
||||
IllagerRenderer.extractRenderState port crates/horizon-client/src/mobs/illager.rs::holding_crossbow crossbow charge and aggressive pose
|
||||
IllusionerRenderer.extractRenderState port crates/horizon-client/src/mobs/illusioner.rs::offsets mirror-image offsets; casting flag
|
||||
IronGolemRenderer.extractRenderState port crates/horizon-client/src/mobs/golem.rs::client_tick gap: player: the offered poppy is not drawn in the golem hand and cracks from damage are not drawn
|
||||
IronGolemRenderer.setupRotations port crates/horizon-client/src/mobs/render.rs::base_transform_at (IronGolem branch) the 6.5-degree triangle wave over a 13-block walk phase
|
||||
IronGolemRenderer.setupRotations port crates/horizon-client/src/mobs/render.rs::base_transform_at the 6.5-degree triangle wave over a 13-block walk phase
|
||||
ItemEntityRenderer.extractRenderState port crates/horizon-client/src/game/item_draw.rs::Game::dropped_item_geometry bob offset, spin and spread
|
||||
ItemEntityRenderer.renderMultipleFromCount port crates/horizon-client/src/item_render.rs::cluster_poses copies of a stack laid out by depth
|
||||
ItemFrameRenderer.submit port crates/horizon-client/src/game/frames.rs::Game::frame_geometry frame model, item or map
|
||||
@@ -96,7 +96,7 @@ PaintingRenderer.vertex inline crates/horizon-client/src/game/paintings.rs::Game
|
||||
PandaRenderer.extractRenderState port crates/horizon-client/src/mobs/body_anim.rs::panda sit, lie, sneeze and roll states
|
||||
ParrotRenderer.extractRenderState port crates/horizon-client/src/mobs/body_anim.rs::parrot pose and flap angle
|
||||
PhantomRenderer.extractRenderState port crates/horizon-client/src/mobs/phantom.rs::anim gap: player: phantom size is not read, so phantoms never grow
|
||||
PhantomRenderer.scale port crates/horizon-client/src/mobs/render.rs::species_scale (Species::Phantom) 1 + 0.15 * size, with the model's 1.3125 offset
|
||||
PhantomRenderer.scale port crates/horizon-client/src/mobs/render.rs::species_scale 1 + 0.15 * size, with the model's 1.3125 offset
|
||||
PhantomRenderer.setupRotations port crates/horizon-client/src/mobs/phantom.rs::anim gap: minor: body pitch (xRot) is not applied; wings and tail are
|
||||
PigRenderer.bakeModels skip:data model baking; Horizon loads model data from files
|
||||
PigRenderer.submit port crates/horizon-client/src/mobs/render.rs::body cold or normal model by variant
|
||||
@@ -107,7 +107,7 @@ PiglinRenderer.extractRenderState port crates/horizon-client/src/mobs/anim.rs::h
|
||||
PiglinRenderer.isShaking port crates/horizon-client/src/mobs/render.rs::shaking converting piglin shakes
|
||||
PolarBearRenderer.extractRenderState port crates/horizon-client/src/mobs/body_anim.rs::polar_bear standing scale
|
||||
PufferfishRenderer.submit port crates/horizon-client/src/mobs/render.rs::body small, medium or big model by size
|
||||
PufferfishRenderer.setupRotations port crates/horizon-client/src/mobs/render.rs::pufferfish_bob / pufferfish_bob_y the 0.08-block bob
|
||||
PufferfishRenderer.setupRotations port crates/horizon-client/src/mobs/render.rs::pufferfish_bob the 0.08-block bob
|
||||
RabbitRenderer.extractRenderState port crates/horizon-client/src/mobs/body_anim.rs::rabbit hop and idle head-tilt animations; Toast name in mobs/render.rs::body
|
||||
RavagerRenderer.extractRenderState port crates/horizon-client/src/mobs/ravager.rs::anim stun, bite and roar clocks
|
||||
SalmonRenderer.setupRotations port crates/horizon-client/src/mobs/aquatic.rs::rotations the 4.3-amplitude swim wiggle and the flop
|
||||
@@ -132,7 +132,7 @@ ThrownItemRenderer.submit port crates/horizon-client/src/game/creatures.rs::Game
|
||||
ThrownItemRenderer.extractRenderState inline crates/horizon-client/src/game/creatures.rs::Game::mob_geometry item sprite read there
|
||||
ThrownTridentRenderer.submit port crates/horizon-client/src/game/trident.rs::Game::trident_geometry gap: minor: foil glint is not drawn on a thrown trident
|
||||
ThrownTridentRenderer.extractRenderState inline crates/horizon-client/src/game/trident.rs::Game::trident_geometry yaw, pitch and foil read there
|
||||
TippableArrowRenderer.extractRenderState port crates/horizon-client/src/projectiles.rs brewing::potion_color gives a tipped arrow its potion colour
|
||||
TippableArrowRenderer.extractRenderState port crates/horizon-client/src/projectiles.rs::Projectile::arrow_effect_color brewing::potion_color gives a tipped arrow its potion colour
|
||||
TntMinecartRenderer.submitMinecartContents port crates/horizon-client/src/game/minecarts.rs::Game::cart_geometry TNT block swell and white flash
|
||||
TntMinecartRenderer.submitWhiteSolidBlock inline crates/horizon-client/src/game/minecarts.rs::Game::cart_geometry white overlay built inline
|
||||
TntMinecartRenderer.extractRenderState inline crates/horizon-client/src/game/minecarts.rs::Game::cart_geometry fuse read from the cart
|
||||
|
||||
|
@@ -5,7 +5,7 @@ CloudRenderer.isEastEmpty inline crates/horizon-client-render/src/clouds.rs::Mes
|
||||
CloudRenderer.isSouthEmpty inline crates/horizon-client-render/src/clouds.rs::Mesh::prepare neighbour-empty flags passed to face_mask in N,E,S,W order
|
||||
CloudRenderer.isWestEmpty inline crates/horizon-client-render/src/clouds.rs::Mesh::prepare neighbour-empty flags passed to face_mask in N,E,S,W order
|
||||
CloudRenderer.render port crates/horizon-client-render/src/gfx.rs::Renderer::draw_frame gap: minor: one cloud pipeline; fancy vs flat is chosen by the mesh fast flag, not by pipeline
|
||||
CloudRenderer.renderOit port crates/horizon-client-render/src/gfx/oit.rs the clouds draw through the OIT pipeline there, with their own depth beside the world's
|
||||
CloudRenderer.renderOit port crates/horizon-client-render/src/gfx/oit.rs::Oit::render_clouds the clouds draw through the OIT pipeline there, with their own depth beside the world's
|
||||
CloudRenderer.buildMesh port crates/horizon-client-render/src/clouds.rs::Mesh::prepare cell ring walk via cells(radius), nearest first
|
||||
CloudRenderer.tryBuildCell inline crates/horizon-client-render/src/clouds.rs::Mesh::prepare skip empty cells, then build the cell's faces
|
||||
CloudRenderer.encodeFace inline crates/horizon-client-render/src/clouds.rs::Mesh::prepare 3-byte face records replaced by vertex quads
|
||||
@@ -40,7 +40,7 @@ FaceInfo.Extent.select skip:data corner selection; Horizon's unit-cube corner t
|
||||
FaceInfo.VertexInfo.select skip:data corner selection; Horizon's unit-cube corner table PORTAL_FACES in block_entity_render.rs has the same order
|
||||
FirstPersonHandsAndItemsRenderer.calculateMapTilt inline crates/horizon-client/src/game/maps.rs::Game::two_handed_map_geometry tilt computed inline with the calculateMapTilt comment
|
||||
FirstPersonHandsAndItemsRenderer.renderPlayerHand port crates/horizon-client/src/game.rs::Game::hand_geometry empty main hand's arm model with the player skin
|
||||
FirstPersonHandsAndItemsRenderer.renderMapHand port crates/horizon-client/src/first_person.rs::map_arm_poses (MapHold::TwoHanded) both arms behind a two-handed map, at the jar's rotations and offsets
|
||||
FirstPersonHandsAndItemsRenderer.renderMapHand port crates/horizon-client/src/first_person.rs::map_arm_poses both arms behind a two-handed map, at the jar's rotations and offsets
|
||||
FirstPersonHandsAndItemsRenderer.renderOneHandedMap port crates/horizon-client/src/first_person.rs::one_handed_map_pose gap: player: the arm holding a map is not drawn; the map hand branch skips the arm
|
||||
FirstPersonHandsAndItemsRenderer.renderTwoHandedMap port crates/horizon-client/src/game/maps.rs::Game::two_handed_map_geometry gap: player: the arms that hold a two-handed map are not drawn
|
||||
FirstPersonHandsAndItemsRenderer.renderMap port crates/horizon-client/src/game/maps.rs::Game::held_map map background with its 7-pixel border, then the map contents
|
||||
@@ -57,17 +57,17 @@ GameRenderer.processBlurEffect port crates/horizon-client-render/src/menu_blur.r
|
||||
GameRenderer.preloadUiShader skip:other GPU pipeline precompile at load; Horizon builds its pipelines in Renderer::new
|
||||
GameRenderer.update port crates/horizon-client-render/src/camera.rs::AvatarBob::update_bob gap: minor: the requested post-effect list (from /postEffect) is not built
|
||||
GameRenderer.extract skip:other render-state copy for the vanilla render thread; Horizon renders from game state
|
||||
GameRenderer.preparePostEffects port crates/horizon-client-render/src/gfx.rs::post_effects (post_chain::PostEffects, built at gfx.rs:8424) vanilla post chains are made on first use from the request the game builds
|
||||
GameRenderer.applyPostEffects port crates/horizon-client-render/src/gfx.rs::post_effects the same chain runs after the world pass
|
||||
GameRenderer.tryTakeScreenshotIfNeeded port crates/horizon-client/src/game.rs::tick_world_icon (called at game.rs:17852) a local world without an icon requests a 64x64 ScreenshotRequest for saves/<folder>/icon.png once the streamer, uploads and backlog are idle and more than 10 sections are drawn
|
||||
GameRenderer.preparePostEffects port crates/horizon-client-render/src/post_chain.rs::PostEffects::apply vanilla post chains are made on first use from the request the game builds
|
||||
GameRenderer.applyPostEffects port crates/horizon-client-render/src/post_chain.rs::PostEffects::apply the same chain runs after the world pass
|
||||
GameRenderer.tryTakeScreenshotIfNeeded port crates/horizon-client/src/game.rs::Game::tick_world_icon a local world without an icon requests a 64x64 ScreenshotRequest for saves/<folder>/icon.png once the streamer, uploads and backlog are idle and more than 10 sections are drawn
|
||||
GameRenderer.takeAutoScreenshot inline crates/horizon-client/src/game.rs::tick_world_icon the same retry, once a second until the file exists; the path comes from game/integrated.rs:485
|
||||
GameRenderer.integrate3DHudDepth inline crates/horizon-client-render/src/gfx.rs::post_effects runs with a post chain applied, which is vanilla's consistentDepthRequired condition
|
||||
GameRenderer.integrate3DHudDepth inline crates/horizon-client-render/src/post_chain.rs::PostEffects::apply runs with a post chain applied, which is vanilla's consistentDepthRequired condition
|
||||
GameRenderer.extractWindow skip:other render-state copy of the window size; Horizon reads the window directly
|
||||
GameRenderer.extractOptions skip:other render-state copy of options; Horizon reads settings directly
|
||||
GameRenderer.extractCamera skip:other render-state copy of camera and fog; Horizon computes them in camera.rs and fog.rs
|
||||
GameRenderer.resetData inline crates/horizon-client/src/game.rs::Game::finish_close_world boss overlay reset on world close; camera and map-texture resets not verified here
|
||||
GameRenderer.projectPointToScreen skip:other no callers in vanilla 26.3
|
||||
GameRenderer.useImprovedTransparency port crates/horizon-client-render/src/gfx.rs::improved_transparency / oit: Option<oit::Oit> the option builds the OIT pass
|
||||
GameRenderer.useImprovedTransparency port crates/horizon-client-render/src/gfx.rs::Renderer::draw_frame the option builds the OIT pass
|
||||
GlobalSettingsUniform.update skip:other global uniform (window size, time, gamma, glint) written per pass; Horizon sets its own uniforms
|
||||
GpuWarnlistManager.getAllWarnings missing minor: no GPU driver warnlist; Horizon does not check the driver against known-bad lists
|
||||
GpuWarnlistManager.compilePatterns missing minor: no GPU driver warnlist patterns
|
||||
@@ -82,9 +82,9 @@ LevelRenderer.addMainPass port crates/horizon-client-render/src/gfx.rs::Renderer
|
||||
LevelRenderer.executeAlwaysOnTop port crates/horizon-client/src/game.rs::Game::render always-on-top debug lines and crosshair drawn over the world
|
||||
LevelRenderer.executeSolid port crates/horizon-client-render/src/gfx.rs::Renderer::draw_sections solid section draws in arena page order
|
||||
LevelRenderer.prepareTranslucents port crates/horizon-client-render/src/gfx/translucency.rs::Sorter::sort translucent quads sorted from the camera each frame
|
||||
LevelRenderer.executeOit port crates/horizon-client-render/src/gfx/oit.rs + gfx::mark_oit (gfx/entity_batch.rs:31) translucent geometry is marked and resolved by the OIT pass
|
||||
LevelRenderer.executeDepthBoundsCull inline crates/horizon-client-render/src/gfx/oit.rs part of the same pass
|
||||
LevelRenderer.executeOitWaterMask port crates/horizon-client-render/src/gfx.rs::WATER_MASK_OVERLAY the water mask overlay of the same pass
|
||||
LevelRenderer.executeOit port crates/horizon-client-render/src/gfx/oit.rs::Oit::run translucent geometry is marked and resolved by the OIT pass
|
||||
LevelRenderer.executeDepthBoundsCull inline crates/horizon-client-render/src/gfx/oit.rs::Oit::execute_depth_bounds_cull part of the same pass
|
||||
LevelRenderer.executeOitWaterMask port crates/horizon-client-render/src/gfx/oit.rs::Oit::execute_water_mask the water mask overlay of the same pass
|
||||
LevelRenderer.extractSectionDrawGroups port crates/horizon-client-render/src/gfx.rs::Renderer::draw_frame section draws grouped per layer and arena page from the visible sections
|
||||
LevelRenderer.prepareChunkRenders port crates/horizon-client-render/src/gfx.rs::Renderer::draw_frame per-layer draw list built for the frame, reversed for translucent layers
|
||||
LevelRenderer.performTransientBlockRemovals inline crates/horizon-client/src/game/transient_blocks.rs::add_transient_block transient blocks are tracked and dropped by that list
|
||||
@@ -93,7 +93,7 @@ LevelRenderer.compileSections port crates/horizon-client-render/src/streaming.rs
|
||||
LevelRenderer.submitEntities port crates/horizon-client-render/src/gfx.rs::Renderer::draw_frame entity models drawn with their batches
|
||||
LevelRenderer.submitBlockEntities port crates/horizon-client-render/src/gfx.rs::Renderer::block_entities block entity list submitted for drawing
|
||||
LevelRenderer.submitBlockDestroyAnimation port crates/horizon-client-render/src/gfx.rs::Renderer::draw_frame crumbling overlay drawn on the block being broken
|
||||
LevelRenderer.submitTransientBlocks port crates/horizon-client/src/game/remote.rs: C::AddTransientBlock => add_transient_block the packet is handled and the block is drawn
|
||||
LevelRenderer.submitTransientBlocks port crates/horizon-client/src/game/transient_blocks.rs::Game::transient_block_geometry the packet is handled and the block is drawn
|
||||
LevelRenderer.submitHitOutline port crates/horizon-client/src/game.rs::Game::outline_boxes outline of the block under the crosshair
|
||||
LevelRenderer.close skip:other GPU resource teardown
|
||||
LevelRenderer.invalidateCompiledGeometry port crates/horizon-client-render/src/gfx.rs::Renderer::clear_sections all section meshes dropped and rebuilt when settings are applied
|
||||
|
||||
|
@@ -9,7 +9,7 @@ ItemFrame.getSlot inline crates/horizon-client/src/game/commands/items.rs::Game:
|
||||
ItemFrame.onSyncedDataUpdated skip:other network data-sync hook; the item is read from the frame field on demand
|
||||
ItemFrame.setRotation inline crates/horizon-client/src/game/frames.rs::Game::interact_frame the rotate-by-one step; the neighbour update is in frame_output_changed
|
||||
ItemFrame.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::item_frame_from_nbt gap: minor: Facing defaults to 2 where vanilla defaults to DOWN
|
||||
ItemFrame.getFrameItemStackWithData missing minor: a broken frame never carries its custom name; drop_frame_items builds a plain item_frame stack
|
||||
ItemFrame.getFrameItemStackWithData missing `game/frames.rs::drop_frame_items` builds a plain `item_frame` stack, so a broken frame never carries its custom name
|
||||
ItemFrame.getVisualRotationYInDegrees inline crates/horizon-client/src/game/frames.rs::Game::frame_geometry the 45 degree step per rotation step is drawn in frame_geometry
|
||||
LeashFenceKnotEntity.recalculateBoundingBox port crates/horizon-client/src/mobs/leash.rs::knot_state position and box from the block position
|
||||
LeashFenceKnotEntity.getOrCreateKnot inline crates/horizon-client/src/game/leads.rs::Game::interact_knot knots are not entities in game; the knot is used when a lead is tied
|
||||
@@ -35,15 +35,15 @@ ItemEntity.hurtClient skip:other client-side hurt prediction; items are not att
|
||||
ItemEntity.hurtServer inline crates/horizon-client/src/game/explosion.rs::Game::explosion_hurt_entities gap: minor: explosions destroy items at damage 5 without subtracting health; fire and lava hurt are in environment_tick
|
||||
ItemEntity.addAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::item_entity_to_nbt
|
||||
ItemEntity.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::item_entity_from_nbt a missing Item returns None, which matches the discard
|
||||
ItemEntity.getName missing minor: a dropped item's name (custom or item name) is not used anywhere in Horizon
|
||||
ItemEntity.getName missing a dropped item's name (custom or the item's own) is not read anywhere
|
||||
ItemEntity.teleport inline crates/horizon-client/src/entities.rs::merge_with_neighbours the arrival merge is the normal per-tick merge, not a teleport step
|
||||
ItemEntity.getVisualRotationYInDegrees inline crates/horizon-client/src/game/item_draw.rs::Game::dropped_item_geometry spin is applied in radians there; no degree conversion
|
||||
ItemEntity.getSlot missing unsure: no slot 0 access for dropped items found in commands.rs
|
||||
ItemEntity.getSlot missing a dropped item is not addressable as an entity slot: `game/commands.rs` has no item entity case
|
||||
PrimedTnt.defineSynchedData skip:other fuse and block state are struct fields; spawn sets fuse 80; the block is always TNT
|
||||
PrimedTnt.explode inline crates/horizon-client/src/game/tnt.rs::Game::tick_tnt the power is the tnt's own field (`explosion_power`, 4 by default); gap: minor: no used-portal damage calculator
|
||||
PrimedTnt.addAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::tnt_to_nbt the fuse and the block state are saved
|
||||
PrimedTnt.restoreFrom missing minor: PrimedTnt has no owner field, so the igniter is not kept through portal moves
|
||||
PrimedTnt.teleport missing unsure: no used-portal flag on PrimedTnt; the portal damage calculator is not applied
|
||||
PrimedTnt.restoreFrom missing `PrimedTnt` has no owner field, so the igniter is not kept through a dimension change
|
||||
PrimedTnt.teleport missing `PrimedTnt` carries no used-portal flag and the portal damage calculator is not applied to it
|
||||
Blaze.registerGoals port crates/horizon-client/src/mobs/nether.rs::register_blaze gap: minor: MoveTowardsRestriction goal (priority 5) is not registered
|
||||
Blaze.createAttributes skip:data attribute defaults come from data tables
|
||||
Blaze.isCharged inline crates/horizon-client/src/mobs/nether.rs::BlazeAttack::tick the charged state is m.extra.flag, set in the attack goal tick
|
||||
@@ -88,7 +88,7 @@ Guardian.checkGuardianSpawnRules port crates/horizon-client/src/mobs/spawn_rules
|
||||
Guardian.GuardianAttackGoal.canContinueToUse port crates/horizon-client/src/mobs/guardian.rs::GuardianAttack::can_continue
|
||||
Guardian.GuardianAttackSelector.test inline crates/horizon-client/src/mobs/guardian.rs::register the selector is the player, squid and axolotl beyond 9 filters in register
|
||||
Monster.updateNoActionTime port crates/horizon-client/src/mobs/ai.rs::server_ai_step the idle counter is counted up there and read by the despawn path in raids.rs
|
||||
Monster.getProjectile missing unsure: no held-ammo choice for mobs found in mobs/*.rs
|
||||
Monster.getProjectile missing no held-ammo choice in `mobs/*.rs`: a monster's shot takes its projectile kind from the species, not from the stack it holds
|
||||
PatrollingMonster.addAdditionalSaveData port crates/horizon-client/src/mobs/raider.rs::write_nbt
|
||||
PatrollingMonster.readAdditionalSaveData port crates/horizon-client/src/mobs/raider.rs::read_nbt
|
||||
PatrollingMonster.checkPatrollingMonsterSpawnRules port crates/horizon-client/src/mobs/spawn_rules.rs::check_spawn_rules the Pillager arm: block light 8 or less, then mob rules
|
||||
@@ -103,12 +103,12 @@ Phantom.PhantomBodyRotationControl.clientTick inline crates/horizon-client/src/m
|
||||
Phantom.PhantomMoveTargetGoal.touchingTarget inline crates/horizon-client/src/mobs/phantom.rs::CircleAroundAnchor::tick the squared distance under 4 check near the move target
|
||||
Phantom.PhantomSweepAttackGoal.canContinueToUse port crates/horizon-client/src/mobs/phantom.rs::SweepAttack::can_continue the cat hiss and scare check
|
||||
Ravager.registerGoals port crates/horizon-client/src/mobs/ravager.rs::register
|
||||
Ravager.updateControlFlags missing unsure: no rider control-flag switch for ravagers in ravager.rs
|
||||
Ravager.updateControlFlags missing the goal set has no control flags, so a mob driving a boat does not switch off the ravager's own move, jump and look goals
|
||||
Ravager.createAttributes skip:data attribute defaults come from data tables
|
||||
Ravager.addAdditionalSaveData inline crates/horizon-client/src/entity_nbt.rs::mob_to_nbt AttackTick, StunTick and RoarTick are written there
|
||||
Ravager.readAdditionalSaveData inline crates/horizon-client/src/entity_nbt.rs::mob_from_nbt the same three tick values are read there
|
||||
Ravager.stunEffect port crates/horizon-client/src/mobs/ravager.rs::ai_step the stun particle is emitted from ai_step
|
||||
Ravager.hasLineOfSight missing unsure: no stunned or roaring line-of-sight cut-off in ravager.rs
|
||||
Ravager.hasLineOfSight missing `ravager::immobile` knows the biting, stunned and roaring states, but no target goal is gated on it, so an immobile ravager still sees and takes targets
|
||||
Ravager.applyRoarKnockbackClient inline crates/horizon-client/src/mobs/ravager.rs::roar the roar push is applied in roar
|
||||
Ravager.strongKnockback inline crates/horizon-client/src/mobs/ravager.rs::roar the push of 4 along the direction is in roar
|
||||
Ravager.addRoarParticleEffects inline crates/horizon-client/src/mobs/ravager.rs::roar the poof particles are emitted in roar
|
||||
@@ -119,18 +119,18 @@ Shulker.addAdditionalSaveData inline crates/horizon-client/src/entity_nbt.rs::mo
|
||||
Shulker.findNewAttachment inline crates/horizon-client/src/mobs/shulker.rs::tick keeps its face or teleports in tick, through attachable and teleport
|
||||
Shulker.makeBoundingBox port crates/horizon-client/src/mobs/shulker.rs::progress_aabb
|
||||
Shulker.getDefaultDimensions inline crates/horizon-client/src/mobs/shulker.rs::progress_aabb gap: minor: the down-face height scale is not seen here
|
||||
Shulker.startRiding missing unsure: no attach-face reset to DOWN when a shulker mounts a vehicle in shulker.rs
|
||||
Shulker.stopRiding missing unsure: no body yaw reset after a shulker dismounts in shulker.rs
|
||||
Shulker.move missing unsure: no shulker-box move type (teleport) found in the piston code
|
||||
Shulker.startRiding missing no attach-face reset on boarding: `mobs/shulker.rs` sets `extra.variant3` from `Attachable` and the save only, so a shulker on a vehicle keeps the face it had
|
||||
Shulker.stopRiding inline crates/horizon-client/src/mobs/shulker.rs::tick the body yaw is pinned to 0 on every tick (`shulker::tick`, and `client_tick` for a remote one), so a dismounted shulker shows no leftover body yaw without a reset of its own
|
||||
Shulker.move missing the `SHULKER_BOX` mover type has no counterpart: `shulker::teleport` runs from the tick and the bullet hit only, so a shulker box pushed by a piston never takes its shulker along
|
||||
Shulker.findAttachableSurface port crates/horizon-client/src/mobs/shulker.rs::attachable
|
||||
Shulker.isPositionBlocked inline crates/horizon-client/src/mobs/shulker.rs::can_stay_at the blocked-position check is part of can_stay_at
|
||||
Shulker.onSyncedDataUpdated skip:other network sync hook; the box is computed from the face on demand
|
||||
Shulker.getRenderPosition missing unsure: no render slide after a shulker teleport in shulker.rs
|
||||
Shulker.getRenderPosition missing no render slide: `client_tick` eases the lid and squares the body, and nothing interpolates the position across a teleport
|
||||
Shulker.setVariant inline crates/horizon-client/src/mobs/shulker.rs::register colour comes from spawn data; NO_COLOR when unset
|
||||
Shulker.getColor inline crates/horizon-client/src/mobs/shulker.rs::texture colour is read from extra.color for the texture
|
||||
Shulker.get skip:other component getter; Horizon reads extra.color directly
|
||||
Shulker.applyImplicitComponent missing minor: a shulker spawned with a SHULKER_COLOR component ignores it
|
||||
Shulker.ShulkerDefenseAttackGoal.getTargetSearchArea missing minor: shulkers do not defend nearby shulkers; there is no defense target goal
|
||||
Shulker.applyImplicitComponent missing a shulker spawned from a shulker box item ignores its colour: the implicit components are applied to a mob coming out of a bucket only (`entity_nbt::bucket_components`)
|
||||
Shulker.ShulkerDefenseAttackGoal.getTargetSearchArea missing no defense target goal: `mobs/shulker.rs::register` gives the shulker `HurtByTarget` with `alert` (a hurt shulker wakes the targetless ones around it) and a player search box 4 deep along the attach axis, nothing that defends another shulker
|
||||
Shulker.ShulkerNearestAttackGoal.getTargetSearchArea inline crates/horizon-client/src/mobs/shulker.rs::register the 4-deep search box is the when filter in register
|
||||
Shulker.ShulkerPeekGoal.canContinueToUse port crates/horizon-client/src/mobs/shulker.rs::Peek::can_continue
|
||||
Silverfish.registerGoals port crates/horizon-client/src/mobs/hostile.rs::register
|
||||
|
||||
|
@@ -6,7 +6,7 @@ ZombieVillager.initializeZombieVillagerData port crates/horizon-client/src/mobs/
|
||||
ZombieVillager.handleEntityEvent port crates/horizon-client/src/mobs/zombie_villager.rs::start_converting cure sound (event 16) is queued when the cure starts
|
||||
ZombieVillager.setVillagerData inline crates/horizon-client/src/game/zombie_villagers.rs::carry_over gap: minor: the cure path copies type and profession there; no setter for the implicit component path
|
||||
ZombieVillager.get skip:other DataComponents getter for the villager variant; Horizon has no entity component map
|
||||
ZombieVillager.applyImplicitComponent missing minor: a zombie villager spawned from an item carrying a villager variant component keeps a random type; Horizon applies implicit components only to buckets (load_bucket_data)
|
||||
ZombieVillager.applyImplicitComponent missing an implicit component is applied only to a mob coming out of a bucket (`entity_nbt::bucket_components`), so a zombie villager spawned from an item carrying a villager variant component keeps a random type
|
||||
ZombifiedPiglin.createAttributes skip:data attribute defaults are entity-type data read in Mob::new (mobs.rs)
|
||||
ZombifiedPiglin.maybePlayFirstAngerSound port crates/horizon-client/src/mobs/undead.rs::zombified_piglin_step first-anger counter and sound run in the step
|
||||
ZombifiedPiglin.maybeAlertOthers port crates/horizon-client/src/mobs/undead.rs::zombified_piglin_step alert timer and line-of-sight check run in the step
|
||||
@@ -39,8 +39,8 @@ Villager.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::re
|
||||
Villager.getAmbientSound port crates/horizon-client/src/mobs/ai.rs::play_ambient gap: minor: the trade ambient sound is not played while trading
|
||||
Villager.rewardTradeXp port crates/horizon-client/src/merchant.rs::Session::notify_trade gap: minor: no 10 second Regeneration on the villager at level-up
|
||||
Villager.die port crates/horizon-client/src/game/villagers.rs::Game::villager_murdered gap: minor: releaseAllPois is not done on death
|
||||
Villager.releaseAllPois missing unsure: releasePoi frees the bed, job site, potential job site and meeting point claims; Horizon has no claim store, only the block occupied property, so the effect is unclear
|
||||
Villager.releasePoi missing unsure: same as releaseAllPois; the claim store is not clear in Horizon
|
||||
Villager.releaseAllPois missing claims are per-mob fields (`extra.bed`, `extra.home` for the job site, `extra.meeting`) with no world-side point-of-interest store to release from; freeing a claim for another villager while this one lives has no counterpart
|
||||
Villager.releasePoi missing same as `releaseAllPois`: a single claim is that mob's field, dropped with the mob
|
||||
Villager.eatUntilFull port crates/horizon-client/src/mobs/villager.rs::Ext::eat_and_digest
|
||||
Villager.shouldIncreaseLevel inline crates/horizon-client/src/merchant.rs::Session::notify_trade level-up check: can level up and xp at the maximum
|
||||
Villager.increaseMerchantCareer inline crates/horizon-client/src/merchant.rs::Session::notify_trade level plus the new level trade set
|
||||
@@ -53,7 +53,7 @@ Villager.updateTrades port crates/horizon-client/src/game/trading.rs::Game::ensu
|
||||
Villager.maybeDecayGossip port crates/horizon-client/src/mobs/villager.rs::Ext::maybe_decay
|
||||
Villager.golemSpawnConditionsMet port crates/horizon-client/src/mobs/villager.rs::Ext::wants_golem sleep memory check; the golem cooldown is an extra check here
|
||||
Villager.get skip:other DataComponents getter for the villager variant; Horizon has no entity component map
|
||||
Villager.applyImplicitComponent missing minor: a villager spawned from an item carrying a villager variant component keeps its biome type; Horizon applies implicit components only to buckets
|
||||
Villager.applyImplicitComponent missing the same bucket-only path (`entity_nbt::bucket_components`), so a villager spawned from an item keeps the type its biome gives it
|
||||
VillagerData.withType skip:java immutable record copy; Horizon keeps type and profession as Mob fields
|
||||
VillagerData.withProfession skip:java immutable record copy; the profession change is villager::set_profession
|
||||
VillagerProfession.bootstrap skip:data registry bootstrap of professions; Horizon reads them from data tables
|
||||
@@ -87,9 +87,9 @@ Inventory.removeFromSelected port crates/horizon-world/src/player_inventory.rs::
|
||||
Player.defineSynchedData skip:java synched data definitions; Horizon keeps the fields in game and player state
|
||||
Player.turtleHelmetTick port crates/horizon-client/src/game/potions.rs::Game::turtle_helmet_tick
|
||||
Player.isEquipped inline crates/horizon-client/src/game/potions.rs::Game::turtle_helmet_tick the head-slot check for the turtle helmet
|
||||
Player.canPlayerFitWithinBlocksAndEntitiesWhen missing unsure: the pose fit check (no collision in the new pose box) was not found in Horizon
|
||||
Player.canPlayerFitWithinBlocksAndEntitiesWhen port crates/horizon-client/src/pose.rs::MotionWorld::can_fit_pose the box of the pose's dimensions at the player's position, deflated by 1e-7, tested against the player's collision; `update_player_pose` asks it for the swimming, the desired and the crouching pose in that order, as `updatePlayerPose` does
|
||||
Player.increaseScore skip:other the score is experience.total in Horizon (death screen); there is no separate score counter
|
||||
Player.getWeaponItem missing unsure: the riptide spin-attack item (autoSpinAttackItemStack) was not found; Horizon keeps spin_attack parts only
|
||||
Player.getWeaponItem missing the riptide spin attack keeps only `auto_spin_attack_ticks` and `auto_spin_attack_dmg`, not the stack it started with (`autoSpinAttackItemStack`), so there is no item to answer with mid-spin
|
||||
Player.destroyVanishingCursedItems port crates/horizon-client/src/game.rs::Game::drop_inventory vanishing-curse stacks are removed from the death drops
|
||||
Player.hasCorrectToolForDrops port crates/horizon-client/src/game.rs::Game::tick_mining the correct-tool test for harvest drops
|
||||
Player.readAdditionalSaveData port crates/horizon-client/src/save.rs::Level::read_player
|
||||
@@ -100,12 +100,12 @@ Player.canFallAtLeast inline crates/horizon-world/src/edge_backoff.rs::back_off_
|
||||
Player.deflectProjectile port crates/horizon-client/src/game/deflect.rs::Game::attack_redirectable
|
||||
Player.baseDamageScaleFactor inline crates/horizon-client/src/game/mace.rs::Game::sweep_damage
|
||||
Player.isEffectiveAi skip:other client-side AI gate; Horizon has no effective-AI check in its loop
|
||||
Player.hasContainerOpen missing unsure: the test for an open container menu (not the inventory menu) was not located
|
||||
Player.hasContainerOpen missing no test for an open container menu: screens are `self.screen`, and nothing distinguishes a container menu from the inventory menu
|
||||
Player.startSleepInBed port crates/horizon-client/src/game.rs::Game::try_sleep
|
||||
Player.isSleepingLongEnough port crates/horizon-client/src/game/lan_actions.rs::sleep_counts 100 ticks in bed is what counts a sleeper as deep for SleepStatus.update
|
||||
Player.updateSwimming port crates/horizon-client/src/motion.rs::MotionWorld::swimming
|
||||
Player.getXpNeededForNextLevel port crates/horizon-client/src/entities.rs::Experience::needed
|
||||
Player.hasEnoughFoodToDoExhaustiveManoeuvres missing unsure: the food-or-flying test for exhaustive moves was not found in motion.rs
|
||||
Player.hasEnoughFoodToDoExhaustiveManoeuvres missing no food-or-flying gate anywhere in `motion.rs`: the exhaustion of a jump or a move is added without asking the food level
|
||||
Player.mayUseItemAt port crates/horizon-client/src/game.rs::Game::use_hand the may-build test before use-on in adventure mode
|
||||
Player.isSwimming port crates/horizon-client/src/motion.rs::MotionWorld::swimming
|
||||
Player.decorateDisplayNameComponent port crates/horizon-client/src/game/commands.rs::player_display_name click suggests /tell, hover shows the player entity, plus the insertion
|
||||
@@ -133,10 +133,10 @@ StackedContents.RecipePicker.getVisitedIngredientIndex skip:java bitset index a
|
||||
StackedContents.RecipePicker.getVisitiedItemIndex skip:java bitset index arithmetic; plain bool vectors in Horizon
|
||||
StackedContents.RecipePicker.clearAllVisited port crates/horizon-client/src/menu/place.rs::Picker::clear_visited
|
||||
StackedItemContents.getBiggestCraftableStack port crates/horizon-client/src/menu/place.rs::Stacked::try_pick_all the largest craftable count, found by a bounded search
|
||||
EvokerFangs.readAdditionalSaveData missing minor: fangs are not saved; a save in mid-bite drops them
|
||||
EvokerFangs.addAdditionalSaveData missing minor: fangs are not saved; the warmup and owner are not written
|
||||
EvokerFangs.readAdditionalSaveData missing fangs are not saved: `entity_nbt.rs` has no evoker fangs writer, so a save in mid-bite drops them
|
||||
EvokerFangs.addAdditionalSaveData missing no writer for the warmup delay or the owner either
|
||||
EvokerFangs.getAnimationProgress port crates/horizon-client/src/game/fangs.rs::Game::fangs_geometry the bite progress that positions the fang model
|
||||
EyeOfEnder.setItem skip:ui the item sprite a flying eye shows; Horizon draws the eye itself
|
||||
EyeOfEnder.shouldRenderAtSqrDistance skip:ui render distance culling only
|
||||
EyeOfEnder.spawnParticles port crates/horizon-client/src/game/end.rs::Game::eye_break_particles the portal crumbs drawn in toward the eye
|
||||
EyeOfEnder.readAdditionalSaveData missing minor: flying eyes are not saved; a save mid-flight loses them
|
||||
EyeOfEnder.readAdditionalSaveData missing a flying eye of ender is not saved: `entity_nbt.rs` has no writer for it (nor `saved_with_chunk` a kind for it), so a save mid-flight loses it
|
||||
|
@@ -20,16 +20,16 @@ FishingHook.handleEntityEvent inline crates/horizon-client/src/game/fishing.rs::
|
||||
FishingHook.getPlayerOwner skip:other owner is implicit: the host's hook is self.hook and a guest's is in that guest's play state
|
||||
FishingHook.getAddEntityPacket port crates/horizon-client/src/game/fishing.rs::Hook::track add-entity payload: the owner entity id is the bobber's data field
|
||||
LlamaSpit.onHitEntity port crates/horizon-client/src/game/creatures.rs::Game::projectile_hit spit hit: 1.0 damage with the spit source to a player (mob_hits_player) or a mob
|
||||
Projectile.applyImplicitComponent missing minor: a projectile given a can_break component by its item keeps no break predicate; Horizon applies implicit components only to buckets
|
||||
Projectile.applyImplicitComponent missing an implicit component is applied only to a mob coming out of a bucket (`entity_nbt::bucket_components`, `game/creatures.rs:5058`), so a projectile spawned from an item with a `can_break` component keeps no break predicate
|
||||
Projectile.addAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::put_projectile Owner and LeftOwner only when set, HasBeenShot once it has ticked (a save before the first tick keeps what it loaded)
|
||||
Projectile.ownedBy inline crates/horizon-client/src/projectiles.rs::Projectile::tick the owner is skipped in the target loop until the projectile has left it
|
||||
Projectile.readAdditionalSaveData inline crates/horizon-client/src/entity_nbt.rs::throwable_from_nbt no shared reader: every kind's own reader takes Owner, LeftOwner and NoGravity (this one, `fireball_from_nbt`, `arrow_from_nbt`); HasBeenShot is read for arrows only, and no projectile keeps a `can_break` predicate
|
||||
Projectile.restoreFrom missing minor: the owner is not carried over a dimension change; Horizon has no projectile dimension change
|
||||
Projectile.restoreFrom missing no projectile `restoreFrom`: the owner is not carried over a dimension change
|
||||
Projectile.isOutsideOwnerCollisionRange inline crates/horizon-client/src/projectiles.rs::Projectile::tick gap: minor: left_owner tests the owner's box inflated 1.0 at the position only; vanilla sweeps along the motion and checks the owner's passengers too
|
||||
Projectile.getMovementToShoot port crates/horizon-client/src/projectiles.rs::Projectile::shoot direction plus triangle spread 0.0172275 times uncertainty, scaled by power
|
||||
Projectile.onAboveBubbleColumn port crates/horizon-client/src/arrows.rs::Ticker::bubble_column gap: minor: arrows only; thrown ender pearls get their push in creatures.rs tick_projectiles and other projectiles get none
|
||||
Projectile.onInsideBubbleColumn port crates/horizon-client/src/arrows.rs::Ticker::bubble_column gap: minor: arrows only; thrown ender pearls get their push in creatures.rs tick_projectiles and other projectiles get none
|
||||
Projectile.canBreakBlockInAdventureMode missing minor: the can_break predicate and adventure-mode rules for projectile block breaking are not modelled
|
||||
Projectile.canBreakBlockInAdventureMode missing a projectile's block breaking ignores the `can_break` predicate and the adventure-mode rules
|
||||
Projectile.spawnProjectileFromRotation port crates/horizon-client/src/projectiles.rs::Projectile::shoot_from_rotation gap: minor: the aim and inherited velocity only; the add-to-world step is the caller's
|
||||
Projectile.spawnProjectile inline crates/horizon-client/src/game/creatures.rs::Game::throw_item each shooter adds its own projectile (throw_item, shoot_crossbow, shoot_projectile_item, the bow in game.rs); throw_item is the player throw path
|
||||
Projectile.applyOnProjectileSpawned port crates/horizon-client/src/game/enchant_effects.rs::Game::bow_projectile_ignite gap: minor: Flame ignite only; Piercing and Punch go through Projectile::set_bonus
|
||||
@@ -43,14 +43,14 @@ ProjectileUtil.getHitResultOnViewVector port crates/horizon-client/src/game/pick
|
||||
ProjectileUtil.getHitResult port crates/horizon-client/src/projectiles.rs::Projectile::tick block clip first, then the nearest entity box on the segment; same core as the move-vector sweep
|
||||
ProjectileUtil.rotateTowardsMovement port crates/horizon-client/src/projectiles.rs::Projectile::update_rotation gap: minor: rotation is set at once, vanilla lerps it by rotationSpeed
|
||||
ProjectileUtil.getMobArrow port crates/horizon-client/src/mobs/undead.rs::perform_ranged_attack gap: minor: builds the skeleton-family arrow with base damage from power; illager crossbow arrows are built in mobs/illager.rs::shoot_crossbow
|
||||
ShulkerBullet.addAdditionalSaveData missing minor: the final target and move direction are not saved; Horizon does not save flying projectiles
|
||||
ShulkerBullet.readAdditionalSaveData missing minor: the final target and move direction are not read back; Horizon does not save flying projectiles
|
||||
ShulkerBullet.canHitEntity missing minor: no-physics entities are not skipped by the bullet's target search; Projectile::tick checks owner and boxes only
|
||||
ShulkerBullet.addAdditionalSaveData missing `entity_nbt.rs::saved_with_chunk` leaves `ShulkerBullet` out of the kinds a chunk writes, so a bullet in flight is dropped with the save; the final target and the move direction have no writer
|
||||
ShulkerBullet.readAdditionalSaveData missing the same gap on the load side: nothing reads a bullet's final target or move direction back
|
||||
ShulkerBullet.canHitEntity missing the bullet's target search (`Projectile::tick` on the client, `projectiles` in `game/creatures.rs`) tests the owner and the boxes only; a no-physics entity is not skipped
|
||||
ShulkerBullet.onHitBlock port crates/horizon-client/src/game/creatures.rs::Game::projectile_hit shulker bullet block hit: hit sound and two explosion particles at the bullet
|
||||
ShulkerBullet.destroy inline crates/horizon-client/src/game/creatures.rs::Game::tick_projectiles the bullet is dropped from the flight list on any hit; the game event is not sent
|
||||
ShulkerBullet.hurtServer port crates/horizon-client/src/game/deflect.rs::Game::hit_shulker_bullet
|
||||
ThrowableProjectile.applyInertia port crates/horizon-client/src/projectiles.rs::Projectile::tick default arm: gravity, then drag 0.8 in water and 0.99 in air; gap: minor: no bubble particles in water
|
||||
ThrowableProjectile.handleFirstTickBubbleColumn missing minor: a thrown item that spawns inside a bubble column is not pushed on its first tick
|
||||
ThrowableProjectile.handleFirstTickBubbleColumn missing a thrown item spawned inside a bubble column is not pushed on its first tick: `game/creatures.rs` has bubble-column particles and the pearl's own column hooks, nothing for a throwable's first tick
|
||||
AbstractArrow.<init> port crates/horizon-client/src/arrows.rs::ArrowState::new arrow defaults: pickup, pickup item, shake, pierce; the owner and drawn ammo come from set_owner and set_item
|
||||
AbstractArrow.defineSynchedData skip:java flags, pierce level and in-ground sync fields; Horizon keeps them on ArrowState and Projectile
|
||||
AbstractArrow.lerpMotion skip:other client interpolation of a server motion packet; Horizon's remote arrows take the server position each update
|
||||
@@ -68,7 +68,7 @@ AbstractArrow.tryPickup port crates/horizon-client/src/arrows.rs::ArrowState::pl
|
||||
AbstractArrow.setFlag port crates/horizon-client/src/projectiles.rs::Projectile::set_critical gap: minor: only the critical flag; the no-physics flag is a plain field
|
||||
AbstractArrow.setPickupItemStack port crates/horizon-client/src/arrows.rs::ArrowState::set_item an empty stack falls back to the default pickup item
|
||||
AbstractArrow.isNoPhysics inline crates/horizon-client/src/arrows.rs::Ticker::tick no_physics is a field read by the tick
|
||||
AbstractArrow.getSlot missing minor: the arrow's pickup stack is not reachable as an entity slot, so item commands on it do nothing
|
||||
AbstractArrow.getSlot missing an arrow's pickup stack is not addressable as an entity slot, so item commands on an arrow do nothing
|
||||
AbstractArrow.Pickup.byOrdinal port crates/horizon-client/src/arrows.rs::Pickup::from_byte out of range is DISALLOWED
|
||||
Arrow.updateColor port crates/horizon-client/src/projectiles.rs::Projectile::arrow_effect_color the tipped arrow's opaque potion colour for its flight and hit particles (-1 for none)
|
||||
Arrow.makeParticle port crates/horizon-client/src/game/remote_projectiles.rs::Game::arrow_makeparticle ticked in #3514; `Arrow.makeParticle` is cited at crates/horizon-client/src/game/remote_projectiles.rs:218
|
||||
@@ -82,11 +82,11 @@ ThrownTrident.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.r
|
||||
ThrownTrident.getLoyaltyFromItem port crates/horizon-client/src/game/trident.rs::Game::release_trident loyalty level read from the thrown stack when the trident is released
|
||||
ThrownTrident.tickDespawn inline crates/horizon-client/src/arrows.rs::Ticker::tick gap: minor: the 1200-tick life limit applies to every trident; the loyalty exemption is not checked
|
||||
AbstractHurtingProjectile.shouldRenderAtSqrDistance skip:ui distance culling for the fireball model; Horizon culls with its own range check
|
||||
AbstractHurtingProjectile.canHitEntity missing minor: no-physics entities are not skipped by the fireball's target search
|
||||
AbstractHurtingProjectile.canHitEntity missing same as `ShulkerBullet.canHitEntity`: the fireball's target search has no no-physics filter
|
||||
WitherSkull.getBlockExplosionResistance port crates/horizon-client/src/game/explosion.rs::blast_resistance a dangerous skull's blast caps a breakable block's resistance at 0.8; `#wither_immune` blocks keep theirs
|
||||
WitherSkull.onHit port crates/horizon-client/src/game/wither.rs::Game::wither_skull_hit skull hit damage, wither effect, then the blast at the impact point
|
||||
AbstractWindCharge.makeBoundingBox port crates/horizon-client/src/projectiles.rs::ProjectileKind::size gap: minor: gives the width only; the box sits 0.15 below the position in vanilla
|
||||
AbstractWindCharge.canHitEntity missing minor: no filter for end crystals or other wind charges found in the target list; unsure whether the caller excludes them
|
||||
AbstractWindCharge.canHitEntity missing the wind charge's target list is `creatures.rs::wind_charge_hitboxes`, which collects the other live, unstuck wind charges as hittable boxes: nothing there drops a wind charge or an end crystal, so the filter `canHitEntity` makes is not applied
|
||||
AbstractWindCharge.onHitBlock port crates/horizon-client/src/game/wind.rs::Game::wind_charge_hit a burst a quarter block out along the hit face
|
||||
WindCharge.deflect port crates/horizon-client/src/game/wind.rs::reverse gap: minor: turns the charge at half speed; the noDeflectTicks delay is not modelled
|
||||
WindCharge.shouldRenderAtSqrDistance skip:ui distance culling for the wind charge model; Horizon culls with its own range check
|
||||
@@ -97,10 +97,10 @@ ThrownEnderpearl.getOwner skip:other owner is kept as a Who value and resolved
|
||||
ThrownEnderpearl.findOwnerIncludingDeadPlayer skip:other owner lookup by UUID across dead players; Horizon keeps the owner id directly
|
||||
ThrownEnderpearl.onHitEntity port crates/horizon-client/src/game/creatures.rs::Game::projectile_hit a mob hit takes zero damage; the pearl still lands
|
||||
ThrownEnderpearl.onHit port crates/horizon-client/src/game/creatures.rs::Game::projectile_hit gap: minor: no 32 portal particles and no dimension change; the owner is teleported to where the pearl was, with 5 damage and a 5 percent endermite
|
||||
ThrownEnderpearl.isAllowedToTeleportOwner missing minor: a pearl whose owner is dead or asleep when it lands still teleports the owner
|
||||
ThrownEnderpearl.isAllowedToTeleportOwner missing `game/creatures.rs` teleports the owner on a pearl's hit without asking whether they are dead or sleeping
|
||||
ThrownEnderpearl.playSound port crates/horizon-client/src/game/creatures.rs::Game::projectile_hit teleport sound at the landing point
|
||||
ThrownEnderpearl.teleport port crates/horizon-client/src/game/creatures.rs::Game::projectile_hit moves the player to the pearl's previous position and clears velocity and fall distance
|
||||
ThrownEnderpearl.canTeleport missing minor: a pearl from the End to the Overworld needs the owner's seen-credits flag before it may teleport
|
||||
ThrownEnderpearl.canTeleport missing no seen-credits test: a pearl whose owner came from the End teleports them back to the Overworld regardless
|
||||
ThrownEnderpearl.onInsideBlock port crates/horizon-client/src/game/end.rs::Game::pearl_owner_in_gateway a pearl that flies into an End gateway sends its owner through and fires enter_block
|
||||
Raid.<init> port crates/horizon-client/src/raid.rs::Raid::new centre, difficulty-based group count, cooldown and active flag
|
||||
Raid.getAllRaiders port crates/horizon-client/src/game/raids.rs::Game::raid_members the raiders of this raid, from the mobs carrying its id
|
||||
|
||||
|
@@ -34,7 +34,7 @@ StonecutterMenu.removed port crates/horizon-client/src/game/stations.rs::Game::c
|
||||
ArrowItem.asProjectile port crates/horizon-client/src/game/dispenser.rs::Game::shoot_projectile_item dispensed arrows are built in the arrow arm of the item shot
|
||||
BlockItem.updateBlockEntityComponents port crates/horizon-client/src/block_entity_item.rs::place the placed block entity takes the item's components
|
||||
BlockItem.canPlace port crates/horizon-world/src/level/place.rs::Sim::place the placement checks, including the survive check, run in Sim::place
|
||||
BlockItem.onDestroyed missing unsure: a container item whose item entity is destroyed should spill its contents; no Horizon code does this
|
||||
BlockItem.onDestroyed missing no `onDestroyed` on any item: an item entity of a shulker box or a bundle destroyed by fire or lava spills nothing
|
||||
BoatItem.getBoat port crates/horizon-client/src/game/boats.rs::Game::place_boat boat placed at the hit point with the item's stack config
|
||||
BucketItem.playEmptySound port crates/horizon-world/src/level/buckets.rs::Sim::empty_sound
|
||||
BundleItem.getWeightSafe port crates/horizon-items/src/bundle.rs::Contents::weight_fraction float fraction; vanilla compares an exact Fraction
|
||||
@@ -42,7 +42,7 @@ BundleItem.getBarWidth port crates/horizon-items/src/bundle.rs::Contents::bar
|
||||
BundleItem.getBarColor port crates/horizon-items/src/bundle.rs::Contents::bar
|
||||
BundleItem.removeOneItemFromBundle port crates/horizon-items/src/bundle.rs::Contents::remove_one
|
||||
BundleItem.getTooltipImage port crates/horizon-items/src/bundle.rs::tooltip_layout gap: minor: the tooltip_display rule that hides the bundle contents is not checked here
|
||||
BundleItem.onDestroyed missing unsure: a bundle whose item entity is destroyed should spill its contents; no Horizon code does this
|
||||
BundleItem.onDestroyed missing same as `BlockItem.onDestroyed`: nothing spills a destroyed item entity's contents
|
||||
BundleItem.playRemoveOneSound port crates/horizon-client/src/game.rs::Game::drop_bundle_content the menu queues the same sound in menu/bundle.rs
|
||||
BundleItem.playInsertSound port crates/horizon-client/src/menu/bundle.rs::Menu::carried_bundle_click queues the insert or insert-fail sound
|
||||
BundleItem.playDropContentsSound port crates/horizon-client/src/game.rs::Game::drop_bundle_content
|
||||
@@ -54,8 +54,8 @@ CrossbowItem.createProjectile port crates/horizon-client/src/arrows.rs::Ticker::
|
||||
CrossbowItem.getShotPitch inline crates/horizon-client/src/game/crossbow.rs::Game::shoot_crossbow the shot pitch per index is inline
|
||||
CrossbowItem.getRandomShotPitch inline crates/horizon-client/src/game/crossbow.rs::Game::shoot_crossbow the random pitch formula is inline
|
||||
CrossbowItem.getPowerForTime inline crates/horizon-client/src/game/crossbow.rs::Game::tick_crossbow the charge fraction capped at one is the tick check against the charge time
|
||||
DyeColor.getMixedColor missing player: a baby sheep, wolf or cat from parents of two dye colours gets a mixed colour; Horizon has no breeding dye mix (only sheep_color for spawns)
|
||||
DyeColor.findColorMixInRecipes missing player: part of the same breeding dye mix; the crafting recipe lookup for two dyes is not done for offspring
|
||||
DyeColor.getMixedColor port crates/horizon-client/src/game/creatures.rs::Game::dye_mix the colour of the crafting recipe that mixes the two dyes, else one of the two picked at random (`next_boolean`); `creatures.rs:3153`, `:3176` and `:3222` give the offspring of a sheep, wolf or cat its colour with it
|
||||
DyeColor.findColorMixInRecipes port crates/horizon-client/src/game/creatures.rs::Game::dye_color_mix_in_recipes the two dyes laid out as a 2x2 crafting grid through `Recipes::find`, and the result read back as a dye colour by its `_dye` suffix
|
||||
FireworkRocketItem.createDispenseConfig port crates/horizon-client/src/game/dispenser.rs::Game::dispense_projectile rocket config: offset 0.5000099, uncertainty 1, power 0.5, event 1004
|
||||
FireworkRocketItem.getEntityJustOutsideOfBlockPos port crates/horizon-client/src/game/dispenser.rs::Game::dispense_position the position just outside the dispenser on the facing side
|
||||
HangingEntityItem.mayPlace port crates/horizon-client/src/game/paintings.rs::Game::place_painting paintings take no vertical face; an item frame (ItemFrameItem.mayPlace) may go on one and only needs the build height, which place_item_frame checks
|
||||
@@ -84,7 +84,7 @@ Item.Properties.horseArmor skip:data registration builder
|
||||
Item.Properties.nautilusArmor skip:data registration builder
|
||||
Item.Properties.requiredFeatures skip:data registration builder
|
||||
Item.Properties.delayedHolderComponent skip:data registration builder
|
||||
Item.Properties.loweredMobVisibility missing unsure: the mob_visibility component is not read by Horizon's mob targeting
|
||||
Item.Properties.loweredMobVisibility missing the `mob_visibility` component is parsed (it is in `item_model.rs`'s component table and the protocol's item hash), but no mob targeting reads it
|
||||
Item.Properties.finalizeInitializer skip:data registration builder
|
||||
Item.TooltipContext.of skip:java tooltip context factory; Horizon passes the level and player to the tooltip functions
|
||||
ItemStack.validateStrict skip:java codec-side validation of components and stack size
|
||||
@@ -97,7 +97,7 @@ ItemStack.isBroken inline crates/horizon-client/src/game.rs::Game::damage_held t
|
||||
ItemStack.nextDamageWillBreak inline crates/horizon-client/src/game/trident.rs::Game::use_trident gap: minor: only the trident's check is ported here
|
||||
ItemStack.processDurabilityChange port crates/horizon-client/src/game/enchant_effects.rs::Game::durability_loss the Unbreaking-adjusted loss
|
||||
ItemStack.applyDamage port crates/horizon-client/src/game.rs::Game::damage_held gap: minor: no item durability advancement trigger fires
|
||||
ItemStack.hurtAndConvertOnBreak missing unsure: a carrot or fungus on a stick that breaks should become a fishing rod; no conversion was found
|
||||
ItemStack.hurtAndConvertOnBreak missing nothing converts a broken carrot on a stick or fungus on a stick into a fishing rod
|
||||
ItemStack.hurtEnemy port crates/horizon-client/src/game/mace.rs::Game::weapon_wear the per-hit durability loss of a weapon
|
||||
ItemStack.interactLivingEntity port crates/horizon-client/src/game.rs::Game::interact_entity equip-on-interact is in the mob paths (nautili.rs, ghasts.rs)
|
||||
ItemStack.copy skip:java value copy
|
||||
@@ -111,21 +111,21 @@ ItemStack.hashStackList skip:java hash for collections
|
||||
ItemStack.onCraftedBy inline crates/horizon-client/src/game.rs::Game::click_inventory gap: minor: the crafted statistic is added only on the inventory grid craft path here
|
||||
ItemStack.set skip:other component write; Horizon stores component data in typed fields
|
||||
ItemStack.update skip:java functional update helper
|
||||
ItemStack.applyComponentsAndValidate missing unsure: a validated component patch has no single Horizon counterpart; /item modify is a separate path
|
||||
ItemStack.applyComponentsAndValidate missing a validated component patch has no single counterpart: `/item modify` goes through its own path
|
||||
ItemStack.getStyledHoverName port crates/horizon-client/src/game/stations.rs::Game::stack_hover_name gap: minor: plain text; the rarity colour and italic style are not applied here
|
||||
ItemStack.addToTooltip port crates/horizon-client/src/item_tooltip.rs::lines
|
||||
ItemStack.addUnitComponentToTooltip inline crates/horizon-client/src/game/stations.rs::Game::stack_tooltip_lines unit lines such as unbreakable, gated by the tooltip display
|
||||
ItemStack.canPlaceOnBlockInAdventureMode port crates/horizon-client/src/game/game_mode.rs::Game::can_place_on_in_adventure the can_place_on predicates gate a placement in adventure mode
|
||||
ItemStack.canBreakBlockInAdventureMode missing player: in adventure mode a block can be broken only when the held item's can_break list allows it; Horizon does not read that component
|
||||
ItemStack.getDamageSource missing unsure: a stack's damage_type component is not read when a weapon hits; no Horizon reader found
|
||||
ItemStack.canBreakBlockInAdventureMode port crates/horizon-client/src/game/game_mode.rs::Game::adventure_predicate the main-hand stack's `can_break` predicate against the block at the position (one predicate or a list, block ids or tags, exact or ranged state properties; a predicate with `nbt` or `components` never matches), read by `game_mode.rs:200`; the same fn answers `can_place_on`
|
||||
ItemStack.getDamageSource missing a stack's `damage_type` component is not read when a weapon hits: the attack builds its damage from the mob's own sources (`mobs::Damage`), and `damage_type` is only used for the enchantment damage tags
|
||||
ItemStackTemplate.fromNonEmptyStack port crates/horizon-items/src/item_data.rs::template_tag stack to template tag
|
||||
ItemStackTemplate.withCount skip:java record copy with a new count
|
||||
ItemStackTemplate.apply port crates/horizon-assets/src/item_model.rs::stack_from_template
|
||||
ItemUtils.onContainerDestroyed missing unsure: destroyed container items do not spill their contents; see BlockItem.onDestroyed
|
||||
ItemUtils.onContainerDestroyed missing the shared spill of a destroyed container item has no counterpart; see `BlockItem.onDestroyed`
|
||||
Items.registerBlock skip:data item registry bootstrap
|
||||
Items.registerItem skip:data item registry bootstrap
|
||||
JukeboxSong.hasFinished port crates/horizon-items/src/items.rs::Song::finished
|
||||
JukeboxSongPlayer.setSongWithoutPlaying missing unsure: no load path restores a jukebox's song start tick
|
||||
JukeboxSongPlayer.setSongWithoutPlaying inline crates/horizon-client/src/save.rs::decode_block_entity the load sets the record and `ticks_since_song_started` straight into the `Jukebox` block entity, which is the whole of `setSongWithoutPlaying`; the song carries on from that tick in `tick_jukebox`
|
||||
JukeboxSongPlayer.play port crates/horizon-client/src/game/jukebox.rs::Game::start_song the song starts at the jukebox with its sound
|
||||
JukeboxSongPlayer.shouldEmitJukeboxPlayingEvent inline crates/horizon-client/src/game/jukebox.rs::Game::tick_jukebox
|
||||
JukeboxSongPlayer.spawnMusicParticles inline crates/horizon-client/src/game/jukebox.rs::Game::tick_jukebox the note particle is emitted in tick_jukebox
|
||||
@@ -138,7 +138,7 @@ MapItem.createNewSavedData port crates/horizon-client/src/game/maps.rs::MapData:
|
||||
MapItem.getCorrectStateForFluidBlock inline crates/horizon-client/src/game/maps.rs::Game::update_map the map colour pass
|
||||
MapItem.inventoryTick port crates/horizon-client/src/game/maps.rs::Game::tick_maps
|
||||
MapItem.lockMap port crates/horizon-client/src/menu.rs::Menu::map_crafted the lock branch of the post processing
|
||||
MapItem.useOn missing player: using a map on a banner block should toggle that banner's marker on the map; Horizon has no banner toggle
|
||||
MapItem.useOn port crates/horizon-client/src/game/maps.rs::Game::use_map_on_banner a map used on a block in the `minecraft:banners` tag toggles that banner's marker (`MapData::toggle_banner`); a failed toggle ends the use without a swing, and `game/remote.rs` predicts the same for its own map
|
||||
MobBucketItem.emptyContents port crates/horizon-world/src/level/buckets.rs::Sim::bucket_empty the MobWater and Nothing contents
|
||||
PotionItem.getDefaultInstance skip:data default stack data
|
||||
PotionItem.useOn port crates/horizon-client/src/game/potions.rs::Game::use_water_bottle_on_block a water bottle poured on #convertible_to_mud makes mud and empties the bottle
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -64,8 +64,8 @@ KeyboardHandler.copyCreateBlockCommand port crates/horizon-client/src/game/debug
|
||||
KeyboardHandler.copyCreateEntityCommand port crates/horizon-client/src/game/debug_keys.rs::Game::copy_recreate_command
|
||||
KeyboardHandler.charTyped port crates/horizon-client/src/game.rs::Game::window_event typed text routed to the open screen
|
||||
KeyboardHandler.textInput port crates/horizon-client/src/game.rs::Game::window_event typed text routed to the open screen
|
||||
KeyboardHandler.textEditing port crates/horizon-client/src/game.rs::ime_preedit WindowEvent::Ime(Preedit) draws the composition in the focused field
|
||||
KeyboardHandler.submitPreeditEvent inline crates/horizon-client/src/game.rs: WindowEvent::Ime arm the preedit reaches the screen's field directly, and Commit types the text
|
||||
KeyboardHandler.textEditing port crates/horizon-client/src/game/ime.rs::Game::ime_preedit WindowEvent::Ime(Preedit) draws the composition in the focused field
|
||||
KeyboardHandler.submitPreeditEvent inline crates/horizon-client-render/src/preedit.rs::set_preedit the preedit reaches the screen's field directly, and Commit types the text
|
||||
Minecraft.<init> inline crates/horizon-client/src/game.rs::run_loading gap: minor: no unclean-start recovery; startedCleanly is only a default in options_file.rs
|
||||
Minecraft.onGameLoadFinished missing minor: startedCleanly is never written after load
|
||||
Minecraft.createTitle missing minor: the window title is always Horizon (with_title in game.rs), no version or world type
|
||||
@@ -117,10 +117,10 @@ Minecraft.updateReportEnvironment skip:service reporting context for chat repor
|
||||
Minecraft.getGameProfile skip:service Mojang profile fetch; Horizon's account is local (auth.rs)
|
||||
Minecraft.showOnlyReducedInfo port crates/horizon-client/src/game/debug_keys.rs::Game::reduced_debug_info
|
||||
Minecraft.grabPanoramixScreenshot skip:debug only runs with SharedConstants.DEBUG_PANORAMA_SCREENSHOT
|
||||
Minecraft.onTextInputFocusChange inline crates/horizon-client/src/game.rs::sync_ime the IME is synced to the focused widget each frame
|
||||
Minecraft.onTextInputFocusChange inline crates/horizon-client/src/game/ime.rs::Game::sync_ime the IME is synced to the focused widget each frame
|
||||
Minecraft.getTickTargetMillis port crates/horizon-client/src/clock.rs::TickRateManager::milliseconds_per_tick gap: minor: the max with the default tick time is not in this fn
|
||||
MouseHandler.fillMousePositionDetails skip:debug crash report mouse details
|
||||
MouseHandler.onDrop port crates/horizon-client/src/game.rs:16709 -> title::drop_file a file dropped on the window joins the create screen's data packs
|
||||
MouseHandler.onDrop port crates/horizon-client/src/title.rs::Title::drop_file a file dropped on the window joins the create screen's data packs
|
||||
MouseHandler.getScaledYPos port crates/horizon-client-input/src/window.rs::scaled_pos
|
||||
MouseHandler.resyncMousePosition skip:other winit reports the cursor position; no SDL global-state resync
|
||||
MouseHandler.releaseMouse port crates/horizon-client/src/game.rs::Game::capture release path (capture false)
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
OptionInstance.SliderableEnum.toSliderValue port crates/horizon-client-render/src/options_ui.rs::Opt::kind slider position of the graphics preset (index / 3)
|
||||
OptionInstance.SliderableEnum.next inline crates/horizon-client-render/src/options_ui.rs Opt::Preset is a 0..3 slider (unit/step at options_ui.rs:335, 417, 844), so stepping happens through the slider rather than a cycle arm
|
||||
OptionInstance.SliderableEnum.previous inline crates/horizon-client-render/src/options_ui.rs same slider
|
||||
OptionInstance.SliderableEnum.next inline crates/horizon-client-render/src/options_ui.rs::Opt::set_slider Opt::Preset is a 0..3 slider (unit/step at options_ui.rs:335, 417, 844), so stepping happens through the slider rather than a cycle arm
|
||||
OptionInstance.SliderableEnum.previous inline crates/horizon-client-render/src/options_ui.rs::Opt::set_slider same slider
|
||||
OptionInstance.SliderableEnum.fromSliderValue port crates/horizon-client-render/src/options_ui.rs::Opt::set_slider gap: minor: rounds v*3 where vanilla floors v*4
|
||||
OptionInstance.SliderableEnum.validateValue port crates/horizon-client-render/src/options_file.rs::Spec::validate named spec accepts the four preset indexes
|
||||
OptionInstance.SliderableOrCyclableValueSet.createButton port crates/horizon-client-render/src/options_ui.rs::option_widget cycle or slider button by option kind
|
||||
@@ -8,14 +8,14 @@ OptionInstance.SliderableValueSet.createButton port crates/horizon-client-render
|
||||
OptionInstance.UnitDouble.validateValue port crates/horizon-client-render/src/options_file.rs::Spec::validate unit range 0..1
|
||||
OptionInstance.UnitDouble.codec port crates/horizon-client-render/src/options_file.rs::codec_double
|
||||
Options.updateResourcePacks port crates/horizon-client/src/game/options.rs::Game::handle_option_events gap: minor: saves and reloads on every packs event; vanilla only when the list changed
|
||||
Options.maxAnisotropyValue port crates/horizon-client/src/game/options.rs + horizon-client-render::gfx::set_texture_filtering the options.txt maxAnisotropyBit (and textureFiltering) reach the sampler
|
||||
Options.maxAnisotropyValue port crates/horizon-client-render/src/gfx.rs::sampler_anisotropy the options.txt maxAnisotropyBit (and textureFiltering) reach the sampler
|
||||
Options.createSoundSliderOptionInstance port crates/horizon-client-render/src/audio.rs::Audio::sync_volumes gap: player: no now-playing toast when master or music changes
|
||||
Options.setGraphicsPresetToCustom inline crates/horizon-client-render/src/options_ui.rs::Opt::set_slider gap: minor: cycle buttons set custom in Opt::cycle as well
|
||||
Options.processOptions inline crates/horizon-client-render/src/options_file.rs::OptionsFile::parse gap: minor: the save direction is OptionsFile::serialize
|
||||
Options.dataFix port crates/horizon-client-render/src/options_fixes.rs::update
|
||||
Options.getFullscreenVideoModeString inline crates/horizon-client-render/src/options_file.rs options.txt keeps fullscreen_resolution as the monitor index the option was picked from instead of a 'WxH@Hz' string; the window is restored from that index
|
||||
Options.getFullscreenVideoModeString inline crates/horizon-client-input/src/window.rs::VideoMode::write options.txt keeps fullscreenResolution as `VideoMode::write`'s mode JSON, not as a separate 'WxH@Hz' string; `Settings.video_mode_index` reads it back to place the Video screen's mode slider
|
||||
Options.buildPlayerInformation port crates/horizon-client/src/net.rs::run gap: player: only view distance and chat visibility are sent; language, skin parts, main hand, particles and text filtering stay at defaults
|
||||
Options.setModelPart port crates/horizon-client-render/src/options_file.rs::MODEL_PARTS + options_ui model part keys the seven modelPart_* options are read back and sent as the PlayerModelPart set
|
||||
Options.setModelPart port crates/horizon-client-render/src/options_file.rs::OptionsFile::model_customisation the seven modelPart_* options are read back and sent as the PlayerModelPart set
|
||||
Options.loadSelectedResourcePacks port crates/horizon-client-render/src/packs.rs::enabled gap: minor: dropped packs are not written back to resourcePacks
|
||||
Options.readListOfStrings port crates/horizon-client-render/src/options_file.rs::read_list
|
||||
Options.dumpOptionsForReport skip:debug options dump for the debug report
|
||||
@@ -87,14 +87,14 @@ ClientMannequin.tick missing minor: unsure: the mannequin avatar state (walk bo
|
||||
AbstractSelectionList.getRowLeft port crates/horizon-client-render/src/pack_screen.rs::List::row_left
|
||||
CycleButton.setValue skip:ui widgets read the settings each frame; no separate value setter
|
||||
CreateFlatWorldScreen.DetailsList.LayerEntry.getNarration skip:ui screen-reader text
|
||||
InputQuirks.isQuitShortcutDown port crates/horizon-client/src/game.rs the close path reads settings.quit_shortcuts and input::is_quit_shortcut_down, so with the option off the shortcut no longer closes the window
|
||||
InputQuirks.isQuitShortcutDown port crates/horizon-client-input/src/input.rs::is_quit_shortcut_down the close path reads settings.quit_shortcuts and input::is_quit_shortcut_down, so with the option off the shortcut no longer closes the window
|
||||
InputWithModifiers.isSelection inline crates/horizon-client/src/game/key_focus.rs::nav_of Enter, Space and keypad Enter all map to Nav::Select, which presses the focused widget
|
||||
InputWithModifiers.isConfirmation inline crates/horizon-client-render/src/chat.rs::ChatScreen::key Enter and keypad Enter only, as the text-field screens do (chat, sign, command block, books); no shared predicate
|
||||
InputWithModifiers.isCopy port crates/horizon-client-input/src/clipboard.rs::edit_shortcut
|
||||
InputWithModifiers.isPaste port crates/horizon-client-input/src/clipboard.rs::edit_shortcut
|
||||
InputWithModifiers.isCut port crates/horizon-client-input/src/clipboard.rs::edit_shortcut
|
||||
PreeditEvent.fromSdlTextEditing port crates/horizon-client/src/game.rs::ime_preedit WindowEvent::Ime(Preedit) feeds the composition text to the focused field
|
||||
PreeditEvent.toFormattedText inline crates/horizon-client/src/game.rs::ime_preedit the composing text is drawn as the field's preedit, not as formatted chat
|
||||
PreeditEvent.fromSdlTextEditing port crates/horizon-client-render/src/preedit.rs::PreeditEvent::from_sdl_text_editing WindowEvent::Ime(Preedit) feeds the composition text to the focused field
|
||||
PreeditEvent.toFormattedText inline crates/horizon-client-render/src/preedit.rs::PreeditEvent::to_formatted_text the composing text is drawn as the field's preedit, not as formatted chat
|
||||
GameConfig.FolderData.getExternalAssetSource skip:data Horizon reads assets from the prepared resources folder
|
||||
net/minecraft/client/main/Main#getQuickPlayVariant port crates/horizon-client/src/game.rs::Options::read gap: minor: only the quick play flags of the launch parser; realms branch not checked
|
||||
net/minecraft/client/main/Main#parseArgument inline crates/horizon-client/src/game.rs::Options::read
|
||||
@@ -135,7 +135,7 @@ LocalPlayer.resetPos inline crates/horizon-client/src/game/screens.rs::Game::res
|
||||
LocalPlayer.shouldStopRunSprinting port crates/horizon-client/src/sprint.rs::Conditions::should_stop gap: player: minor_horizontal_collision is always false, so wall slides stop the sprint
|
||||
LocalPlayer.shouldStopSwimSprinting port crates/horizon-client/src/sprint.rs::Conditions::should_stop
|
||||
LocalPlayer.tickDeath missing minor: unsure: the dead local player is not removed after 20 ticks; the death screen flow covers it
|
||||
LocalPlayer.rideTick port crates/horizon-client/src/game/boats.rs::Game::tick_riding the keys are latched into the boat's input and the seat follows its turn
|
||||
LocalPlayer.rideTick port crates/horizon-client/src/game/remote_vehicles.rs::Game::remote_ride the keys are latched into the boat's input and the seat follows its turn
|
||||
LocalPlayer.isHorizontalCollisionMinor port crates/horizon-client/src/motion.rs::EntityMoveWorld::minor_horizontal_collision the sprint keeps when a move hits a wall at under 8 degrees
|
||||
LocalPlayer.isMoving inline crates/horizon-client/src/auto_jump.rs::can_auto_jump
|
||||
LocalPlayer.isSprintingPossible port crates/horizon-client/src/sprint.rs::Conditions::possible
|
||||
|
||||
|
+13
-13
@@ -31,7 +31,7 @@ SectionPos.blockToSection port crates/horizon-core/src/position.rs::BlockPos::se
|
||||
SectionPos.origin inline crates/horizon-client-render/src/gfx/resident.rs::origin gap: minor: camera-relative origin; the block corner is the same sum without the camera
|
||||
SectionPos.asLong port crates/horizon-core/src/position.rs::SectionPos::pack asLong(BlockPos): BlockPos::section then this pack; bit layout matches
|
||||
SectionPos.blocksInside inline crates/horizon-client/src/mobs/villager_brain.rs::chunk_pois the POI scan visits every block of each section
|
||||
SectionPos.aroundChunk missing minor: unsure: ensureLoadedAndValid pre-loads POI chunks around a point; Horizon's chunk_pois reads loaded chunks only
|
||||
SectionPos.aroundChunk missing `mobs/villager_brain.rs::chunk_pois` reads the chunks it has, with no `ensureLoadedAndValid`: a search near an unloaded chunk sees fewer points of interest
|
||||
SectionPos.betweenClosedStream skip:java Java stream over a 3-D section box; the loops in pois_in_range and chunk_pois do the iteration
|
||||
SectionPos.aroundAndAtBlockPos port crates/horizon-client-render/src/light.rs::LightChanges::dirty_sections the sections within one block of a changed cell
|
||||
TypedInstance.is skip:java holder type checks; Horizon compares registry names
|
||||
@@ -78,15 +78,15 @@ DataComponentInitializers.Initializer.add skip:data item default component init
|
||||
DataComponentInitializers.SingleComponentInitializer.asInitializer skip:data item default component initializers; Horizon reads item defaults from its tables
|
||||
DataComponentLookup.getOrCreateStorage skip:java cache
|
||||
DataComponentLookup.scanForComponents skip:java index build behind the lookup
|
||||
DataComponentLookup.findAll missing minor: unsure: DyeColor mixing and the recipe fuel list read it; Horizon finds dyes and fuels by name and table
|
||||
DataComponentLookup.ComponentStorage.findMatching missing minor: unsure: spawn egg from an entity type; Horizon builds egg ids by name
|
||||
DataComponentLookup.findAll missing no component-driven item search: dyes are found by their `_dye` name and fuels by the table in `crafting.rs`/`furnace.rs`
|
||||
DataComponentLookup.ComponentStorage.findMatching missing a spawn egg is built from the entity type's id by name, with no component lookup
|
||||
DataComponentMap.makeCodecFromMap skip:java codec
|
||||
DataComponentMap.iterator skip:java iterator
|
||||
DataComponentMap.Builder.setUnchecked skip:java builder
|
||||
DataComponentMap.Builder.addAll skip:java builder
|
||||
DataComponentMap.Builder.buildFromMapTrusted skip:java builder
|
||||
DataComponentPatch.getFromPatchAndPrototype skip:java patch-or-default read; typed StackData fields plus the item table cover it
|
||||
DataComponentPatch.forget missing minor: unsure: BlockEntity.setComponents strips implicit components with forget; not checked in Horizon's block entity placement
|
||||
DataComponentPatch.forget missing a block entity's implicit components are not stripped on placement: `block_entity_item.rs` merges the explicit ones over the implicit
|
||||
DataComponentPatch.split skip:java added and removed parts kept as separate fields (protocol Stack.removed, StackData)
|
||||
DataComponentPatch.equals skip:java equality
|
||||
DataComponentPatch.toString skip:debug debug string
|
||||
@@ -109,20 +109,20 @@ PatchedDataComponentMap.asPatch skip:java generic component map; Horizon sets t
|
||||
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
|
||||
AttributeModifiersPredicate.matches missing minor: unsure: item() in advancements.rs ignores this key, so a criterion using it matches too broadly
|
||||
AttributeModifiersPredicate.EntryPredicate.test missing minor: unsure: item() in advancements.rs ignores this key, so a criterion using it matches too broadly
|
||||
BundlePredicate.matches missing minor: unsure: item() in advancements.rs ignores this key, so a criterion using it matches too broadly
|
||||
ContainerPredicate.matches missing minor: unsure: item() in advancements.rs ignores this key, so a criterion using it matches too broadly
|
||||
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
|
||||
ContainerPredicate.matches missing ignored by `advancements.rs::item`, so a container contents criterion matches too broadly
|
||||
net/minecraft/core/component/predicates/DamagePredicate#matches inline crates/horizon-client/src/advancements.rs::item gap: minor: only the damage range; the durability range (max damage minus damage) is not checked
|
||||
DataComponentPredicate.Single.wrapCodec skip:java codec
|
||||
DataComponentPredicate.Type.unpackType skip:java codec helper
|
||||
EnchantmentsPredicate.matches inline crates/horizon-client/src/advancements.rs::item the enchantments predicate inside item()
|
||||
FireworkExplosionPredicate.FireworkPredicate.test missing minor: unsure: item() in advancements.rs ignores this key, so a criterion using it matches too broadly
|
||||
FireworksPredicate.matches missing minor: unsure: item() in advancements.rs ignores this key, so a criterion using it matches too broadly
|
||||
JukeboxPlayablePredicate.matches missing minor: unsure: item() in advancements.rs ignores this key, so a criterion using it matches too broadly
|
||||
FireworkExplosionPredicate.FireworkPredicate.test missing ignored by `advancements.rs::item`
|
||||
FireworksPredicate.matches missing ignored by `advancements.rs::item`, so a fireworks criterion matches any firework
|
||||
JukeboxPlayablePredicate.matches missing ignored by `advancements.rs::item`
|
||||
PotionsPredicate.matches port crates/horizon-client/src/advancements.rs::potions
|
||||
WritableBookPredicate.matches missing minor: unsure: item() in advancements.rs ignores this key, so a criterion using it matches too broadly
|
||||
WrittenBookPredicate.matches missing minor: unsure: item() in advancements.rs ignores this key, so a criterion using it matches too broadly
|
||||
WritableBookPredicate.matches missing ignored by `advancements.rs::item`
|
||||
WrittenBookPredicate.matches missing ignored by `advancements.rs::item`
|
||||
BoatDispenseItemBehavior.execute port crates/horizon-client/src/game/dispenser.rs::Game::dispense_boat
|
||||
DefaultDispenseItemBehavior.dispense port crates/horizon-client/src/game/dispenser.rs::Game::dispense_default the click and dispense events of dispense_default
|
||||
DefaultDispenseItemBehavior.addToInventoryOrDispense port crates/horizon-client/src/game/dispenser.rs::Game::consume_with_remainder
|
||||
|
||||
|
@@ -89,7 +89,7 @@ SignedMessageValidator.KeyBased.validate port crates/horizon-protocol/src/secure
|
||||
SignedMessageValidator.KeyBased.updateAndValidate port crates/horizon-protocol/src/secure_chat.rs::MessageValidator::update_and_validate a failed message breaks the chain: nothing more is accepted
|
||||
Style.create skip:java builder from optionals; Style is a plain struct
|
||||
Style.checkEmptyAfterChange skip:java canonical empty style instance
|
||||
Style.withShadowColor missing minor: Style has no shadow colour, so a component's shadow_color is dropped
|
||||
Style.withShadowColor port crates/horizon-client-core/src/component.rs::shadow_color the style's shadow colour is kept on Style (text.rs:92) as packed ARGB, read from a packed int or a normalized RGBA array and written back out
|
||||
Style.withBold port crates/horizon-client-core/src/text.rs::Text::bold gap: minor: sets true only; a null or false setter is not kept
|
||||
Style.withItalic port crates/horizon-client-core/src/text.rs::Text::italic gap: minor: sets true only; a null or false setter is not kept
|
||||
Style.withUnderlined port crates/horizon-client-core/src/text.rs::Text::underlined gap: minor: sets true only; a null or false setter is not kept
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 18 and column 126.
|
@@ -33,8 +33,8 @@ ClientboundBossEventPacket.createUpdateStylePacket inline crates/horizon-client/
|
||||
ClientboundBossEventPacket.createUpdatePropertiesPacket inline crates/horizon-client/src/game/bossbars.rs::Game::diff_boss_events UpdateProperties when darken, boss music or fog differ
|
||||
ClientboundBossEventPacket.encodeProperties port crates/horizon-protocol/src/packets/play.rs::boss_flags the three flag bits (darken, music, fog)
|
||||
ClientboundBossEventPacket.AddOperation.dispatch port crates/horizon-client/src/boss_overlay.rs::BossOverlay::apply
|
||||
ClientboundChunksBiomesPacket.ChunkBiomeData.calculateChunkSize missing minor: biome-only chunk updates are never sent; no ChunksBiomes packet in ids.rs
|
||||
ClientboundChunksBiomesPacket.ChunkBiomeData.extractChunkData missing minor: biome-only chunk updates are never sent; no ChunksBiomes packet in ids.rs
|
||||
ClientboundChunksBiomesPacket.ChunkBiomeData.calculateChunkSize inline crates/horizon-protocol/src/packets/play.rs::ChunkBiomeData::from_biomes the buffer grows as each section's container is written, so no size is computed or prefixed on the wire
|
||||
ClientboundChunksBiomesPacket.ChunkBiomeData.extractChunkData port crates/horizon-protocol/src/packets/play.rs::ChunkBiomeData::from_biomes the column's sections' biome containers written one after another into the buffer
|
||||
ClientboundCommandSuggestionsPacket.toSuggestions port crates/horizon-client/src/game/remote.rs::Game::handle_command_suggestions start, length and tooltip become the suggestion range
|
||||
ClientboundCommandsPacket.validateEntries missing minor: an impossible command tree is not rejected; from_packet skips bad child indexes
|
||||
ClientboundCommandsPacket.enumerateNodes port crates/horizon-client/src/lan/commands.rs::CommandTree::packet_with breadth-first node ids on the LAN host
|
||||
|
||||
|
+14
-13
@@ -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)
|
||||
@@ -35,7 +35,7 @@ PackRepository.isAbleToClearAnyPack port crates/horizon-client-render/src/packs.
|
||||
PackRepository.addPack inline crates/horizon-client-render/src/packs.rs::SelectionModel::activate gap: minor: vanilla appends by id with no position rules
|
||||
PackRepository.removePack inline crates/horizon-client-render/src/packs.rs::SelectionModel::activate gap: minor: vanilla removes by id even when the pack is required; the screen never offers that
|
||||
PackRepository.getAvailablePacks inline crates/horizon-client-render/src/packs.rs::SelectionModel::rebuild rebuild also adds required packs (vanilla and active downloaded server packs from Game::pack_infos)
|
||||
PackRepository.getRequestedFeatureFlags missing minor: no pack feature flags are read, so a pack's requested features are never checked
|
||||
PackRepository.getRequestedFeatureFlags missing no pack's `feature_flags` are read from its metadata: the only feature list is the world's own `enabled_features` (`world_loading.rs`), which the game rules, the datapack function filter and the command dispatcher read
|
||||
PackSource.decorateWithSource port crates/horizon-client-render/src/pack_screen.rs::PackInfo::extended_description source label after the pack name (built-in, world, server)
|
||||
ServerPacksSource.createVanillaPackSource port crates/horizon-client-render/src/datapack.rs::DataPack::builtin the built-in vanilla data source (id vanilla), used by functions.rs
|
||||
ServerPacksSource.createPackRepository port crates/horizon-client-render/src/datapack.rs::discover gap: minor: finds world datapacks; the enable list is kept by functions.rs::reload_datapacks
|
||||
@@ -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
|
||||
@@ -104,15 +105,15 @@ IpBanList.remove port crates/horizon-client/src/game/dedicated.rs::Game::console
|
||||
IpBanList.clear skip:server only the JSON-RPC ban API calls clear (MinecraftBanListServiceImpl); Horizon has no JSON-RPC server
|
||||
IpBanListEntry.createIpInfo port crates/horizon-server-files/src/user_lists.rs::BanEntry::from_json reads the ip field of an entry
|
||||
IpBanListEntry.serialize port crates/horizon-server-files/src/user_lists.rs::BanEntry::serialize writes the ip field and the ban fields
|
||||
OldUsersConverter.readOldListFormat missing minor: reads the pre-1.7 flat ban and op lists; Horizon reads only its json files
|
||||
OldUsersConverter.lookupPlayers missing minor: name to uuid lookup for the old list conversion
|
||||
OldUsersConverter.convertUserBanlist missing minor: converts an old banned-players.txt when the server starts
|
||||
OldUsersConverter.convertIpBanlist missing minor: converts an old banned-ips.txt when the server starts
|
||||
OldUsersConverter.convertOpsList missing minor: converts an old ops.txt when the server starts
|
||||
OldUsersConverter.convertWhiteList missing minor: converts an old whitelist.txt when the server starts
|
||||
OldUsersConverter.convertMobOwnerIfNecessary missing minor: name owners on old tamed mobs are not turned into uuids; datafix handles OwnerUUID only
|
||||
OldUsersConverter.convertPlayers missing minor: old world/players .dat files are not moved to playerdata
|
||||
OldUsersConverter.readOldListFormat missing the server reads `ops.json`, `whitelist.json` and `banned-players.json` (`game/dedicated.rs:439-441`); the pre-1.7 text lists have no reader
|
||||
OldUsersConverter.lookupPlayers missing part of the old list conversion, which has no counterpart: names are resolved by the json lists' own uuids
|
||||
OldUsersConverter.convertUserBanlist missing no conversion of an old `banned-players.txt`: the ban list is `banned-players.json` (`game/dedicated.rs:441`)
|
||||
OldUsersConverter.convertIpBanlist missing no conversion of an old `banned-ips.txt`
|
||||
OldUsersConverter.convertOpsList missing no conversion of an old `ops.txt`: the op list is `ops.json` (`game/dedicated.rs:439`)
|
||||
OldUsersConverter.convertWhiteList missing no conversion of an old `whitelist.txt`: the whitelist is `whitelist.json` (`game/dedicated.rs:440`)
|
||||
OldUsersConverter.convertMobOwnerIfNecessary missing a name owner on an old tamed mob is not turned into a uuid; the datafix handles `OwnerUUID` only
|
||||
OldUsersConverter.convertPlayers missing old `world/players/*.dat` files are not moved into `playerdata`
|
||||
OldUsersConverter.ensureDirectoryExists skip:other helper of the file moves, which Horizon does not do
|
||||
OldUsersConverter.areOldUserlistsRemoved missing minor: no start-up refusal while old list files remain
|
||||
OldUsersConverter.renameOldFile missing minor: converted files are not renamed to .converted
|
||||
OldUsersConverter.areOldUserlistsRemoved missing the server does not refuse to start while old list files are still there
|
||||
OldUsersConverter.renameOldFile missing nothing renames a converted file to `.converted`
|
||||
OldUsersConverter.parseDate port crates/horizon-server-files/src/user_lists.rs::parse_date parses the ban date format
|
||||
|
+17
-17
@@ -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
|
||||
|
||||
|
@@ -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]
|
||||
|
||||
|
||||
@@ -68,6 +68,14 @@ def scenarios():
|
||||
add('skeleton_day_burn', 120, ['mob minecraft:skeleton 0.5 -60 0.5 0 32 persistent'])
|
||||
# NoAI only skips serverAiStep: Mob.aiStep's burnUndead still sets a NoAI zombie alight (#3430).
|
||||
add('zombie_noai_day_burn', 120, ['mob minecraft:zombie 0.5 -60 0.5 0 33 persistent noai'])
|
||||
# AgeableMob.aiStep runs with NoAI, including both signs and age-lock controls.
|
||||
for species in ('pig', 'cow', 'horse', 'turtle'):
|
||||
for age in (-2, 2):
|
||||
for noai in (False, True):
|
||||
for locked in (False, True):
|
||||
flags = (' noai' if noai else '') + (' agelocked' if locked else '')
|
||||
add(f'{species}_age_{age}_noai_{noai}_locked_{locked}', 4,
|
||||
[f'mob minecraft:{species} 0.5 -60 0.5 0 34 persistent age={age}{flags}'])
|
||||
# The wither (summoned, not built: no invulnerable spawn phase).
|
||||
add('wither_player', 200, ['player 10.5 -60 0.5', 'mob minecraft:wither 0.5 -58 0.5 0 41 persistent'])
|
||||
# A villager with a job site, a bed and a bell.
|
||||
|
||||
+13
-6
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user