From a91ac6f21483938fef99a2fc356c740d48338e3b Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Mon, 3 Jul 2023 22:10:11 +0100 Subject: [PATCH] Make turtle tools a little more flexible Turtle tools now accept two additional JSON fields - allowEnchantments: Whether items with enchantments (or any non-standard NBT) can be equipped. - consumesDurability: Whether durability will be consumed. This can be "never" (the current and default behaviour), "always", and "when_enchanted". Closes #1501. --- .../api/turtle/TurtleToolDurability.java | 48 ++++ .../api/turtle/TurtleUpgradeDataProvider.java | 30 +++ .../shared/turtle/core/TurtleBrain.java | 19 -- .../turtle/core/TurtlePlaceCommand.java | 14 +- .../turtle/inventory/UpgradeContainer.java | 2 +- .../shared/turtle/upgrades/TurtleTool.java | 234 ++++++++++++++---- .../turtle/upgrades/TurtleToolSerialiser.java | 11 +- .../shared/platform/FakePlayer.java | 6 + .../shared/platform/FakePlayerExt.java | 6 + 9 files changed, 281 insertions(+), 89 deletions(-) create mode 100644 projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleToolDurability.java diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleToolDurability.java b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleToolDurability.java new file mode 100644 index 000000000..c149a185d --- /dev/null +++ b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleToolDurability.java @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.api.turtle; + +import net.minecraft.util.StringRepresentable; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.ItemStack; + +/** + * Indicates if an equipped turtle item will consume durability. + * + * @see TurtleUpgradeDataProvider.ToolBuilder#consumesDurability(TurtleToolDurability) + */ +public enum TurtleToolDurability implements StringRepresentable { + /** + * The equipped tool always consumes durability when using. + */ + ALWAYS("always"), + + /** + * The equipped tool consumes durability if it is {@linkplain ItemStack#isEnchanted() enchanted} or has + * {@linkplain ItemStack#getAttributeModifiers(EquipmentSlot) custom attribute modifiers}. + */ + WHEN_ENCHANTED("when_enchanted"), + + /** + * The equipped tool never consumes durability. Tools which have been damaged cannot be used as upgrades. + */ + NEVER("never"); + + private final String serialisedName; + + /** + * The codec which may be used for serialising/deserialising {@link TurtleToolDurability}s. + */ + public static final StringRepresentable.EnumCodec CODEC = StringRepresentable.fromEnum(TurtleToolDurability::values); + + TurtleToolDurability(String serialisedName) { + this.serialisedName = serialisedName; + } + + @Override + public String getSerializedName() { + return serialisedName; + } +} diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeDataProvider.java b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeDataProvider.java index 7aef277d6..a2048e969 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeDataProvider.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeDataProvider.java @@ -13,8 +13,10 @@ import net.minecraft.data.DataGenerator; import net.minecraft.data.PackOutput; import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.TagKey; +import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.block.Block; import javax.annotation.Nullable; @@ -61,6 +63,8 @@ public abstract class TurtleUpgradeDataProvider extends UpgradeDataProvider breakable; + private boolean allowEnchantments = false; + private TurtleToolDurability consumesDurability = TurtleToolDurability.NEVER; ToolBuilder(ResourceLocation id, TurtleUpgradeSerialiser serialiser, Item toolItem) { this.id = id; @@ -104,6 +108,28 @@ public abstract class TurtleUpgradeDataProvider extends UpgradeDataProvider= 0 && colour <= 0xFFFFFF) { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java index 1e9c9357a..d1b9156b3 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java @@ -74,19 +74,7 @@ public class TurtlePlaceCommand implements TurtleCommand { } } - 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.player().getInventory().clearContent(); - return result; - } - - private static boolean deploy( + public static boolean deploy( ItemStack stack, ITurtleAccess turtle, TurtlePlayer turtlePlayer, Direction direction, @Nullable Object[] extraArguments, @Nullable ErrorMessage outErrorMessage ) { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/inventory/UpgradeContainer.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/inventory/UpgradeContainer.java index a0f0b6987..9fff2789a 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/inventory/UpgradeContainer.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/inventory/UpgradeContainer.java @@ -59,7 +59,7 @@ class UpgradeContainer implements Container { private ItemStack setUpgradeStack(int slot, @Nullable UpgradeData upgrade) { var stack = upgrade == null ? ItemStack.EMPTY : upgrade.getUpgradeItem(); - lastUpgrade.set(slot, upgrade); + lastUpgrade.set(slot, UpgradeData.copyOf(upgrade)); lastStack.set(slot, stack); return stack; } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java index 7f75e7456..564fcbb6c 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java @@ -6,6 +6,7 @@ package dan200.computercraft.shared.turtle.upgrades; import dan200.computercraft.api.ComputerCraftTags; import dan200.computercraft.api.turtle.*; +import dan200.computercraft.core.util.Nullability; import dan200.computercraft.shared.platform.PlatformHelper; import dan200.computercraft.shared.turtle.TurtleUtil; import dan200.computercraft.shared.turtle.core.TurtlePlaceCommand; @@ -17,14 +18,19 @@ import net.minecraft.core.Direction; import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; import net.minecraft.tags.TagKey; +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.entity.MobType; 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.item.enchantment.EnchantmentHelper; import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; @@ -33,6 +39,8 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.EntityHitResult; import javax.annotation.Nullable; +import java.util.Objects; +import java.util.function.Function; import static net.minecraft.nbt.Tag.TAG_COMPOUND; import static net.minecraft.nbt.Tag.TAG_LIST; @@ -43,31 +51,39 @@ public class TurtleTool extends AbstractTurtleUpgrade { final ItemStack item; final float damageMulitiplier; - @Nullable - final TagKey breakable; + final boolean allowsEnchantments; + final TurtleToolDurability consumesDurability; + final @Nullable TagKey breakable; - public TurtleTool(ResourceLocation id, String adjective, Item craftItem, ItemStack toolItem, float damageMulitiplier, @Nullable TagKey breakable) { + public TurtleTool( + ResourceLocation id, String adjective, Item craftItem, ItemStack toolItem, float damageMulitiplier, + boolean allowsEnchantments, TurtleToolDurability consumesDurability, @Nullable TagKey breakable + ) { super(id, TurtleUpgradeType.TOOL, adjective, new ItemStack(craftItem)); item = toolItem; this.damageMulitiplier = damageMulitiplier; + this.allowsEnchantments = allowsEnchantments; + this.consumesDurability = consumesDurability; this.breakable = breakable; } @Override public boolean isItemSuitable(ItemStack stack) { - var tag = stack.getTag(); - if (tag == null || tag.isEmpty()) return true; - - // 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()) return false; - if (tag.contains("AttributeModifiers", TAG_LIST) && !tag.getList("AttributeModifiers", TAG_COMPOUND).isEmpty()) { - return false; - } - + if (consumesDurability == TurtleToolDurability.NEVER && stack.isDamaged()) return false; + if (!allowsEnchantments && isEnchanted(stack)) return false; return true; } + private static boolean isEnchanted(ItemStack stack) { + return !stack.isEmpty() && isEnchanted(stack.getTag()); + } + + private static boolean isEnchanted(@Nullable CompoundTag tag) { + if (tag == null || tag.isEmpty()) return false; + return (tag.contains(ItemStack.TAG_ENCH, TAG_LIST) && !tag.getList(ItemStack.TAG_ENCH, TAG_COMPOUND).isEmpty()) + || (tag.contains("AttributeModifiers", TAG_LIST) && !tag.getList("AttributeModifiers", TAG_COMPOUND).isEmpty()); + } + @Override public CompoundTag getUpgradeData(ItemStack stack) { // Just use the current item's tag. @@ -83,11 +99,64 @@ public class TurtleTool extends AbstractTurtleUpgrade { return item; } + private ItemStack getToolStack(ITurtleAccess turtle, TurtleSide side) { + var item = getCraftingItem(); + var tag = turtle.getUpgradeNBTData(side); + if (!tag.isEmpty()) item.setTag(tag); + return item.copy(); + } + + private void setToolStack(ITurtleAccess turtle, TurtleSide side, ItemStack stack) { + var tag = turtle.getUpgradeNBTData(side); + + var useDurability = switch (consumesDurability) { + case NEVER -> false; + case WHEN_ENCHANTED -> isEnchanted(tag); + case ALWAYS -> true; + }; + if (!useDurability) return; + + // If the tool has broken, remove the upgrade! + if (stack.isEmpty()) { + turtle.setUpgradeWithData(side, null); + return; + } + + // If the tool has changed, no clue what's going on. + if (stack.getItem() != item.getItem()) return; + + var itemTag = stack.getTag(); + + // Early return if the item hasn't changed to avoid redundant syncs with the client. + if ((itemTag == null && tag.isEmpty()) || Objects.equals(itemTag, tag)) return; + + if (itemTag == null) { + tag.getAllKeys().clear(); + } else { + for (var key : itemTag.getAllKeys()) tag.put(key, Nullability.assertNonNull(itemTag.get(key))); + tag.getAllKeys().removeIf(x -> !itemTag.contains(x)); + } + + turtle.updateUpgradeNBTData(side); + } + + private T withEquippedItem(ITurtleAccess turtle, TurtleSide side, Direction direction, Function action) { + var turtlePlayer = TurtlePlayer.getWithPosition(turtle, turtle.getPosition(), direction); + turtlePlayer.loadInventory(getToolStack(turtle, side)); + + var result = action.apply(turtlePlayer); + + setToolStack(turtle, side, turtlePlayer.player().getItemInHand(InteractionHand.MAIN_HAND)); + turtlePlayer.player().getInventory().clearContent(); + + return result; + } + @Override public TurtleCommandResult useTool(ITurtleAccess turtle, TurtleSide side, TurtleVerb verb, Direction direction) { return switch (verb) { - case ATTACK -> attack(turtle, direction); - case DIG -> dig(turtle, direction); + case ATTACK -> attack(turtle, side, direction); + case DIG -> dig(turtle, side, direction); }; } @@ -102,16 +171,14 @@ public class TurtleTool extends AbstractTurtleUpgrade { } /** - * Attack an entity. This is a very 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). + * Attack an entity. * * @param turtle The current turtle. + * @param side The side the tool is on. * @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, TurtleSide side, Direction direction) { // Create a fake player, and orient it appropriately var world = turtle.getLevel(); var position = turtle.getPosition(); @@ -123,10 +190,11 @@ public class TurtleTool extends AbstractTurtleUpgrade { var turtlePos = player.position(); var rayDir = player.getViewVector(1.0f); var hit = WorldUtil.clip(world, turtlePos, rayDir, 1.5, null); + var attacked = false; if (hit instanceof EntityHitResult entityHit) { // Load up the turtle's inventory - var stackCopy = item.copy(); - turtlePlayer.loadInventory(stackCopy); + var stack = getToolStack(turtle, side); + turtlePlayer.loadInventory(stack); var hitEntity = entityHit.getEntity(); @@ -134,62 +202,120 @@ public class TurtleTool extends AbstractTurtleUpgrade { DropConsumer.set(hitEntity, TurtleUtil.dropConsumer(turtle)); // Attack the entity - var attacked = false; 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 = player.damageSources().playerAttack(player); - if (hitEntity instanceof ArmorStand) { - // Special case for armor stands: attack twice to guarantee destroy - hitEntity.hurt(source, damage); - if (hitEntity.isAlive()) hitEntity.hurt(source, damage); - attacked = true; - } else { - if (hitEntity.hurt(source, damage)) attacked = true; - } - } + attacked = attack(player, direction, hitEntity); } // Stop claiming drops TurtleUtil.stopConsuming(turtle); - // Put everything we collected into the turtles inventory, then return + // Put everything we collected into the turtles inventory. + setToolStack(turtle, side, player.getItemInHand(InteractionHand.MAIN_HAND)); player.getInventory().clearContent(); - if (attacked) return TurtleCommandResult.success(); } - return TurtleCommandResult.failure("Nothing to attack here"); + return attacked ? TurtleCommandResult.success() : TurtleCommandResult.failure("Nothing to attack here"); } - private TurtleCommandResult dig(ITurtleAccess turtle, Direction direction) { - if (PlatformHelper.get().hasToolUsage(item) && TurtlePlaceCommand.deployCopiedItem(item.copy(), turtle, direction, null, null)) { - return TurtleCommandResult.success(); + /** + * Attack an entity. This is a copy of {@link Player#attack(Entity)}, with some unwanted features removed (sweeping + * edge). This is a little limited. + *

