package io.lampnet.travelerssuitcase.block; import io.lampnet.travelerssuitcase.TravelersSuitcase; import io.lampnet.travelerssuitcase.block.entity.ModBlockEntities; import io.lampnet.travelerssuitcase.block.entity.SuitcaseBlockEntity; import io.lampnet.travelerssuitcase.data.PlayerEntryData; import io.lampnet.travelerssuitcase.item.KeystoneItem; import net.minecraft.world.level.block.BaseEntityBlock; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.RenderShape; import net.minecraft.world.level.block.state.BlockBehaviour; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.BlockItem; import net.minecraft.world.level.storage.loot.LootParams; import net.minecraft.world.level.storage.loot.parameters.LootContextParams; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.StringTag; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerLevel; import net.minecraft.sounds.SoundSource; import net.minecraft.sounds.SoundEvents; import net.minecraft.world.level.block.state.StateDefinition; import net.minecraft.world.level.block.state.properties.BooleanProperty; import net.minecraft.world.level.block.state.properties.DirectionProperty; import net.minecraft.world.level.block.state.properties.EnumProperty; import net.minecraft.world.level.block.state.properties.BlockStateProperties; import net.minecraft.network.chat.Component; import net.minecraft.ChatFormatting; import net.minecraft.world.InteractionResult; import net.minecraft.world.InteractionHand; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.VoxelShape; import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; import net.minecraft.world.item.DyeColor; import net.minecraft.network.protocol.game.ClientboundStopSoundPacket; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; public class SuitcaseBlock extends BaseEntityBlock { public static final BooleanProperty OPEN = BooleanProperty.create("open"); public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING; public static final EnumProperty COLOR = EnumProperty.create("color", DyeColor.class); private final static VoxelShape SHAPE_N = Block.box(0, 0, 2, 16, 4, 14); private final static VoxelShape SHAPE_S = Block.box(0, 0, 2, 16, 4, 14); private final static VoxelShape SHAPE_E = Block.box(2, 0, 0, 14, 4, 16); private final static VoxelShape SHAPE_W = Block.box(2, 0, 0, 14, 4, 16); public SuitcaseBlock(BlockBehaviour.Properties properties) { super(properties); this.registerDefaultState(this.stateDefinition.any() .setValue(OPEN, false) .setValue(FACING, Direction.NORTH) .setValue(COLOR, DyeColor.BROWN)); } @Override public @NotNull RenderShape getRenderShape(@NotNull BlockState pState) { return RenderShape.MODEL; } @Nullable @Override public BlockEntity newBlockEntity(@NotNull BlockPos pPos, @NotNull BlockState pState) { return new SuitcaseBlockEntity(pPos, pState); } @Override protected void createBlockStateDefinition(StateDefinition.Builder pBuilder) { pBuilder.add(OPEN, FACING, COLOR); } @Override public void setPlacedBy(@NotNull Level world, @NotNull BlockPos pos, @NotNull BlockState state, @Nullable LivingEntity placer, @NotNull ItemStack itemStack) { super.setPlacedBy(world, pos, state, placer, itemStack); if (!world.isClientSide()) { BlockEntity blockEntity = world.getBlockEntity(pos); if (blockEntity instanceof SuitcaseBlockEntity suitcase) { CompoundTag blockEntityTag = BlockItem.getBlockEntityData(itemStack); if (blockEntityTag != null) { suitcase.load(blockEntityTag); String keystoneName = suitcase.getBoundKeystoneName(); if (keystoneName != null) { List players = suitcase.getEnteredPlayers(); for (SuitcaseBlockEntity.EnteredPlayerData player : players) { suitcase.updatePlayerSuitcasePosition(player.uuid(), pos); Map suitcases = SuitcaseBlockEntity.SUITCASE_REGISTRY.computeIfAbsent( keystoneName, k -> new HashMap<>() ); suitcases.put(player.uuid(), pos); } } } } } } @Override public void onRemove(BlockState state, @NotNull Level world, @NotNull BlockPos pos, BlockState newState, boolean isMoving) { if (!state.is(newState.getBlock())) { super.onRemove(state, world, pos, newState, isMoving); } } @Override public @NotNull InteractionResult use(@NotNull BlockState state, Level world, @NotNull BlockPos pos, @NotNull Player player, @NotNull InteractionHand hand, @NotNull BlockHitResult hit) { if (world.isClientSide()) { return InteractionResult.SUCCESS; } ItemStack heldItem = player.getItemInHand(hand); BlockEntity blockEntity = world.getBlockEntity(pos); if (!(blockEntity instanceof SuitcaseBlockEntity suitcase)) { return InteractionResult.PASS; } if (!suitcase.canOpenInDimension(world)) { world.playSound(null, pos, SoundEvents.IRON_DOOR_CLOSE, SoundSource.BLOCKS, 0.3F, 2.0F); player.displayClientMessage(Component.literal("§c☒"), true); return InteractionResult.SUCCESS; } String boundKeystone = suitcase.getBoundKeystoneName(); if (heldItem.getItem() instanceof KeystoneItem) { String keystoneName = heldItem.getHoverName().getString().toLowerCase(); if (boundKeystone != null && boundKeystone.equals(keystoneName)) { boolean newLockState = !suitcase.isLocked(); suitcase.setLocked(newLockState); world.playSound(null, pos, newLockState ? SoundEvents.IRON_DOOR_CLOSE : SoundEvents.IRON_DOOR_OPEN, SoundSource.BLOCKS, 0.3F, 2.0F); player.displayClientMessage(Component.literal(newLockState ? "§7☒" : "§7☐"), true); return InteractionResult.SUCCESS; } if (suitcase.isLocked()) { world.playSound(null, pos, SoundEvents.IRON_DOOR_CLOSE, SoundSource.BLOCKS, 0.3F, 2.0F); player.displayClientMessage(Component.literal("§c☒"), true); return InteractionResult.FAIL; } if (keystoneName.equals(Component.translatable("item.travelerssuitcase.keystone").getString().toLowerCase()) || !KeystoneItem.isValidKeystone(heldItem)) { player.displayClientMessage(Component.literal("§cName the key to bind or ensure it is valid."), false); return InteractionResult.FAIL; } suitcase.bindKeystone(keystoneName); world.playSound(null, pos, SoundEvents.LODESTONE_COMPASS_LOCK, SoundSource.BLOCKS, 2.0F, 0.0F); return InteractionResult.SUCCESS; } if (!player.isShiftKeyDown() || heldItem.isEmpty()) { if (boundKeystone == null) { world.playSound(null, pos, SoundEvents.CHAIN_PLACE, SoundSource.BLOCKS, 0.5F, 2.0F); return InteractionResult.FAIL; } if (suitcase.isLocked()) { world.playSound(null, pos, SoundEvents.IRON_DOOR_CLOSE, SoundSource.BLOCKS, 0.3F, 2.0F); return InteractionResult.FAIL; } boolean isOpen = state.getValue(OPEN); world.setBlockAndUpdate(pos, state.setValue(OPEN, !isOpen)); if (!isOpen) { world.playSound(null, pos, SoundEvents.LADDER_STEP, SoundSource.BLOCKS, 0.3F, 0.0F); world.playSound(null, pos, SoundEvents.CHEST_LOCKED, SoundSource.BLOCKS, 0.3F, 2.0F); } else { world.playSound(null, pos, SoundEvents.LADDER_BREAK, SoundSource.BLOCKS, 0.3F, 0.0F); world.playSound(null, pos, SoundEvents.BAMBOO_WOOD_TRAPDOOR_CLOSE, SoundSource.BLOCKS, 0.3F, 0.0F); } return InteractionResult.SUCCESS; } return InteractionResult.PASS; } @Override public void entityInside(@NotNull BlockState state, Level world, @NotNull BlockPos pos, @NotNull Entity entity) { if (!world.isClientSide() && entity instanceof ServerPlayer player) { if (!state.getValue(OPEN) || !player.isShiftKeyDown()) { return; } BlockEntity blockEntity = world.getBlockEntity(pos); if (!(blockEntity instanceof SuitcaseBlockEntity suitcase)) { return; } String keystoneName = suitcase.getBoundKeystoneName(); if (keystoneName == null) { return; } String dimensionName = "pocket_dimension_" + keystoneName; ResourceLocation dimensionRL = ResourceLocation.fromNamespaceAndPath(TravelersSuitcase.MODID, dimensionName); ResourceKey dimensionKey = ResourceKey.create(Registries.DIMENSION, dimensionRL); ServerLevel targetWorld = Objects.requireNonNull(world.getServer()).getLevel(dimensionKey); if (targetWorld != null) { boolean wasFirstTime = suitcase.isFirstTimeEntering(player); suitcase.playerEntered(player); if (wasFirstTime) { TravelersSuitcase.ENTER_POCKET_DIMENSION.trigger(player); } player.stopRiding(); player.setDeltaMovement(Vec3.ZERO); player.fallDistance = 0f; PlayerEntryData ped = PlayerEntryData.get(targetWorld); // Use default position (17, 97, 9) unless manually configured if (!ped.isManuallySet()) { ped.setEntryAutomatic(new Vec3(17.5, 97.0, 9.5), 0f, 0f); } Vec3 dest = ped.getEntryPos(); float entryYaw = ped.getEntryYaw(); float pitch = player.getXRot(); // Ensure destination chunk is loaded before teleportation ChunkPos destChunkPos = new ChunkPos(BlockPos.containing(dest)); targetWorld.setChunkForced(destChunkPos.x, destChunkPos.z, true); try { // Use changeDimension instead of teleportTo to avoid "Player moved wrongly" errors player.changeDimension(targetWorld, new net.minecraftforge.common.util.ITeleporter() { @Override public Entity placeEntity(Entity entity, ServerLevel currentWorld, ServerLevel destWorld, float placementYaw, java.util.function.Function repositionEntity) { Entity e = repositionEntity.apply(false); e.teleportTo(dest.x, dest.y, dest.z); e.setYRot(entryYaw); e.setXRot(pitch); return e; } }); // Schedule chunk unforcing on next tick to allow proper entity placement Objects.requireNonNull(world.getServer()).execute(() -> { targetWorld.setChunkForced(destChunkPos.x, destChunkPos.z, false); }); } finally { // Ensure chunk is unforced even if teleportation fails targetWorld.setChunkForced(destChunkPos.x, destChunkPos.z, false); } player.connection.send(new ClientboundStopSoundPacket(null, null)); world.playSound( null, pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, SoundEvents.BUNDLE_DROP_CONTENTS, SoundSource.PLAYERS, 2.0f, 1.0f ); } } } @Override public @NotNull VoxelShape getShape(BlockState state, @NotNull BlockGetter world, @NotNull BlockPos pos, @NotNull CollisionContext context) { return switch (state.getValue(FACING)) { case SOUTH -> SHAPE_S; case EAST -> SHAPE_E; case WEST -> SHAPE_W; default -> SHAPE_N; }; } @Nullable @Override public BlockState getStateForPlacement(BlockPlaceContext ctx) { return this.defaultBlockState() .setValue(FACING, ctx.getHorizontalDirection().getOpposite()) .setValue(OPEN, false); } @Override public @NotNull ItemStack getCloneItemStack(@NotNull BlockGetter world, @NotNull BlockPos pos, @NotNull BlockState state) { ItemStack stack = super.getCloneItemStack(world, pos, state); BlockEntity blockEntity = world.getBlockEntity(pos); if (blockEntity instanceof SuitcaseBlockEntity suitcase) { CompoundTag beNbt = new CompoundTag(); suitcase.saveAdditional(beNbt); if (!beNbt.isEmpty()) { BlockItem.setBlockEntityData(stack, ModBlockEntities.SUITCASE_BLOCK_ENTITY.get(), beNbt); } String boundKeystone = suitcase.getBoundKeystoneName(); if (boundKeystone != null) { CompoundTag display = stack.getOrCreateTagElement("display"); ListTag lore = new ListTag(); if (!suitcase.getEnteredPlayers().isEmpty()) { Component warningText = Component.literal("§c⚠ Contains " + suitcase.getEnteredPlayers().size() + " Travelers!") .withStyle(ChatFormatting.RED); lore.add(StringTag.valueOf(Component.Serializer.toJson(warningText))); } String displayName = boundKeystone.replace("_", " "); Component boundText; if (suitcase.isLocked()) { boundText = Component.literal("Bound to: §k" + displayName) .withStyle(ChatFormatting.GRAY); } else { boundText = Component.literal("Bound to: " + displayName) .withStyle(ChatFormatting.GRAY); } lore.add(StringTag.valueOf(Component.Serializer.toJson(boundText))); Component lockText = Component.literal(suitcase.isLocked() ? "§cLocked" : "§aUnlocked") .withStyle(ChatFormatting.GRAY); lore.add(StringTag.valueOf(Component.Serializer.toJson(lockText))); display.put("Lore", lore); } } return stack; } @Override public @NotNull List getDrops(@NotNull BlockState state, LootParams.Builder builder) { ItemStack stack = new ItemStack(this); BlockEntity blockEntity = builder.getOptionalParameter(LootContextParams.BLOCK_ENTITY); if (blockEntity instanceof SuitcaseBlockEntity suitcase) { CompoundTag beNbt = new CompoundTag(); suitcase.saveAdditional(beNbt); if (!beNbt.isEmpty()) { BlockItem.setBlockEntityData(stack, ModBlockEntities.SUITCASE_BLOCK_ENTITY.get(), beNbt); } String boundKeystone = suitcase.getBoundKeystoneName(); if (boundKeystone != null) { CompoundTag display = stack.getOrCreateTagElement("display"); ListTag lore = new ListTag(); if (!suitcase.getEnteredPlayers().isEmpty()) { Component warningText = Component.literal("§c⚠ Contains " + suitcase.getEnteredPlayers().size() + " Traveler's!") .withStyle(ChatFormatting.RED); lore.add(StringTag.valueOf(Component.Serializer.toJson(warningText))); } String displayName = boundKeystone.replace("_", " "); Component boundText; if (suitcase.isLocked()) { boundText = Component.literal("Bound to: §k" + displayName).withStyle(ChatFormatting.GRAY); } else { boundText = Component.literal("Bound to: " + displayName).withStyle(ChatFormatting.GRAY); } lore.add(StringTag.valueOf(Component.Serializer.toJson(boundText))); Component lockText = Component.literal(suitcase.isLocked() ? "§cLocked" : "§aUnlocked") .withStyle(ChatFormatting.GRAY); lore.add(StringTag.valueOf(Component.Serializer.toJson(lockText))); display.put("Lore", lore); } } return Collections.singletonList(stack); } @Override public boolean triggerEvent(@NotNull BlockState state, @NotNull Level world, @NotNull BlockPos pos, int type, int data) { super.triggerEvent(state, world, pos, type, data); BlockEntity blockentity = world.getBlockEntity(pos); return blockentity != null && blockentity.triggerEvent(type, data); } }