1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-06-26 07:03:22 +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 int turtleFuelLimit = 20000;
public static int advancedTurtleFuelLimit = 100000;
public static boolean turtlesObeyBlockProtection = true;
public static boolean turtlesCanPush = true;
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<Integer> turtleFuelLimit;
private static final ConfigValue<Integer> advancedTurtleFuelLimit;
private static final ConfigValue<Boolean> turtlesObeyBlockProtection;
private static final ConfigValue<Boolean> turtlesCanPush;
private static final ConfigValue<Integer> computerTermWidth;
@ -283,12 +282,6 @@ CIDR notation ("127.0.0.0/8").
.comment("The fuel limit for Advanced Turtles.")
.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
.comment("""
If set to true, Turtles will push entities out of the way instead of stopping if
@ -400,7 +393,6 @@ private static void syncServer() {
Config.turtlesNeedFuel = turtlesNeedFuel.get();
Config.turtleFuelLimit = turtleFuelLimit.get();
Config.advancedTurtleFuelLimit = advancedTurtleFuelLimit.get();
Config.turtlesObeyBlockProtection = turtlesObeyBlockProtection.get();
Config.turtlesCanPush = turtlesCanPush.get();
// 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;
import com.mojang.authlib.GameProfile;
import dan200.computercraft.api.network.wired.IWiredElement;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.shared.network.NetworkMessage;
@ -20,10 +21,11 @@
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
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.world.Container;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.WorldlyContainer;
import net.minecraft.world.*;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.CraftingContainer;
@ -39,6 +41,7 @@
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.items.IItemHandlerModifiable;
@ -296,6 +299,15 @@ static PlatformHelper get() {
*/
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.
*
@ -316,4 +328,55 @@ default boolean isFakePlayer(ServerPlayer player) {
default double getReachDistance(Player player) {
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;
import com.google.auto.service.AutoService;
import com.mojang.authlib.GameProfile;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.network.wired.IWiredElement;
import dan200.computercraft.api.peripheral.IPeripheral;
@ -27,9 +28,8 @@
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.TagKey;
import net.minecraft.world.Container;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.WorldlyContainerHolder;
import net.minecraft.world.*;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.CraftingContainer;
@ -37,6 +37,7 @@
import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.Item;
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.Recipe;
import net.minecraft.world.level.Level;
@ -45,14 +46,17 @@
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.common.ForgeHooks;
import net.minecraftforge.common.Tags;
import net.minecraftforge.common.ToolActions;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.ForgeCapabilities;
import net.minecraftforge.common.extensions.IForgeMenuType;
import net.minecraftforge.common.util.NonNullConsumer;
import net.minecraftforge.event.ForgeEventFactory;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.items.IItemHandlerModifiable;
import net.minecraftforge.items.wrapper.InvWrapper;
@ -271,11 +275,52 @@ public boolean onNotifyNeighbour(Level level, BlockPos pos, BlockState block, Di
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
public double getReachDistance(Player player) {
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>(
ResourceLocation name, ForgeRegistry<T> registry
) implements Registries.RegistryWrapper<T> {

View File

@ -9,12 +9,11 @@
import dan200.computercraft.api.turtle.ITurtleCommand;
import dan200.computercraft.api.turtle.TurtleAnimation;
import dan200.computercraft.api.turtle.TurtleCommandResult;
import dan200.computercraft.shared.TurtlePermissions;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.util.WorldUtil;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
@ -32,7 +31,7 @@ public TurtleCommandResult execute(ITurtleAccess turtle) {
var direction = this.direction.toWorldDir(turtle);
// Check if we can move
var oldWorld = turtle.getLevel();
var oldWorld = (ServerLevel) turtle.getLevel();
var oldPosition = turtle.getPosition();
var newPosition = oldPosition.relative(direction);
@ -97,14 +96,14 @@ public TurtleCommandResult execute(ITurtleAccess turtle) {
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)) {
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");
// Check spawn protection
if (Config.turtlesObeyBlockProtection && !TurtlePermissions.isBlockEnterable(world, position, turtlePlayer)) {
if (turtlePlayer.isBlockProtected(world, position)) {
return TurtleCommandResult.failure("Cannot enter protected area");
}

View File

@ -5,12 +5,12 @@
*/
package dan200.computercraft.shared.turtle.core;
import com.google.common.base.Splitter;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleCommand;
import dan200.computercraft.api.turtle.TurtleAnimation;
import dan200.computercraft.api.turtle.TurtleCommandResult;
import dan200.computercraft.shared.TurtlePermissions;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.turtle.TurtleUtil;
import dan200.computercraft.shared.util.DropConsumer;
import dan200.computercraft.shared.util.InventoryUtil;
@ -18,12 +18,12 @@
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
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.InteractionResult;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.*;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.SignItem;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level;
@ -33,8 +33,6 @@
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.common.ForgeHooks;
import net.minecraftforge.eventbus.api.Event.Result;
import javax.annotation.Nullable;
@ -76,13 +74,15 @@ public TurtleCommandResult execute(ITurtleAccess turtle) {
}
}
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
var playerPosition = turtle.getPosition().relative(direction);
var turtlePlayer = TurtlePlayer.getWithPosition(turtle, playerPosition, direction);
turtlePlayer.loadInventory(stack);
var result = deploy(stack, turtle, turtlePlayer, direction, extraArguments, outErrorMessage);
turtlePlayer.getInventory().clearContent();
turtlePlayer.player().getInventory().clearContent();
return result;
}
@ -91,7 +91,7 @@ private static boolean deploy(
@Nullable Object[] extraArguments, @Nullable ErrorMessage outErrorMessage
) {
// Deploy on an entity
if (deployOnEntity(stack, turtle, turtlePlayer)) return true;
if (deployOnEntity(turtle, turtlePlayer)) return true;
var position = turtle.getPosition();
var newPosition = position.relative(direction);
@ -107,11 +107,11 @@ private static boolean deploy(
|| 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
final var world = turtle.getLevel();
var turtlePos = turtlePlayer.position();
var rayDir = turtlePlayer.getViewVector(1.0f);
var world = turtle.getLevel();
var turtlePos = turtlePlayer.player().position();
var rayDir = turtlePlayer.player().getViewVector(1.0f);
var hit = WorldUtil.clip(world, turtlePos, rayDir, 1.5, null);
if (!(hit instanceof EntityHitResult entityHit)) return false;
@ -119,49 +119,17 @@ private static boolean deployOnEntity(ItemStack stack, final ITurtleAccess turtl
var hitEntity = entityHit.getEntity();
var hitPos = entityHit.getLocation();
DropConsumer.set(hitEntity, drop -> InventoryUtil.storeItemsFromOffset(turtlePlayer.getInventory(), drop, 1));
var placed = doDeployOnEntity(stack, turtlePlayer, hitEntity, hitPos);
DropConsumer.set(hitEntity, drop -> InventoryUtil.storeItemsFromOffset(turtlePlayer.player().getInventory(), drop, 1));
var placed = PlatformHelper.get().interactWithEntity(turtlePlayer.player(), hitEntity, hitPos);
TurtleUtil.stopConsuming(turtle);
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(
BlockPlaceContext context, ITurtleAccess turtle, TurtlePlayer player, BlockPos position,
Direction side, boolean allowReplaceable, @Nullable ErrorMessage outErrorMessage
) {
var world = turtle.getLevel();
var world = (ServerLevel) turtle.getLevel();
if (!world.isInWorldBounds(position) || world.isEmptyBlock(position) ||
(context.getItemInHand().getItem() instanceof BlockItem && WorldUtil.isLiquidBlock(world, position))) {
return false;
@ -172,15 +140,13 @@ private static boolean canDeployOnBlock(
var replaceable = state.canBeReplaced(context);
if (!allowReplaceable && replaceable) return false;
if (Config.turtlesObeyBlockProtection) {
// Check spawn protection
var editable = replaceable
? TurtlePermissions.isBlockEditable(world, position, player)
: TurtlePermissions.isBlockEditable(world, position.relative(side), player);
if (!editable) {
if (outErrorMessage != null) outErrorMessage.message = "Cannot place in protected area";
return false;
}
// Check spawn protection
var isProtected = replaceable
? player.isBlockProtected(world, position)
: player.isBlockProtected(world, position.relative(side));
if (isProtected) {
if (outErrorMessage != null) outErrorMessage.message = "Cannot place in protected area";
return false;
}
return true;
@ -203,7 +169,7 @@ private static boolean deployOnBlock(
// Check if there's something suitable to place onto
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)) {
return false;
}
@ -211,7 +177,7 @@ private static boolean deployOnBlock(
var item = stack.getItem();
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
if (placed && item instanceof SignItem && extraArguments != null && extraArguments.length >= 1 && extraArguments[0] instanceof String message) {
@ -232,38 +198,19 @@ private static boolean deployOnBlock(
*
* @param stack The stack the player is using.
* @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.
* @return If this item was deployed.
* @see net.minecraft.server.level.ServerPlayerGameMode#useItemOn For the original implementation.
*/
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);
if (event.isCanceled()) return event.getCancellationResult();
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;
}
var result = PlatformHelper.get().useOn(turtlePlayer.player(), stack, hit);
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();
if (item instanceof BucketItem || item instanceof BoatItem || item instanceof PlaceOnWaterBlockItem || item instanceof BottleItem) {
var actionResult = ForgeHooks.onItemRightClick(turtlePlayer, 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();
}
if (item.getUseDuration(stack) == 0) {
return turtlePlayer.player().gameMode.useItem(turtlePlayer.player(), turtlePlayer.player().level, stack, InteractionHand.MAIN_HAND);
}
return InteractionResult.PASS;
@ -271,11 +218,11 @@ private static InteractionResult doDeployOnBlock(
private static void setSignText(Level world, BlockEntity tile, String message) {
var signTile = (SignBlockEntity) tile;
var split = message.split("\n");
var firstLine = split.length <= 2 ? 1 : 0;
var split = Splitter.on('\n').splitToList(message);
var firstLine = split.size() <= 2 ? 1 : 0;
for (var i = 0; i < 4; i++) {
if (i >= firstLine && i < firstLine + split.length) {
var line = split[i - firstLine];
if (i >= firstLine && i < firstLine + split.size()) {
var line = split.get(i - firstLine);
signTile.setMessage(i, line.length() > 15
? Component.literal(line.substring(0, 15))
: Component.literal(line)

View File

@ -7,56 +7,40 @@
import com.mojang.authlib.GameProfile;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.turtle.TurtleUtil;
import dan200.computercraft.shared.util.DirectionUtil;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.Container;
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.server.level.ServerPlayer;
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.LoggerFactory;
import javax.annotation.Nullable;
import java.util.OptionalInt;
import java.util.UUID;
public final class TurtlePlayer extends FakePlayer {
private static final Logger LOG = LoggerFactory.getLogger(TurtlePlayer.class);
public final class TurtlePlayer {
private static final Logger LOGGER = LoggerFactory.getLogger(TurtlePlayer.class);
private static final GameProfile DEFAULT_PROFILE = new GameProfile(
UUID.fromString("0d0c4ca0-4ff1-11e4-916c-0800200c9a66"),
"[ComputerCraft]"
);
private TurtlePlayer(ServerLevel world, GameProfile name) {
super(world, name);
private final ServerPlayer player;
private TurtlePlayer(ServerPlayer player) {
this.player = player;
}
private static TurtlePlayer create(ITurtleAccess turtle) {
var world = (ServerLevel) turtle.getLevel();
var profile = turtle.getOwningPlayer();
var player = new TurtlePlayer(world, getProfile(profile));
var player = new TurtlePlayer(PlatformHelper.get().createFakePlayer(world, getProfile(profile)));
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;
}
@ -65,11 +49,11 @@ private static GameProfile getProfile(@Nullable GameProfile profile) {
}
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;
if (player == null || player.getGameProfile() != getProfile(access.getOwningPlayer())
|| player.getCommandSenderWorld() != access.getLevel()) {
if (player == null || player.player.getGameProfile() != getProfile(access.getOwningPlayer())
|| player.player.getCommandSenderWorld() != access.getLevel()) {
player = brain.cachedPlayer = create(brain);
} else {
player.setState(access);
@ -84,18 +68,26 @@ public static TurtlePlayer getWithPosition(ITurtleAccess turtle, BlockPos positi
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) {
if (containerMenu != inventoryMenu) {
LOG.warn("Turtle has open container ({})", containerMenu);
doCloseContainer();
if (player.containerMenu != player.inventoryMenu) {
LOGGER.warn("Turtle has open container ({})", player.containerMenu);
player.doCloseContainer();
}
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);
getInventory().clearContent();
player.getInventory().clearContent();
}
public void setPosition(ITurtleAccess turtle, BlockPos position, Direction direction) {
@ -111,116 +103,62 @@ public void setPosition(ITurtleAccess turtle, BlockPos position, Direction direc
}
if (direction.getAxis() != Direction.Axis.Y) {
setRot(direction.toYRot(), 0);
setRotation(direction.toYRot(), 0);
} else {
setRot(turtle.getDirection().toYRot(), DirectionUtil.toPitchAngle(direction));
setRotation(turtle.getDirection().toYRot(), DirectionUtil.toPitchAngle(direction));
}
setPosRaw(posX, posY, posZ);
xo = posX;
yo = posY;
zo = posZ;
xRotO = getXRot();
yRotO = getYRot();
yHeadRot = getYRot();
yHeadRotO = yHeadRot;
player.setPosRaw(posX, posY, posZ);
player.xo = posX;
player.yo = posY;
player.zo = posZ;
player.xRotO = player.getXRot();
player.yHeadRotO = player.yHeadRot = player.yRotO = player.getYRot();
}
public void loadInventory(ItemStack stack) {
getInventory().clearContent();
getInventory().selected = 0;
getInventory().setItem(0, stack);
player.getInventory().clearContent();
player.getInventory().selected = 0;
player.getInventory().setItem(0, stack);
}
public void loadInventory(ITurtleAccess turtle) {
getInventory().clearContent();
var inventory = player.getInventory();
var turtleInventory = turtle.getInventory();
var currentSlot = turtle.getSelectedSlot();
var slots = turtle.getInventory().getContainerSize();
var slots = turtleInventory.getContainerSize();
// Load up the fake inventory
getInventory().selected = 0;
inventory.selected = 0;
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) {
if (player.isUsingItem()) player.stopUsingItem();
var inventory = player.getInventory();
var turtleInventory = turtle.getInventory();
var currentSlot = turtle.getSelectedSlot();
var slots = turtle.getInventory().getContainerSize();
var slots = turtleInventory.getContainerSize();
// Load up the fake inventory
getInventory().selected = 0;
inventory.selected = 0;
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
var totalSize = getInventory().getContainerSize();
var totalSize = inventory.getContainerSize();
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 Vec3 position() {
return new Vec3(getX(), getY(), getZ());
public boolean isBlockProtected(ServerLevel level, BlockPos pos) {
return level.getServer().isUnderSpawnProtection(level, pos, player);
}
@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 List<ItemStack> doCrafting(Level world, int maxCount) {
// Special case: craft(0) just returns an empty list if crafting was possible
if (maxCount == 0) return Collections.emptyList();
var player = TurtlePlayer.get(turtle);
var player = TurtlePlayer.get(turtle).player();
var results = new ArrayList<ItemStack>();
for (var i = 0; i < maxCount && recipe.matches(this, world); i++) {

View File

@ -7,8 +7,7 @@
import dan200.computercraft.api.ComputerCraftTags;
import dan200.computercraft.api.turtle.*;
import dan200.computercraft.shared.TurtlePermissions;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.turtle.TurtleUtil;
import dan200.computercraft.shared.turtle.core.TurtlePlaceCommand;
import dan200.computercraft.shared.turtle.core.TurtlePlayer;
@ -17,22 +16,22 @@
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.TagKey;
import net.minecraft.world.InteractionResult;
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.decoration.ArmorStand;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.GameMasterBlock;
import net.minecraft.world.level.block.state.BlockState;
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;
@ -63,8 +62,7 @@ public boolean isItemSuitable(ItemStack stack) {
// 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.
if (stack.isDamaged() || stack.isEnchanted() || stack.hasCustomHoverName()) return false;
if (tag.contains("AttributeModifiers", TAG_LIST) &&
!tag.getList("AttributeModifiers", TAG_COMPOUND).isEmpty()) {
if (tag.contains("AttributeModifiers", TAG_LIST) && !tag.getList("AttributeModifiers", TAG_COMPOUND).isEmpty()) {
return false;
}
@ -79,11 +77,9 @@ public TurtleCommandResult useTool(ITurtleAccess turtle, TurtleSide side, Turtle
};
}
protected TurtleCommandResult checkBlockBreakable(BlockState state, Level world, BlockPos pos, TurtlePlayer player) {
var block = state.getBlock();
if (state.isAir() || block == Blocks.BEDROCK
|| state.getDestroyProgress(player, world, pos) <= 0
|| !block.canEntityDestroy(state, world, pos, player)) {
protected TurtleCommandResult checkBlockBreakable(Level world, BlockPos pos, TurtlePlayer player) {
var state = world.getBlockState(pos);
if (state.isAir() || state.getBlock() instanceof GameMasterBlock || state.getDestroyProgress(player.player(), world, pos) <= 0) {
return UNBREAKABLE;
}
@ -91,6 +87,16 @@ protected TurtleCommandResult checkBlockBreakable(BlockState state, Level world,
? 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) {
// Create a fake player, and orient it appropriately
var world = turtle.getLevel();
@ -99,8 +105,9 @@ private TurtleCommandResult attack(ITurtleAccess turtle, Direction direction) {
final var turtlePlayer = TurtlePlayer.getWithPosition(turtle, position, direction);
// See if there is an entity present
var turtlePos = turtlePlayer.position();
var rayDir = turtlePlayer.getViewVector(1.0f);
var player = turtlePlayer.player();
var turtlePos = player.position();
var rayDir = player.getViewVector(1.0f);
var hit = WorldUtil.clip(world, turtlePos, rayDir, 1.5, null);
if (hit instanceof EntityHitResult entityHit) {
// Load up the turtle's inventory
@ -109,20 +116,18 @@ private TurtleCommandResult attack(ITurtleAccess turtle, Direction direction) {
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
DropConsumer.set(hitEntity, TurtleUtil.dropConsumer(turtle));
// Attack the entity
var attacked = false;
if (!hitEntity.skipAttackInteraction(turtlePlayer)) {
var damage = (float) turtlePlayer.getAttributeValue(Attributes.ATTACK_DAMAGE) * damageMulitiplier;
var result = PlatformHelper.get().canAttackEntity(player, hitEntity);
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) {
var source = DamageSource.playerAttack(turtlePlayer);
var source = DamageSource.playerAttack(player);
if (hitEntity instanceof ArmorStand) {
// Special case for armor stands: attack twice to guarantee destroy
hitEntity.hurt(source, damage);
@ -138,78 +143,42 @@ private TurtleCommandResult attack(ITurtleAccess turtle, Direction direction) {
TurtleUtil.stopConsuming(turtle);
// Put everything we collected into the turtles inventory, then return
if (attacked) {
turtlePlayer.getInventory().clearContent();
return TurtleCommandResult.success();
}
player.getInventory().clearContent();
if (attacked) return TurtleCommandResult.success();
}
return TurtleCommandResult.failure("Nothing to attack here");
}
private TurtleCommandResult dig(ITurtleAccess turtle, Direction direction) {
if (item.canPerformAction(ToolActions.SHOVEL_FLATTEN) || item.canPerformAction(ToolActions.HOE_TILL)) {
if (TurtlePlaceCommand.deployCopiedItem(item.copy(), turtle, direction, null, null)) {
return TurtleCommandResult.success();
}
if (PlatformHelper.get().hasToolUsage(item) && TurtlePlaceCommand.deployCopiedItem(item.copy(), turtle, direction, null, null)) {
return TurtleCommandResult.success();
}
// Get ready to dig
var world = turtle.getLevel();
var level = (ServerLevel) turtle.getLevel();
var turtlePosition = turtle.getPosition();
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");
}
var state = world.getBlockState(blockPosition);
var fluidState = world.getFluidState(blockPosition);
var turtlePlayer = TurtlePlayer.getWithPosition(turtle, turtlePosition, direction);
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
var breakable = checkBlockBreakable(state, world, blockPosition, turtlePlayer);
var breakable = checkBlockBreakable(level, blockPosition, turtlePlayer);
if (!breakable.isSuccess()) return breakable;
// Consume the items the block drops
DropConsumer.set(world, blockPosition, TurtleUtil.dropConsumer(turtle));
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());
}
DropConsumer.set(level, blockPosition, TurtleUtil.dropConsumer(turtle));
var broken = !turtlePlayer.isBlockProtected(level, blockPosition) && turtlePlayer.player().gameMode.destroyBlock(blockPosition);
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)
// Allow breaking any "instabreak" block.
|| state.getDestroySpeed(reader, pos) == 0;

View File

@ -10,6 +10,9 @@
import dan200.computercraft.api.lua.ObjectArguments
import dan200.computercraft.core.apis.PeripheralAPI
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.media.items.ItemPrintout
import dan200.computercraft.shared.peripheral.monitor.BlockMonitor
@ -396,10 +399,13 @@ fun No_ghost_peripheral(helper: GameTestHelper) = helper.sequence {
*/
@GameTest
fun Peripheral_change(helper: GameTestHelper) = helper.sequence {
val testInfo = (helper as GameTestHelperAccessor).testInfo as GameTestInfoAccessor
val events = mutableListOf<Pair<String, String>>()
thenStartComputer("listen") {
while (true) {
val event = pullEvent()
TestHooks.LOG.info("[{}] Got event {} at tick {}", testInfo, event[0], testInfo.`computercraft$getTick`())
if (event[0] == "peripheral" || event[0] == "peripheral_detach") {
events.add((event[0] as String) to (event[1] as String))
}
@ -408,8 +414,9 @@ fun Peripheral_change(helper: GameTestHelper) = helper.sequence {
thenOnComputer("turtle") {
turtle.forward().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 {
assertEquals(
listOf(