fix(block): apply vanilla position offsets to block shapes (#2869)

* fix: apply vanilla position offsets to block shapes

* refactor: use extracted block shape offsets
This commit is contained in:
Megalith
2026-08-13 18:06:22 +02:00
committed by GitHub
parent 5a5bc19bbb
commit cb856eb074
9 changed files with 484 additions and 18 deletions

View File

@@ -1,5 +1,4 @@
use pumpkin_util::math::boundingbox::BoundingBox;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_util::math::{boundingbox::BoundingBox, position::BlockPos, vector3::Vector3};
use crate::block_properties::{COLLISION_SHAPES, NoteblockInstrument};
use crate::{Block, BlockDirection, BlockId};
@@ -178,6 +177,16 @@ impl BlockState {
.map(|&id| COLLISION_SHAPES[id as usize])
}
/// Returns block-local collision shapes with vanilla's coordinate-derived offset applied.
pub fn get_block_collision_shapes_at(
&self,
pos: &BlockPos,
) -> impl Iterator<Item = BoundingBox> + '_ {
let offset = Block::from_state_id(self.id).shape_offset_delta(pos);
self.get_block_collision_shapes()
.map(move |shape| shape.shift(offset))
}
pub fn get_block_outline_shapes(&self) -> impl Iterator<Item = BoundingBox> + '_ {
let base_shapes = self
.outline_shapes
@@ -190,6 +199,24 @@ impl BlockState {
base_shapes.chain(water_shape)
}
/// Returns block-local outline shapes with vanilla's coordinate-derived offset applied.
pub fn get_block_outline_shapes_at(
&self,
pos: &BlockPos,
) -> impl Iterator<Item = BoundingBox> + '_ {
let offset = Block::from_state_id(self.id).shape_offset_delta(pos);
let base_shapes = self
.outline_shapes
.iter()
.map(move |&id| COLLISION_SHAPES[id as usize].shift(offset));
let water_shape = self
.is_waterlogged()
.then(|| BoundingBox::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.875, 1.0)));
base_shapes.chain(water_shape)
}
}
impl BlockStateId {
@@ -281,3 +308,61 @@ const WEST_SIDE_SOLID: u8 = 1 << 4;
const EAST_SIDE_SOLID: u8 = 1 << 5;
const DOWN_CENTER_SOLID: u8 = 1 << 6;
const UP_CENTER_SOLID: u8 = 1 << 7;
#[cfg(test)]
mod tests {
use crate::{Block, BlockStateId, block_state_remap::remap_block_state_for_version};
use pumpkin_util::{math::position::BlockPos, version::JavaMinecraftVersion};
fn assert_close(actual: f64, expected: f64) {
assert!((actual - expected).abs() < 1.0e-6, "{actual} != {expected}");
}
#[test]
fn bamboo_collision_shape_uses_its_world_position() {
let state = Block::BAMBOO.default_state;
let origin_shape = state
.get_block_collision_shapes_at(&BlockPos::new(0, 64, 0))
.next()
.unwrap();
let shifted_shape = state
.get_block_collision_shapes_at(&BlockPos::new(-18, 64, -7))
.next()
.unwrap();
assert_close(origin_shape.min.x, 0.15625);
assert_close(origin_shape.max.x, 0.34375);
assert_close(shifted_shape.min.x, 0.65625);
assert_close(shifted_shape.max.x, 0.84375);
assert_close(shifted_shape.min.z, 0.65625);
assert_close(shifted_shape.max.z, 0.84375);
}
#[test]
fn supported_client_versions_keep_offset_collisions_mapped() {
let versions = [
JavaMinecraftVersion::V_1_20_5,
JavaMinecraftVersion::V_1_21,
JavaMinecraftVersion::V_1_21_2,
JavaMinecraftVersion::V_1_21_4,
JavaMinecraftVersion::V_1_21_5,
JavaMinecraftVersion::V_1_21_6,
JavaMinecraftVersion::V_1_21_7,
JavaMinecraftVersion::V_1_21_9,
JavaMinecraftVersion::V_1_21_11,
JavaMinecraftVersion::V_26_1,
JavaMinecraftVersion::V_26_2,
];
for version in versions {
for block in [Block::BAMBOO, Block::POINTED_DRIPSTONE] {
assert_ne!(
remap_block_state_for_version(block.default_state.id.as_u16(), version),
BlockStateId::AIR.as_u16(),
"{} mapped to air for {version}",
block.name
);
}
}
}
}

View File

@@ -4,7 +4,8 @@ use crate::{
};
use pumpkin_util::{
loot_table::LootTable,
math::experience::Experience,
math::{experience::Experience, position::BlockPos, vector3::Vector3},
random::hash_block_pos,
resource_location::{FromResourceLocation, ResourceLocation, ToResourceLocation},
};
use std::hash::{Hash, Hasher};
@@ -122,6 +123,35 @@ impl FromResourceLocation for &'static Block {
}
impl Block {
pub(crate) fn shape_offset_delta(&self, pos: &BlockPos) -> Vector3<f64> {
let Some(shape_offset) = self.shape_offset() else {
return Vector3::new(0.0, 0.0, 0.0);
};
let seed = hash_block_pos(pos.0.x, 0, pos.0.z) as u64;
let max_horizontal = f64::from(shape_offset.max_horizontal);
let x = (f64::from((seed & 15) as f32 / 15.0) - 0.5) * 0.5;
let x = x.clamp(-max_horizontal, max_horizontal);
let z = (f64::from(((seed >> 8) & 15) as f32 / 15.0) - 0.5) * 0.5;
let z = z.clamp(-max_horizontal, max_horizontal);
let y = match shape_offset.offset_type {
ShapeOffsetType::Xz => 0.0,
ShapeOffsetType::Xyz => {
(f64::from(((seed >> 4) & 15) as f32 / 15.0) - 1.0)
* f64::from(shape_offset.max_vertical)
}
};
// Extracted shapes are sampled at BlockPos::ZERO, where vanilla uses the
// negative horizontal limits and, for XYZ offsets, the negative vertical
// limit. Return only the delta from that sample.
Vector3::new(
x + max_horizontal,
y + shape_offset.offset_type.origin_y(shape_offset.max_vertical),
z + max_horizontal,
)
}
#[must_use]
pub fn is_waterlogged(&self, id: BlockStateId) -> bool {
self.properties(id).is_some_and(|properties| {
@@ -196,6 +226,28 @@ impl Block {
}
}
#[derive(Clone, Copy)]
pub(crate) enum ShapeOffsetType {
Xz,
Xyz,
}
#[derive(Clone, Copy)]
pub(crate) struct ShapeOffset {
pub offset_type: ShapeOffsetType,
pub max_horizontal: f32,
pub max_vertical: f32,
}
impl ShapeOffsetType {
fn origin_y(self, max_vertical: f32) -> f64 {
match self {
Self::Xz => 0.0,
Self::Xyz => f64::from(max_vertical),
}
}
}
impl BlockId {
// depends on generated impl:
// pub(crate) const BLOCK_COUNT: u16;
@@ -269,3 +321,78 @@ pub struct Flammable {
pub spread_chance: u8,
pub burn_chance: u8,
}
#[cfg(test)]
mod tests {
use super::{Block, BlockId, ShapeOffsetType};
use pumpkin_util::math::position::BlockPos;
fn assert_close(actual: f64, expected: f64) {
assert!((actual - expected).abs() < 1.0e-6, "{actual} != {expected}");
}
#[test]
fn shape_offset_registry_matches_vanilla_26_2() {
let mut xz = 0;
let mut xyz = 0;
for raw_id in 0..BlockId::BLOCK_COUNT {
match Block::from_id(BlockId::new(raw_id).unwrap())
.shape_offset()
.map(|offset| offset.offset_type)
{
Some(ShapeOffsetType::Xz) => xz += 1,
Some(ShapeOffsetType::Xyz) => xyz += 1,
None => {}
}
}
assert_eq!(xz, 34);
assert_eq!(xyz, 5);
}
#[test]
fn shape_offset_limits_match_vanilla_26_2() {
let bamboo = Block::BAMBOO.shape_offset().unwrap();
assert_eq!(bamboo.max_horizontal, 0.25);
assert_eq!(bamboo.max_vertical, 0.2);
let pointed_dripstone = Block::POINTED_DRIPSTONE.shape_offset().unwrap();
assert_eq!(pointed_dripstone.max_horizontal, 0.125);
let small_dripleaf = Block::SMALL_DRIPLEAF.shape_offset().unwrap();
assert_eq!(small_dripleaf.max_vertical, 0.1);
}
#[test]
fn shape_offset_delta_matches_vanilla_coordinate_hash() {
let origin = BlockPos::new(0, 64, 0);
let positive_extreme = BlockPos::new(-18, 64, -7);
let origin_delta = Block::BAMBOO.shape_offset_delta(&origin);
assert_eq!(origin_delta.x, 0.0);
assert_eq!(origin_delta.y, 0.0);
assert_eq!(origin_delta.z, 0.0);
let bamboo_delta = Block::BAMBOO.shape_offset_delta(&positive_extreme);
assert_eq!(bamboo_delta.x, 0.5);
assert_eq!(bamboo_delta.y, 0.0);
assert_eq!(bamboo_delta.z, 0.5);
let speleothem_delta = Block::POINTED_DRIPSTONE.shape_offset_delta(&positive_extreme);
assert_eq!(speleothem_delta.x, 0.25);
assert_eq!(speleothem_delta.y, 0.0);
assert_eq!(speleothem_delta.z, 0.25);
assert_eq!(
Block::SULFUR_SPIKE.shape_offset_delta(&positive_extreme),
speleothem_delta
);
let xyz_delta = Block::SHORT_GRASS.shape_offset_delta(&positive_extreme);
assert_eq!(xyz_delta.x, 0.5);
assert_close(xyz_delta.y, 0.08);
assert_eq!(xyz_delta.z, 0.5);
assert_eq!(Block::STONE.shape_offset_delta(&positive_extreme).x, 0.0);
}
}

View File

@@ -1,6 +1,9 @@
/* This file is generated. Do not edit manually. */
use crate::block_state::PistonBehavior;
use crate::{Block, BlockId, BlockState, BlockStateId, blocks::Flammable};
use crate::{
Block, BlockId, BlockState, BlockStateId,
blocks::{Flammable, ShapeOffset, ShapeOffsetType},
};
use pumpkin_util::loot_table::*;
#[allow(
clippy::wildcard_imports,
@@ -567411,6 +567414,206 @@ impl Block {
}),
experience: None,
};
pub(crate) const fn shape_offset(&self) -> Option<ShapeOffset> {
match self.id {
BlockId::MANGROVE_PROPAGULE => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::SHORT_GRASS => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xyz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::FERN => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xyz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::SHORT_DRY_GRASS => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xyz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::TALL_DRY_GRASS => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xyz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::TALL_SEAGRASS => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::DANDELION => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::GOLDEN_DANDELION => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::TORCHFLOWER => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::POPPY => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::BLUE_ORCHID => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::ALLIUM => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::AZURE_BLUET => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::RED_TULIP => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::ORANGE_TULIP => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::WHITE_TULIP => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::PINK_TULIP => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::OXEYE_DAISY => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::CORNFLOWER => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::WITHER_ROSE => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::LILY_OF_THE_VALLEY => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::SUNFLOWER => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::LILAC => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::ROSE_BUSH => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::PEONY => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::TALL_GRASS => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::LARGE_FERN => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::PITCHER_PLANT => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::BAMBOO_SAPLING => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::BAMBOO => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::WARPED_ROOTS => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::NETHER_SPROUTS => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::CRIMSON_ROOTS => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::POINTED_DRIPSTONE => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.125f32,
max_vertical: 0.2f32,
}),
BlockId::SULFUR_SPIKE => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.125f32,
max_vertical: 0.2f32,
}),
BlockId::SMALL_DRIPLEAF => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xyz,
max_horizontal: 0.25f32,
max_vertical: 0.1f32,
}),
BlockId::HANGING_ROOTS => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::OPEN_EYEBLOSSOM => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
BlockId::CLOSED_EYEBLOSSOM => Some(ShapeOffset {
offset_type: ShapeOffsetType::Xz,
max_horizontal: 0.25f32,
max_vertical: 0.2f32,
}),
_ => None,
}
}
#[doc = r" Try to parse a block from a resource location string."]
#[inline]
#[must_use]