diff --git a/src/main/java/com/ferreusveritas/dynamictrees/api/TreeHelper.java b/src/main/java/com/ferreusveritas/dynamictrees/api/TreeHelper.java index 13e5790d9..a166edead 100644 --- a/src/main/java/com/ferreusveritas/dynamictrees/api/TreeHelper.java +++ b/src/main/java/com/ferreusveritas/dynamictrees/api/TreeHelper.java @@ -152,7 +152,7 @@ public static BlockPos dereferenceTrunkShell(Level level, BlockPos pos) { if (blockState.getBlock() instanceof TrunkShellBlock) { TrunkShellBlock.ShellMuse muse = ((TrunkShellBlock) blockState.getBlock()).getMuse(level, blockState, pos); if (muse != null) { - return muse.pos; + return muse.pos(); } } diff --git a/src/main/java/com/ferreusveritas/dynamictrees/block/branch/ThickBranchBlock.java b/src/main/java/com/ferreusveritas/dynamictrees/block/branch/ThickBranchBlock.java index 44e6c342b..9c9d40714 100644 --- a/src/main/java/com/ferreusveritas/dynamictrees/block/branch/ThickBranchBlock.java +++ b/src/main/java/com/ferreusveritas/dynamictrees/block/branch/ThickBranchBlock.java @@ -7,7 +7,7 @@ import com.ferreusveritas.dynamictrees.init.DTRegistries; import com.ferreusveritas.dynamictrees.systems.BranchConnectables; import com.ferreusveritas.dynamictrees.util.CoordUtils; -import com.ferreusveritas.dynamictrees.util.CoordUtils.Surround; +import com.ferreusveritas.dynamictrees.util.CoordUtils.ShellOffset; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.resources.ResourceLocation; @@ -16,6 +16,7 @@ import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelAccessor; import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.StateDefinition; import net.minecraft.world.level.block.state.properties.IntegerProperty; @@ -31,10 +32,14 @@ public class ThickBranchBlock extends BasicBranchBlock implements Musable { - public static final int MAX_RADIUS_THICK = 24; + public static final int MAX_RADIUS_THICK = (TrunkShellBlock.MAX_DISTANCE*2+1)*8; + public static final int RADIUS_TO_INNER_SHELL = 8; // > 8 needs 3×3 + public static final int RADIUS_TO_OUTER_SHELL = 24; // > 24 needs 5×5 + public static final int RADIUS_TO_OUTERMOST_SHELL = 40; // > 40 needs 7×7 - protected static final IntegerProperty RADIUS_DOUBLE = IntegerProperty.create("radius", 1, MAX_RADIUS_THICK); //39 ? + protected static final IntegerProperty RADIUS_DOUBLE = IntegerProperty.create("radius", 1, MAX_RADIUS_THICK); + @Deprecated public ThickBranchBlock(ResourceLocation name, MapColor mapColor) { this(name, Properties.of().mapColor(mapColor)); } @@ -52,8 +57,8 @@ public void createBlockStateDefinition(StateDefinition.Builder RADIUS_TO_INNER_SHELL; // > 8 + boolean needsOuterRing = radius > RADIUS_TO_OUTER_SHELL; // > 24 + boolean needsOutermostRing = radius > RADIUS_TO_OUTERMOST_SHELL; // > 40 + + // No shells needed + if (!needsInnerRing) { return true; } - boolean setable = true; - final ReplaceableState[] repStates = new ReplaceableState[8]; - - for (Surround dir : Surround.values()) { - final BlockPos dPos = pos.offset(dir.getOffset()); - final ReplaceableState rep = getReplaceability(level, dPos, pos); - - repStates[dir.ordinal()] = rep; - + // === Check inner ring === + final ReplaceableState[] innerRepStates = new ReplaceableState[8]; + ShellOffset[] innerDirs = ShellOffset.levelValues(1); + for (int i = 0; i < innerDirs.length; i++) { + ShellDirection dir = innerDirs[i]; + BlockPos dPos = pos.offset(dir.getOffset()); + ReplaceableState rep = getReplaceability(level, dPos, pos, 1); + innerRepStates[i] = rep; if (rep == ReplaceableState.BLOCKING) { - setable = false; - break; + return false; } } - if (setable) { - BlockState trunkState = level.getBlockState(pos); - boolean isWaterlogged = trunkState.hasProperty(WATERLOGGED) && trunkState.getValue(WATERLOGGED); - for (Surround dir : Surround.values()) { - final BlockPos dPos = pos.offset(dir.getOffset()); - final ReplaceableState rep = repStates[dir.ordinal()]; - final boolean replacingWater = isWaterlogged || level.getBlockState(dPos).getFluidState() == Fluids.WATER.getSource(false); + // === Check outer ring (if needed) === + final ReplaceableState[] outerRepStates = new ReplaceableState[16]; + ShellDirection[] outerDirs = ShellDirection.outerValues(); + if (needsOuterRing) { + for (int i = 0; i < outerDirs.length; i++) { + ShellDirection dir = outerDirs[i]; + BlockPos dPos = pos.offset(dir.getOffset()); + ReplaceableState rep = getReplaceability(level, dPos, pos, 2); + outerRepStates[i] = rep; + if (rep == ReplaceableState.BLOCKING) { + return false; + } + } + } - if (rep == ReplaceableState.REPLACEABLE) { - level.setBlock(dPos, getTrunkShell().defaultBlockState().setValue(TrunkShellBlock.CORE_DIR, dir.getOpposite()).setValue(TrunkShellBlock.WATERLOGGED, replacingWater), flags); + // === Check outermost ring (if needed) === + final ReplaceableState[] outermostRepStates = new ReplaceableState[24]; + ShellDirection[] outermostDirs = ShellDirection.outermostValues(); + if (needsOutermostRing) { + for (int i = 0; i < outermostDirs.length; i++) { + ShellDirection dir = outermostDirs[i]; + BlockPos dPos = pos.offset(dir.getOffset()); + ReplaceableState rep = getReplaceability(level, dPos, pos, 3); + outermostRepStates[i] = rep; + if (rep == ReplaceableState.BLOCKING) { + return false; } } - return true; } - return false; - } - @Override - public int getRadiusForConnection(BlockState state, BlockGetter level, BlockPos pos, BranchBlock from, Direction side, int fromRadius) { - if (from instanceof ThickBranchBlock) { - return getRadius(state); + // === Place shells === + BlockState trunkState = level.getBlockState(pos); + boolean isWaterlogged = trunkState.hasProperty(WATERLOGGED) && trunkState.getValue(WATERLOGGED); + + // Place inner ring + for (int i = 0; i < innerDirs.length; i++) { + ShellDirection dir = innerDirs[i]; + BlockPos dPos = pos.offset(dir.getOffset()); + ReplaceableState rep = innerRepStates[i]; + boolean replacingWater = isWaterlogged || level.getBlockState(dPos).getFluidState() == Fluids.WATER.getSource(false); + + if (rep == ReplaceableState.REPLACEABLE) { + level.setBlock(dPos, getTrunkShell().defaultBlockState() + .setValue(TrunkShellBlock.CORE_DIR, dir.getOpposite()) + .setValue(TrunkShellBlock.WATERLOGGED, replacingWater), flags); + } } - return Math.min(getRadius(state), MAX_RADIUS); - } - @Override - protected int getSideConnectionRadius(BlockGetter level, BlockPos pos, int radius, Direction side) { - final BlockPos deltaPos = pos.relative(side); - final BlockState blockState = CoordUtils.getStateSafe(level, deltaPos); + // Place outer ring (if needed) + if (needsOuterRing) { + for (int i = 0; i < outerDirs.length; i++) { + ShellDirection dir = outerDirs[i]; + BlockPos dPos = pos.offset(dir.getOffset()); + ReplaceableState rep = outerRepStates[i]; + boolean replacingWater = isWaterlogged || level.getBlockState(dPos).getFluidState() == Fluids.WATER.getSource(false); - if (blockState == null) { - return 0; + if (rep == ReplaceableState.REPLACEABLE) { + level.setBlock(dPos, getTrunkShell().defaultBlockState() + .setValue(TrunkShellBlock.CORE_DIR, dir.getOpposite()) + .setValue(TrunkShellBlock.WATERLOGGED, replacingWater), flags); + } + } } - final int connectionRadius = TreeHelper.getTreePart(blockState).getRadiusForConnection(blockState, level, deltaPos, this, side, radius); + // Place outermost ring (if needed) + if (needsOutermostRing) { + for (int i = 0; i < outermostDirs.length; i++) { + ShellDirection dir = outermostDirs[i]; + BlockPos dPos = pos.offset(dir.getOffset()); + ReplaceableState rep = outermostRepStates[i]; + boolean replacingWater = isWaterlogged || level.getBlockState(dPos).getFluidState() == Fluids.WATER.getSource(false); - return Math.min(MAX_RADIUS, connectionRadius); - } + if (rep == ReplaceableState.REPLACEABLE) { + level.setBlock(dPos, getTrunkShell().defaultBlockState() + .setValue(TrunkShellBlock.CORE_DIR, dir.getOpposite()) + .setValue(TrunkShellBlock.WATERLOGGED, replacingWater), flags); + } + } + } - public ReplaceableState getReplaceability(LevelAccessor level, BlockPos pos, BlockPos corePos) { + return true; + } + public ReplaceableState getReplaceability(LevelAccessor level, BlockPos pos, BlockPos corePos, int ringLevel) { final BlockState state = level.getBlockState(pos); final Block block = state.getBlock(); if (block instanceof TrunkShellBlock) { - // Determine if this shell belongs to the trunk. Block otherwise. - Surround surr = state.getValue(TrunkShellBlock.CORE_DIR); - return pos.offset(surr.getOffset()).equals(corePos) ? ReplaceableState.SHELL : ReplaceableState.BLOCKING; + ShellDirection dir = state.getValue(TrunkShellBlock.CORE_DIR); + return pos.offset(dir.getOffset()).equals(corePos) ? ReplaceableState.SHELL : ReplaceableState.BLOCKING; } if (state.canBeReplaced() || state.is(DTBlockTags.FOLIAGE)) { @@ -165,7 +214,7 @@ public ReplaceableState getReplaceability(LevelAccessor level, BlockPos pos, Blo return ReplaceableState.TREEPART; } - if (block instanceof FruitBlock || block instanceof PodBlock){ + if (block instanceof FruitBlock || block instanceof PodBlock) { return ReplaceableState.TREEPART; } @@ -173,14 +222,57 @@ public ReplaceableState getReplaceability(LevelAccessor level, BlockPos pos, Blo return ReplaceableState.REPLACEABLE; } + if (ringLevel == 1) { + float hardness = state.getDestroySpeed(level, pos); + if (hardness >= 0 && hardness < 1) { + return ReplaceableState.REPLACEABLE; + } + } + + if (ringLevel == 2) { + float hardness = state.getDestroySpeed(level, pos); + if (hardness >= 0 && hardness < 3) { + return ReplaceableState.REPLACEABLE; + } + } + + // Outermost ring can break most hard blocks + if (ringLevel == 3) { + float hardness = state.getDestroySpeed(level, pos); + if (hardness >= 0 && hardness < 5) { + return ReplaceableState.REPLACEABLE; + } + } + return ReplaceableState.BLOCKING; } enum ReplaceableState { - SHELL, // This indicates that the block is already a shell. - REPLACEABLE, // This indicates that the block is truly replaceable and will be erased. - BLOCKING, // This indicates that the block is not replaceable, will NOT be erased, and will prevent the tree from growing. - TREEPART // This indicates that the block is part of a tree, will NOT be erase, and will NOT prevent the tree from growing. + SHELL, + REPLACEABLE, + BLOCKING, + TREEPART + } + + @Override + public int getRadiusForConnection(BlockState state, BlockGetter level, BlockPos pos, BranchBlock from, Direction side, int fromRadius) { + if (from instanceof ThickBranchBlock) { + return getRadius(state); + } + return Math.min(getRadius(state), MAX_RADIUS); + } + + @Override + protected int getSideConnectionRadius(BlockGetter level, BlockPos pos, int radius, Direction side) { + final BlockPos deltaPos = pos.relative(side); + final BlockState blockState = CoordUtils.getStateSafe(level, deltaPos); + + if (blockState == null) { + return 0; + } + + final int connectionRadius = TreeHelper.getTreePart(blockState).getRadiusForConnection(blockState, level, deltaPos, this, side, radius); + return Math.min(MAX_RADIUS, connectionRadius); } @Override @@ -188,9 +280,8 @@ public int getMaxRadius() { return MAX_RADIUS_THICK; } - - /////////////////////////////////////////// - // PHYSICAL BOUNDS +/////////////////////////////////////////// +// PHYSICAL BOUNDS /////////////////////////////////////////// @Nonnull @@ -209,5 +300,4 @@ public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, Co public boolean isMusable(BlockGetter level, BlockState state, BlockPos pos) { return getRadius(state) > 8; } - -} \ No newline at end of file +} diff --git a/src/main/java/com/ferreusveritas/dynamictrees/block/branch/TrunkShellBlock.java b/src/main/java/com/ferreusveritas/dynamictrees/block/branch/TrunkShellBlock.java index ae5cc16b2..3706fecaf 100644 --- a/src/main/java/com/ferreusveritas/dynamictrees/block/branch/TrunkShellBlock.java +++ b/src/main/java/com/ferreusveritas/dynamictrees/block/branch/TrunkShellBlock.java @@ -2,12 +2,11 @@ import com.ferreusveritas.dynamictrees.block.BlockWithDynamicHardness; import com.ferreusveritas.dynamictrees.util.CoordUtils; -import com.ferreusveritas.dynamictrees.util.CoordUtils.Surround; +import com.ferreusveritas.dynamictrees.util.CoordUtils.ShellOffset; import com.ferreusveritas.dynamictrees.util.Null; import net.minecraft.client.particle.ParticleEngine; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; -import net.minecraft.core.Vec3i; import net.minecraft.core.particles.BlockParticleOption; import net.minecraft.core.particles.ParticleTypes; import net.minecraft.server.level.ServerLevel; @@ -26,6 +25,7 @@ import net.minecraft.world.level.block.state.properties.BlockStateProperties; import net.minecraft.world.level.block.state.properties.BooleanProperty; import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.level.block.state.properties.IntegerProperty; import net.minecraft.world.level.material.Fluid; import net.minecraft.world.level.material.FluidState; import net.minecraft.world.level.material.Fluids; @@ -34,7 +34,6 @@ import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.HitResult; -import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; @@ -47,32 +46,28 @@ @SuppressWarnings("deprecation") public class TrunkShellBlock extends BlockWithDynamicHardness implements SimpleWaterloggedBlock { - - public static final EnumProperty CORE_DIR = EnumProperty.create("coredir", Surround.class); + public static final int MAX_DISTANCE = 3; + // Single unified property for all 24 shell directions + @Deprecated + public static final EnumProperty CORE_DIR = EnumProperty.create("coredir", CoordUtils.Surround.class); + public static final IntegerProperty CORE_OFFSET_X = IntegerProperty.create("shell_offset_x", 0, MAX_DISTANCE*2); + public static final IntegerProperty CORE_OFFSET_Z = IntegerProperty.create("shell_offset_z", 0, MAX_DISTANCE*2); public static final BooleanProperty WATERLOGGED = BlockStateProperties.WATERLOGGED; - public static class ShellMuse { - public final BlockState state; - public final BlockPos pos; - public final BlockPos museOffset; - public final Surround dir; - - public ShellMuse(BlockState state, BlockPos pos, Surround dir, BlockPos museOffset) { - this.state = state; - this.pos = pos; - this.dir = dir; - this.museOffset = museOffset; - } - + public record ShellMuse(BlockState state, BlockPos pos, ShellOffset dir, BlockPos museOffset) { public int getRadius() { - final Block block = this.state.getBlock(); - return block instanceof BranchBlock ? ((BranchBlock) block).getRadius(state) : 0; + final Block block = this.state.getBlock(); + return block instanceof BranchBlock ? ((BranchBlock) block).getRadius(state) : 0; + } } - } public TrunkShellBlock() { super(Properties.of().ignitedByLava().pushReaction(PushReaction.BLOCK).noOcclusion()); - registerDefaultState(defaultBlockState().setValue(WATERLOGGED, false)); + registerDefaultState(defaultBlockState() + .setValue(CORE_DIR, CoordUtils.Surround.N) + .setValue(CORE_OFFSET_X, MAX_DISTANCE) + .setValue(CORE_OFFSET_Z, MAX_DISTANCE) + .setValue(WATERLOGGED, false)); } /////////////////////////////////////////// @@ -80,21 +75,38 @@ public TrunkShellBlock() { /////////////////////////////////////////// protected void createBlockStateDefinition(StateDefinition.Builder builder) { - builder.add(CORE_DIR).add(WATERLOGGED); + builder.add(CORE_DIR, WATERLOGGED); } @Override public void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { ShellMuse muse = this.getMuseUnchecked(level, state, pos); - if (!isValid(muse)) { - if (state.getValue(WATERLOGGED)) { - level.setBlockAndUpdate(pos, Blocks.WATER.defaultBlockState()); - } else { - level.removeBlock(pos, false); + + if (muse == null) { + ShellOffset museOffset = getMuseDir(state); + BlockPos targetPos = pos.offset(museOffset.getOffset()); + + if (!CoordUtils.canAccessStateSafely(level, targetPos)) { + level.scheduleTick(pos, this, 20); + return; } + // Chunk accessible, muse gone - remove shell + removeShell(state, level, pos); + return; + } + + if (!isValid(muse)) { + removeShell(state, level, pos); } } + private void removeShell(BlockState state, ServerLevel level, BlockPos pos) { + if (state.getValue(WATERLOGGED)) { + level.setBlockAndUpdate(pos, Blocks.WATER.defaultBlockState()); + } else { + level.removeBlock(pos, false); + } + } /////////////////////////////////////////// // INTERACTION /////////////////////////////////////////// @@ -130,17 +142,22 @@ public boolean canBeReplaced(BlockState state, BlockPlaceContext useContext) { final BlockPos clickedPos = useContext.getClickedPos(); if (this.museDoesNotExist(level, state, clickedPos)) { this.scheduleUpdateTick(level, clickedPos); - return false; } return false; } - public Surround getMuseDir(BlockState state, BlockPos pos) { - return state.getValue(CORE_DIR); + public ShellOffset getMuseOffset(BlockState state) { + ShellOffset offset = new ShellOffset(state.getValue(CORE_OFFSET_X)-MAX_DISTANCE, state.getValue(CORE_OFFSET_Z)-MAX_DISTANCE); + if (offset.getShellLevel() == 0) { //This means the shell offset value is 0 (legacy world) + CoordUtils.Surround offsetDir = state.getValue(CORE_DIR); + return new ShellOffset(offsetDir.getOffset()); + } else { + return offset; + } } public boolean museDoesNotExist(BlockGetter level, BlockState state, BlockPos pos) { - final BlockPos musePos = pos.offset(this.getMuseDir(state, pos).getOffset()); + final BlockPos musePos = pos.offset(this.getMuseDir(state).getOffset()); return CoordUtils.getStateSafe(level, musePos) == null; } @@ -156,7 +173,7 @@ public ShellMuse getMuseUnchecked(BlockGetter level, BlockState state, BlockPos @Nullable public ShellMuse getMuseUnchecked(BlockGetter level, BlockState state, BlockPos pos, BlockPos originalPos) { - final Surround museDir = getMuseDir(state, pos); + final ShellDirection museDir = getMuseDir(state); final BlockPos musePos = pos.offset(museDir.getOffset()); final BlockState museState = CoordUtils.getStateSafe(level, musePos); @@ -167,11 +184,9 @@ public ShellMuse getMuseUnchecked(BlockGetter level, BlockState state, BlockPos final Block block = museState.getBlock(); if (block instanceof Musable && ((Musable) block).isMusable(level, museState, musePos)) { return new ShellMuse(museState, musePos, museDir, musePos.subtract(originalPos)); - } else if (block instanceof TrunkShellBlock) { // If its another trunkshell, then this trunkshell is on another layer. IF they share a common direction, we return that shell's muse. - final Vec3i offset = ((TrunkShellBlock) block).getMuseDir(museState, musePos).getOffset(); - if (new Vec3(offset.getX(), offset.getY(), offset.getZ()).add(new Vec3(museDir.getOffset().getX(), museDir.getOffset().getY(), museDir.getOffset().getZ())).lengthSqr() > 2.25) { - return (((TrunkShellBlock) block).getMuseUnchecked(level, museState, musePos, originalPos)); - } + } else if (block instanceof TrunkShellBlock shellBlock) { + // If it's another trunk shell, follow the chain to find the core + return shellBlock.getMuseUnchecked(level, museState, musePos, originalPos); } return null; } @@ -185,7 +200,6 @@ public ShellMuse getMuse(BlockGetter level, BlockPos pos) { public ShellMuse getMuse(BlockGetter level, BlockState state, BlockPos pos) { final ShellMuse muse = this.getMuseUnchecked(level, state, pos); - // Check the muse for validity. if (!isValid(muse)) { this.scheduleUpdateTick(level, pos); } @@ -193,20 +207,35 @@ public ShellMuse getMuse(BlockGetter level, BlockState state, BlockPos pos) { return muse; } + /** + * Validates a shell muse based on radius thresholds. + * Inner shells (distance 1) require radius > 8 + * Outer shells (distance 2) require radius > 24 + */ protected boolean isValid(@Nullable ShellMuse muse) { - return muse != null && muse.getRadius() > 8; + if (muse == null) { + return false; + } + int radius = muse.getRadius(); + int shellLevel = muse.dir.getShellLevel(); + + return switch (shellLevel) { + case 1 -> radius > ThickBranchBlock.RADIUS_TO_INNER_SHELL; // > 8 + case 2 -> radius > ThickBranchBlock.RADIUS_TO_OUTER_SHELL; // > 24 + case 3 -> radius > ThickBranchBlock.RADIUS_TO_OUTERMOST_SHELL; // > 40 + default -> false; + }; } public void scheduleUpdateTick(BlockGetter level, BlockPos pos) { if (!(level instanceof LevelAccessor)) { return; } - ((LevelAccessor) level).getBlockTicks().schedule(new ScheduledTick<>(this, pos.immutable(), 0, TickPriority.HIGH, 0)); } @Override - public void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighborBlock, BlockPos neighborPos, boolean p_220069_6_) { + public void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighborBlock, BlockPos neighborPos, boolean isMoving) { this.scheduleUpdateTick(level, pos); } @@ -225,30 +254,25 @@ public void onBlockExploded(BlockState state, Level level, BlockPos pos, Explosi Null.consumeIfNonnull(this.getMuse(level, state, pos), muse -> muse.state.getBlock().onBlockExploded(muse.state, level, muse.pos, explosion)); } - //TODO: This may not even be necessary @Nullable - protected Surround findDetachedMuse(Level level, BlockPos pos) { - for (Surround s : Surround.values()) { - final BlockState state = level.getBlockState(pos.offset(s.getOffset())); - + protected ShellDirection findDetachedMuse(Level level, BlockPos pos) { + for (ShellDirection dir : ShellDirection.values()) { + final BlockState state = level.getBlockState(pos.offset(dir.getOffset())); if (state.getBlock() instanceof Musable) { - return s; + return dir; } } return null; } - //TODO: This may not even be necessary @Override public void destroy(LevelAccessor level, BlockPos pos, BlockState state) { final BlockState newState = level.getBlockState(pos); - if (newState.getBlock() != Blocks.AIR) { return; } - Null.consumeIfNonnull(this.findDetachedMuse((Level) level, pos), - surround -> level.setBlock(pos, defaultBlockState().setValue(CORE_DIR, surround), 1)); + dir -> level.setBlock(pos, defaultBlockState().setValue(CORE_DIR, dir), 1)); } @Override @@ -258,12 +282,12 @@ public InteractionResult use(BlockState state, Level level, BlockPos pos, Player @Override public boolean isFlammable(BlockState state, BlockGetter level, BlockPos pos, Direction face) { - return false; // This is the simple solution to the problem. Maybe I'll work it out later. + return false; } @Override public int getFlammability(BlockState state, BlockGetter level, BlockPos pos, Direction face) { - return 0; // This is the simple solution to the problem. Maybe I'll work it out later. + return 0; } public boolean isFullBlockShell(BlockGetter level, BlockPos pos) { @@ -295,7 +319,7 @@ public FluidState getFluidState(BlockState state) { @Override public BlockState updateShape(BlockState state, Direction facing, BlockState facingState, LevelAccessor level, BlockPos currentPos, BlockPos facingPos) { if (state.getValue(WATERLOGGED)) { - level.getFluidTicks().schedule(new ScheduledTick<>(Fluids.WATER,currentPos, Fluids.WATER.getTickDelay(level),0)); + level.getFluidTicks().schedule(new ScheduledTick<>(Fluids.WATER, currentPos, Fluids.WATER.getTickDelay(level), 0)); } return super.updateShape(state, facing, facingState, level, currentPos, facingPos); } @@ -326,7 +350,6 @@ public boolean addLandingEffects(BlockState state1, ServerLevel level, BlockPos return true; } - @Override public void initializeClient(Consumer consumer) { consumer.accept(new IClientBlockExtensions() { @@ -339,9 +362,8 @@ public boolean addHitEffects(BlockState state, Level level, HitResult target, Pa return false; } - if (state.getBlock() instanceof TrunkShellBlock) { - final ShellMuse muse = ((TrunkShellBlock)state.getBlock()).getMuseUnchecked(level, state, shellPos); - + if (state.getBlock() instanceof TrunkShellBlock shellBlock) { + final ShellMuse muse = shellBlock.getMuseUnchecked(level, state, shellPos); if (muse == null) { return true; } @@ -367,30 +389,22 @@ public boolean addHitEffects(BlockState state, Level level, HitResult target, Pa case EAST -> d0 = x + axisalignedbb.maxX + 0.1D; } - // Safe to spawn particles here since this is a client side only member function. level.addParticle(new BlockParticleOption(ParticleTypes.BLOCK, museState), d0, d1, d2, 0, 0, 0); } - return true; } @Override public boolean addDestroyEffects(BlockState state, Level level, BlockPos pos, ParticleEngine manager) { - if (state.getBlock() instanceof TrunkShellBlock) { - final ShellMuse muse = ((TrunkShellBlock)state.getBlock()).getMuseUnchecked(level, state, pos); - + if (state.getBlock() instanceof TrunkShellBlock shellBlock) { + final ShellMuse muse = shellBlock.getMuseUnchecked(level, state, pos); if (muse == null) { return true; } - - final BlockState museState = muse.state; - final BlockPos musePos = muse.pos; - - manager.destroy(musePos, museState); + manager.destroy(muse.pos, muse.state); } return true; } }); } - } diff --git a/src/main/java/com/ferreusveritas/dynamictrees/compat/waila/WailaBranchHandler.java b/src/main/java/com/ferreusveritas/dynamictrees/compat/waila/WailaBranchHandler.java index 5eb0d053d..10a0ed24d 100644 --- a/src/main/java/com/ferreusveritas/dynamictrees/compat/waila/WailaBranchHandler.java +++ b/src/main/java/com/ferreusveritas/dynamictrees/compat/waila/WailaBranchHandler.java @@ -147,9 +147,9 @@ private NetVolumeNode.Volume getTreeVolume(Level level, BlockPos pos, Species sp if (block instanceof TrunkShellBlock) { ShellMuse muse = ((TrunkShellBlock) block).getMuse(level, pos); if (muse != null) { - state = muse.state; + state = muse.state(); block = state.getBlock(); - pos = muse.pos; + pos = muse.pos(); } } diff --git a/src/main/java/com/ferreusveritas/dynamictrees/models/baked/ThickBranchBlockBakedModel.java b/src/main/java/com/ferreusveritas/dynamictrees/models/baked/ThickBranchBlockBakedModel.java index f6d8068ac..5f4f0c8e4 100644 --- a/src/main/java/com/ferreusveritas/dynamictrees/models/baked/ThickBranchBlockBakedModel.java +++ b/src/main/java/com/ferreusveritas/dynamictrees/models/baked/ThickBranchBlockBakedModel.java @@ -45,10 +45,10 @@ @OnlyIn(Dist.CLIENT) public class ThickBranchBlockBakedModel extends BasicBranchBlockBakedModel { - private final BakedModel[] trunksBark = new BakedModel[16]; // The trunk will always feature bark on its sides. - private final BakedModel[] trunksTopBark = new BakedModel[16]; // The trunk will feature bark on its top when there's a branch on top of it. - private final BakedModel[] trunksTopRings = new BakedModel[16]; // The trunk will feature rings on its top when there's no branches on top of it. - private final BakedModel[] trunksBotRings = new BakedModel[16]; // The trunk will always feature rings on its bottom surface if nothing is below it. + private final BakedModel[] trunksBark = new BakedModel[ThickBranchBlock.MAX_RADIUS_THICK - BranchBlock.MAX_RADIUS]; + private final BakedModel[] trunksTopBark = new BakedModel[ThickBranchBlock.MAX_RADIUS_THICK - BranchBlock.MAX_RADIUS]; + private final BakedModel[] trunksTopRings = new BakedModel[ThickBranchBlock.MAX_RADIUS_THICK - BranchBlock.MAX_RADIUS]; + private final BakedModel[] trunksBotRings = new BakedModel[ThickBranchBlock.MAX_RADIUS_THICK - BranchBlock.MAX_RADIUS]; public ThickBranchBlockBakedModel(IGeometryBakingContext customData, ResourceLocation modelLocation, ResourceLocation barkTextureLocation, ResourceLocation ringsTextureLocation, ResourceLocation thickRingsTextureLocation, Function spriteGetter) { @@ -74,27 +74,56 @@ private boolean isTextureNull(@Nullable TextureAtlasSprite sprite) { return sprite == null || sprite.equals(ModelUtils.getTexture(new ResourceLocation(""))); } - public BakedModel bakeTrunkBark(int radius, TextureAtlasSprite bark, boolean side) { + /** + * Determines grid size based on radius. + * @return 3 for radius 9-24, 5 for radius 25-40, 7 for radius 41-56 + */ + private int getGridSize(int radius) { + if (radius > ThickBranchBlock.RADIUS_TO_OUTERMOST_SHELL) { + return 7; + } else if (radius > ThickBranchBlock.RADIUS_TO_OUTER_SHELL) { + return 5; + } else { + return 3; + } + } + + /** + * Generates grid offsets based on grid size. + */ + private ArrayList getGridOffsets(int gridSize) { + ArrayList offsets = new ArrayList<>(); + int halfGrid = gridSize / 2; + for (int x = -halfGrid; x <= halfGrid; x++) { + for (int z = -halfGrid; z <= halfGrid; z++) { + offsets.add(new Vec3i(x, 0, z)); + } + } + return offsets; + } + public BakedModel bakeTrunkBark(int radius, TextureAtlasSprite bark, boolean side) { IModelBuilder builder = ModelUtils.getModelBuilder(this.blockModel.customData, bark); AABB wholeVolume = new AABB(8 - radius, 0, 8 - radius, 8 + radius, 16, 8 + radius); final Direction[] run = side ? CoordUtils.HORIZONTALS : new Direction[]{Direction.UP, Direction.DOWN}; - ArrayList offsets = new ArrayList<>(); - for (Surround dir : Surround.values()) { - offsets.add(dir.getOffset()); // 8 surrounding component pieces - } - offsets.add(new Vec3i(0, 0, 0));//Center + int gridSize = getGridSize(radius); + ArrayList offsets = getGridOffsets(gridSize); for (Direction face : run) { final Vec3i dirVector = face.getNormal(); for (Vec3i offset : offsets) { - if (face.getAxis() == Axis.Y || new Vec3(dirVector.getX(), dirVector.getY(), dirVector.getZ()).add(new Vec3(offset.getX(), offset.getY(), offset.getZ())).lengthSqr() > 2.25) { //This means that the dir and face share a common direction - Vec3 scaledOffset = new Vec3(offset.getX() * 16, offset.getY() * 16, offset.getZ() * 16);//Scale the dimensions to match standard minecraft texels + if (face.getAxis() == Axis.Y || new Vec3(dirVector.getX(), dirVector.getY(), dirVector.getZ()).add(new Vec3(offset.getX(), offset.getY(), offset.getZ())).lengthSqr() > 2.25) { + Vec3 scaledOffset = new Vec3(offset.getX() * 16, offset.getY() * 16, offset.getZ() * 16); AABB partBoundary = new AABB(0, 0, 0, 16, 16, 16).move(scaledOffset).intersect(wholeVolume); + // Skip if intersection is empty + if (partBoundary.getXsize() <= 0 || partBoundary.getYsize() <= 0 || partBoundary.getZsize() <= 0) { + continue; + } + Vector3f[] limits = ModelUtils.AABBLimits(partBoundary); Map mapFacesIn = Maps.newEnumMap(Direction.class); @@ -105,7 +134,6 @@ public BakedModel bakeTrunkBark(int radius, TextureAtlasSprite bark, boolean sid BlockElement part = new BlockElement(limits[0], limits[1], mapFacesIn, null, true); builder.addCulledFace(face, ModelUtils.makeBakedQuad(part, part.faces.get(face), bark, face, BlockModelRotation.X0_Y0, this.modelLocation)); } - } } @@ -115,34 +143,38 @@ public BakedModel bakeTrunkBark(int radius, TextureAtlasSprite bark, boolean sid public BakedModel bakeTrunkRings(int radius, TextureAtlasSprite ring, Direction face) { IModelBuilder builder = ModelUtils.getModelBuilder(this.blockModel.customData, ring); AABB wholeVolume = new AABB(8 - radius, 0, 8 - radius, 8 + radius, 16, 8 + radius); - int wholeVolumeWidth = 48; - ArrayList offsets = new ArrayList<>(); + int gridSize = getGridSize(radius); + // Texture width: 48 for 3×3, 80 for 5×5, 112 for 7×7 + int wholeVolumeWidth = gridSize * 16; - for (Surround dir : Surround.values()) { - offsets.add(dir.getOffset()); // 8 surrounding component pieces - } - offsets.add(new Vec3i(0, 0, 0)); // Center + ArrayList offsets = getGridOffsets(gridSize); + + // Texture offset based on grid size + float textureOffset = -(gridSize / 2) * 16f; for (Vec3i offset : offsets) { - Vec3 scaledOffset = new Vec3(offset.getX() * 16, offset.getY() * 16, offset.getZ() * 16); // Scale the dimensions to match standard minecraft texels + Vec3 scaledOffset = new Vec3(offset.getX() * 16, offset.getY() * 16, offset.getZ() * 16); AABB partBoundary = new AABB(0, 0, 0, 16, 16, 16).move(scaledOffset).intersect(wholeVolume); + // Skip if intersection is empty + if (partBoundary.getXsize() <= 0 || partBoundary.getYsize() <= 0 || partBoundary.getZsize() <= 0) { + continue; + } + Vector3f posFrom = new Vector3f((float) partBoundary.minX, (float) partBoundary.minY, (float) partBoundary.minZ); Vector3f posTo = new Vector3f((float) partBoundary.maxX, (float) partBoundary.maxY, (float) partBoundary.maxZ); Map mapFacesIn = Maps.newEnumMap(Direction.class); - float textureOffsetX = -16f; - float textureOffsetZ = -16f; - float minX = ((float) ((partBoundary.minX - textureOffsetX) / wholeVolumeWidth)) * 16f; - float maxX = ((float) ((partBoundary.maxX - textureOffsetX) / wholeVolumeWidth)) * 16f; - float minZ = ((float) ((partBoundary.minZ - textureOffsetZ) / wholeVolumeWidth)) * 16f; - float maxZ = ((float) ((partBoundary.maxZ - textureOffsetZ) / wholeVolumeWidth)) * 16f; + float minX = ((float) ((partBoundary.minX - textureOffset) / wholeVolumeWidth)) * 16f; + float maxX = ((float) ((partBoundary.maxX - textureOffset) / wholeVolumeWidth)) * 16f; + float minZ = ((float) ((partBoundary.minZ - textureOffset) / wholeVolumeWidth)) * 16f; + float maxZ = ((float) ((partBoundary.maxZ - textureOffset) / wholeVolumeWidth)) * 16f; if (face == Direction.DOWN) { - minZ = ((float) ((partBoundary.maxZ - textureOffsetZ) / wholeVolumeWidth)) * 16f; - maxZ = ((float) ((partBoundary.minZ - textureOffsetZ) / wholeVolumeWidth)) * 16f; + minZ = ((float) ((partBoundary.maxZ - textureOffset) / wholeVolumeWidth)) * 16f; + maxZ = ((float) ((partBoundary.minZ - textureOffset) / wholeVolumeWidth)) * 16f; } float[] uvs = new float[]{minX, minZ, maxX, maxZ}; @@ -170,7 +202,7 @@ public List getQuads(@Nullable final BlockState state, @Nullable fina return super.getQuads(state, null, rand, extraData, renderType); } - coreRadius = Mth.clamp(coreRadius, 9, 24); + coreRadius = Mth.clamp(coreRadius, 9, ThickBranchBlock.MAX_RADIUS_THICK); List quads = new ArrayList<>(30); @@ -198,19 +230,21 @@ public List getQuads(@Nullable final BlockState state, @Nullable fina return quads; } + int arrayIndex = coreRadius - 9; + if (forceRingDir != null) { connections[forceRingDir.get3DDataValue()] = 0; - quads.addAll(this.trunksBotRings[coreRadius - 9].getQuads(state, forceRingDir, rand, extraData, renderType)); + quads.addAll(this.trunksBotRings[arrayIndex].getQuads(state, forceRingDir, rand, extraData, renderType)); } boolean branchesAround = connections[2] + connections[3] + connections[4] + connections[5] != 0; for (Direction face : Direction.values()) { - quads.addAll(this.trunksBark[coreRadius - 9].getQuads(state, face, rand, extraData, renderType)); + quads.addAll(this.trunksBark[arrayIndex].getQuads(state, face, rand, extraData, renderType)); if (face == Direction.UP || face == Direction.DOWN) { if (connections[face.get3DDataValue()] < twigRadius && !branchesAround) { - quads.addAll(this.trunksTopRings[coreRadius - 9].getQuads(state, face, rand, extraData, renderType)); + quads.addAll(this.trunksTopRings[arrayIndex].getQuads(state, face, rand, extraData, renderType)); } else if (connections[face.get3DDataValue()] < coreRadius) { - quads.addAll(this.trunksTopBark[coreRadius - 9].getQuads(state, face, rand, extraData, renderType)); + quads.addAll(this.trunksTopBark[arrayIndex].getQuads(state, face, rand, extraData, renderType)); } } } @@ -218,4 +252,4 @@ public List getQuads(@Nullable final BlockState state, @Nullable fina return quads; } -} \ No newline at end of file +} diff --git a/src/main/java/com/ferreusveritas/dynamictrees/util/BranchDestructionData.java b/src/main/java/com/ferreusveritas/dynamictrees/util/BranchDestructionData.java index 98990de3f..a99495262 100644 --- a/src/main/java/com/ferreusveritas/dynamictrees/util/BranchDestructionData.java +++ b/src/main/java/com/ferreusveritas/dynamictrees/util/BranchDestructionData.java @@ -25,7 +25,7 @@ public class BranchDestructionData { public final Species species; // The species of the tree that was harvested public final int[] destroyedBranchesRadiusPosition; // Encoded branch radius and relative positions - public final int[] destroyedBranchesConnections; // Encoded branch shapes + public final long[] destroyedBranchesConnections; // Encoded branch shapes public final int[] destroyedBranchesBlockIndex; // Encoded valid branch block index for family public final int[] destroyedLeaves; // Encoded leaves relative positions public final int[] destroyedLeavesBlockIndex; // Encoded valid leaves block index for species @@ -42,7 +42,7 @@ public class BranchDestructionData { public BranchDestructionData() { this.species = Species.NULL_SPECIES; - this.destroyedBranchesConnections = new int[0]; + this.destroyedBranchesConnections = new long[0]; this.destroyedBranchesRadiusPosition = new int[0]; this.destroyedBranchesBlockIndex = new int[0]; this.destroyedLeaves = new int[0]; @@ -64,8 +64,8 @@ public BranchDestructionData(Species species, Map branchList) { int[] radPosData = new int[branchList.size()]; - int[] connectionData = new int[branchList.size()]; int[] blockIndexData = new int[branchList.size()]; int index = 0; @@ -172,11 +171,9 @@ private int[][] convertBranchesToIntArrays(Map b if (origConnData != null) { BlockState origState = origConnData.getBlockState(); radPosData[index] = encodeBranchesRadiusPos(BlockPos.ZERO, (BranchBlock) origState.getBlock(), origState); - connectionData[index] = encodeBranchesConnections(origConnData.getConnections()); blockIndexData[index++] = encodeBranchBlocks((BranchBlock) origState.getBlock()); } - //Encode the remaining blocks for (Entry set : branchList.entrySet()) { if (set.getKey().equals(BlockPos.ZERO)) continue; BlockPos relPos = set.getKey(); @@ -184,63 +181,61 @@ private int[][] convertBranchesToIntArrays(Map b BlockState state = connData.getBlockState(); Block block = state.getBlock(); - if (block instanceof BranchBlock && bounds.inBounds(relPos)) { //Place comfortable limits on the system + if (block instanceof BranchBlock && bounds.inBounds(relPos)) { radPosData[index] = encodeBranchesRadiusPos(relPos, (BranchBlock) block, state); - connectionData[index] = encodeBranchesConnections(connData.getConnections()); blockIndexData[index++] = encodeBranchBlocks((BranchBlock) block); } } - //Shrink down the arrays radPosData = Arrays.copyOf(radPosData, index); - connectionData = Arrays.copyOf(connectionData, index); blockIndexData = Arrays.copyOf(blockIndexData, index); - return new int[][]{radPosData, connectionData, blockIndexData}; + return new int[][]{radPosData, blockIndexData}; + } + + private long[] convertBranchConnectionsToLongArray(Map branchList) { + long[] connectionData = new long[branchList.size()]; + int index = 0; + + //Ensure the origin block is at the first index + BranchConnectionData origConnData = branchList.get(BlockPos.ZERO); + if (origConnData != null) { + connectionData[index++] = encodeBranchesConnections(origConnData.getConnections()); + } + + for (Entry set : branchList.entrySet()) { + if (set.getKey().equals(BlockPos.ZERO)) continue; + BlockPos relPos = set.getKey(); + BranchConnectionData connData = set.getValue(); + BlockState state = connData.getBlockState(); + Block block = state.getBlock(); + + if (block instanceof BranchBlock && bounds.inBounds(relPos)) { + connectionData[index++] = encodeBranchesConnections(connData.getConnections()); + } + } + + return Arrays.copyOf(connectionData, index); } private int encodeBranchesRadiusPos(BlockPos relPos, BranchBlock branchBlock, BlockState state) { - return ((branchBlock.getRadius(state) & 0x1F) << 24) | //Radius 0 - 31 + return ((branchBlock.getRadius(state) & 0xFF) << 24) | // Radius 0 - 255 encodeRelBlockPos(relPos); } - private int encodeBranchesConnections(Connections exState) { - int result = 0; + private long encodeBranchesConnections(Connections exState) { + long result = 0; int[] radii = exState.getAllRadii(); for (Direction face : Direction.values()) { int faceIndex = face.get3DDataValue(); int rad = radii[faceIndex]; - result |= (rad & 0x1F) << (faceIndex * 5);//5 bits per face * 6 faces = 30bits + result |= ((long)(rad & 0xFF)) << (faceIndex * 8); // 8 bits per face * 6 faces = 48 bits } return result; } - private int encodeBranchBlocks(BranchBlock branch) { - return branch.getFamily().getBranchBlockIndex(branch); - } - - public int getNumBranches() { - return destroyedBranchesRadiusPosition.length; - } - - public BlockPos getBranchRelPos(int index) { - BlockPos pos = decodeRelPos(destroyedBranchesRadiusPosition[index]); - if (basePos != cutPos){ //When a root system is involved, the relative positions are moved down - return pos.offset(getRelativeCutPos()); - } - return pos; - } - - public BlockPos getRelativeCutPos(){ - return cutPos.subtract(basePos); - } - - public int getBranchRadius(int index) { - return decodeBranchRadius(destroyedBranchesRadiusPosition[index]); - } - private int decodeBranchRadius(int encoded) { - return (encoded >> 24) & 0x1F; + return (encoded >> 24) & 0xFF; } @Nullable @@ -256,14 +251,34 @@ public BlockState getBranchBlockState(int index) { } public void getConnections(int index, int[] connections) { - int encodedConnections = destroyedBranchesConnections[index]; + long encodedConnections = destroyedBranchesConnections[index]; for (Direction face : Direction.values()) { - int rad = (encodedConnections >> (face.get3DDataValue() * 5) & 0x1F); + int rad = (int)((encodedConnections >> (face.get3DDataValue() * 8)) & 0xFF); connections[face.get3DDataValue()] = Math.max(0, rad); } } + private int encodeBranchBlocks(BranchBlock branch) { + return branch.getFamily().getBranchBlockIndex(branch); + } + + public int getNumBranches() { + return destroyedBranchesRadiusPosition.length; + } + + public BlockPos getBranchRelPos(int index) { + BlockPos pos = decodeRelPos(destroyedBranchesRadiusPosition[index]); + if (basePos != cutPos) { // When a root system is involved, the relative positions are moved down + return pos.offset(getRelativeCutPos()); + } + return pos; + } + + public int getBranchRadius(int index) { + return decodeBranchRadius(destroyedBranchesRadiusPosition[index]); + } + public static class BlockStateWithConnections { private final BlockState blockState; private final int[] connections; @@ -459,4 +474,8 @@ public static BlockPos decodeRelPos(int encoded) { ); } + public BlockPos getRelativeCutPos() { + return cutPos.subtract(basePos); + } + } diff --git a/src/main/java/com/ferreusveritas/dynamictrees/util/CoordUtils.java b/src/main/java/com/ferreusveritas/dynamictrees/util/CoordUtils.java index 265e54368..248b185e0 100644 --- a/src/main/java/com/ferreusveritas/dynamictrees/util/CoordUtils.java +++ b/src/main/java/com/ferreusveritas/dynamictrees/util/CoordUtils.java @@ -33,6 +33,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.HashMap; import java.util.Iterator; import java.util.function.BiFunction; import java.util.function.Function; @@ -83,6 +84,110 @@ public Surround getOpposite() { } } +// // Inner ring (distance 1) - 8 positions +// N(0, -1, 1), +// NE(1, -1, 1), +// E(1, 0, 1), +// SE(1, 1, 1), +// S(0, 1, 1), +// SW(-1, 1, 1), +// W(-1, 0, 1), +// NW(-1, -1, 1), +// +// // Outer ring (distance 2) - 16 positions +// N2(0, -2, 2), +// NE2(1, -2, 2), +// E2(2, 0, 2), +// SE2(1, 2, 2), +// S2(0, 2, 2), +// SW2(-1, 2, 2), +// W2(-2, 0, 2), +// NW2(-1, -2, 2), +// NNE(2, -2, 2), +// ENE(2, -1, 2), +// ESE(2, 1, 2), +// SSE(2, 2, 2), +// SSW(-2, 2, 2), +// WSW(-2, 1, 2), +// WNW(-2, -1, 2), +// NNW(-2, -2, 2), +// +// // Outermost ring (distance 3) - 24 positions +// N3(0, -3, 3), +// E3(3, 0, 3), +// S3(0, 3, 3), +// W3(-3, 0, 3), +// CORNER_NE(3, -3, 3), +// CORNER_SE(3, 3, 3), +// CORNER_SW(-3, 3, 3), +// CORNER_NW(-3, -3, 3), +// N3E(1, -3, 3), +// N3EE(2, -3, 3), +// N3W(-1, -3, 3), +// N3WW(-2, -3, 3), +// E3N(3, -1, 3), +// E3NN(3, -2, 3), +// E3S(3, 1, 3), +// E3SS(3, 2, 3), +// S3E(1, 3, 3), +// S3EE(2, 3, 3), +// S3W(-1, 3, 3), +// S3WW(-2, 3, 3), +// W3N(-3, -1, 3), +// W3NN(-3, -2, 3), +// W3S(-3, 1, 3), +// W3SS(-3, 2, 3); + + public static class ShellOffset { + + private static final HashMap ringCache = new HashMap<>(); + + private final BlockPos offset; + + public ShellOffset(Vec3i offset){ + this(offset.getX(), offset.getZ()); + } + public ShellOffset(int x, int z) { + this.offset = new BlockPos(x, 0, z); + } + + public static int getShellLevel(int x, int z){ + return Math.max(Math.abs(x), Math.abs(z)); + } + + public BlockPos getOffset() { + return offset; + } + + public int getShellLevel() { + return getShellLevel(offset.getX(), offset.getZ()); + } + + public ShellOffset getOpposite() { + return new ShellOffset(-offset.getX(), -offset.getZ()); + } + + public static ShellOffset[] levelValues(int level) { + if (level < 1) return new ShellOffset[0]; + if (ringCache.containsKey(level)){ + return ringCache.get(level); + } else { + //There are 8N blocks on each ring (Chebyshev distance) + ShellOffset[] array = new ShellOffset[8 * level]; + int i = 0; + for (int x = -level; x <= level; x++) { + for (int z = -level; z <= level; z++) { + if (getShellLevel(x, z) == level) { + array[i++] = new ShellOffset(x, z); + } + } + } + ringCache.put(level, array); + return array; + } + } + } + public static boolean isSurroundedByLoadedChunks(Level level, BlockPos pos) { for (Surround surr : CoordUtils.Surround.values()) { Vec3i dir = surr.getOffset();