1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-12-15 04:30:29 +00:00

Refactor turtle actions for easier multi-loader support

- TurtlePlayer now returns a fake player, rather than extending from
   Forge's implementation.

 - Remove "turtle obeys block protection" config option.

 - Put some of the more complex turtle actions behind the
   PlatformHelper.
This commit is contained in:
Jonathan Coates 2022-11-09 20:50:43 +00:00
parent 4d50b48ea6
commit 8a7156785d
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
12 changed files with 310 additions and 324 deletions

View File

@ -1,22 +0,0 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
public final class TurtlePermissions {
public static boolean isBlockEnterable(Level world, BlockPos pos, Player player) {
var server = world.getServer();
return server == null || world.isClientSide || (world instanceof ServerLevel serverLevel && !server.isUnderSpawnProtection(serverLevel, pos, player));
}
public static boolean isBlockEditable(Level world, BlockPos pos, Player player) {
return isBlockEnterable(world, pos, player);
}
}

View File

@ -32,7 +32,6 @@ public final class Config {
public static boolean turtlesNeedFuel = true; public static boolean turtlesNeedFuel = true;
public static int turtleFuelLimit = 20000; public static int turtleFuelLimit = 20000;
public static int advancedTurtleFuelLimit = 100000; public static int advancedTurtleFuelLimit = 100000;
public static boolean turtlesObeyBlockProtection = true;
public static boolean turtlesCanPush = true; public static boolean turtlesCanPush = true;
public static int computerTermWidth = 51; public static int computerTermWidth = 51;

View File

@ -63,7 +63,6 @@ public final class ConfigSpec {
private static final ConfigValue<Boolean> turtlesNeedFuel; private static final ConfigValue<Boolean> turtlesNeedFuel;
private static final ConfigValue<Integer> turtleFuelLimit; private static final ConfigValue<Integer> turtleFuelLimit;
private static final ConfigValue<Integer> advancedTurtleFuelLimit; private static final ConfigValue<Integer> advancedTurtleFuelLimit;
private static final ConfigValue<Boolean> turtlesObeyBlockProtection;
private static final ConfigValue<Boolean> turtlesCanPush; private static final ConfigValue<Boolean> turtlesCanPush;
private static final ConfigValue<Integer> computerTermWidth; private static final ConfigValue<Integer> computerTermWidth;
@ -283,12 +282,6 @@ public final class ConfigSpec {
.comment("The fuel limit for Advanced Turtles.") .comment("The fuel limit for Advanced Turtles.")
.defineInRange("advanced_fuel_limit", Config.advancedTurtleFuelLimit, 0, Integer.MAX_VALUE); .defineInRange("advanced_fuel_limit", Config.advancedTurtleFuelLimit, 0, Integer.MAX_VALUE);
turtlesObeyBlockProtection = builder
.comment("""
If set to true, Turtles will be unable to build, dig, or enter protected areas
(such as near the server spawn point).""")
.define("obey_block_protection", Config.turtlesObeyBlockProtection);
turtlesCanPush = builder turtlesCanPush = builder
.comment(""" .comment("""
If set to true, Turtles will push entities out of the way instead of stopping if If set to true, Turtles will push entities out of the way instead of stopping if
@ -400,7 +393,6 @@ public final class ConfigSpec {
Config.turtlesNeedFuel = turtlesNeedFuel.get(); Config.turtlesNeedFuel = turtlesNeedFuel.get();
Config.turtleFuelLimit = turtleFuelLimit.get(); Config.turtleFuelLimit = turtleFuelLimit.get();
Config.advancedTurtleFuelLimit = advancedTurtleFuelLimit.get(); Config.advancedTurtleFuelLimit = advancedTurtleFuelLimit.get();
Config.turtlesObeyBlockProtection = turtlesObeyBlockProtection.get();
Config.turtlesCanPush = turtlesCanPush.get(); Config.turtlesCanPush = turtlesCanPush.get();
// Terminal size // Terminal size

View File

@ -0,0 +1,49 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared.platform;
import com.mojang.authlib.GameProfile;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityDimensions;
import net.minecraft.world.entity.Pose;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.common.util.FakePlayer;
import javax.annotation.Nullable;
import java.util.OptionalInt;
class FakePlayerExt extends FakePlayer {
FakePlayerExt(ServerLevel serverLevel, GameProfile profile) {
super(serverLevel, profile);
}
@Override
public void doTick() {
super.doTick();
}
@Override
public boolean canHarmPlayer(Player other) {
return true;
}
@Override
public OptionalInt openMenu(@Nullable MenuProvider menu) {
return OptionalInt.empty();
}
@Override
public boolean startRiding(Entity vehicle, boolean force) {
return false;
}
@Override
public float getStandingEyeHeight(Pose pose, EntityDimensions dimensions) {
return 0;
}
}

View File

@ -5,6 +5,7 @@
*/ */
package dan200.computercraft.shared.platform; package dan200.computercraft.shared.platform;
import com.mojang.authlib.GameProfile;
import dan200.computercraft.api.network.wired.IWiredElement; import dan200.computercraft.api.network.wired.IWiredElement;
import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.shared.network.NetworkMessage; import dan200.computercraft.shared.network.NetworkMessage;
@ -20,10 +21,11 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ServerPlayerGameMode;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import net.minecraft.tags.TagKey; import net.minecraft.tags.TagKey;
import net.minecraft.world.Container; import net.minecraft.world.*;
import net.minecraft.world.MenuProvider; import net.minecraft.world.entity.Entity;
import net.minecraft.world.WorldlyContainer;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.CraftingContainer; import net.minecraft.world.inventory.CraftingContainer;
@ -39,6 +41,7 @@ import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
import net.minecraftforge.items.IItemHandlerModifiable; import net.minecraftforge.items.IItemHandlerModifiable;
@ -296,6 +299,15 @@ public interface PlatformHelper extends dan200.computercraft.impl.PlatformHelper
*/ */
boolean onNotifyNeighbour(Level level, BlockPos pos, BlockState block, Direction direction); boolean onNotifyNeighbour(Level level, BlockPos pos, BlockState block, Direction direction);
/**
* Create a new fake player.
*
* @param level The level the player should be created in.
* @param profile The user this player should mimic.
* @return The newly constructed fake player.
*/
ServerPlayer createFakePlayer(ServerLevel level, GameProfile profile);
/** /**
* Determine if a player is not a real player. * Determine if a player is not a real player.
* *
@ -316,4 +328,55 @@ public interface PlatformHelper extends dan200.computercraft.impl.PlatformHelper
default double getReachDistance(Player player) { default double getReachDistance(Player player) {
return player.isCreative() ? 5 : 4.5; return player.isCreative() ? 5 : 4.5;
} }
/**
* Check if this item is a tool and has some secondary usage.
* <p>
* In practice, this only checks if a tool is a hoe or shovel. We don't want to include things like axes,
*
* @param stack The stack to check.
* @return Whether this tool has a secondary usage.
*/
boolean hasToolUsage(ItemStack stack);
/**
* Check if an entity can be attacked according to platform-specific events.
*
* @param player The player who is attacking.
* @param entity The entity we're attacking.
* @return If this entity can be attacked.
* @see Player#attack(Entity)
*/
InteractionResult canAttackEntity(ServerPlayer player, Entity entity);
/**
* Interact with an entity, for instance feeding cows.
* <p>
* Implementations should follow Minecraft behaviour - we try {@link Entity#interactAt(Player, Vec3, InteractionHand)}
* and then {@link Player#interactOn(Entity, InteractionHand)}. Loader-specific hooks should also be called.
*
* @param player The player which is interacting with the entity.
* @param entity The entity we're interacting with.
* @param hitPos The position our ray trace hit the entity. This is a position in-world, unlike
* {@link Entity#interactAt(Player, Vec3, InteractionHand)} which is relative to the entity.
* @return Whether any interaction occurred.
* @see Entity#interactAt(Player, Vec3, InteractionHand)
* @see Player#interactOn(Entity, InteractionHand)
* @see ServerGamePacketListenerImpl#handleInteract
*/
boolean interactWithEntity(ServerPlayer player, Entity entity, Vec3 hitPos);
/**
* Place an item against a block.
* <p>
* Implementations should largely mirror {@link ServerPlayerGameMode#useItemOn(ServerPlayer, Level, ItemStack, InteractionHand, BlockHitResult)}
* (including any loader-specific modifications), except they should skip the call to {@link BlockState#use(Level, Player, InteractionHand, BlockHitResult)}.
*
* @param player The player which is placing this item.
* @param stack The item to place.
* @param hit The collision with the block we're placing against.
* @return Whether any interaction occurred.
* @see ServerPlayerGameMode#useItemOn(ServerPlayer, Level, ItemStack, InteractionHand, BlockHitResult)
*/
InteractionResult useOn(ServerPlayer player, ItemStack stack, BlockHitResult hit);
} }

View File

@ -6,6 +6,7 @@
package dan200.computercraft.shared.platform; package dan200.computercraft.shared.platform;
import com.google.auto.service.AutoService; import com.google.auto.service.AutoService;
import com.mojang.authlib.GameProfile;
import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.network.wired.IWiredElement; import dan200.computercraft.api.network.wired.IWiredElement;
import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.IPeripheral;
@ -27,9 +28,8 @@ import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.TagKey; import net.minecraft.tags.TagKey;
import net.minecraft.world.Container; import net.minecraft.world.*;
import net.minecraft.world.MenuProvider; import net.minecraft.world.entity.Entity;
import net.minecraft.world.WorldlyContainerHolder;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.CraftingContainer; import net.minecraft.world.inventory.CraftingContainer;
@ -37,6 +37,7 @@ import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.item.crafting.Recipe; import net.minecraft.world.item.crafting.Recipe;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
@ -45,14 +46,17 @@ import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
import net.minecraftforge.common.ForgeHooks; import net.minecraftforge.common.ForgeHooks;
import net.minecraftforge.common.Tags; import net.minecraftforge.common.Tags;
import net.minecraftforge.common.ToolActions;
import net.minecraftforge.common.capabilities.Capability; import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.ForgeCapabilities; import net.minecraftforge.common.capabilities.ForgeCapabilities;
import net.minecraftforge.common.extensions.IForgeMenuType; import net.minecraftforge.common.extensions.IForgeMenuType;
import net.minecraftforge.common.util.NonNullConsumer; import net.minecraftforge.common.util.NonNullConsumer;
import net.minecraftforge.event.ForgeEventFactory; import net.minecraftforge.event.ForgeEventFactory;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.items.IItemHandlerModifiable; import net.minecraftforge.items.IItemHandlerModifiable;
import net.minecraftforge.items.wrapper.InvWrapper; import net.minecraftforge.items.wrapper.InvWrapper;
@ -271,11 +275,52 @@ public class PlatformHelperImpl implements PlatformHelper {
return !ForgeEventFactory.onNeighborNotify(level, pos, block, EnumSet.of(direction), false).isCanceled(); return !ForgeEventFactory.onNeighborNotify(level, pos, block, EnumSet.of(direction), false).isCanceled();
} }
@Override
public ServerPlayer createFakePlayer(ServerLevel world, GameProfile profile) {
return new FakePlayerExt(world, profile);
}
@Override @Override
public double getReachDistance(Player player) { public double getReachDistance(Player player) {
return player.getReachDistance(); return player.getReachDistance();
} }
@Override
public boolean hasToolUsage(ItemStack stack) {
return stack.canPerformAction(ToolActions.SHOVEL_FLATTEN) || stack.canPerformAction(ToolActions.HOE_TILL);
}
@Override
public InteractionResult canAttackEntity(ServerPlayer player, Entity entity) {
return ForgeHooks.onPlayerAttackTarget(player, entity) ? InteractionResult.PASS : InteractionResult.SUCCESS;
}
@Override
public boolean interactWithEntity(ServerPlayer player, Entity entity, Vec3 hitPos) {
// Our behaviour is slightly different here - we call onInteractEntityAt before the interact methods, while
// Forge does the call afterwards (on the server, not on the client).
var interactAt = ForgeHooks.onInteractEntityAt(player, entity, hitPos, InteractionHand.MAIN_HAND);
if (interactAt == null) {
interactAt = entity.interactAt(player, hitPos.subtract(entity.position()), InteractionHand.MAIN_HAND);
}
return interactAt.consumesAction() || player.interactOn(entity, InteractionHand.MAIN_HAND).consumesAction();
}
@Override
public InteractionResult useOn(ServerPlayer player, ItemStack stack, BlockHitResult hit) {
var level = player.level;
var pos = hit.getBlockPos();
var event = ForgeHooks.onRightClickBlock(player, InteractionHand.MAIN_HAND, pos, hit);
if (event.isCanceled()) return event.getCancellationResult();
var context = new UseOnContext(player, InteractionHand.MAIN_HAND, hit);
if (event.getUseItem() == Event.Result.DENY) return InteractionResult.PASS;
var result = stack.onItemUseFirst(context);
return result != InteractionResult.PASS ? result : stack.useOn(context);
}
private record RegistryWrapperImpl<T>( private record RegistryWrapperImpl<T>(
ResourceLocation name, ForgeRegistry<T> registry ResourceLocation name, ForgeRegistry<T> registry
) implements Registries.RegistryWrapper<T> { ) implements Registries.RegistryWrapper<T> {

View File

@ -9,12 +9,11 @@ import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleCommand; import dan200.computercraft.api.turtle.ITurtleCommand;
import dan200.computercraft.api.turtle.TurtleAnimation; import dan200.computercraft.api.turtle.TurtleAnimation;
import dan200.computercraft.api.turtle.TurtleCommandResult; import dan200.computercraft.api.turtle.TurtleCommandResult;
import dan200.computercraft.shared.TurtlePermissions;
import dan200.computercraft.shared.config.Config; import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.util.WorldUtil; import dan200.computercraft.shared.util.WorldUtil;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape; import net.minecraft.world.phys.shapes.VoxelShape;
@ -32,7 +31,7 @@ public class TurtleMoveCommand implements ITurtleCommand {
var direction = this.direction.toWorldDir(turtle); var direction = this.direction.toWorldDir(turtle);
// Check if we can move // Check if we can move
var oldWorld = turtle.getLevel(); var oldWorld = (ServerLevel) turtle.getLevel();
var oldPosition = turtle.getPosition(); var oldPosition = turtle.getPosition();
var newPosition = oldPosition.relative(direction); var newPosition = oldPosition.relative(direction);
@ -97,14 +96,14 @@ public class TurtleMoveCommand implements ITurtleCommand {
return TurtleCommandResult.success(); return TurtleCommandResult.success();
} }
private static TurtleCommandResult canEnter(TurtlePlayer turtlePlayer, Level world, BlockPos position) { private static TurtleCommandResult canEnter(TurtlePlayer turtlePlayer, ServerLevel world, BlockPos position) {
if (world.isOutsideBuildHeight(position)) { if (world.isOutsideBuildHeight(position)) {
return TurtleCommandResult.failure(position.getY() < 0 ? "Too low to move" : "Too high to move"); return TurtleCommandResult.failure(position.getY() < 0 ? "Too low to move" : "Too high to move");
} }
if (!world.isInWorldBounds(position)) return TurtleCommandResult.failure("Cannot leave the world"); if (!world.isInWorldBounds(position)) return TurtleCommandResult.failure("Cannot leave the world");
// Check spawn protection // Check spawn protection
if (Config.turtlesObeyBlockProtection && !TurtlePermissions.isBlockEnterable(world, position, turtlePlayer)) { if (turtlePlayer.isBlockProtected(world, position)) {
return TurtleCommandResult.failure("Cannot enter protected area"); return TurtleCommandResult.failure("Cannot enter protected area");
} }

View File

@ -5,12 +5,12 @@
*/ */
package dan200.computercraft.shared.turtle.core; package dan200.computercraft.shared.turtle.core;
import com.google.common.base.Splitter;
import dan200.computercraft.api.turtle.ITurtleAccess; import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleCommand; import dan200.computercraft.api.turtle.ITurtleCommand;
import dan200.computercraft.api.turtle.TurtleAnimation; import dan200.computercraft.api.turtle.TurtleAnimation;
import dan200.computercraft.api.turtle.TurtleCommandResult; import dan200.computercraft.api.turtle.TurtleCommandResult;
import dan200.computercraft.shared.TurtlePermissions; import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.turtle.TurtleUtil; import dan200.computercraft.shared.turtle.TurtleUtil;
import dan200.computercraft.shared.util.DropConsumer; import dan200.computercraft.shared.util.DropConsumer;
import dan200.computercraft.shared.util.InventoryUtil; import dan200.computercraft.shared.util.InventoryUtil;
@ -18,12 +18,12 @@ import dan200.computercraft.shared.util.WorldUtil;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.game.ServerboundInteractPacket; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult; import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.Entity; import net.minecraft.world.item.BlockItem;
import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.*; import net.minecraft.world.item.SignItem;
import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.item.context.UseOnContext; import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
@ -33,8 +33,6 @@ import net.minecraft.world.level.block.entity.SignBlockEntity;
import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.EntityHitResult; import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
import net.minecraftforge.common.ForgeHooks;
import net.minecraftforge.eventbus.api.Event.Result;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -76,13 +74,15 @@ public class TurtlePlaceCommand implements ITurtleCommand {
} }
} }
public static boolean deployCopiedItem(ItemStack stack, ITurtleAccess turtle, Direction direction, @Nullable Object[] extraArguments, @Nullable ErrorMessage outErrorMessage) { public static boolean deployCopiedItem(
ItemStack stack, ITurtleAccess turtle, Direction direction, @Nullable Object[] extraArguments, @Nullable ErrorMessage outErrorMessage
) {
// Create a fake player, and orient it appropriately // Create a fake player, and orient it appropriately
var playerPosition = turtle.getPosition().relative(direction); var playerPosition = turtle.getPosition().relative(direction);
var turtlePlayer = TurtlePlayer.getWithPosition(turtle, playerPosition, direction); var turtlePlayer = TurtlePlayer.getWithPosition(turtle, playerPosition, direction);
turtlePlayer.loadInventory(stack); turtlePlayer.loadInventory(stack);
var result = deploy(stack, turtle, turtlePlayer, direction, extraArguments, outErrorMessage); var result = deploy(stack, turtle, turtlePlayer, direction, extraArguments, outErrorMessage);
turtlePlayer.getInventory().clearContent(); turtlePlayer.player().getInventory().clearContent();
return result; return result;
} }
@ -91,7 +91,7 @@ public class TurtlePlaceCommand implements ITurtleCommand {
@Nullable Object[] extraArguments, @Nullable ErrorMessage outErrorMessage @Nullable Object[] extraArguments, @Nullable ErrorMessage outErrorMessage
) { ) {
// Deploy on an entity // Deploy on an entity
if (deployOnEntity(stack, turtle, turtlePlayer)) return true; if (deployOnEntity(turtle, turtlePlayer)) return true;
var position = turtle.getPosition(); var position = turtle.getPosition();
var newPosition = position.relative(direction); var newPosition = position.relative(direction);
@ -107,11 +107,11 @@ public class TurtlePlaceCommand implements ITurtleCommand {
|| deployOnBlock(stack, turtle, turtlePlayer, position, direction, extraArguments, false, outErrorMessage); || deployOnBlock(stack, turtle, turtlePlayer, position, direction, extraArguments, false, outErrorMessage);
} }
private static boolean deployOnEntity(ItemStack stack, final ITurtleAccess turtle, TurtlePlayer turtlePlayer) { private static boolean deployOnEntity(ITurtleAccess turtle, TurtlePlayer turtlePlayer) {
// See if there is an entity present // See if there is an entity present
final var world = turtle.getLevel(); var world = turtle.getLevel();
var turtlePos = turtlePlayer.position(); var turtlePos = turtlePlayer.player().position();
var rayDir = turtlePlayer.getViewVector(1.0f); var rayDir = turtlePlayer.player().getViewVector(1.0f);
var hit = WorldUtil.clip(world, turtlePos, rayDir, 1.5, null); var hit = WorldUtil.clip(world, turtlePos, rayDir, 1.5, null);
if (!(hit instanceof EntityHitResult entityHit)) return false; if (!(hit instanceof EntityHitResult entityHit)) return false;
@ -119,49 +119,17 @@ public class TurtlePlaceCommand implements ITurtleCommand {
var hitEntity = entityHit.getEntity(); var hitEntity = entityHit.getEntity();
var hitPos = entityHit.getLocation(); var hitPos = entityHit.getLocation();
DropConsumer.set(hitEntity, drop -> InventoryUtil.storeItemsFromOffset(turtlePlayer.getInventory(), drop, 1)); DropConsumer.set(hitEntity, drop -> InventoryUtil.storeItemsFromOffset(turtlePlayer.player().getInventory(), drop, 1));
var placed = PlatformHelper.get().interactWithEntity(turtlePlayer.player(), hitEntity, hitPos);
var placed = doDeployOnEntity(stack, turtlePlayer, hitEntity, hitPos);
TurtleUtil.stopConsuming(turtle); TurtleUtil.stopConsuming(turtle);
return placed; return placed;
} }
/**
* Place a block onto an entity. For instance, feeding cows.
*
* @param stack The stack we're placing.
* @param turtlePlayer The player of the turtle we're placing.
* @param hitEntity The entity we're interacting with.
* @param hitPos The position our ray trace hit the entity.
* @return If this item was deployed.
* @see net.minecraft.server.network.ServerGamePacketListenerImpl#handleInteract(ServerboundInteractPacket)
* @see net.minecraft.world.entity.player.Player#interactOn(Entity, InteractionHand)
*/
private static boolean doDeployOnEntity(ItemStack stack, TurtlePlayer turtlePlayer, Entity hitEntity, Vec3 hitPos) {
// Placing "onto" a block follows two flows. First we try to interactAt. If that doesn't succeed, then we try to
// call the normal interact path. Cancelling an interactAt *does not* cancel a normal interact path.
var interactAt = ForgeHooks.onInteractEntityAt(turtlePlayer, hitEntity, hitPos, InteractionHand.MAIN_HAND);
if (interactAt == null) interactAt = hitEntity.interactAt(turtlePlayer, hitPos, InteractionHand.MAIN_HAND);
if (interactAt.consumesAction()) return true;
var interact = ForgeHooks.onInteractEntity(turtlePlayer, hitEntity, InteractionHand.MAIN_HAND);
if (interact != null) return interact.consumesAction();
if (hitEntity.interact(turtlePlayer, InteractionHand.MAIN_HAND).consumesAction()) return true;
if (hitEntity instanceof LivingEntity hitLiving) {
return stack.interactLivingEntity(turtlePlayer, hitLiving, InteractionHand.MAIN_HAND).consumesAction();
}
return false;
}
private static boolean canDeployOnBlock( private static boolean canDeployOnBlock(
BlockPlaceContext context, ITurtleAccess turtle, TurtlePlayer player, BlockPos position, BlockPlaceContext context, ITurtleAccess turtle, TurtlePlayer player, BlockPos position,
Direction side, boolean allowReplaceable, @Nullable ErrorMessage outErrorMessage Direction side, boolean allowReplaceable, @Nullable ErrorMessage outErrorMessage
) { ) {
var world = turtle.getLevel(); var world = (ServerLevel) turtle.getLevel();
if (!world.isInWorldBounds(position) || world.isEmptyBlock(position) || if (!world.isInWorldBounds(position) || world.isEmptyBlock(position) ||
(context.getItemInHand().getItem() instanceof BlockItem && WorldUtil.isLiquidBlock(world, position))) { (context.getItemInHand().getItem() instanceof BlockItem && WorldUtil.isLiquidBlock(world, position))) {
return false; return false;
@ -172,15 +140,13 @@ public class TurtlePlaceCommand implements ITurtleCommand {
var replaceable = state.canBeReplaced(context); var replaceable = state.canBeReplaced(context);
if (!allowReplaceable && replaceable) return false; if (!allowReplaceable && replaceable) return false;
if (Config.turtlesObeyBlockProtection) { // Check spawn protection
// Check spawn protection var isProtected = replaceable
var editable = replaceable ? player.isBlockProtected(world, position)
? TurtlePermissions.isBlockEditable(world, position, player) : player.isBlockProtected(world, position.relative(side));
: TurtlePermissions.isBlockEditable(world, position.relative(side), player); if (isProtected) {
if (!editable) { if (outErrorMessage != null) outErrorMessage.message = "Cannot place in protected area";
if (outErrorMessage != null) outErrorMessage.message = "Cannot place in protected area"; return false;
return false;
}
} }
return true; return true;
@ -203,7 +169,7 @@ public class TurtlePlaceCommand implements ITurtleCommand {
// Check if there's something suitable to place onto // Check if there's something suitable to place onto
var hit = new BlockHitResult(new Vec3(hitX, hitY, hitZ), side, position, false); var hit = new BlockHitResult(new Vec3(hitX, hitY, hitZ), side, position, false);
var context = new UseOnContext(turtlePlayer, InteractionHand.MAIN_HAND, hit); var context = new UseOnContext(turtlePlayer.player(), InteractionHand.MAIN_HAND, hit);
if (!canDeployOnBlock(new BlockPlaceContext(context), turtle, turtlePlayer, position, side, allowReplace, outErrorMessage)) { if (!canDeployOnBlock(new BlockPlaceContext(context), turtle, turtlePlayer, position, side, allowReplace, outErrorMessage)) {
return false; return false;
} }
@ -211,7 +177,7 @@ public class TurtlePlaceCommand implements ITurtleCommand {
var item = stack.getItem(); var item = stack.getItem();
var existingTile = turtle.getLevel().getBlockEntity(position); var existingTile = turtle.getLevel().getBlockEntity(position);
var placed = doDeployOnBlock(stack, turtlePlayer, position, context, hit).consumesAction(); var placed = doDeployOnBlock(stack, turtlePlayer, hit).consumesAction();
// Set text on signs // Set text on signs
if (placed && item instanceof SignItem && extraArguments != null && extraArguments.length >= 1 && extraArguments[0] instanceof String message) { if (placed && item instanceof SignItem && extraArguments != null && extraArguments.length >= 1 && extraArguments[0] instanceof String message) {
@ -232,38 +198,19 @@ public class TurtlePlaceCommand implements ITurtleCommand {
* *
* @param stack The stack the player is using. * @param stack The stack the player is using.
* @param turtlePlayer The player which represents the turtle * @param turtlePlayer The player which represents the turtle
* @param position The block we're deploying against's position.
* @param context The context of this place action.
* @param hit Where the block we're placing against was clicked. * @param hit Where the block we're placing against was clicked.
* @return If this item was deployed. * @return If this item was deployed.
* @see net.minecraft.server.level.ServerPlayerGameMode#useItemOn For the original implementation.
*/ */
private static InteractionResult doDeployOnBlock( private static InteractionResult doDeployOnBlock(
ItemStack stack, TurtlePlayer turtlePlayer, BlockPos position, UseOnContext context, BlockHitResult hit ItemStack stack, TurtlePlayer turtlePlayer, BlockHitResult hit
) { ) {
var event = ForgeHooks.onRightClickBlock(turtlePlayer, InteractionHand.MAIN_HAND, position, hit); var result = PlatformHelper.get().useOn(turtlePlayer.player(), stack, hit);
if (event.isCanceled()) return event.getCancellationResult(); if (result != InteractionResult.PASS) return result;
if (event.getUseItem() != Result.DENY) {
var result = stack.onItemUseFirst(context);
if (result != InteractionResult.PASS) return result;
}
if (event.getUseItem() != Result.DENY) {
var result = stack.useOn(context);
if (result != InteractionResult.PASS) return result;
}
// We special case some items which we allow to place "normally". Yes, this is very ugly.
var item = stack.getItem(); var item = stack.getItem();
if (item instanceof BucketItem || item instanceof BoatItem || item instanceof PlaceOnWaterBlockItem || item instanceof BottleItem) { if (item.getUseDuration(stack) == 0) {
var actionResult = ForgeHooks.onItemRightClick(turtlePlayer, InteractionHand.MAIN_HAND); return turtlePlayer.player().gameMode.useItem(turtlePlayer.player(), turtlePlayer.player().level, stack, InteractionHand.MAIN_HAND);
if (actionResult != null && actionResult != InteractionResult.PASS) return actionResult;
var result = stack.use(context.getLevel(), turtlePlayer, InteractionHand.MAIN_HAND);
if (result.getResult().consumesAction() && !ItemStack.matches(stack, result.getObject())) {
turtlePlayer.setItemInHand(InteractionHand.MAIN_HAND, result.getObject());
return result.getResult();
}
} }
return InteractionResult.PASS; return InteractionResult.PASS;
@ -271,11 +218,11 @@ public class TurtlePlaceCommand implements ITurtleCommand {
private static void setSignText(Level world, BlockEntity tile, String message) { private static void setSignText(Level world, BlockEntity tile, String message) {
var signTile = (SignBlockEntity) tile; var signTile = (SignBlockEntity) tile;
var split = message.split("\n"); var split = Splitter.on('\n').splitToList(message);
var firstLine = split.length <= 2 ? 1 : 0; var firstLine = split.size() <= 2 ? 1 : 0;
for (var i = 0; i < 4; i++) { for (var i = 0; i < 4; i++) {
if (i >= firstLine && i < firstLine + split.length) { if (i >= firstLine && i < firstLine + split.size()) {
var line = split[i - firstLine]; var line = split.get(i - firstLine);
signTile.setMessage(i, line.length() > 15 signTile.setMessage(i, line.length() > 15
? Component.literal(line.substring(0, 15)) ? Component.literal(line.substring(0, 15))
: Component.literal(line) : Component.literal(line)

View File

@ -7,56 +7,40 @@ package dan200.computercraft.shared.turtle.core;
import com.mojang.authlib.GameProfile; import com.mojang.authlib.GameProfile;
import dan200.computercraft.api.turtle.ITurtleAccess; import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.turtle.TurtleUtil; import dan200.computercraft.shared.turtle.TurtleUtil;
import dan200.computercraft.shared.util.DirectionUtil; import dan200.computercraft.shared.util.DirectionUtil;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.Container; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityDimensions;
import net.minecraft.world.entity.Pose;
import net.minecraft.world.entity.animal.horse.AbstractHorse;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.entity.SignBlockEntity;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.common.util.FakePlayer;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.OptionalInt;
import java.util.UUID; import java.util.UUID;
public final class TurtlePlayer extends FakePlayer { public final class TurtlePlayer {
private static final Logger LOG = LoggerFactory.getLogger(TurtlePlayer.class); private static final Logger LOGGER = LoggerFactory.getLogger(TurtlePlayer.class);
private static final GameProfile DEFAULT_PROFILE = new GameProfile( private static final GameProfile DEFAULT_PROFILE = new GameProfile(
UUID.fromString("0d0c4ca0-4ff1-11e4-916c-0800200c9a66"), UUID.fromString("0d0c4ca0-4ff1-11e4-916c-0800200c9a66"),
"[ComputerCraft]" "[ComputerCraft]"
); );
private TurtlePlayer(ServerLevel world, GameProfile name) { private final ServerPlayer player;
super(world, name);
private TurtlePlayer(ServerPlayer player) {
this.player = player;
} }
private static TurtlePlayer create(ITurtleAccess turtle) { private static TurtlePlayer create(ITurtleAccess turtle) {
var world = (ServerLevel) turtle.getLevel(); var world = (ServerLevel) turtle.getLevel();
var profile = turtle.getOwningPlayer(); var profile = turtle.getOwningPlayer();
var player = new TurtlePlayer(world, getProfile(profile)); var player = new TurtlePlayer(PlatformHelper.get().createFakePlayer(world, getProfile(profile)));
player.setState(turtle); player.setState(turtle);
if (profile != null && profile.getId() != null) {
// Constructing a player overrides the "active player" variable in advancements. As fake players cannot
// get advancements, this prevents a normal player who has placed a turtle from getting advancements.
// We try to locate the "actual" player and restore them.
var actualPlayer = world.getServer().getPlayerList().getPlayer(profile.getId());
if (actualPlayer != null) player.getAdvancements().setPlayer(actualPlayer);
}
return player; return player;
} }
@ -65,11 +49,11 @@ public final class TurtlePlayer extends FakePlayer {
} }
public static TurtlePlayer get(ITurtleAccess access) { public static TurtlePlayer get(ITurtleAccess access) {
if (!(access instanceof TurtleBrain brain)) return create(access); if (!(access instanceof TurtleBrain brain)) throw new IllegalStateException("ITurtleAccess is not a brain");
var player = brain.cachedPlayer; var player = brain.cachedPlayer;
if (player == null || player.getGameProfile() != getProfile(access.getOwningPlayer()) if (player == null || player.player.getGameProfile() != getProfile(access.getOwningPlayer())
|| player.getCommandSenderWorld() != access.getLevel()) { || player.player.getCommandSenderWorld() != access.getLevel()) {
player = brain.cachedPlayer = create(brain); player = brain.cachedPlayer = create(brain);
} else { } else {
player.setState(access); player.setState(access);
@ -84,18 +68,26 @@ public final class TurtlePlayer extends FakePlayer {
return turtlePlayer; return turtlePlayer;
} }
public ServerPlayer player() {
return player;
}
private void setRotation(float y, float x) {
player.setYRot(y);
player.setXRot(x);
}
private void setState(ITurtleAccess turtle) { private void setState(ITurtleAccess turtle) {
if (containerMenu != inventoryMenu) { if (player.containerMenu != player.inventoryMenu) {
LOG.warn("Turtle has open container ({})", containerMenu); LOGGER.warn("Turtle has open container ({})", player.containerMenu);
doCloseContainer(); player.doCloseContainer();
} }
var position = turtle.getPosition(); var position = turtle.getPosition();
setPosRaw(position.getX() + 0.5, position.getY() + 0.5, position.getZ() + 0.5); player.setPosRaw(position.getX() + 0.5, position.getY() + 0.5, position.getZ() + 0.5);
setRotation(turtle.getDirection().toYRot(), 0);
setRot(turtle.getDirection().toYRot(), 0); player.getInventory().clearContent();
getInventory().clearContent();
} }
public void setPosition(ITurtleAccess turtle, BlockPos position, Direction direction) { public void setPosition(ITurtleAccess turtle, BlockPos position, Direction direction) {
@ -111,116 +103,62 @@ public final class TurtlePlayer extends FakePlayer {
} }
if (direction.getAxis() != Direction.Axis.Y) { if (direction.getAxis() != Direction.Axis.Y) {
setRot(direction.toYRot(), 0); setRotation(direction.toYRot(), 0);
} else { } else {
setRot(turtle.getDirection().toYRot(), DirectionUtil.toPitchAngle(direction)); setRotation(turtle.getDirection().toYRot(), DirectionUtil.toPitchAngle(direction));
} }
setPosRaw(posX, posY, posZ); player.setPosRaw(posX, posY, posZ);
xo = posX; player.xo = posX;
yo = posY; player.yo = posY;
zo = posZ; player.zo = posZ;
xRotO = getXRot(); player.xRotO = player.getXRot();
yRotO = getYRot(); player.yHeadRotO = player.yHeadRot = player.yRotO = player.getYRot();
yHeadRot = getYRot();
yHeadRotO = yHeadRot;
} }
public void loadInventory(ItemStack stack) { public void loadInventory(ItemStack stack) {
getInventory().clearContent(); player.getInventory().clearContent();
getInventory().selected = 0; player.getInventory().selected = 0;
getInventory().setItem(0, stack); player.getInventory().setItem(0, stack);
} }
public void loadInventory(ITurtleAccess turtle) { public void loadInventory(ITurtleAccess turtle) {
getInventory().clearContent(); var inventory = player.getInventory();
var turtleInventory = turtle.getInventory();
var currentSlot = turtle.getSelectedSlot(); var currentSlot = turtle.getSelectedSlot();
var slots = turtle.getInventory().getContainerSize(); var slots = turtleInventory.getContainerSize();
// Load up the fake inventory // Load up the fake inventory
getInventory().selected = 0; inventory.selected = 0;
for (var i = 0; i < slots; i++) { for (var i = 0; i < slots; i++) {
getInventory().setItem(i, turtle.getInventory().getItem((currentSlot + i) % slots)); inventory.setItem(i, turtleInventory.getItem((currentSlot + i) % slots));
} }
} }
public void unloadInventory(ITurtleAccess turtle) { public void unloadInventory(ITurtleAccess turtle) {
if (player.isUsingItem()) player.stopUsingItem();
var inventory = player.getInventory();
var turtleInventory = turtle.getInventory();
var currentSlot = turtle.getSelectedSlot(); var currentSlot = turtle.getSelectedSlot();
var slots = turtle.getInventory().getContainerSize(); var slots = turtleInventory.getContainerSize();
// Load up the fake inventory // Load up the fake inventory
getInventory().selected = 0; inventory.selected = 0;
for (var i = 0; i < slots; i++) { for (var i = 0; i < slots; i++) {
turtle.getInventory().setItem((currentSlot + i) % slots, getInventory().getItem(i)); turtleInventory.setItem((currentSlot + i) % slots, inventory.getItem(i));
} }
// Store (or drop) anything else we found // Store (or drop) anything else we found
var totalSize = getInventory().getContainerSize(); var totalSize = inventory.getContainerSize();
for (var i = slots; i < totalSize; i++) { for (var i = slots; i < totalSize; i++) {
TurtleUtil.storeItemOrDrop(turtle, getInventory().getItem(i)); TurtleUtil.storeItemOrDrop(turtle, inventory.getItem(i));
} }
getInventory().setChanged(); inventory.setChanged();
} }
@Override public boolean isBlockProtected(ServerLevel level, BlockPos pos) {
public Vec3 position() { return level.getServer().isUnderSpawnProtection(level, pos, player);
return new Vec3(getX(), getY(), getZ());
} }
@Override
public float getEyeHeight(Pose pose) {
return 0;
}
@Override
public float getStandingEyeHeight(Pose pose, EntityDimensions size) {
return 0;
}
//region Code which depends on the connection
@Override
public OptionalInt openMenu(@Nullable MenuProvider prover) {
return OptionalInt.empty();
}
@Override
public void onEnterCombat() {
}
@Override
public void onLeaveCombat() {
}
@Override
public boolean startRiding(Entity entityIn, boolean force) {
return false;
}
@Override
public void stopRiding() {
}
@Override
public void openTextEdit(SignBlockEntity signTile) {
}
@Override
public void openHorseInventory(AbstractHorse horse, Container inventory) {
}
@Override
public void openItemGui(ItemStack stack, InteractionHand hand) {
}
@Override
public void closeContainer() {
}
@Override
protected void onEffectRemoved(MobEffectInstance effect) {
}
//endregion
} }

View File

@ -70,7 +70,7 @@ public class TurtleInventoryCrafting extends CraftingContainer {
// Special case: craft(0) just returns an empty list if crafting was possible // Special case: craft(0) just returns an empty list if crafting was possible
if (maxCount == 0) return Collections.emptyList(); if (maxCount == 0) return Collections.emptyList();
var player = TurtlePlayer.get(turtle); var player = TurtlePlayer.get(turtle).player();
var results = new ArrayList<ItemStack>(); var results = new ArrayList<ItemStack>();
for (var i = 0; i < maxCount && recipe.matches(this, world); i++) { for (var i = 0; i < maxCount && recipe.matches(this, world); i++) {

View File

@ -7,8 +7,7 @@ package dan200.computercraft.shared.turtle.upgrades;
import dan200.computercraft.api.ComputerCraftTags; import dan200.computercraft.api.ComputerCraftTags;
import dan200.computercraft.api.turtle.*; import dan200.computercraft.api.turtle.*;
import dan200.computercraft.shared.TurtlePermissions; import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.turtle.TurtleUtil; import dan200.computercraft.shared.turtle.TurtleUtil;
import dan200.computercraft.shared.turtle.core.TurtlePlaceCommand; import dan200.computercraft.shared.turtle.core.TurtlePlaceCommand;
import dan200.computercraft.shared.turtle.core.TurtlePlayer; import dan200.computercraft.shared.turtle.core.TurtlePlayer;
@ -17,22 +16,22 @@ import dan200.computercraft.shared.util.WorldUtil;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.TagKey; import net.minecraft.tags.TagKey;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.decoration.ArmorStand; import net.minecraft.world.entity.decoration.ArmorStand;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.GameMasterBlock;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.EntityHitResult; import net.minecraft.world.phys.EntityHitResult;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.ToolActions;
import net.minecraftforge.event.entity.player.AttackEntityEvent;
import net.minecraftforge.event.level.BlockEvent;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -63,8 +62,7 @@ public class TurtleTool extends AbstractTurtleUpgrade {
// Check we've not got anything vaguely interesting on the item. We allow other mods to add their // Check we've not got anything vaguely interesting on the item. We allow other mods to add their
// own NBT, with the understanding such details will be lost to the mist of time. // own NBT, with the understanding such details will be lost to the mist of time.
if (stack.isDamaged() || stack.isEnchanted() || stack.hasCustomHoverName()) return false; if (stack.isDamaged() || stack.isEnchanted() || stack.hasCustomHoverName()) return false;
if (tag.contains("AttributeModifiers", TAG_LIST) && if (tag.contains("AttributeModifiers", TAG_LIST) && !tag.getList("AttributeModifiers", TAG_COMPOUND).isEmpty()) {
!tag.getList("AttributeModifiers", TAG_COMPOUND).isEmpty()) {
return false; return false;
} }
@ -79,11 +77,9 @@ public class TurtleTool extends AbstractTurtleUpgrade {
}; };
} }
protected TurtleCommandResult checkBlockBreakable(BlockState state, Level world, BlockPos pos, TurtlePlayer player) { protected TurtleCommandResult checkBlockBreakable(Level world, BlockPos pos, TurtlePlayer player) {
var block = state.getBlock(); var state = world.getBlockState(pos);
if (state.isAir() || block == Blocks.BEDROCK if (state.isAir() || state.getBlock() instanceof GameMasterBlock || state.getDestroyProgress(player.player(), world, pos) <= 0) {
|| state.getDestroyProgress(player, world, pos) <= 0
|| !block.canEntityDestroy(state, world, pos, player)) {
return UNBREAKABLE; return UNBREAKABLE;
} }
@ -91,6 +87,16 @@ public class TurtleTool extends AbstractTurtleUpgrade {
? TurtleCommandResult.success() : INEFFECTIVE; ? TurtleCommandResult.success() : INEFFECTIVE;
} }
/**
* Attack an entity. This is a <em>very</em> cut down version of {@link Player#attack(Entity)}, which doesn't handle
* enchantments, knockback, etc... Unfortunately we can't call attack directly as damage calculations are rather
* different (and we don't want to play sounds/particles).
*
* @param turtle The current turtle.
* @param direction The direction we're attacking in.
* @return Whether an attack occurred.
* @see Player#attack(Entity)
*/
private TurtleCommandResult attack(ITurtleAccess turtle, Direction direction) { private TurtleCommandResult attack(ITurtleAccess turtle, Direction direction) {
// Create a fake player, and orient it appropriately // Create a fake player, and orient it appropriately
var world = turtle.getLevel(); var world = turtle.getLevel();
@ -99,8 +105,9 @@ public class TurtleTool extends AbstractTurtleUpgrade {
final var turtlePlayer = TurtlePlayer.getWithPosition(turtle, position, direction); final var turtlePlayer = TurtlePlayer.getWithPosition(turtle, position, direction);
// See if there is an entity present // See if there is an entity present
var turtlePos = turtlePlayer.position(); var player = turtlePlayer.player();
var rayDir = turtlePlayer.getViewVector(1.0f); var turtlePos = player.position();
var rayDir = player.getViewVector(1.0f);
var hit = WorldUtil.clip(world, turtlePos, rayDir, 1.5, null); var hit = WorldUtil.clip(world, turtlePos, rayDir, 1.5, null);
if (hit instanceof EntityHitResult entityHit) { if (hit instanceof EntityHitResult entityHit) {
// Load up the turtle's inventory // Load up the turtle's inventory
@ -109,20 +116,18 @@ public class TurtleTool extends AbstractTurtleUpgrade {
var hitEntity = entityHit.getEntity(); var hitEntity = entityHit.getEntity();
// Fire several events to ensure we have permissions.
if (MinecraftForge.EVENT_BUS.post(new AttackEntityEvent(turtlePlayer, hitEntity)) || !hitEntity.isAttackable()) {
return TurtleCommandResult.failure("Nothing to attack here");
}
// Start claiming entity drops // Start claiming entity drops
DropConsumer.set(hitEntity, TurtleUtil.dropConsumer(turtle)); DropConsumer.set(hitEntity, TurtleUtil.dropConsumer(turtle));
// Attack the entity // Attack the entity
var attacked = false; var attacked = false;
if (!hitEntity.skipAttackInteraction(turtlePlayer)) { var result = PlatformHelper.get().canAttackEntity(player, hitEntity);
var damage = (float) turtlePlayer.getAttributeValue(Attributes.ATTACK_DAMAGE) * damageMulitiplier; if (result.consumesAction()) {
attacked = true;
} else if (result == InteractionResult.PASS && hitEntity.isAttackable() && !hitEntity.skipAttackInteraction(player)) {
var damage = (float) player.getAttributeValue(Attributes.ATTACK_DAMAGE) * damageMulitiplier;
if (damage > 0.0f) { if (damage > 0.0f) {
var source = DamageSource.playerAttack(turtlePlayer); var source = DamageSource.playerAttack(player);
if (hitEntity instanceof ArmorStand) { if (hitEntity instanceof ArmorStand) {
// Special case for armor stands: attack twice to guarantee destroy // Special case for armor stands: attack twice to guarantee destroy
hitEntity.hurt(source, damage); hitEntity.hurt(source, damage);
@ -138,78 +143,42 @@ public class TurtleTool extends AbstractTurtleUpgrade {
TurtleUtil.stopConsuming(turtle); TurtleUtil.stopConsuming(turtle);
// Put everything we collected into the turtles inventory, then return // Put everything we collected into the turtles inventory, then return
if (attacked) { player.getInventory().clearContent();
turtlePlayer.getInventory().clearContent(); if (attacked) return TurtleCommandResult.success();
return TurtleCommandResult.success();
}
} }
return TurtleCommandResult.failure("Nothing to attack here"); return TurtleCommandResult.failure("Nothing to attack here");
} }
private TurtleCommandResult dig(ITurtleAccess turtle, Direction direction) { private TurtleCommandResult dig(ITurtleAccess turtle, Direction direction) {
if (item.canPerformAction(ToolActions.SHOVEL_FLATTEN) || item.canPerformAction(ToolActions.HOE_TILL)) { if (PlatformHelper.get().hasToolUsage(item) && TurtlePlaceCommand.deployCopiedItem(item.copy(), turtle, direction, null, null)) {
if (TurtlePlaceCommand.deployCopiedItem(item.copy(), turtle, direction, null, null)) { return TurtleCommandResult.success();
return TurtleCommandResult.success();
}
} }
// Get ready to dig var level = (ServerLevel) turtle.getLevel();
var world = turtle.getLevel();
var turtlePosition = turtle.getPosition(); var turtlePosition = turtle.getPosition();
var blockPosition = turtlePosition.relative(direction); var blockPosition = turtlePosition.relative(direction);
if (world.isEmptyBlock(blockPosition) || WorldUtil.isLiquidBlock(world, blockPosition)) { if (level.isEmptyBlock(blockPosition) || WorldUtil.isLiquidBlock(level, blockPosition)) {
return TurtleCommandResult.failure("Nothing to dig here"); return TurtleCommandResult.failure("Nothing to dig here");
} }
var state = world.getBlockState(blockPosition);
var fluidState = world.getFluidState(blockPosition);
var turtlePlayer = TurtlePlayer.getWithPosition(turtle, turtlePosition, direction); var turtlePlayer = TurtlePlayer.getWithPosition(turtle, turtlePosition, direction);
turtlePlayer.loadInventory(item.copy()); turtlePlayer.loadInventory(item.copy());
if (Config.turtlesObeyBlockProtection) {
// Check spawn protection
if (MinecraftForge.EVENT_BUS.post(new BlockEvent.BreakEvent(world, blockPosition, state, turtlePlayer))) {
return TurtleCommandResult.failure("Cannot break protected block");
}
if (!TurtlePermissions.isBlockEditable(world, blockPosition, turtlePlayer)) {
return TurtleCommandResult.failure("Cannot break protected block");
}
}
// Check if we can break the block // Check if we can break the block
var breakable = checkBlockBreakable(state, world, blockPosition, turtlePlayer); var breakable = checkBlockBreakable(level, blockPosition, turtlePlayer);
if (!breakable.isSuccess()) return breakable; if (!breakable.isSuccess()) return breakable;
// Consume the items the block drops DropConsumer.set(level, blockPosition, TurtleUtil.dropConsumer(turtle));
DropConsumer.set(world, blockPosition, TurtleUtil.dropConsumer(turtle)); var broken = !turtlePlayer.isBlockProtected(level, blockPosition) && turtlePlayer.player().gameMode.destroyBlock(blockPosition);
var tile = world.getBlockEntity(blockPosition);
// Much of this logic comes from PlayerInteractionManager#tryHarvestBlock, so it's a good idea
// to consult there before making any changes.
// Play the destruction sound and particles
world.levelEvent(2001, blockPosition, Block.getId(state));
// Destroy the block
var canHarvest = state.canHarvestBlock(world, blockPosition, turtlePlayer);
var canBreak = state.onDestroyedByPlayer(world, blockPosition, turtlePlayer, canHarvest, fluidState);
if (canBreak) state.getBlock().destroy(world, blockPosition, state);
if (canHarvest && canBreak) {
state.getBlock().playerDestroy(world, turtlePlayer, blockPosition, state, tile, turtlePlayer.getMainHandItem());
}
TurtleUtil.stopConsuming(turtle); TurtleUtil.stopConsuming(turtle);
return TurtleCommandResult.success(); // Check spawn protection
return broken ? TurtleCommandResult.success() : TurtleCommandResult.failure("Cannot break protected block");
} }
protected boolean isTriviallyBreakable(BlockGetter reader, BlockPos pos, BlockState state) { private static boolean isTriviallyBreakable(BlockGetter reader, BlockPos pos, BlockState state) {
return state.is(ComputerCraftTags.Blocks.TURTLE_ALWAYS_BREAKABLE) return state.is(ComputerCraftTags.Blocks.TURTLE_ALWAYS_BREAKABLE)
// Allow breaking any "instabreak" block. // Allow breaking any "instabreak" block.
|| state.getDestroySpeed(reader, pos) == 0; || state.getDestroySpeed(reader, pos) == 0;

View File

@ -10,6 +10,9 @@ import dan200.computercraft.api.detail.VanillaDetailRegistries
import dan200.computercraft.api.lua.ObjectArguments import dan200.computercraft.api.lua.ObjectArguments
import dan200.computercraft.core.apis.PeripheralAPI import dan200.computercraft.core.apis.PeripheralAPI
import dan200.computercraft.gametest.api.* import dan200.computercraft.gametest.api.*
import dan200.computercraft.gametest.core.TestHooks
import dan200.computercraft.mixin.gametest.GameTestHelperAccessor
import dan200.computercraft.mixin.gametest.GameTestInfoAccessor
import dan200.computercraft.shared.ModRegistry import dan200.computercraft.shared.ModRegistry
import dan200.computercraft.shared.media.items.ItemPrintout import dan200.computercraft.shared.media.items.ItemPrintout
import dan200.computercraft.shared.peripheral.monitor.BlockMonitor import dan200.computercraft.shared.peripheral.monitor.BlockMonitor
@ -396,10 +399,13 @@ class Turtle_Test {
*/ */
@GameTest @GameTest
fun Peripheral_change(helper: GameTestHelper) = helper.sequence { fun Peripheral_change(helper: GameTestHelper) = helper.sequence {
val testInfo = (helper as GameTestHelperAccessor).testInfo as GameTestInfoAccessor
val events = mutableListOf<Pair<String, String>>() val events = mutableListOf<Pair<String, String>>()
thenStartComputer("listen") { thenStartComputer("listen") {
while (true) { while (true) {
val event = pullEvent() val event = pullEvent()
TestHooks.LOG.info("[{}] Got event {} at tick {}", testInfo, event[0], testInfo.`computercraft$getTick`())
if (event[0] == "peripheral" || event[0] == "peripheral_detach") { if (event[0] == "peripheral" || event[0] == "peripheral_detach") {
events.add((event[0] as String) to (event[1] as String)) events.add((event[0] as String) to (event[1] as String))
} }
@ -408,8 +414,9 @@ class Turtle_Test {
thenOnComputer("turtle") { thenOnComputer("turtle") {
turtle.forward().await().assertArrayEquals(true, message = "Moved turtle forward") turtle.forward().await().assertArrayEquals(true, message = "Moved turtle forward")
turtle.back().await().assertArrayEquals(true, message = "Moved turtle forward") turtle.back().await().assertArrayEquals(true, message = "Moved turtle forward")
TestHooks.LOG.info("[{}] Finished turtle at {}", testInfo, testInfo.`computercraft$getTick`())
} }
thenIdle(2) // Should happen immediately, but computers might be slow. thenIdle(3) // Should happen immediately, but computers might be slow.
thenExecute { thenExecute {
assertEquals( assertEquals(
listOf( listOf(