diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index 8baff3d9c..d94091eb9 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -8,16 +8,16 @@ jobs: runs-on: ubuntu-latest steps: - - name: Clone repository + - name: 📥 Clone repository uses: actions/checkout@v3 - - name: Set up Java + - name: 📥 Set up Java uses: actions/setup-java@v3 with: java-version: 17 distribution: 'temurin' - - name: Setup Gradle + - name: 📥 Setup Gradle uses: gradle/gradle-build-action@v2 with: cache-read-only: ${{ !startsWith(github.ref, 'refs/heads/mc-') }} @@ -27,42 +27,45 @@ jobs: mkdir -p ~/.gradle echo "org.gradle.daemon=false" >> ~/.gradle/gradle.properties - - name: Build with Gradle + - name: ⚒️ Build run: ./gradlew assemble || ./gradlew assemble - - name: Download assets for game tests + - name: 💡 Lint + uses: pre-commit/action@v3.0.0 + + - name: 🧪 Run tests + run: ./gradlew test validateMixinNames checkChangelog + + - name: 📥 Download assets for game tests run: ./gradlew downloadAssets || ./gradlew downloadAssets - - name: Run tests and linters - run: ./gradlew build + - name: 🧪 Run integration tests + run: ./gradlew runGametest - - name: Run client tests + - name: 🧪 Run client tests run: ./gradlew runGametestClient # Not checkClient, as no point running rendering tests. # These are a little flaky on GH actions: its useful to run them, but don't break the build. continue-on-error: true - - name: Prepare Jars + - name: 🧪 Parse test reports + run: ./tools/parse-reports.py + if: ${{ failure() }} + + - name: 📦 Prepare Jars run: | # Find the main jar and append the git hash onto it. mkdir -p jars find projects/forge/build/libs projects/fabric/build/libs -type f -regex '.*[0-9.]+\(-SNAPSHOT\)?\.jar$' -exec bash -c 'cp {} "jars/$(basename {} .jar)-$(git rev-parse HEAD).jar"' \; - - name: Upload Jar + - name: 📤 Upload Jar uses: actions/upload-artifact@v3 with: name: CC-Tweaked path: ./jars - - name: Upload coverage + - name: 📤 Upload coverage uses: codecov/codecov-action@v3 - - name: Parse test reports - run: ./tools/parse-reports.py - if: ${{ failure() }} - - - name: Run linters - uses: pre-commit/action@v3.0.0 - build-core: strategy: fail-fast: false diff --git a/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt b/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt index 50e33a84b..a74d0008d 100644 --- a/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt +++ b/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt @@ -30,41 +30,22 @@ internal fun setRunConfigInternal(project: Project, spec: JavaExecSpec, config: val originalTask = project.tasks.named(config.taskName, MinecraftRunTask::class.java) // Add argument and JVM argument via providers, to be as lazy as possible with fetching artifacts. - fun lazyTokens(): MutableMap> { - return RunConfigGenerator.configureTokensLazy( - project, config, RunConfigGenerator.mapModClassesToGradle(project, config), - originalTask.get().minecraftArtifacts.files, - originalTask.get().runtimeClasspathArtifacts.files, - ) - } + val lazyTokens = RunConfigGenerator.configureTokensLazy( + project, config, RunConfigGenerator.mapModClassesToGradle(project, config), + originalTask.get().minecraftArtifacts, + originalTask.get().runtimeClasspathArtifacts, + ) spec.argumentProviders.add( CommandLineArgumentProvider { - RunConfigGenerator.getArgsStream(config, lazyTokens(), false).toList() + RunConfigGenerator.getArgsStream(config, lazyTokens, false).toList() }, ) spec.jvmArgumentProviders.add( CommandLineArgumentProvider { - val lazyTokens = lazyTokens() (if (config.isClient) config.jvmArgs + originalTask.get().additionalClientArgs.get() else config.jvmArgs).map { config.replace(lazyTokens, it) } + config.properties.map { (k, v) -> "-D${k}=${config.replace(lazyTokens, v)}" } }, ) - // We can't configure environment variables lazily, so we do these now with a more minimal lazyTokens set. - val lazyTokens = mutableMapOf>() - for ((k, v) in config.tokens) lazyTokens[k] = Supplier { v } - for ((k, v) in config.lazyTokens) lazyTokens[k] = v - lazyTokens.compute( - "source_roots", - { _, sourceRoots -> - Supplier { - val modClasses = RunConfigGenerator.mapModClassesToGradle(project, config) - (when (sourceRoots) { - null -> modClasses - else -> Stream.concat(sourceRoots.get().split(File.pathSeparator).stream(), modClasses) - }).distinct().collect(Collectors.joining(File.pathSeparator)) - } - }, - ) for ((key, value) in config.environment) spec.environment(key, config.replace(lazyTokens, value)) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9152cadaa..d3e1f4fc4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,8 +53,8 @@ checkstyle = "10.3.4" curseForgeGradle = "1.0.14" errorProne-core = "2.18.0" errorProne-plugin = "3.0.1" -fabric-loom = "1.2.7" -forgeGradle = "6.0.6" +fabric-loom = "1.3.7" +forgeGradle = "6.0.8" githubRelease = "2.2.12" ideaExt = "1.1.6" illuaminate = "0.1.0-28-ga7efd71" 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 0", - "gui.computercraft.config.http.rules.tooltip": "Une liste de règles qui contrôlent le comportement de l'API \"http\" pour des domaines\nou des IP spécifiques. Chaque règle est un élément avec un 'hôte' à comparer et une série\nde propriétés. Les règles sont évaluées dans l'ordre, ce qui signifie que les règles antérieures\nremplacent les suivantes.\nL'hôte peut être un nom de domaine (\"pastebin.com\"), un astérisque (\"*.pastebin.com\") ou\nune notation CIDR (\"127.0.0.0/8\").\nS'il n'y a pas de règles, le domaine est bloqué." + "gui.computercraft.config.http.rules.tooltip": "Une liste de règles qui contrôlent le comportement de l'API \"http\" pour des domaines\nou des IP spécifiques. Chaque règle est un élément avec un 'hôte' à comparer et une série\nde propriétés. Les règles sont évaluées dans l'ordre, ce qui signifie que les règles antérieures\nremplacent les suivantes.\nL'hôte peut être un nom de domaine (\"pastebin.com\"), un astérisque (\"*.pastebin.com\") ou\nune notation CIDR (\"127.0.0.0/8\").\nS'il n'y a pas de règles, le domaine est bloqué.", + "gui.computercraft.config.http.proxy.host.tooltip": "Le nom d'hôte ou l'adresse IP du serveur proxy.", + "gui.computercraft.config.http.proxy.tooltip": "Tunnelise les requêtes HTTP et websocket via un serveur proxy. Affecte uniquement\nles règles HTTP avec \"use_proxy\" défini sur true (désactivé par défaut).\nSi l'authentification est requise pour le proxy, créez un fichier \"computercraft-proxy.pw\"\ndans le même dossier que \"computercraft-server.toml\", contenant le\nnom d'utilisateur et mot de passe séparés par deux-points, par ex. \"monutilisateur:monmotdepasse\". Pour\nProxy SOCKS4, seul le nom d'utilisateur est requis.", + "gui.computercraft.config.upload_max_size.tooltip": "La taille limite de téléversement de fichier, en octets. Doit être compris entre 1 Kio et 16 Mio.\nGardez à l'esprit que les téléversements sont traités en un seul clic - les fichiers volumineux ou\nde mauvaises performances réseau peuvent bloquer le thread du réseau. Et attention à l'espace disque !\nPlage : 1024 ~ 16777216", + "gui.computercraft.config.http.proxy": "Proxy", + "gui.computercraft.config.http.proxy.host": "Nom d'hôte", + "gui.computercraft.config.http.proxy.port": "Port", + "gui.computercraft.config.http.proxy.port.tooltip": "Le port du serveur proxy.\nPlage : 1 ~ 65536", + "gui.computercraft.config.http.proxy.type": "Type de proxy", + "gui.computercraft.config.http.proxy.type.tooltip": "Le type de proxy à utiliser.\nValeurs autorisées : HTTP, HTTPS, SOCKS4, SOCKS5", + "gui.computercraft.config.upload_max_size": "Taille limite de téléversement de fichiers (octets)" } diff --git a/projects/common/src/main/resources/assets/computercraft/lang/ru_ru.json b/projects/common/src/main/resources/assets/computercraft/lang/ru_ru.json index 8495d2376..66a7390ca 100644 --- a/projects/common/src/main/resources/assets/computercraft/lang/ru_ru.json +++ b/projects/common/src/main/resources/assets/computercraft/lang/ru_ru.json @@ -177,5 +177,23 @@ "gui.computercraft.config.turtle.need_fuel": "Включить механику топлива", "gui.computercraft.config.turtle.normal_fuel_limit": "Лимит топлива Черепашек", "gui.computercraft.config.turtle.normal_fuel_limit.tooltip": "Лимит топлива для Черепашек.\nОграничение: > 0", - "gui.computercraft.config.turtle.tooltip": "Разные настройки, связанные с черепашками." + "gui.computercraft.config.turtle.tooltip": "Разные настройки, связанные с черепашками.", + "gui.computercraft.config.http.proxy.port": "Порт", + "gui.computercraft.config.http.proxy.port.tooltip": "Порт прокси-сервера.\nДиапазон: 1 ~ 65536", + "gui.computercraft.config.http.proxy.host": "Имя хоста", + "gui.computercraft.config.http.proxy": "Proxy", + "gui.computercraft.config.http.proxy.host.tooltip": "Имя хоста или IP-адрес прокси-сервера.", + "gui.computercraft.config.http.proxy.tooltip": "Туннелирует HTTP-запросы и запросы websocket через прокси-сервер. Влияет только на HTTP\nправила с параметром \"use_proxy\" в значении true (отключено по умолчанию).\nЕсли для прокси-сервера требуется аутентификация, создайте \"computercraft-proxy.pw\"\nфайл в том же каталоге, что и \"computercraft-server.toml\", содержащий имя\nпользователя и пароль, разделенные двоеточием, например \"myuser:mypassword\". Для\nпрокси-серверов SOCKS4 требуется только имя пользователя.", + "gui.computercraft.config.http.proxy.type": "Тип прокси-сервера", + "gui.computercraft.config.http.proxy.type.tooltip": "Тип используемого прокси-сервера.\nДопустимые значения: HTTP, HTTPS, SOCKS4, SOCKS5", + "gui.computercraft.upload.no_response.msg": "Ваш компьютер не использовал переданные вами файлы. Возможно, вам потребуется запустить программу %s и повторить попытку.", + "tracking_field.computercraft.max": "%s (максимальное)", + "tracking_field.computercraft.count": "%s (количество)", + "gui.computercraft.config.http.rules": "Разрешающие/запрещающие правила", + "gui.computercraft.config.http.websocket_enabled": "Включить веб-сокеты", + "gui.computercraft.config.http.websocket_enabled.tooltip": "Включить использование http веб-сокетов. Для этого необходимо, чтобы параметр «http_enable» был true.", + "gui.computercraft.config.log_computer_errors": "Регистрировать ошибки компьютера", + "gui.computercraft.config.log_computer_errors.tooltip": "Регистрировать исключения, вызванные периферийными устройствами и другими объектами Lua. Это облегчает\nдля авторам модов устранение проблем, но может привести к спаму в логах, если люди будут использовать\nглючные методы.", + "gui.computercraft.config.maximum_open_files": "Максимальное количество файлов, открытых на одном компьютере", + "gui.computercraft.config.http.rules.tooltip": "Список правил, которые контролируют поведение «http» API для определенных доменов или\nIP-адресов. Каждое правило представляет собой элемент с «узлом» для сопоставления и набором\nсвойств. Правила оцениваются по порядку, то есть более ранние правила перевешивают\nболее поздние.\nХост может быть доменным именем (\"pastebin.com\"), wildcard-сертификатом (\"*.pastebin.com\") или\nнотацией CIDR (\"127.0.0.0/8\").\nЕсли правил нет, домен блокируется." } 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 c48c1db1f..4bce69373 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 @@ -6,6 +6,7 @@ package dan200.computercraft.shared.platform; import com.mojang.authlib.GameProfile; import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.EntityDimensions; import net.minecraft.world.entity.Pose; @@ -39,4 +40,9 @@ public final class FakePlayer extends net.fabricmc.fabric.api.entity.FakePlayer public double getBlockReach() { return MAX_REACH; } + + @Override + public boolean broadcastToPlayer(ServerPlayer player) { + return false; + } } 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 aa8340722..ad32e9280 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 @@ -6,6 +6,7 @@ package dan200.computercraft.shared.platform; import com.mojang.authlib.GameProfile; import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.MenuProvider; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityDimensions; @@ -57,4 +58,9 @@ class FakePlayerExt extends FakePlayer { public double getEntityReach() { return MAX_REACH; } + + @Override + public boolean broadcastToPlayer(ServerPlayer player) { + return false; + } }