+ * Ideally we'd use attack directly (if other mods mixin to that method, we won't support their features). + * Unfortunately,that doesn't give us any feedback to whether the attack occurred or not (and we don't want to play + * sounds/particles). + * + * @param player The fake player doing the attacking. + * @param direction The direction the turtle is attacking. + * @param entity The entity to attack. + * @return Whether we attacked or not. + * @see Player#attack(Entity) + */ + private boolean attack(ServerPlayer player, Direction direction, Entity entity) { + var baseDamage = (float) player.getAttributeValue(Attributes.ATTACK_DAMAGE) * damageMulitiplier; + var bonusDamage = EnchantmentHelper.getDamageBonus( + player.getItemInHand(InteractionHand.MAIN_HAND), entity instanceof LivingEntity target ? target.getMobType() : MobType.UNDEFINED + ); + var damage = baseDamage + bonusDamage; + if (damage <= 0) return false; + + var knockBack = EnchantmentHelper.getKnockbackBonus(player); + + // We follow the logic in Player.attack of setting the entity on fire before attacking, so it's burning when it + // (possibly) dies. + var fireAspect = EnchantmentHelper.getFireAspect(player); + var onFire = false; + if (entity instanceof LivingEntity target && fireAspect > 0 && !target.isOnFire()) { + onFire = true; + target.setSecondsOnFire(1); } - var level = (ServerLevel) turtle.getLevel(); - var turtlePosition = turtle.getPosition(); + var source = player.damageSources().playerAttack(player); + if (!entity.hurt(source, damage)) { + // If we failed to damage the entity, undo us setting the entity on fire. + if (onFire) entity.clearFire(); + return false; + } - var blockPosition = turtlePosition.relative(direction); + // Special case for armor stands: attack twice to guarantee destroy + if (entity.isAlive() && entity instanceof ArmorStand) entity.hurt(source, damage); + + // Apply knockback + if (knockBack > 0) { + if (entity instanceof LivingEntity target) { + target.knockback(knockBack * 0.5, -direction.getStepX(), -direction.getStepZ()); + } else { + entity.push(direction.getStepX() * knockBack * 0.5, 0.1, direction.getStepZ() * knockBack * 0.5); + } + } + + // Apply remaining enchantments + if (entity instanceof LivingEntity target) EnchantmentHelper.doPostHurtEffects(target, player); + EnchantmentHelper.doPostDamageEffects(player, entity); + + // Damage the original item stack. + if (entity instanceof LivingEntity target) { + player.getItemInHand(InteractionHand.MAIN_HAND).hurtEnemy(target, player); + } + + // Apply fire aspect + if (entity instanceof LivingEntity target && fireAspect > 0 && !target.isOnFire()) { + target.setSecondsOnFire(4 * fireAspect); + } + + return true; + } + + private TurtleCommandResult dig(ITurtleAccess turtle, TurtleSide side, Direction direction) { + var level = (ServerLevel) turtle.getLevel(); + + var blockPosition = turtle.getPosition().relative(direction); if (level.isEmptyBlock(blockPosition) || WorldUtil.isLiquidBlock(level, blockPosition)) { return TurtleCommandResult.failure("Nothing to dig here"); } - var turtlePlayer = TurtlePlayer.getWithPosition(turtle, turtlePosition, direction); - turtlePlayer.loadInventory(item.copy()); + return withEquippedItem(turtle, side, direction, turtlePlayer -> { + var stack = turtlePlayer.player().getItemInHand(InteractionHand.MAIN_HAND); - // Check if we can break the block - var breakable = checkBlockBreakable(level, blockPosition, turtlePlayer); - if (!breakable.isSuccess()) return breakable; + // Right-click the block when using a shovel/hoe. + if (PlatformHelper.get().hasToolUsage(item) && TurtlePlaceCommand.deploy(stack, turtle, turtlePlayer, direction, null, null)) { + return TurtleCommandResult.success(); + } - DropConsumer.set(level, blockPosition, TurtleUtil.dropConsumer(turtle)); - var broken = !turtlePlayer.isBlockProtected(level, blockPosition) && turtlePlayer.player().gameMode.destroyBlock(blockPosition); - TurtleUtil.stopConsuming(turtle); + // Check if we can break the block + var breakable = checkBlockBreakable(level, blockPosition, turtlePlayer); + if (!breakable.isSuccess()) return breakable; - // Check spawn protection - return broken ? TurtleCommandResult.success() : TurtleCommandResult.failure("Cannot break protected block"); + // And break it! + DropConsumer.set(level, blockPosition, TurtleUtil.dropConsumer(turtle)); + var broken = !turtlePlayer.isBlockProtected(level, blockPosition) && turtlePlayer.player().gameMode.destroyBlock(blockPosition); + TurtleUtil.stopConsuming(turtle); + + return broken ? TurtleCommandResult.success() : TurtleCommandResult.failure("Cannot break protected block"); + }); } private static boolean isTriviallyBreakable(BlockGetter reader, BlockPos pos, BlockState state) { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleToolSerialiser.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleToolSerialiser.java index 7a635c214..f46fdfcb2 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleToolSerialiser.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleToolSerialiser.java @@ -5,6 +5,7 @@ package dan200.computercraft.shared.turtle.upgrades; import com.google.gson.JsonObject; +import dan200.computercraft.api.turtle.TurtleToolDurability; import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser; import dan200.computercraft.api.upgrades.UpgradeBase; import dan200.computercraft.shared.platform.RegistryWrappers; @@ -28,6 +29,8 @@ public final class TurtleToolSerialiser implements TurtleUpgradeSerialiser breakable = null; if (object.has("breakable")) { @@ -35,7 +38,7 @@ public final class TurtleToolSerialiser implements TurtleUpgradeSerialiser