1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-04-18 16:53:18 +00:00

Redo turtle move checks

Oh, this is so broken, and really has been since the 1.13 update, if not
earlier.

 - Fix call to isUnobstructed using the bounding box of the
   *destination* block rather than the turtle. This is almost always
   air, so the box is empty.

 - Because the above check has been wrong for so many years, we now
   significantly relax the "can push" checks for entities. We now allow
   pushing entities in any direction.

   We also remove the "isUnobstructed" check for the destination entity
   pos. This causes problems (if two entities are standing on a turtle,
   they'll obstruct each other), and given pistons don't perform such a
   check, I don't think we need it.

 - Also do a bit of cleanup around air/liquid checks. We often ended up
   reading the block state multiple times, which is a little ugly.
This commit is contained in:
Jonathan Coates 2025-01-27 22:43:29 +00:00
parent 7d8f609c49
commit b69a44a927
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
11 changed files with 215 additions and 56 deletions

View File

@ -276,7 +276,6 @@ public class ServerComputer implements ComputerEnvironment, ComputerEvents.Recei
}
public static final class Properties {
private final int computerID;
private @Nullable String label;
private final ComputerFamily family;

View File

@ -18,6 +18,7 @@ import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
@ -237,7 +238,7 @@ public class MonitorBlockEntity extends BlockEntity {
getLevel().setBlock(getBlockPos(), getBlockState()
.setValue(MonitorBlock.STATE, MonitorEdgeState.fromConnections(
yIndex < height - 1, yIndex > 0,
xIndex > 0, xIndex < width - 1)), 2);
xIndex > 0, xIndex < width - 1)), Block.UPDATE_CLIENTS);
}
// region Sizing and placement stuff

View File

