mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
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:
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user