From 672c2cf029f99ea5d5138fab1a63f270743dccf1 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 24 Jun 2023 17:09:34 +0100 Subject: [PATCH] Limit turtle's reach distance in Item.use When a turtle attempts to place a block, it does so by searching for nearby blocks and attempting to place the item against that block. This has slightly strange behaviour when working with "placable" non-block items though (such as buckets or boats). In this case, we call Item.use, which doesn't take in the position of the block we're placing against. Instead these items do their own ray trace, using the default reach distance. If the block we're trying to place against is non-solid, the ray trace will go straight through it and continue (up to the maximum of 5 blocks), allowing placing the item much further away. Our fix here is to override the default reach distance of our fake players, limiting it to 2. This is easy on Forge (it has built-in support), and requires a mixin on Fabric. Closes #1497. --- .../shared/platform/FakePlayerConstants.java | 31 ++++ .../computercraft/gametest/Turtle_Test.kt | 24 ++- .../turtle_test.place_use_reach_limit.snbt | 139 ++++++++++++++++++ .../dan200/computercraft/mixin/ItemMixin.java | 32 ++++ .../shared/platform/FakePlayer.java | 8 +- .../computercraft.fabric.mixins.json | 1 + .../shared/platform/FakePlayerExt.java | 12 ++ 7 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 projects/common/src/main/java/dan200/computercraft/shared/platform/FakePlayerConstants.java create mode 100644 projects/common/src/testMod/resources/data/cctest/structures/turtle_test.place_use_reach_limit.snbt create mode 100644 projects/fabric/src/main/java/dan200/computercraft/mixin/ItemMixin.java diff --git a/projects/common/src/main/java/dan200/computercraft/shared/platform/FakePlayerConstants.java b/projects/common/src/main/java/dan200/computercraft/shared/platform/FakePlayerConstants.java new file mode 100644 index 000000000..a50c8be85 --- /dev/null +++ b/projects/common/src/main/java/dan200/computercraft/shared/platform/FakePlayerConstants.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.platform; + +import com.mojang.authlib.GameProfile; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.ItemStack; + +/** + * Shared constants for {@linkplain PlatformHelper#createFakePlayer(ServerLevel, GameProfile) fake player} + * implementations. + * + * @see net.minecraft.server.level.ServerPlayer + * @see net.minecraft.world.entity.player.Player + */ +final class FakePlayerConstants { + private FakePlayerConstants() { + } + + /** + * The maximum distance this player can reach. + *