@ -36,6 +36,7 @@ import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.MoverType;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.material.PushReaction;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
@ -280,7 +281,7 @@ public class TurtleBrain implements TurtleAccessInternal {
try {
// We use Block.UPDATE_CLIENTS here to ensure that neighbour updates caused in Block.updateNeighbourShapes
// are sent to the client. We want to avoid doing a full block update until the turtle state is copied over.
if (world.setBlock(pos, newState, 2)) {
if (world.setBlock(pos, newState, Block.UPDATE_CLIENTS)) {
var block = world.getBlockState(pos).getBlock();
if (block == oldBlock.getBlock()) {
var newTile = world.getBlockEntity(pos);
@ -691,7 +692,7 @@ public class TurtleBrain implements TurtleAccessInternal {
}
var aabb = new AABB(minX, minY, minZ, maxX, maxY, maxZ);
var list = world.getEntitiesOfClass(Entity.class, aabb, TurtleBrain::canPush);
var list = world.getEntities((Entity) null, aabb, TurtleBrain::canPush);
if (!list.isEmpty()) {
double pushStep = 1.0f / ANIM_DURATION;
var pushStepX = moveDir.getStepX() * pushStep;

View File

@ -22,11 +22,10 @@ public class TurtleDetectCommand implements TurtleCommand {
var direction = this.direction.toWorldDir(turtle);
// Check if thing in front is air or not
var world = turtle.getLevel();
var oldPosition = turtle.getPosition();
var newPosition = oldPosition.relative(direction);
var level = turtle.getLevel();
var pos = turtle.getPosition().relative(direction);
return !WorldUtil.isLiquidBlock(world, newPosition) && !world.isEmptyBlock(newPosition)
return !WorldUtil.isEmptyBlock(level.getBlockState(pos))
? TurtleCommandResult.success()
: TurtleCommandResult.failure();
}

View File

@ -13,9 +13,9 @@ 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.level.material.PushReaction;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
public class TurtleMoveCommand implements TurtleCommand {
private final MoveDirection direction;
@ -30,57 +30,32 @@ public class TurtleMoveCommand implements TurtleCommand {
var direction = this.direction.toWorldDir(turtle);
// Check if we can move
var oldWorld = (ServerLevel) turtle.getLevel();
var level = (ServerLevel) turtle.getLevel();
var oldPosition = turtle.getPosition();
var newPosition = oldPosition.relative(direction);
var turtlePlayer = TurtlePlayer.getWithPosition(turtle, oldPosition, direction);
var canEnterResult = canEnter(turtlePlayer, oldWorld, newPosition);
if (!canEnterResult.isSuccess()) {
return canEnterResult;
}
var canEnterResult = canEnter(turtlePlayer, level, newPosition);
if (!canEnterResult.isSuccess()) return canEnterResult;
// Check existing block is air or replaceable
var state = oldWorld.getBlockState(newPosition);
if (!oldWorld.isEmptyBlock(newPosition) &&
!WorldUtil.isLiquidBlock(oldWorld, newPosition) &&
!state.canBeReplaced()) {
// Check existing block is air or replaceable.
var existingState = level.getBlockState(newPosition);
if (!(WorldUtil.isEmptyBlock(existingState) || existingState.canBeReplaced())) {
return TurtleCommandResult.failure("Movement obstructed");
}
// Check there isn't anything in the way
var collision = state.getCollisionShape(oldWorld, oldPosition).move(
newPosition.getX(),
newPosition.getY(),
newPosition.getZ()
);
if (!oldWorld.isUnobstructed(null, collision)) {
if (!Config.turtlesCanPush || this.direction == MoveDirection.UP || this.direction == MoveDirection.DOWN) {
return TurtleCommandResult.failure("Movement obstructed");
}
// Check there is space for all the pushable entities to be pushed
var list = oldWorld.getEntitiesOfClass(Entity.class, getBox(collision), x -> x != null && x.isAlive() && x.blocksBuilding);
for (var entity : list) {
var pushedBB = entity.getBoundingBox().move(
direction.getStepX(),
direction.getStepY(),
direction.getStepZ()
);
if (!oldWorld.isUnobstructed(null, Shapes.create(pushedBB))) {
return TurtleCommandResult.failure("Movement obstructed");
}
}
// Check there isn't an entity in the way.
var turtleShape = level.getBlockState(oldPosition).getCollisionShape(level, oldPosition)
.move(newPosition.getX(), newPosition.getY(), newPosition.getZ());
if (!level.isUnobstructed(null, turtleShape) && !canPushEntities(level, turtleShape.bounds())) {
return TurtleCommandResult.failure("Movement obstructed");
}
// Check fuel level
if (turtle.isFuelNeeded() && turtle.getFuelLevel() < 1) {
return TurtleCommandResult.failure("Out of fuel");
}
if (turtle.isFuelNeeded() && turtle.getFuelLevel() < 1) return TurtleCommandResult.failure("Out of fuel");
// Move
if (!turtle.teleportTo(oldWorld, newPosition)) return TurtleCommandResult.failure("Movement failed");
if (!turtle.teleportTo(level, newPosition)) return TurtleCommandResult.failure("Movement failed");
// Consume fuel
turtle.consumeFuel(1);
@ -114,9 +89,20 @@ public class TurtleMoveCommand implements TurtleCommand {
return TurtleCommandResult.success();
}
private static AABB getBox(VoxelShape shape) {
return shape.isEmpty() ? EMPTY_BOX : shape.bounds();
}
private static final AABB EMPTY_BOX = new AABB(0, 0, 0, 0, 0, 0);
/**
* Determine if all entities in the given bounds can be pushed by the turtle.
*
* @param level The current level.
* @param bounds The bounding box.
* @return Whether all entities can be pushed.
*/
private boolean canPushEntities(Level level, AABB bounds) {
if (!Config.turtlesCanPush) return false;
// Check there is space for all the pushable entities to be pushed
return level.getEntities((Entity) null, bounds, e -> e.isAlive()
&& !e.isSpectator() && e.blocksBuilding && e.getPistonPushReaction() == PushReaction.IGNORE
).isEmpty();
}
}

View File

@ -29,9 +29,14 @@ import net.minecraft.world.phys.shapes.VoxelShape;
import javax.annotation.Nullable;
public final class WorldUtil {
@SuppressWarnings("deprecation")
public static boolean isLiquidBlock(Level world, BlockPos pos) {
if (!world.isInWorldBounds(pos)) return false;
return world.getBlockState(pos).liquid();
return world.isInWorldBounds(pos) && world.getBlockState(pos).liquid();
}
@SuppressWarnings("deprecation")
public static boolean isEmptyBlock(BlockState state) {
return state.isAir() || state.liquid();
}
public static boolean isVecInside(VoxelShape shape, Vec3 vec) {

View File

@ -642,6 +642,27 @@ class Turtle_Test {
}
}
/**
* Test turtles can push entities.
*/
@GameTest
fun Move_push_entity(helper: GameTestHelper) = helper.sequence {
thenOnComputer { turtle.up().await().assertArrayEquals(true) }
thenIdle(9)
thenExecute {
// The turtle has moved up
helper.assertBlockPresent(ModRegistry.Blocks.TURTLE_NORMAL.get(), BlockPos(2, 3, 2))
// As has the villager
val pos = BlockPos(2, 4, 2)
helper.assertEntityPresent(EntityType.VILLAGER, pos)
val villager = helper.getEntity(EntityType.VILLAGER)
val expectedY = helper.absolutePos(pos).y - 0.125
if (villager.y < expectedY) helper.fail("Expected villager at y>=$expectedY, but at ${villager.y}", pos)
}
}
/**
* Test a turtle can attack an entity and capture its drops.
*/

View File

@ -36,6 +36,10 @@ object ManagedComputers : ILuaMachine.Factory {
private val LOGGER = LoggerFactory.getLogger(ManagedComputers::class.java)
private val computers: MutableMap<String, Queue<suspend LuaTaskContext.() -> Unit>> = mutableMapOf()
internal fun reset() {
computers.clear()
}
internal fun enqueue(test: GameTestInfo, label: String, task: suspend LuaTaskContext.() -> Unit): Monitor {
val monitor = Monitor(test, label)
computers.computeIfAbsent(label) { ConcurrentLinkedDeque() }.add {

View File

@ -73,6 +73,8 @@ object TestHooks {
LOG.info("Cleaning up after last run")
GameTestRunner.clearAllTests(server.overworld(), BlockPos(0, -60, 0), GameTestTicker.SINGLETON, 200)
ManagedComputers.reset()
// Delete server context and add one with a mutable machine factory. This allows us to set the factory for
// specific test batches without having to reset all computers.
for (computer in ServerContext.get(server).registry().computers) {

View File

@ -0,0 +1,140 @@
{
DataVersion: 3465,
size: [5, 5, 5],
data: [
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
{pos: [0, 0, 1], state: "minecraft:polished_andesite"},
{pos: [0, 0, 2], state: "minecraft:polished_andesite"},
{pos: [0, 0, 3], state: "minecraft:polished_andesite"},
{pos: [0, 0, 4], state: "minecraft:polished_andesite"},
{pos: [1, 0, 0], state: "minecraft:polished_andesite"},
{pos: [1, 0, 1], state: "minecraft:polished_andesite"},
{pos: [1, 0, 2], state: "minecraft:polished_andesite"},
{pos: [1, 0, 3], state: "minecraft:polished_andesite"},
{pos: [1, 0, 4], state: "minecraft:polished_andesite"},
{pos: [2, 0, 0], state: "minecraft:polished_andesite"},
{pos: [2, 0, 1], state: "minecraft:polished_andesite"},
{pos: [2, 0, 2], state: "minecraft:polished_andesite"},
{pos: [2, 0, 3], state: "minecraft:polished_andesite"},
{pos: [2, 0, 4], state: "minecraft:polished_andesite"},
{pos: [3, 0, 0], state: "minecraft:polished_andesite"},
{pos: [3, 0, 1], state: "minecraft:polished_andesite"},
{pos: [3, 0, 2], state: "minecraft:polished_andesite"},
{pos: [3, 0, 3], state: "minecraft:polished_andesite"},
{pos: [3, 0, 4], state: "minecraft:polished_andesite"},
{pos: [4, 0, 0], state: "minecraft:polished_andesite"},
{pos: [4, 0, 1], state: "minecraft:polished_andesite"},
{pos: [4, 0, 2], state: "minecraft:polished_andesite"},
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
{pos: [0, 1, 0], state: "minecraft:air"},
{pos: [0, 1, 1], state: "minecraft:air"},
{pos: [0, 1, 2], state: "minecraft:air"},
{pos: [0, 1, 3], state: "minecraft:air"},
{pos: [0, 1, 4], state: "minecraft:air"},
{pos: [1, 1, 0], state: "minecraft:air"},
{pos: [1, 1, 1], state: "minecraft:white_stained_glass"},
{pos: [1, 1, 2], state: "minecraft:white_stained_glass"},
{pos: [1, 1, 3], state: "minecraft:white_stained_glass"},
{pos: [1, 1, 4], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "minecraft:white_stained_glass"},
{pos: [2, 1, 2], state: "computercraft:turtle_normal{facing:west,waterlogged:false}", nbt: {ComputerId: 1, Label: "turtle_test.move_push_entity", Fuel: 80, Items: [], On: 1b, Slot: 0, id: "computercraft:turtle_normal"}},
{pos: [2, 1, 3], state: "minecraft:white_stained_glass"},
{pos: [2, 1, 4], state: "minecraft:air"},
{pos: [3, 1, 0], state: "minecraft:air"},
{pos: [3, 1, 1], state: "minecraft:white_stained_glass"},
{pos: [3, 1, 2], state: "minecraft:white_stained_glass"},
{pos: [3, 1, 3], state: "minecraft:white_stained_glass"},
{pos: [3, 1, 4], state: "minecraft:air"},
{pos: [4, 1, 0], state: "minecraft:air"},
{pos: [4, 1, 1], state: "minecraft:air"},
{pos: [4, 1, 2], state: "minecraft:air"},
{pos: [4, 1, 3], state: "minecraft:air"},
{pos: [4, 1, 4], state: "minecraft:air"},
{pos: [0, 2, 0], state: "minecraft:air"},
{pos: [0, 2, 1], state: "minecraft:air"},
{pos: [0, 2, 2], state: "minecraft:air"},
{pos: [0, 2, 3], state: "minecraft:air"},
{pos: [0, 2, 4], state: "minecraft:air"},
{pos: [1, 2, 0], state: "minecraft:air"},
{pos: [1, 2, 1], state: "minecraft:white_stained_glass"},
{pos: [1, 2, 2], state: "minecraft:white_stained_glass"},
{pos: [1, 2, 3], state: "minecraft:white_stained_glass"},
{pos: [1, 2, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:white_stained_glass"},
{pos: [2, 2, 2], state: "minecraft:air"},
{pos: [2, 2, 3], state: "minecraft:white_stained_glass"},
{pos: [2, 2, 4], state: "minecraft:air"},
{pos: [3, 2, 0], state: "minecraft:air"},
{pos: [3, 2, 1], state: "minecraft:white_stained_glass"},
{pos: [3, 2, 2], state: "minecraft:white_stained_glass"},
{pos: [3, 2, 3], state: "minecraft:white_stained_glass"},
{pos: [3, 2, 4], state: "minecraft:air"},
{pos: [4, 2, 0], state: "minecraft:air"},
{pos: [4, 2, 1], state: "minecraft:air"},
{pos: [4, 2, 2], state: "minecraft:air"},
{pos: [4, 2, 3], state: "minecraft:air"},
{pos: [4, 2, 4], state: "minecraft:air"},
{pos: [0, 3, 0], state: "minecraft:air"},
{pos: [0, 3, 1], state: "minecraft:air"},
{pos: [0, 3, 2], state: "minecraft:air"},
{pos: [0, 3, 3], state: "minecraft:air"},
{pos: [0, 3, 4], state: "minecraft:air"},
{pos: [1, 3, 0], state: "minecraft:air"},
{pos: [1, 3, 1], state: "minecraft:white_stained_glass"},
{pos: [1, 3, 2], state: "minecraft:white_stained_glass"},
{pos: [1, 3, 3], state: "minecraft:white_stained_glass"},
{pos: [1, 3, 4], state: "minecraft:air"},
{pos: [2, 3, 0], state: "minecraft:air"},
{pos: [2, 3, 1], state: "minecraft:white_stained_glass"},
{pos: [2, 3, 2], state: "minecraft:air"},
{pos: [2, 3, 3], state: "minecraft:white_stained_glass"},
{pos: [2, 3, 4], state: "minecraft:air"},
{pos: [3, 3, 0], state: "minecraft:air"},
{pos: [3, 3, 1], state: "minecraft:white_stained_glass"},
{pos: [3, 3, 2], state: "minecraft:white_stained_glass"},
{pos: [3, 3, 3], state: "minecraft:white_stained_glass"},
{pos: [3, 3, 4], state: "minecraft:air"},
{pos: [4, 3, 0], state: "minecraft:air"},
{pos: [4, 3, 1], state: "minecraft:air"},
{pos: [4, 3, 2], state: "minecraft:air"},
{pos: [4, 3, 3], state: "minecraft:air"},
{pos: [4, 3, 4], state: "minecraft:air"},
{pos: [0, 4, 0], state: "minecraft:air"},
{pos: [0, 4, 1], state: "minecraft:air"},
{pos: [0, 4, 2], state: "minecraft:air"},
{pos: [0, 4, 3], state: "minecraft:air"},
{pos: [0, 4, 4], state: "minecraft:air"},
{pos: [1, 4, 0], state: "minecraft:air"},
{pos: [1, 4, 1], state: "minecraft:white_stained_glass"},
{pos: [1, 4, 2], state: "minecraft:white_stained_glass"},
{pos: [1, 4, 3], state: "minecraft:white_stained_glass"},
{pos: [1, 4, 4], state: "minecraft:air"},
{pos: [2, 4, 0], state: "minecraft:air"},
{pos: [2, 4, 1], state: "minecraft:white_stained_glass"},
{pos: [2, 4, 2], state: "minecraft:air"},
{pos: [2, 4, 3], state: "minecraft:white_stained_glass"},
{pos: [2, 4, 4], state: "minecraft:air"},
{pos: [3, 4, 0], state: "minecraft:air"},
{pos: [3, 4, 1], state: "minecraft:white_stained_glass"},
{pos: [3, 4, 2], state: "minecraft:white_stained_glass"},
{pos: [3, 4, 3], state: "minecraft:white_stained_glass"},
{pos: [3, 4, 4], state: "minecraft:air"},
{pos: [4, 4, 0], state: "minecraft:air"},
{pos: [4, 4, 1], state: "minecraft:air"},
{pos: [4, 4, 2], state: "minecraft:air"},
{pos: [4, 4, 3], state: "minecraft:air"},
{pos: [4, 4, 4], state: "minecraft:air"}
],
entities: [
{blockPos: [2, 1, 2], pos: [2.5d, 1.875d, 2.5d], nbt: {AbsorptionAmount: 0.0f, Age: 0, Air: 300s, ArmorDropChances: [0.085f, 0.085f, 0.085f, 0.085f], ArmorItems: [{}, {}, {}, {}], Attributes: [{Base: 0.5d, Name: "minecraft:generic.movement_speed"}, {Base: 48.0d, Modifiers: [{Amount: -0.01165046535152748d, Name: "Random spawn bonus", Operation: 1, UUID: [I; 1412502412, 1522745411, -1211155694, 2103054347]}], Name: "minecraft:generic.follow_range"}], Brain: {memories: {}}, CanPickUpLoot: 1b, DeathTime: 0s, FallDistance: 0.0f, FallFlying: 0b, Fire: -1s, FoodLevel: 0b, ForcedAge: 0, Gossips: [], HandDropChances: [0.085f, 0.085f], HandItems: [{}, {}], Health: 20.0f, HurtByTimestamp: 0, HurtTime: 0s, Inventory: [], Invulnerable: 0b, LastGossipDecay: 52357L, LastRestock: 0L, LeftHanded: 0b, Motion: [0.0d, -0.0784000015258789d, 0.0d], OnGround: 1b, PersistenceRequired: 0b, PortalCooldown: 0, Pos: [-33.5d, 58.875d, -21.5d], RestocksToday: 0, Rotation: [-102.704926f, 0.0f], UUID: [I; 164071932, -867285780, -1817215456, -2129864016], VillagerData: {level: 1, profession: "minecraft:none", type: "minecraft:desert"}, Xp: 0, id: "minecraft:villager"}}
],
palette: [
"minecraft:polished_andesite",
"minecraft:white_stained_glass",
"minecraft:air",
"computercraft:turtle_normal{facing:west,waterlogged:false}"
]
}

View File

@ -408,8 +408,9 @@ public class PlatformHelperImpl implements PlatformHelper {
}
}
private record WrappedMenuProvider(Component title, MenuConstructor menu,
ContainerData data) implements ExtendedScreenHandlerFactory {
private record WrappedMenuProvider(
Component title, MenuConstructor menu, ContainerData data
) implements ExtendedScreenHandlerFactory {
@Nullable
@Override
public AbstractContainerMenu createMenu(int id, Inventory inventory, Player player) {