diff --git a/src/main/java/dev/dubhe/anvilcraft/block/AccelerationRingBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/AccelerationRingBlock.java index 1e5f1f7daf..e5f2e8a17b 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/AccelerationRingBlock.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/AccelerationRingBlock.java @@ -215,15 +215,11 @@ protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, if (context.isHoldingItem(state.getBlock().asItem()) || context.isHoldingItem(ModBlocks.DEFLECTION_RING.asItem())) { return Shapes.block(); } - return getPreciseShape(state); + return super.getShape(state, level, pos, context); } @Override - protected VoxelShape getCollisionShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { - return getPreciseShape(state); - } - - private static VoxelShape getPreciseShape(BlockState state) { + public VoxelShape getPartShape(BlockState state) { Direction.Axis axis = state.getValue(FACING).getAxis(); if (isChannelPart(state.getValue(HALF), axis)) return Shapes.empty(); return COLLISION_SHAPES.get(axis).get(state.getValue(HALF)); diff --git a/src/main/java/dev/dubhe/anvilcraft/block/DeflectionRingBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/DeflectionRingBlock.java index c84af1e40a..77a7021407 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/DeflectionRingBlock.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/DeflectionRingBlock.java @@ -215,15 +215,11 @@ protected VoxelShape getShape( if (context.isHoldingItem(state.getBlock().asItem()) || context.isHoldingItem(ModBlocks.ACCELERATION_RING.asItem())) { return Shapes.block(); } - return getPreciseShape(state); + return super.getShape(state, level, pos, context); } @Override - protected VoxelShape getCollisionShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { - return getPreciseShape(state); - } - - private static VoxelShape getPreciseShape(BlockState state) { + public VoxelShape getPartShape(BlockState state) { return COLLISION_SHAPES.get(state.getValue(FACING).getAxis()).get(state.getValue(HALF)); } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/GiantAnvilBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/GiantAnvilBlock.java index c0d4945446..9e6a2b7310 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/GiantAnvilBlock.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/GiantAnvilBlock.java @@ -54,7 +54,6 @@ import net.minecraft.world.level.storage.loot.LootParams; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.BlockHitResult; -import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; import net.neoforged.neoforge.common.NeoForge; @@ -198,12 +197,7 @@ public GiantAnvilBlock(Properties properties) { } @Override - public VoxelShape getShape( - BlockState state, - BlockGetter level, - BlockPos pos, - CollisionContext context - ) { + public VoxelShape getPartShape(BlockState state) { return switch (state.getValue(HALF)) { case MID_E -> MID_E; case MID_W -> MID_W; diff --git a/src/main/java/dev/dubhe/anvilcraft/block/LargeCakeBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/LargeCakeBlock.java index c507f03894..330073283b 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/LargeCakeBlock.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/LargeCakeBlock.java @@ -139,12 +139,17 @@ public LargeCakeBlock(Properties properties) { this.registerDefaultState(this.stateDefinition.any().setValue(HALF, Cube3x3PartHalf.BOTTOM_CENTER)); } + /** + 大蛋糕可以逐格吃掉而不是整体破坏,因此每一格只显示自己那一格的轮廓, + 而不是像其它多方块那样显示整个结构的轮廓。 + */ @Override - public VoxelShape getShape( - BlockState state, - BlockGetter level, - BlockPos pos, - CollisionContext context) { + protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return this.getPartShape(state); + } + + @Override + public VoxelShape getPartShape(BlockState state) { return switch (state.getValue(HALF)) { case TOP_CENTER -> TOP_CENTER; case TOP_E -> TOP_E; diff --git a/src/main/java/dev/dubhe/anvilcraft/block/LargeCauldronBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/LargeCauldronBlock.java index 43190fa4cb..38e9033166 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/LargeCauldronBlock.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/LargeCauldronBlock.java @@ -151,11 +151,15 @@ public BlockEntityTicker getTicker( @Override public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { - Cube3x3PartHalf part = state.getValue(HALF); - if (part.getOffsetY() == 2 && context.isHoldingItem(ModBlocks.GIANT_ANVIL.asItem())) { + if (state.getValue(HALF).getOffsetY() == 2 && context.isHoldingItem(ModBlocks.GIANT_ANVIL.asItem())) { return Shapes.block(); } - return SHAPES.get(part); + return super.getShape(state, level, pos, context); + } + + @Override + public VoxelShape getPartShape(BlockState state) { + return SHAPES.get(state.getValue(HALF)); } @Override diff --git a/src/main/java/dev/dubhe/anvilcraft/block/LargeLaserBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/LargeLaserBlock.java index 5b2bd6e47e..734fd9c9f8 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/LargeLaserBlock.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/LargeLaserBlock.java @@ -30,7 +30,6 @@ import net.minecraft.world.level.block.state.properties.EnumProperty; import net.minecraft.world.level.block.state.properties.Property; import net.minecraft.world.phys.AABB; -import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; import org.jetbrains.annotations.Nullable; @@ -223,15 +222,10 @@ protected boolean propagatesSkylightDown(BlockState state, BlockGetter level, Bl } @Override - protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + public VoxelShape getPartShape(BlockState state) { return COLLISION_SHAPES.get(state.getValue(FACING)).get(state.getValue(HALF)); } - @Override - protected VoxelShape getCollisionShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { - return this.getShape(state, level, pos, context); - } - @Override public boolean change(Player player, BlockPos blockPos, Level level, ItemStack anvilHammer) { this.change(blockPos, level, (state) -> state.cycle(FACING)); diff --git a/src/main/java/dev/dubhe/anvilcraft/block/OverseerBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/OverseerBlock.java index cef74c75ad..cb3d08c826 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/OverseerBlock.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/OverseerBlock.java @@ -7,7 +7,6 @@ import dev.dubhe.anvilcraft.block.state.Vertical3PartHalf; import net.minecraft.core.BlockPos; import net.minecraft.world.item.context.BlockPlaceContext; -import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.EntityBlock; @@ -21,7 +20,6 @@ import net.minecraft.world.level.block.state.properties.IntegerProperty; import net.minecraft.world.level.block.state.properties.Property; import net.minecraft.world.level.pathfinder.PathComputationType; -import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; import org.jetbrains.annotations.Nullable; @@ -30,9 +28,9 @@ public class OverseerBlock extends SimpleMultiPartBlock implements IHammerRemovable, IHasMultiBlock, EntityBlock { private static final VoxelShape OVERSEER_BOTTOM = - Shapes.or(Block.box(0, 0, 0, 16, 4, 16), Block.box(2, 8, 2, 14, 48, 14)); - private static final VoxelShape OVERSEER_MID = OVERSEER_BOTTOM.move(0, -1, 0); - private static final VoxelShape OVERSEER_TOP = OVERSEER_BOTTOM.move(0, -2, 0); + Shapes.or(Block.box(0, 0, 0, 16, 4, 16), Block.box(2, 8, 2, 14, 16, 14)); + private static final VoxelShape OVERSEER_MID = Block.box(2, 0, 2, 14, 16, 14); + private static final VoxelShape OVERSEER_TOP = Block.box(2, 0, 2, 14, 16, 14); public static final int MAX_LEVEL = 4; public static final EnumProperty HALF = EnumProperty.create("half", Vertical3PartHalf.class); public static final IntegerProperty LEVEL = IntegerProperty.create("level", 0, MAX_LEVEL); @@ -77,12 +75,7 @@ public RenderShape getRenderShape(BlockState state) { } @Override - public VoxelShape getShape( - BlockState state, - BlockGetter level, - BlockPos pos, - CollisionContext context - ) { + public VoxelShape getPartShape(BlockState state) { return switch (state.getValue(HALF)) { case TOP -> OVERSEER_TOP; case MID -> OVERSEER_MID; diff --git a/src/main/java/dev/dubhe/anvilcraft/block/RedstoneWireNetworkManager.java b/src/main/java/dev/dubhe/anvilcraft/block/RedstoneWireNetworkManager.java index dc47154ffc..b5f5e02f16 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/RedstoneWireNetworkManager.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/RedstoneWireNetworkManager.java @@ -62,7 +62,10 @@ private RedstoneWireNetworkManager() { public static void topologyChanged(Level level, BlockPos pos) { if (level instanceof ServerLevel serverLevel) { // 客户端只消费同步后的方块状态,拓扑的权威计算必须集中在服务端。 - state(serverLevel).requestTopologyUpdate(pos.asLong()); + LevelNetworks state = state(serverLevel); + // 导线放置会在邻居更新前同步建网,先记录已有侦测器才能避免其错过首次索引。 + state.rememberAdjacentObservers(pos); + state.requestTopologyUpdate(pos.asLong()); } } @@ -594,6 +597,16 @@ private void rememberObserver(BlockPos observerPos) { this.observerChunks.add(ChunkPos.asLong(observerPos.getX() >> 4, observerPos.getZ() >> 4)); } + /** 记录导线六个相邻位置中的侦测器,供当前拓扑重建时建立观察索引。 */ + private void rememberAdjacentObservers(BlockPos wirePos) { + for (Direction direction : Direction.values()) { + BlockPos observerPos = wirePos.relative(direction); + if (this.level.hasChunkAt(observerPos) && this.level.getBlockState(observerPos).is(Blocks.OBSERVER)) { + this.rememberObserver(observerPos); + } + } + } + /** 更新一个可能的侦测器位置与当前网络的对应关系。 */ private void refreshObserver(Network network, BlockPos observerPos, BlockState observerState) { long packedObserverPos = observerPos.asLong(); diff --git a/src/main/java/dev/dubhe/anvilcraft/block/RemoteTransmissionPoleBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/RemoteTransmissionPoleBlock.java index e563fc0f61..0021b3584d 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/RemoteTransmissionPoleBlock.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/RemoteTransmissionPoleBlock.java @@ -10,7 +10,6 @@ import net.minecraft.core.BlockPos; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.context.BlockPlaceContext; -import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.EntityBlock; @@ -24,7 +23,6 @@ import net.minecraft.world.level.block.state.properties.EnumProperty; import net.minecraft.world.level.block.state.properties.Property; import net.minecraft.world.level.pathfinder.PathComputationType; -import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; import org.jetbrains.annotations.Nullable; @@ -86,11 +84,7 @@ public RenderShape getRenderShape(BlockState state) { } @Override - public VoxelShape getShape( - BlockState state, - BlockGetter level, - BlockPos pos, - CollisionContext context) { + public VoxelShape getPartShape(BlockState state) { return switch (state.getValue(HALF)) { case BOTTOM -> TRANSMISSION_POLE_BASE; case MID_UPPER, MID_LOWER -> TRANSMISSION_POLE_MID; @@ -171,10 +165,10 @@ protected void onPlace(BlockState state, Level level, BlockPos pos, BlockState o } @Override - public void onRemove(Level level, BlockPos pos, BlockState state) { + public void onPlace(Level level, BlockPos pos, BlockState state) { } @Override - public void onPlace(Level level, BlockPos pos, BlockState state) { + public void onRemove(Level level, BlockPos pos, BlockState state) { } } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/TeslaTowerBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/TeslaTowerBlock.java index 97afb609a2..f96f1255ac 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/TeslaTowerBlock.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/TeslaTowerBlock.java @@ -21,7 +21,6 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.context.BlockPlaceContext; -import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.RenderShape; @@ -36,7 +35,6 @@ import net.minecraft.world.level.pathfinder.PathComputationType; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.shapes.BooleanOp; -import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; import net.neoforged.neoforge.network.PacketDistributor; @@ -96,11 +94,7 @@ public RenderShape getRenderShape(BlockState state) { } @Override - public VoxelShape getShape( - BlockState state, - BlockGetter level, - BlockPos pos, - CollisionContext context) { + public VoxelShape getPartShape(BlockState state) { return switch (state.getValue(HALF)) { case BOTTOM -> BOTTOM_SHAPE; case MID_LOWER -> LOWER_SHAPE; diff --git a/src/main/java/dev/dubhe/anvilcraft/block/TradingStationBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/TradingStationBlock.java index 6d62cf525c..4cec496593 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/TradingStationBlock.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/TradingStationBlock.java @@ -187,8 +187,17 @@ public Collection getBottomStates() { // Shapes + /** + 交易站的底部方块本身就带有上方那一格的轮廓,上方方块则不显示轮廓, + 因此保持原样,不显示由基类拼出的整体轮廓。 + */ @Override protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return this.getPartShape(state); + } + + @Override + public VoxelShape getPartShape(BlockState state) { if (state.getValue(HALF) == DirectionVertical2PartHalf.TOP) return Shapes.empty(); return switch (state.getValue(FACING)) { case NORTH -> NORTH; diff --git a/src/main/java/dev/dubhe/anvilcraft/block/TransmissionPoleBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/TransmissionPoleBlock.java index 16808ec2d0..b7b92c6f43 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/TransmissionPoleBlock.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/TransmissionPoleBlock.java @@ -9,7 +9,6 @@ import dev.dubhe.anvilcraft.init.block.ModBlocks; import net.minecraft.core.BlockPos; import net.minecraft.world.item.context.BlockPlaceContext; -import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.EntityBlock; @@ -23,7 +22,6 @@ import net.minecraft.world.level.block.state.properties.EnumProperty; import net.minecraft.world.level.block.state.properties.Property; import net.minecraft.world.level.pathfinder.PathComputationType; -import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; import org.jetbrains.annotations.Nullable; @@ -84,16 +82,11 @@ public RenderShape getRenderShape(BlockState state) { } @Override - public VoxelShape getShape( - BlockState state, - BlockGetter level, - BlockPos pos, - CollisionContext context - ) { + public VoxelShape getPartShape(BlockState state) { if (state.getValue(HALF) == Vertical3PartHalf.BOTTOM) return TRANSMISSION_POLE_BASE; if (state.getValue(HALF) == Vertical3PartHalf.MID) return TRANSMISSION_POLE_MID; if (state.getValue(HALF) == Vertical3PartHalf.TOP) return TRANSMISSION_POLE_TOP; - return super.getShape(state, level, pos, context); + return super.getPartShape(state); } @Override diff --git a/src/main/java/dev/dubhe/anvilcraft/block/cfa/CelestialForgingAnvilAmplifierBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/cfa/CelestialForgingAnvilAmplifierBlock.java index 9dae1850f4..8a509a067f 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/cfa/CelestialForgingAnvilAmplifierBlock.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/cfa/CelestialForgingAnvilAmplifierBlock.java @@ -112,7 +112,7 @@ public CelestialForgingAnvilAmplifierBlock(Properties properties) { } @Override - protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + public VoxelShape getPartShape(BlockState state) { return switch (state.getValue(FACING)) { case NORTH -> switch (state.getValue(HALF)) { case BOTTOM_WS -> NORTH_TIP; diff --git a/src/main/java/dev/dubhe/anvilcraft/block/cfa/CelestialForgingAnvilBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/cfa/CelestialForgingAnvilBlock.java index 9c0dae44ee..46a6aa6016 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/cfa/CelestialForgingAnvilBlock.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/cfa/CelestialForgingAnvilBlock.java @@ -44,7 +44,6 @@ import net.minecraft.world.level.storage.loot.parameters.LootContextParams; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.BlockHitResult; -import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; @@ -127,7 +126,7 @@ public CelestialForgingAnvilBlock(Properties properties) { } @Override - protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + public VoxelShape getPartShape(BlockState state) { return switch (state.getValue(HALF)) { case BOTTOM_CENTER -> Shapes.block(); case BOTTOM_W -> BOTTOM_W; diff --git a/src/main/java/dev/dubhe/anvilcraft/block/cfa/CelestialForgingAnvilPortalBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/cfa/CelestialForgingAnvilPortalBlock.java index 37ca70016a..ff1f6c098b 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/cfa/CelestialForgingAnvilPortalBlock.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/cfa/CelestialForgingAnvilPortalBlock.java @@ -297,17 +297,14 @@ protected boolean propagatesSkylightDown(BlockState state, BlockGetter level, Bl @Override protected VoxelShape getCollisionShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { - if (state.getValue(HALF) == DirectionGate331PartHalf.BOTTOM_CENTER || state.getValue(HALF) == DirectionGate331PartHalf.MID_CENTER) { - if (context == CollisionContext.empty()) { - return this.getShape(state, level, pos, context); - } - return Shapes.empty(); - } - return this.getShape(state, level, pos, context); + DirectionGate331PartHalf part = state.getValue(HALF); + boolean gateway = part == DirectionGate331PartHalf.BOTTOM_CENTER || part == DirectionGate331PartHalf.MID_CENTER; + if (gateway && context != CollisionContext.empty()) return Shapes.empty(); + return super.getCollisionShape(state, level, pos, context); } @Override - protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + public VoxelShape getPartShape(BlockState state) { return switch (state.getValue(CelestialForgingAnvilPortalBlock.HALF)) { case BOTTOM_CENTER -> switch (state.getValue(CelestialForgingAnvilPortalBlock.FACING)) { case NORTH -> BOTTOM_CENTER_NORTH; diff --git a/src/main/java/dev/dubhe/anvilcraft/block/container/storage/HyperdimensionStorageStationBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/container/storage/HyperdimensionStorageStationBlock.java index fd3b6e6c41..094db6298c 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/container/storage/HyperdimensionStorageStationBlock.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/container/storage/HyperdimensionStorageStationBlock.java @@ -49,7 +49,6 @@ 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; import net.neoforged.api.distmarker.Dist; @@ -203,7 +202,7 @@ public InteractionResult use( // region VoxelShapes @Override - protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + public VoxelShape getPartShape(BlockState state) { return switch (state.getValue(HALF)) { case BOTTOM_CENTER -> BOTTOM_CENTER; case BOTTOM_W -> BOTTOM_W; diff --git a/src/main/java/dev/dubhe/anvilcraft/block/container/storage/LargeCrateBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/container/storage/LargeCrateBlock.java index c839a54550..ce1afbecb7 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/container/storage/LargeCrateBlock.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/container/storage/LargeCrateBlock.java @@ -32,7 +32,6 @@ import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.HitResult; -import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; import net.neoforged.api.distmarker.Dist; @@ -131,7 +130,7 @@ public InteractionResult use( // region VoxelShapes @Override - protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + public VoxelShape getPartShape(BlockState state) { return switch (state.getValue(HALF)) { case BOTTOM_CENTER -> BOTTOM_CENTER; case BOTTOM_W -> BOTTOM_W; diff --git a/src/main/java/dev/dubhe/anvilcraft/block/container/storage/ShulkerContainerBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/container/storage/ShulkerContainerBlock.java index 0b5eb8b824..460d3f45a1 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/container/storage/ShulkerContainerBlock.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/container/storage/ShulkerContainerBlock.java @@ -52,7 +52,6 @@ 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; import net.neoforged.api.distmarker.Dist; @@ -318,7 +317,7 @@ protected ItemInteractionResult useItemOn( // region VoxelShapes @Override - protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + public VoxelShape getPartShape(BlockState state) { return switch (state.getValue(HALF)) { case BOTTOM_CENTER -> BOTTOM_CENTER; case BOTTOM_W -> BOTTOM_W; diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilBlockEntity.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilBlockEntity.java index c6ea8c3787..5b428c60a1 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilBlockEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilBlockEntity.java @@ -68,9 +68,9 @@ public class CelestialForgingAnvilBlockEntity extends BlockEntity implements Men private final CfaMegastructureManager megastructureManager = new CfaMegastructureManager(); @Getter - private int preRotation = 0; + private float preRotation = 0.0f; @Getter - private int rotation = 0; + private float rotation = 0.0f; @Getter private boolean isAmplify = false; @@ -487,6 +487,10 @@ public boolean isPowerInsufficient() { return this.searchController.isPowerInsufficient(); } + void clearSearchLock() { + this.locked = false; + } + /** Marks search state dirty and sends the small block-entity update used by the CFA screen. */ void markSearchStateChanged() { this.setChanged(); @@ -502,6 +506,11 @@ public void serverTick() { supernovaFlashTicks--; } this.searchController.serverTick(this); + if (this.locked && !this.isAcceleratorActive() && !this.isSearching() && this.celestialBodyData == null) { + /// 锁定状态必须绑定有效天体;兼容清理旧版本断电后遗留的锁定状态。 + this.clearSearchLock(); + this.markSearchStateChanged(); + } /// 管理恒星引力源 this.updateGravitySource(); @@ -570,9 +579,10 @@ public void setChanged() { }; public void tick() { - if (this.rotation == 360) this.rotation = 0; + if (this.rotation >= 360.0f) this.rotation -= 360.0f; this.preRotation = this.rotation; - this.rotation += 3; + float rotationSpeed = 3.0f / (1.0f + this.getRedstoneSignal() * 0.4f); + this.rotation += rotationSpeed; this.bodyRotation += 1; /// 动画计时(仅客户端) @@ -1121,6 +1131,13 @@ public void toggleLocked() { /// 星体演化期间无法解锁 return; } + if (!this.locked && !this.isSearching() && (this.isSearchFailed() + || this.celestialBodyData == null)) { + /// 搜索未产生有效结果时拒绝迟到的锁定请求,并同步清理客户端的乐观状态。 + this.setChanged(); + level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3); + return; + } this.locked = !this.locked; if (!this.locked) { /// 解锁:清除巨构和加速器,恢复为束星环 diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilPortalBlockEntity.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilPortalBlockEntity.java index 4ff87f7b40..b1b3d99ebf 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilPortalBlockEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilPortalBlockEntity.java @@ -15,7 +15,6 @@ import dev.dubhe.anvilcraft.network.LaserEmitPacket; import dev.dubhe.anvilcraft.saved.WormholeNetwork; import dev.dubhe.anvilcraft.util.BreakBlockUtil; -import dev.dubhe.anvilcraft.worldgen.OverworldLikeResetManager; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.HolderLookup; @@ -310,7 +309,7 @@ public void tick() { /// 可登陆特殊星球的传送门不依赖虫洞网络中的另一座 CFA。 CelestialTravelData landing = getLandingData(parent); if (landing != null) { - tickLandingPortal(state, landing); + tickLandingPortal(state); return; } @@ -639,16 +638,7 @@ private CelestialTravelData getLandingData(CelestialForgingAnvilBlockEntity pare } /** Opens a standalone landing gate and performs the same entity scan as a linked gate. */ - private void tickLandingPortal(BlockState state, CelestialTravelData landing) { - if (CelestialTravelManager.OVERWORLD_LIKE_DIMENSION.equals(landing.dimension()) - && level instanceof ServerLevel serverLevel - && !OverworldLikeResetManager.isEntryAllowed(serverLevel.getServer(), CelestialTravelManager.OVERWORLD_LIKE_LEVEL)) { - if (state.getValue(CelestialForgingAnvilPortalBlock.OPEN)) { - level.setBlock(worldPosition, state.setValue(CelestialForgingAnvilPortalBlock.OPEN, false), 3); - } - if (isAnchor()) touchingEntities.clear(); - return; - } + private void tickLandingPortal(BlockState state) { if (!state.getValue(CelestialForgingAnvilPortalBlock.OPEN)) { state = state.setValue(CelestialForgingAnvilPortalBlock.OPEN, true); level.setBlock(worldPosition, state, 3); diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaSearchController.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaSearchController.java index e2dd80a4a8..5ecb7447a6 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaSearchController.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaSearchController.java @@ -51,13 +51,13 @@ void start(CelestialForgingAnvilBlockEntity owner) { owner.isAmplify(), level.getRandom() ) == null) { - this.stop(true, false); + this.stopAndUnlock(owner, true, false); sync(owner); return; } if (!hasEnoughPower(owner)) { - this.stop(false, true); + this.stopAndUnlock(owner, false, true); sync(owner); return; } @@ -79,7 +79,7 @@ void serverTick(CelestialForgingAnvilBlockEntity owner) { } if (this.ticksRemaining <= 0) return; if (!enoughPower) { - this.stop(false, true); + this.stopAndUnlock(owner, false, true); sync(owner); return; } @@ -314,6 +314,15 @@ private void stop(boolean failed, boolean powerInsufficient) { this.capturedSnapshot = null; } + private void stopAndUnlock( + CelestialForgingAnvilBlockEntity owner, + boolean failed, + boolean powerInsufficient + ) { + this.stop(failed, powerInsufficient); + owner.clearSearchLock(); + } + private static void sync(CelestialForgingAnvilBlockEntity owner) { owner.markSearchStateChanged(); } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialTravelManager.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialTravelManager.java index 4e86c0594d..db0e1a0f16 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialTravelManager.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialTravelManager.java @@ -74,8 +74,7 @@ public static boolean tryLand( ) { if (entity.isOnPortalCooldown()) return false; ResourceKey destinationKey = ResourceKey.create(Registries.DIMENSION, travel.dimension()); - if (!OverworldLikeResetManager.isEntryAllowed(sourceLevel.getServer(), destinationKey)) return false; - ServerLevel destination = sourceLevel.getServer().getLevel(destinationKey); + ServerLevel destination = OverworldLikeResetManager.getEntryDestination(sourceLevel.getServer(), destinationKey); if (destination == null) return false; BlockPos desired = landingOrigin(entity, destination, travel.coordinateRule()); diff --git a/src/main/java/dev/dubhe/anvilcraft/block/multipart/AbstractMultiPartBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/multipart/AbstractMultiPartBlock.java index 468fd48d3d..fdb89bc687 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/multipart/AbstractMultiPartBlock.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/multipart/AbstractMultiPartBlock.java @@ -6,15 +6,25 @@ import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.BlockGetter; 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.LevelEvent; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.phys.shapes.BooleanOp; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; import org.jetbrains.annotations.Nullable; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + public abstract class AbstractMultiPartBlock

> extends Block implements IMultiPartBlockModelHolder { + private final Map multiPartShapeCache = new ConcurrentHashMap<>(); + public AbstractMultiPartBlock(Properties properties) { super(properties); } @@ -35,6 +45,61 @@ public BlockState placedState(P part, BlockState state) { return state.setValue(this.getPart(), part); } + /** + * 获取单个部件自身的碰撞箱,用于实体碰撞、遮挡与光照计算。 + * + * @param state 部件的方块状态 + */ + public VoxelShape getPartShape(BlockState state) { + return Shapes.block(); + } + + /** + * 获取整个多方块结构的碰撞箱,坐标已偏移至 {@code state} 所在部件的局部坐标系, + * 使玩家看向任意部件时都能看到完整的结构轮廓。 + * + * @param state 部件的方块状态 + */ + public VoxelShape getMultiPartShape(BlockState state) { + if (!state.hasProperty(this.getPart())) return this.getPartShape(state); + VoxelShape cached = this.multiPartShapeCache.get(state); + if (cached != null) return cached; + VoxelShape shape = Shapes.empty(); + for (P part : this.getParts()) { + Vec3i offset = this.offsetFrom(state, part); + VoxelShape partShape = this.getPartShape(state.setValue(this.getPart(), part)); + if (partShape.isEmpty()) continue; + shape = Shapes.joinUnoptimized( + shape, + partShape.move(offset.getX(), offset.getY(), offset.getZ()), + BooleanOp.OR + ); + } + shape = shape.optimize(); + this.multiPartShapeCache.put(state, shape); + return shape; + } + + @Override + protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return this.getMultiPartShape(state); + } + + @Override + protected VoxelShape getCollisionShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return this.hasCollision ? this.getPartShape(state) : Shapes.empty(); + } + + @Override + protected VoxelShape getOcclusionShape(BlockState state, BlockGetter level, BlockPos pos) { + return this.getPartShape(state); + } + + @Override + protected boolean propagatesSkylightDown(BlockState state, BlockGetter level, BlockPos pos) { + return !Block.isShapeFullBlock(this.getPartShape(state)) && state.getFluidState().isEmpty(); + } + @Override public void setPlacedBy( Level level, diff --git a/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/CelestialForgingAnvilBlockEntityRenderer.java b/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/CelestialForgingAnvilBlockEntityRenderer.java index 9212a91639..7040a46843 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/CelestialForgingAnvilBlockEntityRenderer.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/CelestialForgingAnvilBlockEntityRenderer.java @@ -131,6 +131,7 @@ public class CelestialForgingAnvilBlockEntityRenderer implements BlockEntityRend "block/celestial_body/black_hole" )); private static final float PLAYER_HEAD_SCALE = 16.0f; + private static final float PLAYER_HEAD_HALF_HEIGHT = 0.25f; private final BlockRenderDispatcher blockRenderer; private final BlockState whiteConcrete = Blocks.WHITE_CONCRETE.defaultBlockState(); @@ -152,6 +153,18 @@ private static float bodyScale(CelestialBodyData data) { return data.bodyScale(); } + private static float playerHeadCenterY( + SpecialCelestialBodyData playerHead, + boolean isAmplify, + float bodyScaleMultiplier, + float animationProgress + ) { + float initialHeadScale = bodyScale(playerHead) * PLAYER_HEAD_SCALE; + float currentHeadScale = bodyScaleMultiplier * PLAYER_HEAD_SCALE; + float baseCenterY = isAmplify ? 6.5f : 4.5f; + return baseCenterY + (currentHeadScale - initialHeadScale) * animationProgress * PLAYER_HEAD_HALF_HEIGHT; + } + @Override public void render( CelestialForgingAnvilBlockEntity blockEntity, @@ -466,9 +479,13 @@ && isBrownDwarfDysonSphereActive(blockEntity) if (canRender) { renderTractorBeam(beamHeight, animProgress, poseStack, multiBufferSource); /// 天体缩放完全由 StarData.size() 驱动,坍缩期间通过 applyCollapseColor() 缩小 —— 无需额外的 pose 缩放。 + float bodyCenterY = centerY; + if (effectiveBodyData instanceof SpecialCelestialBodyData special && special.isPlayerHead()) { + bodyCenterY = playerHeadCenterY(special, isAmplify, bodyScaleMultiplier, animProgress); + } renderCelestialBody( effectiveBodyData, - centerY, + bodyCenterY, bodyRotation, poseStack, multiBufferSource, @@ -1633,9 +1650,11 @@ public AABB getRenderBoundingBox(CelestialForgingAnvilBlockEntity blockEntity) { BlockState state = blockEntity.getBlockState(); CelestialBodyData body = blockEntity.getCelestialBodyData(); float centerY = dynamicCenterY(body, blockEntity.isAmplify()); - float bs = body != null ? bodyScale(body) * CelestialBodyData.BODY_SCALE_FACTOR : 6.0f; - if (isPlayerHead(body)) { + float fullBodyScale = body != null ? bodyScale(body) * CelestialBodyData.BODY_SCALE_FACTOR : 6.0f; + float bs = fullBodyScale; + if (body instanceof SpecialCelestialBodyData special && special.isPlayerHead()) { bs *= PLAYER_HEAD_SCALE; + centerY = playerHeadCenterY(special, blockEntity.isAmplify(), fullBodyScale, 1.0f); } // The mechanical rings can extend beyond the body, especially for small planets. // Size the box from the larger of the body and the complete ring system. diff --git a/src/main/java/dev/dubhe/anvilcraft/init/block/ModBlocks.java b/src/main/java/dev/dubhe/anvilcraft/init/block/ModBlocks.java index f922dc6849..ee459addc6 100644 --- a/src/main/java/dev/dubhe/anvilcraft/init/block/ModBlocks.java +++ b/src/main/java/dev/dubhe/anvilcraft/init/block/ModBlocks.java @@ -1344,6 +1344,7 @@ public class ModBlocks { .block("large_laser", LargeLaserBlock::new) .initialProperties(RUBY_LASER::get) .properties(properties -> properties + .forceSolidOn() .isSuffocating(ModBlocks::never) .noOcclusion() .isValidSpawn(Blocks::never) diff --git a/src/main/java/dev/dubhe/anvilcraft/mixin/MinecraftServerMixin.java b/src/main/java/dev/dubhe/anvilcraft/mixin/MinecraftServerMixin.java index c392b8ea3d..b825a52c06 100644 --- a/src/main/java/dev/dubhe/anvilcraft/mixin/MinecraftServerMixin.java +++ b/src/main/java/dev/dubhe/anvilcraft/mixin/MinecraftServerMixin.java @@ -25,6 +25,11 @@ public class MinecraftServerMixin { ); } + @Inject(method = "createLevels", at = @At("RETURN")) + private void anvilcraft$configureOverworldLikeBorder(ChunkProgressListener listener, CallbackInfo ci) { + OverworldLikeGenerationBootstrap.configureOverworldLikeBorder((MinecraftServer) (Object) this); + } + @ModifyArgs( method = "createLevels", at = @At( diff --git a/src/main/java/dev/dubhe/anvilcraft/mixin/ParticleEngineMixin.java b/src/main/java/dev/dubhe/anvilcraft/mixin/ParticleEngineMixin.java index 9dc62f8221..c658f94a9b 100644 --- a/src/main/java/dev/dubhe/anvilcraft/mixin/ParticleEngineMixin.java +++ b/src/main/java/dev/dubhe/anvilcraft/mixin/ParticleEngineMixin.java @@ -1,11 +1,19 @@ package dev.dubhe.anvilcraft.mixin; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import dev.dubhe.anvilcraft.block.multipart.AbstractMultiPartBlock; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.particle.ParticleEngine; import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @@ -23,8 +31,41 @@ abstract class ParticleEngineMixin { */ @Inject(method = "addBlockHitEffects", at = @At("HEAD"), cancellable = true) private void cancelHitEffectForEmptyBlock(BlockPos pos, BlockHitResult target, CallbackInfo ci) { - if (this.level.getBlockState(pos).getShape(this.level, pos).isEmpty()) { + BlockState state = this.level.getBlockState(pos); + VoxelShape partShape = anvilcraft$partShape(state); + if ((partShape == null ? state.getShape(this.level, pos) : partShape).isEmpty()) { ci.cancel(); } } + + /** + 多方块的轮廓形状是整个结构的并集,若直接用于生成粒子,结构的每一格都会按整个结构的体积生成粒子, + 3x3x3 的结构就是 27 倍粒子量,足以造成卡顿。粒子只应使用当前部件自身的形状。 + */ + @WrapOperation( + method = {"destroy", "crack"}, + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/state/BlockState;" + + "getShape(Lnet/minecraft/world/level/BlockGetter;Lnet/minecraft/core/BlockPos;)" + + "Lnet/minecraft/world/phys/shapes/VoxelShape;" + ) + ) + private VoxelShape useSinglePartShapeForParticles( + BlockState state, + BlockGetter getter, + BlockPos pos, + Operation original + ) { + VoxelShape partShape = anvilcraft$partShape(state); + return partShape == null ? original.call(state, getter, pos) : partShape; + } + + @Unique + private static @Nullable VoxelShape anvilcraft$partShape(BlockState state) { + if (state.getBlock() instanceof AbstractMultiPartBlock multiPartBlock) { + return multiPartBlock.getPartShape(state); + } + return null; + } } diff --git a/src/main/java/dev/dubhe/anvilcraft/mixin/accessor/DelegateBorderChangeListenerAccessor.java b/src/main/java/dev/dubhe/anvilcraft/mixin/accessor/DelegateBorderChangeListenerAccessor.java new file mode 100644 index 0000000000..0e04f2a77b --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/mixin/accessor/DelegateBorderChangeListenerAccessor.java @@ -0,0 +1,12 @@ +package dev.dubhe.anvilcraft.mixin.accessor; + +import net.minecraft.world.level.border.BorderChangeListener; +import net.minecraft.world.level.border.WorldBorder; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(BorderChangeListener.DelegateBorderChangeListener.class) +public interface DelegateBorderChangeListenerAccessor { + @Accessor("worldBorder") + WorldBorder getWorldBorder(); +} diff --git a/src/main/java/dev/dubhe/anvilcraft/mixin/accessor/WorldBorderAccessor.java b/src/main/java/dev/dubhe/anvilcraft/mixin/accessor/WorldBorderAccessor.java new file mode 100644 index 0000000000..98d2fe6101 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/mixin/accessor/WorldBorderAccessor.java @@ -0,0 +1,14 @@ +package dev.dubhe.anvilcraft.mixin.accessor; + +import net.minecraft.world.level.border.BorderChangeListener; +import net.minecraft.world.level.border.WorldBorder; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +import java.util.List; + +@Mixin(WorldBorder.class) +public interface WorldBorderAccessor { + @Invoker("getListeners") + List invokeGetListeners(); +} diff --git a/src/main/java/dev/dubhe/anvilcraft/saved/OverworldLikeWorldState.java b/src/main/java/dev/dubhe/anvilcraft/saved/OverworldLikeWorldState.java index 21e11d3188..cacfe68236 100644 --- a/src/main/java/dev/dubhe/anvilcraft/saved/OverworldLikeWorldState.java +++ b/src/main/java/dev/dubhe/anvilcraft/saved/OverworldLikeWorldState.java @@ -28,6 +28,7 @@ public class OverworldLikeWorldState extends SavedData { private long collapseStartedAt = -1L; private long nextGenerationSeed; private boolean collapseDamageIssued; + private boolean generationRequestedByEntry; private final Set pendingForcedRespawns = new HashSet<>(); private final Set knownOverworldLikePlayers = new HashSet<>(); private long eclipseCalculatedAt = Long.MIN_VALUE; @@ -54,6 +55,7 @@ private static OverworldLikeWorldState load(CompoundTag tag, HolderLookup.Provid state.collapseStartedAt = tag.contains("collapseStartedAt") ? tag.getLong("collapseStartedAt") : -1L; state.nextGenerationSeed = tag.getLong("nextGenerationSeed"); state.collapseDamageIssued = tag.getBoolean("collapseDamageIssued"); + state.generationRequestedByEntry = tag.getBoolean("generationRequestedByEntry"); ListTag pending = tag.getList("pendingForcedRespawns", Tag.TAG_COMPOUND); for (int index = 0; index < pending.size(); index++) { CompoundTag player = pending.getCompound(index); @@ -81,6 +83,7 @@ private void synchronizeGeneration(OverworldLikeResetManifest manifest, long gam collapseStartedAt = -1L; nextGenerationSeed = manifest.nextSeed(); collapseDamageIssued = false; + generationRequestedByEntry = false; knownOverworldLikePlayers.clear(); eclipseCalculatedAt = Long.MIN_VALUE; setDirty(); @@ -145,6 +148,7 @@ public boolean beginCollapse(long gameTime, long nextSeed) { collapseStartedAt = gameTime; nextGenerationSeed = nextSeed; collapseDamageIssued = false; + generationRequestedByEntry = false; setDirty(); return true; } @@ -161,6 +165,19 @@ public void markResetPending() { setDirty(); } + public boolean requestGenerationByEntry() { + if (phase != Phase.RESET_PENDING) return false; + if (!generationRequestedByEntry) { + generationRequestedByEntry = true; + setDirty(); + } + return true; + } + + public boolean isGenerationRequestedByEntry() { + return generationRequestedByEntry; + } + public void addPendingForcedRespawn(UUID playerId) { if (pendingForcedRespawns.add(playerId)) setDirty(); } @@ -195,6 +212,7 @@ public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) { tag.putLong("collapseStartedAt", collapseStartedAt); tag.putLong("nextGenerationSeed", nextGenerationSeed); tag.putBoolean("collapseDamageIssued", collapseDamageIssued); + tag.putBoolean("generationRequestedByEntry", generationRequestedByEntry); ListTag pending = new ListTag(); for (UUID playerId : pendingForcedRespawns) { CompoundTag player = new CompoundTag(); diff --git a/src/main/java/dev/dubhe/anvilcraft/worldgen/OverworldLikeGenerationBootstrap.java b/src/main/java/dev/dubhe/anvilcraft/worldgen/OverworldLikeGenerationBootstrap.java index ad6993c2c7..a5fecc63a0 100644 --- a/src/main/java/dev/dubhe/anvilcraft/worldgen/OverworldLikeGenerationBootstrap.java +++ b/src/main/java/dev/dubhe/anvilcraft/worldgen/OverworldLikeGenerationBootstrap.java @@ -2,9 +2,29 @@ import dev.dubhe.anvilcraft.AnvilCraft; import dev.dubhe.anvilcraft.block.entity.celestial.CelestialTravelManager; +import dev.dubhe.anvilcraft.mixin.accessor.DelegateBorderChangeListenerAccessor; +import dev.dubhe.anvilcraft.mixin.accessor.MinecraftServerAccessor; +import dev.dubhe.anvilcraft.mixin.accessor.WorldBorderAccessor; import dev.dubhe.anvilcraft.saved.OverworldLikeResetManifest; +import net.minecraft.Util; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.progress.ChunkProgressListener; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.border.BorderChangeListener; +import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.storage.DerivedLevelData; +import net.minecraft.world.level.storage.LevelResource; import net.minecraft.world.level.storage.LevelStorageSource; +import net.minecraft.world.level.storage.ServerLevelData; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.level.LevelEvent; import java.io.IOException; import java.nio.file.FileVisitResult; @@ -12,12 +32,31 @@ import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; +import java.util.List; import java.util.Map; import java.util.WeakHashMap; +import javax.annotation.Nullable; -/** Handles the only safe point for replacing an overworld-like generation. */ +/** Prepares and replaces overworld-like generations. */ public final class OverworldLikeGenerationBootstrap { private static final Map PREPARED_GENERATIONS = new WeakHashMap<>(); + private static final ChunkProgressListener SILENT_PROGRESS_LISTENER = new ChunkProgressListener() { + @Override + public void updateSpawnPos(ChunkPos center) { + } + + @Override + public void onStatusChange(ChunkPos chunkPos, @Nullable ChunkStatus chunkStatus) { + } + + @Override + public void start() { + } + + @Override + public void stop() { + } + }; private OverworldLikeGenerationBootstrap() { } @@ -65,7 +104,7 @@ public static boolean requestReset(MinecraftServer server, long nextSeed) { if (prepared == null || prepared.manifest().resetPending()) return false; OverworldLikeResetManifest updated = prepared.manifest().resetRequested(nextSeed); try { - updated.write(server.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT)); + updated.write(server.getWorldPath(LevelResource.ROOT)); PREPARED_GENERATIONS.put(server, new PreparedGeneration(updated, prepared.resetPerformed())); return true; } catch (IOException exception) { @@ -74,10 +113,95 @@ public static boolean requestReset(MinecraftServer server, long nextSeed) { } } + /** Replaces the collapsed level after an entity asks to enter its next generation. */ + public static boolean activatePendingGeneration(MinecraftServer server) { + PreparedGeneration prepared = PREPARED_GENERATIONS.get(server); + if (prepared == null || !prepared.manifest().resetPending()) return false; + ServerLevel oldLevel = server.getLevel(CelestialTravelManager.OVERWORLD_LIKE_LEVEL); + if (oldLevel == null || !oldLevel.players().isEmpty()) return false; + + LevelStorageSource.LevelStorageAccess storageAccess = ((MinecraftServerAccessor) server).getStorageSource(); + Path worldRoot = server.getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize(); + OverworldLikeResetManifest nextManifest = prepared.manifest().promoteNextGeneration(); + try { + unlinkOverworldLikeBorder(server, oldLevel); + NeoForge.EVENT_BUS.post(new LevelEvent.Unload(oldLevel)); + oldLevel.close(); + deleteDimensionStorage( + worldRoot, + storageAccess.getDimensionPath(CelestialTravelManager.OVERWORLD_LIKE_LEVEL) + ); + nextManifest.write(worldRoot); + PREPARED_GENERATIONS.put(server, new PreparedGeneration(nextManifest, true)); + + ServerLevel replacement = createOverworldLikeLevel(server, storageAccess); + server.forgeGetWorldMap().put(CelestialTravelManager.OVERWORLD_LIKE_LEVEL, replacement); + server.markWorldsDirty(); + configureOverworldLikeBorder(server, replacement); + NeoForge.EVENT_BUS.post(new LevelEvent.Load(replacement)); + return true; + } catch (IOException exception) { + throw new IllegalStateException("Unable to activate the next overworld-like generation", exception); + } + } + public static void clear(MinecraftServer server) { PREPARED_GENERATIONS.remove(server); } + public static void configureOverworldLikeBorder(MinecraftServer server) { + ServerLevel level = server.getLevel(CelestialTravelManager.OVERWORLD_LIKE_LEVEL); + if (level != null) configureOverworldLikeBorder(server, level); + } + + private static ServerLevel createOverworldLikeLevel( + MinecraftServer server, LevelStorageSource.LevelStorageAccess storageAccess + ) { + Registry stems = server.registryAccess().registryOrThrow(Registries.LEVEL_STEM); + ResourceKey stemKey = ResourceKey.create( + Registries.LEVEL_STEM, + CelestialTravelManager.OVERWORLD_LIKE_DIMENSION + ); + LevelStem stem = stems.get(stemKey); + if (stem == null) { + throw new IllegalStateException("Missing overworld-like level stem"); + } + ServerLevelData levelData = new DerivedLevelData(server.getWorldData(), server.getWorldData().overworldData()); + ServerLevel replacement = new ServerLevel( + server, + Util.backgroundExecutor(), + storageAccess, + levelData, + CelestialTravelManager.OVERWORLD_LIKE_LEVEL, + stem, + SILENT_PROGRESS_LISTENER, + server.getWorldData().isDebugWorld(), + BiomeManager.obfuscateSeed(getActiveSeed(server)), + List.of(), + false, + server.overworld().getRandomSequences() + ); + return replacement; + } + + @SuppressWarnings("checkstyle:OverloadMethodsDeclarationOrder") + private static void configureOverworldLikeBorder(MinecraftServer server, ServerLevel level) { + WorldBorder overworldBorder = server.overworld().getWorldBorder(); + unlinkOverworldLikeBorder(server, level); + overworldBorder.addListener(new BorderChangeListener.DelegateBorderChangeListener(level.getWorldBorder())); + level.getWorldBorder().applySettings(overworldBorder.createSettings()); + } + + private static void unlinkOverworldLikeBorder(MinecraftServer server, ServerLevel level) { + WorldBorder overworldBorder = server.overworld().getWorldBorder(); + for (BorderChangeListener listener : ((WorldBorderAccessor) overworldBorder).invokeGetListeners()) { + if (listener instanceof DelegateBorderChangeListenerAccessor delegate + && delegate.getWorldBorder() == level.getWorldBorder()) { + overworldBorder.removeListener(listener); + } + } + } + private static void deleteDimensionStorage(Path worldRoot, Path dimensionPath) throws IOException { Path normalizedDimension = dimensionPath.toAbsolutePath().normalize(); if (normalizedDimension.equals(worldRoot) || !normalizedDimension.startsWith(worldRoot)) { diff --git a/src/main/java/dev/dubhe/anvilcraft/worldgen/OverworldLikeResetManager.java b/src/main/java/dev/dubhe/anvilcraft/worldgen/OverworldLikeResetManager.java index b317704ac4..8dca71779b 100644 --- a/src/main/java/dev/dubhe/anvilcraft/worldgen/OverworldLikeResetManager.java +++ b/src/main/java/dev/dubhe/anvilcraft/worldgen/OverworldLikeResetManager.java @@ -16,9 +16,17 @@ import net.minecraft.world.level.Level; import net.neoforged.neoforge.network.PacketDistributor; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.WeakHashMap; +import javax.annotation.Nullable; + /** Coordinates collapse, player evacuation, reset locking, and orbital-lighting access. */ public final class OverworldLikeResetManager { public static final int COLLAPSE_DELAY_TICKS = 40; + private static final Map> QUEUED_DIRECT_ENTRIES = new WeakHashMap<>(); private OverworldLikeResetManager() { } @@ -56,11 +64,19 @@ public static boolean beginCollapse(CelestialForgingAnvilBlockEntity source) { return true; } - public static boolean isEntryAllowed(MinecraftServer server, ResourceKey destination) { - if (!CelestialTravelManager.isOverworldLike(destination)) return true; + @Nullable + public static ServerLevel getEntryDestination(MinecraftServer server, ResourceKey destination) { + if (!CelestialTravelManager.isOverworldLike(destination)) return server.getLevel(destination); OverworldLikeWorldState state = OverworldLikeWorldState.get(server); - return state.phase() == OverworldLikeWorldState.Phase.ACTIVE - && !OverworldLikeGenerationBootstrap.getManifest(server).resetPending(); + if (state.phase() == OverworldLikeWorldState.Phase.RESET_PENDING) { + state.requestGenerationByEntry(); + return null; + } + if (state.phase() != OverworldLikeWorldState.Phase.ACTIVE + || OverworldLikeGenerationBootstrap.getManifest(server).resetPending()) { + return null; + } + return server.getLevel(destination); } public static int modifySkyDarken(Level level, int original) { @@ -75,28 +91,37 @@ public static int modifySkyDarken(Level level, int original) { public static void tick(MinecraftServer server) { OverworldLikeWorldState state = OverworldLikeWorldState.get(server); - if (state.phase() != OverworldLikeWorldState.Phase.COLLAPSING) return; - ServerLevel overworldLike = server.getLevel(CelestialTravelManager.OVERWORLD_LIKE_LEVEL); - long gameTime = overworldLike == null ? server.overworld().getGameTime() : overworldLike.getGameTime(); - long elapsed = Math.max(0L, gameTime - state.collapseStartedAt()); - if (elapsed >= COLLAPSE_DELAY_TICKS && !state.collapseDamageIssued()) { - for (ServerPlayer player : server.getPlayerList().getPlayers()) { - if (!CelestialTravelManager.isOverworldLike(player.level().dimension())) continue; - state.addPendingForcedRespawn(player.getUUID()); - player.hurt(ModDamageTypes.planetaryCollapse(player.level()), Float.MAX_VALUE); + if (state.phase() == OverworldLikeWorldState.Phase.COLLAPSING) { + ServerLevel overworldLike = server.getLevel(CelestialTravelManager.OVERWORLD_LIKE_LEVEL); + long gameTime = overworldLike == null ? server.overworld().getGameTime() : overworldLike.getGameTime(); + long elapsed = Math.max(0L, gameTime - state.collapseStartedAt()); + if (elapsed >= COLLAPSE_DELAY_TICKS && !state.collapseDamageIssued()) { + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + if (!CelestialTravelManager.isOverworldLike(player.level().dimension())) continue; + state.addPendingForcedRespawn(player.getUUID()); + player.hurt(ModDamageTypes.planetaryCollapse(player.level()), Float.MAX_VALUE); + } + state.markCollapseDamageIssued(); } - state.markCollapseDamageIssued(); - } - if (state.collapseDamageIssued()) { - for (ServerPlayer player : server.getPlayerList().getPlayers()) { - if (!CelestialTravelManager.isOverworldLike(player.level().dimension())) continue; - state.addPendingForcedRespawn(player.getUUID()); - if (player.isAlive()) forceReturn(player, state); + if (state.collapseDamageIssued()) { + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + if (!CelestialTravelManager.isOverworldLike(player.level().dimension())) continue; + state.addPendingForcedRespawn(player.getUUID()); + if (player.isAlive()) forceReturn(player, state); + } + } + if (allOnlinePlayersOutsideOverworldLike(server)) { + state.markResetPending(); + syncStateToAll(server, state); } } - if (allOnlinePlayersOutsideOverworldLike(server)) { - state.markResetPending(); - syncStateToAll(server, state); + + if (state.phase() == OverworldLikeWorldState.Phase.RESET_PENDING + && state.isGenerationRequestedByEntry() + && OverworldLikeGenerationBootstrap.activatePendingGeneration(server)) { + OverworldLikeWorldState activeState = OverworldLikeWorldState.get(server); + syncStateToAll(server, activeState); + moveQueuedDirectEntries(server); } } @@ -139,9 +164,14 @@ public static void onPlayerChangedDimension(ServerPlayer player) { state.removeKnownOverworldLikePlayer(player.getUUID()); state.removePendingForcedRespawn(player.getUUID()); } else if (state.hasPendingForcedRespawn(player.getUUID()) - || state.phase() != OverworldLikeWorldState.Phase.ACTIVE) { + || state.phase() == OverworldLikeWorldState.Phase.COLLAPSING) { state.addPendingForcedRespawn(player.getUUID()); forceReturn(player, state); + } else if (state.phase() == OverworldLikeWorldState.Phase.RESET_PENDING) { + forceReturn(player, state); + queueDirectEntry(server, player.getUUID()); + state.requestGenerationByEntry(); + return; } else { state.markPlayerInOverworldLike(player.getUUID()); } @@ -172,6 +202,31 @@ private static boolean allOnlinePlayersOutsideOverworldLike(MinecraftServer serv .noneMatch(player -> CelestialTravelManager.isOverworldLike(player.level().dimension())); } + private static void queueDirectEntry(MinecraftServer server, UUID playerId) { + QUEUED_DIRECT_ENTRIES.computeIfAbsent(server, ignored -> new HashSet<>()).add(playerId); + } + + private static void moveQueuedDirectEntries(MinecraftServer server) { + Set queued = QUEUED_DIRECT_ENTRIES.remove(server); + if (queued == null || queued.isEmpty()) return; + ServerLevel destination = server.getLevel(CelestialTravelManager.OVERWORLD_LIKE_LEVEL); + if (destination == null) return; + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + if (!queued.contains(player.getUUID()) || !player.isAlive()) continue; + BlockPos fallback = destination.getSharedSpawnPos(); + BlockPos landing = CelestialTravelManager.findSafeLandingPos(destination, fallback); + if (landing == null) landing = fallback; + player.teleportTo( + destination, + landing.getX() + 0.5D, + landing.getY(), + landing.getZ() + 0.5D, + player.getYRot(), + player.getXRot() + ); + } + } + private static void syncStateToAll(MinecraftServer server, OverworldLikeWorldState state) { for (ServerPlayer player : server.getPlayerList().getPlayers()) { syncTo(player, state); diff --git a/src/main/resources/anvilcraft.mixins.json b/src/main/resources/anvilcraft.mixins.json index f0b1fc1c71..62b814fa90 100644 --- a/src/main/resources/anvilcraft.mixins.json +++ b/src/main/resources/anvilcraft.mixins.json @@ -71,6 +71,7 @@ "ZombieVillagerMixin", "accessor.BaseSpawnerAccessor", "accessor.CropBlockAccessor", + "accessor.DelegateBorderChangeListenerAccessor", "accessor.EntityAccessor", "accessor.GrowingPlantAccessor", "accessor.MinecraftServerAccessor", @@ -79,6 +80,7 @@ "accessor.TargetingConditionsAccessor", "accessor.VaultServerDataAccessor", "accessor.VillagerAccessor", + "accessor.WorldBorderAccessor", "invoker.BlockBehaviourInvoker", "invoker.BlockItemInvoker", "piglin.AbstractPiglinMixin",