+ * This is used in the override of {@link net.minecraft.world.entity.player.Player#mayUseItemAt(BlockPos, Direction, ItemStack)}, + * to prevent the fake player reaching more than 2 blocks away. + */ + static final double MAX_REACH = 2; +} diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt index 259ff9b8d..454e5273d 100644 --- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt @@ -32,8 +32,10 @@ import net.minecraft.world.item.ItemStack import net.minecraft.world.item.Items import net.minecraft.world.level.block.Blocks import net.minecraft.world.level.block.FenceBlock +import net.minecraft.world.level.block.state.properties.BlockStateProperties import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.* +import org.hamcrest.Matchers.array +import org.hamcrest.Matchers.instanceOf import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotEquals import java.util.* @@ -78,6 +80,26 @@ class Turtle_Test { thenExecute { helper.assertBlockPresent(Blocks.LAVA, BlockPos(2, 2, 2)) } } + /** + * Checks that calling [net.minecraft.world.item.Item.use] will not place blocks too far away. + * + * This is caused by items using [net.minecraft.world.item.Item.getPlayerPOVHitResult] to perform a ray trace, which + * ignores turtle's reduced reach distance. + * + * @see [#1497](https://github.com/cc-tweaked/CC-Tweaked/issues/1497) + */ + @GameTest + fun Place_use_reach_limit(helper: GameTestHelper) = helper.sequence { + thenOnComputer { + turtle.placeDown(ObjectArguments()).await() + .assertArrayEquals(true, message = "Placed water") + } + thenExecute { + helper.assertBlockPresent(Blocks.AIR, BlockPos(2, 2, 2)) + helper.assertBlockHas(BlockPos(2, 5, 2), BlockStateProperties.WATERLOGGED, true) + } + } + /** * Checks turtles can place when waterlogged. * diff --git a/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.place_use_reach_limit.snbt b/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.place_use_reach_limit.snbt new file mode 100644 index 000000000..aaf9f9181 --- /dev/null +++ b/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.place_use_reach_limit.snbt @@ -0,0 +1,139 @@ +{ + DataVersion: 3337, + 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:air"}, + {pos: [1, 1, 2], state: "minecraft:light_gray_stained_glass"}, + {pos: [1, 1, 3], state: "minecraft:air"}, + {pos: [1, 1, 4], state: "minecraft:air"}, + {pos: [2, 1, 0], state: "minecraft:air"}, + {pos: [2, 1, 1], state: "minecraft:light_gray_stained_glass"}, + {pos: [2, 1, 2], state: "minecraft:air"}, + {pos: [2, 1, 3], state: "minecraft:light_gray_stained_glass"}, + {pos: [2, 1, 4], state: "minecraft:air"}, + {pos: [3, 1, 0], state: "minecraft:air"}, + {pos: [3, 1, 1], state: "minecraft:air"}, + {pos: [3, 1, 2], state: "minecraft:light_gray_stained_glass"}, + {pos: [3, 1, 3], state: "minecraft:air"}, + {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:air"}, + {pos: [1, 2, 2], state: "minecraft:air"}, + {pos: [1, 2, 3], state: "minecraft:air"}, + {pos: [1, 2, 4], state: "minecraft:air"}, + {pos: [2, 2, 0], state: "minecraft:air"}, + {pos: [2, 2, 1], state: "minecraft:air"}, + {pos: [2, 2, 2], state: "minecraft:ladder{facing:north,waterlogged:false}"}, + {pos: [2, 2, 3], state: "minecraft:light_gray_stained_glass"}, + {pos: [2, 2, 4], state: "minecraft:air"}, + {pos: [3, 2, 0], state: "minecraft:air"}, + {pos: [3, 2, 1], state: "minecraft:air"}, + {pos: [3, 2, 2], state: "minecraft:air"}, + {pos: [3, 2, 3], state: "minecraft:air"}, + {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:light_gray_stained_glass"}, + {pos: [1, 3, 2], state: "minecraft:light_gray_stained_glass"}, + {pos: [1, 3, 3], state: "minecraft:light_gray_stained_glass"}, + {pos: [1, 3, 4], state: "minecraft:air"}, + {pos: [2, 3, 0], state: "minecraft:air"}, + {pos: [2, 3, 1], state: "minecraft:light_gray_stained_glass"}, + {pos: [2, 3, 2], state: "minecraft:air"}, + {pos: [2, 3, 3], state: "minecraft:light_gray_stained_glass"}, + {pos: [2, 3, 4], state: "minecraft:air"}, + {pos: [3, 3, 0], state: "minecraft:air"}, + {pos: [3, 3, 1], state: "minecraft:light_gray_stained_glass"}, + {pos: [3, 3, 2], state: "minecraft:light_gray_stained_glass"}, + {pos: [3, 3, 3], state: "minecraft:light_gray_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:light_gray_stained_glass"}, + {pos: [1, 4, 2], state: "minecraft:light_gray_stained_glass"}, + {pos: [1, 4, 3], state: "minecraft:light_gray_stained_glass"}, + {pos: [1, 4, 4], state: "minecraft:air"}, + {pos: [2, 4, 0], state: "minecraft:air"}, + {pos: [2, 4, 1], state: "minecraft:light_gray_stained_glass"}, + {pos: [2, 4, 2], state: "computercraft:turtle_normal{facing:north,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [{Count: 1b, Slot: 0b, id: "minecraft:water_bucket"}], Label: "turtle_test.place_use_reach_limit", On: 1b, Slot: 0, id: "computercraft:turtle_normal"}}, + {pos: [2, 4, 3], state: "minecraft:light_gray_stained_glass"}, + {pos: [2, 4, 4], state: "minecraft:air"}, + {pos: [3, 4, 0], state: "minecraft:air"}, + {pos: [3, 4, 1], state: "minecraft:light_gray_stained_glass"}, + {pos: [3, 4, 2], state: "minecraft:light_gray_stained_glass"}, + {pos: [3, 4, 3], state: "minecraft:light_gray_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: [], + palette: [ + "minecraft:polished_andesite", + "minecraft:light_gray_stained_glass", + "minecraft:air", + "minecraft:ladder{facing:north,waterlogged:false}", + "computercraft:turtle_normal{facing:north,waterlogged:false}" + ] +} diff --git a/projects/fabric/src/main/java/dan200/computercraft/mixin/ItemMixin.java b/projects/fabric/src/main/java/dan200/computercraft/mixin/ItemMixin.java new file mode 100644 index 000000000..b1b18593b --- /dev/null +++ b/projects/fabric/src/main/java/dan200/computercraft/mixin/ItemMixin.java @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.mixin; + +import dan200.computercraft.shared.platform.FakePlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +@Mixin(Item.class) +class ItemMixin { + /** + * Replace the reach distance in {@link Item#getPlayerPOVHitResult(Level, Player, ClipContext.Fluid)}. + * + * @param reach The original reach distance. + * @param level The current level. + * @param player The current player. + * @return The new reach distance. + * @see FakePlayer#getBlockReach() + */ + @ModifyConstant(method = "getPlayerPOVHitResult", constant = @Constant(doubleValue = 5)) + @SuppressWarnings("UnusedMethod") + private static double getReachDistance(double reach, Level level, Player player) { + return player instanceof FakePlayer fp ? fp.getBlockReach() : reach; + } +} diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FakePlayer.java b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FakePlayer.java index 5f2b0c412..c48c1db1f 100644 --- a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FakePlayer.java +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FakePlayer.java @@ -11,7 +11,9 @@ import net.minecraft.world.entity.EntityDimensions; import net.minecraft.world.entity.Pose; import net.minecraft.world.entity.player.Player; -final class FakePlayer extends net.fabricmc.fabric.api.entity.FakePlayer { +import static dan200.computercraft.shared.platform.FakePlayerConstants.MAX_REACH; + +public final class FakePlayer extends net.fabricmc.fabric.api.entity.FakePlayer { private FakePlayer(ServerLevel serverLevel, GameProfile gameProfile) { super(serverLevel, gameProfile); } @@ -33,4 +35,8 @@ final class FakePlayer extends net.fabricmc.fabric.api.entity.FakePlayer { public float getStandingEyeHeight(Pose pose, EntityDimensions dimensions) { return 0; } + + public double getBlockReach() { + return MAX_REACH; + } } diff --git a/projects/fabric/src/main/resources/computercraft.fabric.mixins.json b/projects/fabric/src/main/resources/computercraft.fabric.mixins.json index 186ada5fe..0b6d01a0c 100644 --- a/projects/fabric/src/main/resources/computercraft.fabric.mixins.json +++ b/projects/fabric/src/main/resources/computercraft.fabric.mixins.json @@ -12,6 +12,7 @@ "EntityMixin", "ExplosionDamageCalculatorMixin", "ItemEntityMixin", + "ItemMixin", "ServerLevelMixin", "ShapedRecipeMixin", "TagEntryAccessor", diff --git a/projects/forge/src/main/java/dan200/computercraft/shared/platform/FakePlayerExt.java b/projects/forge/src/main/java/dan200/computercraft/shared/platform/FakePlayerExt.java index 060e080a4..aa8340722 100644 --- a/projects/forge/src/main/java/dan200/computercraft/shared/platform/FakePlayerExt.java +++ b/projects/forge/src/main/java/dan200/computercraft/shared/platform/FakePlayerExt.java @@ -16,6 +16,8 @@ import net.minecraftforge.common.util.FakePlayer; import javax.annotation.Nullable; import java.util.OptionalInt; +import static dan200.computercraft.shared.platform.FakePlayerConstants.MAX_REACH; + class FakePlayerExt extends FakePlayer { FakePlayerExt(ServerLevel serverLevel, GameProfile profile) { super(serverLevel, profile); @@ -45,4 +47,14 @@ class FakePlayerExt extends FakePlayer { public float getStandingEyeHeight(Pose pose, EntityDimensions dimensions) { return 0; } + + @Override + public double getBlockReach() { + return MAX_REACH; + } + + @Override + public double getEntityReach() { + return MAX_REACH; + } }