CarryOn/Common/src/main/java/tschipp/carryon/common/carry/PlacementHandler.java
2025-12-27 23:25:58 +01:00

389 lines
15 KiB
Java

/*
* 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<BlockPos, BlockState, Boolean> 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 <T extends Comparable<T>> BlockState updateProperty(BlockState state, BlockState otherState, Property<T> prop)
{
var val = otherState.getValue(prop);
return state.setValue(prop, val);
}
public static boolean tryPlaceEntity(ServerPlayer player, BlockPos pos, Direction facing, @Nullable BiFunction<Vec3, Entity, Boolean> 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<BlockPos> 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<Entity> 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<Entity> pass = entity.getPassengers();
if (!pass.isEmpty()) {
entity = pass.get(0);
top = entity;
}
}
return top;
}
}