/* * GNU Lesser General Public License v3 * Copyright (C) 2024 Tschipp * mrtschipp@gmail.com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package tschipp.carryon.common.carry; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.server.level.ServerPlayer; import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; import net.minecraft.world.InteractionHand; import net.minecraft.world.effect.MobEffects; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.animal.equine.Horse; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.properties.BlockStateProperties; import net.minecraft.world.level.block.state.properties.Property; import net.minecraft.world.level.gamerules.GameRules; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.shapes.CollisionContext; import tschipp.carryon.Constants; import tschipp.carryon.common.carry.CarryOnData.CarryType; import tschipp.carryon.common.config.ListHandler; import tschipp.carryon.common.scripting.CarryOnScript.ScriptEffects; import tschipp.carryon.networking.clientbound.ClientboundStartRidingOtherPlayerPacket; import tschipp.carryon.platform.Services; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.function.BiFunction; public class PlacementHandler { public static boolean tryPlaceBlock(ServerPlayer player, BlockPos pos, Direction facing, @Nullable BiFunction placementCallback) { CarryOnData carry = CarryOnDataManager.getCarryData(player); if (!carry.isCarrying(CarryOnData.CarryType.BLOCK)) return false; if (player.tickCount == carry.getTick()) return false; Level level = player.level(); BlockState state = carry.getBlock(); BlockPlaceContext context = new BlockPlaceContext(player, InteractionHand.MAIN_HAND, ItemStack.EMPTY, BlockHitResult.miss(player.position(), facing, pos)); if (!level.getBlockState(pos).canBeReplaced(context)) pos = pos.relative(facing); context = new BlockPlaceContext(player, InteractionHand.MAIN_HAND, ItemStack.EMPTY, BlockHitResult.miss(player.position(), facing, pos)); BlockEntity blockEntity = carry.getBlockEntity(pos, level.registryAccess()); state = getPlacementState(state, player, context, pos); boolean canPlace = state.canSurvive(level, pos) && level.mayInteract(player, pos) && level.getBlockState(pos).canBeReplaced(context) && level.isUnobstructed(state, pos, CollisionContext.of(player)); if (!canPlace) { level.playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.LAVA_POP, SoundSource.PLAYERS, 0.5F, 0.5F); return false; } boolean doPlace = placementCallback == null || placementCallback.apply(pos, state); if (!doPlace) return false; if(level.isOutsideBuildHeight(pos)) return false; if (carry.getActiveScript().isPresent()) { ScriptEffects effects = carry.getActiveScript().get().scriptEffects(); String cmd = effects.commandPlace(); if (!cmd.isEmpty()) player.level().getServer().getCommands().performPrefixedCommand(player.level().getServer().createCommandSourceStack(), "/execute as " + player.getGameProfile().name() + " run " + cmd); } level.setBlockAndUpdate(pos, state); if (blockEntity != null) { blockEntity.setBlockState(state); level.setBlockEntity(blockEntity); } level.updateNeighborsAt(pos.relative(Direction.DOWN), level.getBlockState(pos.relative(Direction.DOWN)).getBlock()); carry.clear(); CarryOnDataManager.setCarryData(player, carry); player.playSound(state.getSoundType().getPlaceSound(), 1.0f, 0.5f); level.playSound(null, pos, state.getSoundType().getPlaceSound(), SoundSource.BLOCKS, 1.0f, 0.5f); player.swing(InteractionHand.MAIN_HAND, true); player.removeEffect(MobEffects.SLOWNESS); return true; } private static BlockState getPlacementState(BlockState state, ServerPlayer player, BlockPlaceContext context, BlockPos pos) { BlockState placementState = state.getBlock().getStateForPlacement(context); if (placementState == null || placementState.getBlock() != state.getBlock()) placementState = state; for (var prop : placementState.getProperties()) { if (prop.getValueClass() == Direction.class) state = updateProperty(state, placementState, prop); if (prop.getValueClass() == Direction.Axis.class) state = updateProperty(state, placementState, prop); //This is needed for certain blocks, otherwise we get problems like chests not connecting if (ListHandler.isPropertyException(prop)) { state = updateProperty(state, placementState, prop); } } BlockState updatedState = Block.updateFromNeighbourShapes(state, player.level(), pos); if (updatedState.getBlock() == state.getBlock()) state = updatedState; if (placementState.hasProperty(BlockStateProperties.WATERLOGGED) && state.hasProperty(BlockStateProperties.WATERLOGGED)) state = state.setValue(BlockStateProperties.WATERLOGGED, placementState.getValue(BlockStateProperties.WATERLOGGED)); return state; } private static > BlockState updateProperty(BlockState state, BlockState otherState, Property prop) { var val = otherState.getValue(prop); return state.setValue(prop, val); } public static boolean tryPlaceEntity(ServerPlayer player, BlockPos pos, Direction facing, @Nullable BiFunction placementCallback) { CarryOnData carry = CarryOnDataManager.getCarryData(player); if (!carry.isCarrying(CarryType.ENTITY) && !carry.isCarrying(CarryType.PLAYER)) return false; if (player.tickCount == carry.getTick()) return false; Level level = player.level(); BlockPlaceContext context = new BlockPlaceContext(player, InteractionHand.MAIN_HAND, ItemStack.EMPTY, BlockHitResult.miss(player.position(), facing, pos)); if (!level.getBlockState(pos).canBeReplaced(context)) { pos = pos.relative(facing); context = new BlockPlaceContext(player, InteractionHand.MAIN_HAND, ItemStack.EMPTY, BlockHitResult.miss(player.position(), facing, pos)); } if (!level.getBlockState(pos).canBeReplaced(context)) { level.playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.LAVA_POP, SoundSource.PLAYERS, 0.5F, 0.5F); return false; } Vec3 placementPos = Vec3.atBottomCenterOf(pos); if (carry.isCarrying(CarryType.PLAYER)) { Entity otherPlayer = carry.getCarryingPlayer(level); player.ejectPassengers(); Services.PLATFORM.sendPacketToAllPlayers(Constants.PACKET_ID_START_RIDING_OTHER, new ClientboundStartRidingOtherPlayerPacket(player.getId(), otherPlayer.getId(), false), player.level()); carry.clear(); CarryOnDataManager.setCarryData(player, carry); otherPlayer.teleportTo(placementPos.x, placementPos.y, placementPos.z); player.swing(InteractionHand.MAIN_HAND, true); player.removeEffect(MobEffects.SLOWNESS); return true; } Entity entity = carry.getEntity(level); entity.setPos(placementPos); boolean doPlace = placementCallback == null || placementCallback.apply(placementPos, entity); if (!doPlace) return false; if (carry.getActiveScript().isPresent()) { ScriptEffects effects = carry.getActiveScript().get().scriptEffects(); String cmd = effects.commandPlace(); if (!cmd.isEmpty()) player.level().getServer().getCommands().performPrefixedCommand(player.level().getServer().createCommandSourceStack(), "/execute as " + player.getGameProfile().name() + " run " + cmd); } level.addFreshEntity(entity); if (entity instanceof Mob mob) mob.playAmbientSound(); player.swing(InteractionHand.MAIN_HAND, true); carry.clear(); CarryOnDataManager.setCarryData(player, carry); player.removeEffect(MobEffects.SLOWNESS); return true; } public static void tryStackEntity(ServerPlayer player, Entity entityClicked) { if(!Constants.COMMON_CONFIG.settings.stackableEntities) return; CarryOnData carry = CarryOnDataManager.getCarryData(player); if (!carry.isCarrying(CarryType.ENTITY) && !carry.isCarrying(CarryType.PLAYER)) return; Level level = player.level(); Entity entityHeld; if (carry.isCarrying(CarryType.ENTITY)) entityHeld = carry.getEntity(level); else entityHeld = player.getFirstPassenger(); if(entityHeld == null) return; double sizeHeldEntity = entityHeld.getBbHeight() * entityHeld.getBbWidth(); double distance = entityClicked.blockPosition().distSqr(player.blockPosition()); Entity lowestEntity = entityClicked.getRootVehicle(); int numPassengers = getPassengerCount(lowestEntity); if (numPassengers < Constants.COMMON_CONFIG.settings.maxEntityStackLimit - 1) { Entity topEntity = getTopPassenger(lowestEntity); if (topEntity == entityHeld) return; if (ListHandler.isStackingPermitted(topEntity)) { double sizeEntity = topEntity.getBbHeight() * topEntity.getBbWidth(); if (!Constants.COMMON_CONFIG.settings.entitySizeMattersStacking || sizeHeldEntity <= sizeEntity) { if (topEntity instanceof Horse horse) horse.setTamed(true); if (distance < 6) { double tempX = entityClicked.getX(); double tempY = entityClicked.getY(); double tempZ = entityClicked.getZ(); if (carry.isCarrying(CarryType.ENTITY)) { entityHeld.setPos(tempX, tempY + 2.6, tempZ); level.addFreshEntity(entityHeld); entityHeld.teleportTo(tempX, tempY, tempZ); } entityHeld.startRiding(topEntity, false,false); } else { if (carry.isCarrying(CarryType.ENTITY)) { entityHeld.setPos(entityClicked.getX(), entityClicked.getY(), entityClicked.getZ()); level.addFreshEntity(entityHeld); } entityHeld.startRiding(topEntity, false,false); } if (carry.getActiveScript().isPresent()) { ScriptEffects effects = carry.getActiveScript().get().scriptEffects(); String cmd = effects.commandPlace(); if (!cmd.isEmpty()) player.level().getServer().getCommands().performPrefixedCommand(player.level().getServer().createCommandSourceStack(), "/execute as " + player.getGameProfile().name() + " run " + cmd); } player.swing(InteractionHand.MAIN_HAND, true); carry.clear(); CarryOnDataManager.setCarryData(player, carry); level.playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.HORSE_SADDLE, SoundSource.PLAYERS, 0.5F, 1.5F); player.removeEffect(MobEffects.SLOWNESS); } else { level.playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.LAVA_POP, SoundSource.PLAYERS, 0.5F, 0.5F); } } } else { level.playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.LAVA_POP, SoundSource.PLAYERS, 0.5F, 0.5F); } } public static void placeCarriedOnDeath(ServerPlayer oldPlayer, ServerPlayer newPlayer, boolean died) { CarryOnData carry = CarryOnDataManager.getCarryData(oldPlayer); if (oldPlayer.level().getGameRules().get(GameRules.KEEP_INVENTORY) || !died) { if (!carry.isCarrying(CarryType.PLAYER)) { CarryOnDataManager.setCarryData(newPlayer, carry); newPlayer.getInventory().setSelectedSlot(oldPlayer.getInventory().getSelectedSlot()); return; } } placeCarried(oldPlayer); } public static void placeCarried(ServerPlayer player) { CarryOnData carry = CarryOnDataManager.getCarryData(player); if (carry.isCarrying(CarryType.ENTITY)) { Entity entity = carry.getEntity(player.level()); entity.setPos(player.position()); player.level().addFreshEntity(entity); } else if (carry.isCarrying(CarryType.BLOCK)) { BlockPlaceContext context = new BlockPlaceContext(player, InteractionHand.MAIN_HAND, ItemStack.EMPTY, BlockHitResult.miss(Vec3.atCenterOf(player.blockPosition()), Direction.DOWN, player.blockPosition())); BlockState state = getPlacementState(carry.getBlock(), player, context, player.blockPosition()); BlockPos pos = getDeathPlacementPos(state, player); BlockEntity blockEntity = carry.getBlockEntity(pos, player.level().registryAccess()); player.level().setBlock(pos, state, 3); if (blockEntity != null) player.level().setBlockEntity(blockEntity); } else if (carry.isCarrying(CarryType.PLAYER)) { player.ejectPassengers(); } carry.clear(); CarryOnDataManager.setCarryData(player, carry); player.removeEffect(MobEffects.SLOWNESS); } private static BlockPos getDeathPlacementPos(BlockState state, ServerPlayer player) { BlockPos p = player.blockPosition(); int DISTANCE = 15; List potentialPositions = new ArrayList<>(); for (int j = 0; j < DISTANCE * 2; j++) { for (int i = 0; i < DISTANCE * 2; i++) { for (int k = 0; k < DISTANCE * 2; k++) { int x = i % 2 == 0 ? i / 2 : -(i / 2); int y = j % 2 == 0 ? j / 2 : -(j / 2); int z = k % 2 == 0 ? k / 2 : -(k / 2); potentialPositions.add(new BlockPos(p.getX() + x, p.getY() + y, p.getZ() + z)); } } } potentialPositions.sort(Comparator.comparingDouble(posA -> player.distanceToSqr(posA.getCenter()))); for(BlockPos potential : potentialPositions) { BlockPlaceContext context = new BlockPlaceContext(player, InteractionHand.MAIN_HAND, ItemStack.EMPTY, BlockHitResult.miss(Vec3.atCenterOf(potential), Direction.DOWN, potential)); boolean canPlace = state.canSurvive(player.level(), potential) && player.level().getBlockState(potential).canBeReplaced(context) && player.level().isUnobstructed(state, potential, CollisionContext.of(player)); if (canPlace) return potential; } return p; } private static int getPassengerCount(Entity entity) { int passengers = 0; while (entity.isVehicle()) { List pass = entity.getPassengers(); if (!pass.isEmpty()) { entity = pass.get(0); passengers++; } } return passengers; } private static Entity getTopPassenger(Entity entity) { Entity top = entity; while (entity.isVehicle()) { List pass = entity.getPassengers(); if (!pass.isEmpty()) { entity = pass.get(0); top = entity; } } return top; } }