diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/component/ComputerComponents.java b/projects/common-api/src/main/java/dan200/computercraft/api/component/ComputerComponents.java index 9b391b5a4..4ab99e50e 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/component/ComputerComponents.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/component/ComputerComponents.java @@ -6,6 +6,7 @@ package dan200.computercraft.api.component; import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.pocket.IPocketAccess; +import dan200.computercraft.api.pocket.PocketComputer; import dan200.computercraft.api.turtle.ITurtleAccess; /** @@ -20,7 +21,7 @@ public class ComputerComponents { /** * The {@link IPocketAccess} associated with a pocket computer. */ - public static final ComputerComponent POCKET = ComputerComponent.create(ComputerCraftAPI.MOD_ID, "pocket"); + public static final ComputerComponent POCKET = ComputerComponent.create(ComputerCraftAPI.MOD_ID, "pocket"); /** * This component is only present on "command computers", and other computers with admin capabilities. diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/pocket/IPocketAccess.java b/projects/common-api/src/main/java/dan200/computercraft/api/pocket/IPocketAccess.java index aaacd988a..1916b9a93 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/pocket/IPocketAccess.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/pocket/IPocketAccess.java @@ -7,60 +7,15 @@ package dan200.computercraft.api.pocket; import dan200.computercraft.api.upgrades.UpgradeBase; import dan200.computercraft.api.upgrades.UpgradeData; import net.minecraft.core.component.DataComponentPatch; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.entity.Entity; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.ApiStatus; import org.jspecify.annotations.Nullable; /** - * Wrapper class for pocket computers. + * Access to a pocket computer for {@linkplain IPocketUpgrade pocket upgrades}. */ @ApiStatus.NonExtendable -public interface IPocketAccess { - /** - * Get the level in which the pocket computer exists. - * - * @return The pocket computer's level. - */ - ServerLevel getLevel(); - - /** - * Get the position of the pocket computer. - * - * @return The pocket computer's position. - */ - Vec3 getPosition(); - - /** - * Gets the entity holding this item. - *

- * This must be called on the server thread. - * - * @return The holding entity, or {@code null} if none exists. - */ - @Nullable - Entity getEntity(); - - /** - * Get the colour of this pocket computer as a RGB number. - * - * @return The colour this pocket computer is. This will be a RGB colour between {@code 0x000000} and - * {@code 0xFFFFFF} or -1 if it has no colour. - * @see #setColour(int) - */ - int getColour(); - - /** - * Set the colour of the pocket computer to a RGB number. - * - * @param colour The colour this pocket computer should be changed to. This should be a RGB colour between - * {@code 0x000000} and {@code 0xFFFFFF} or -1 to reset to the default colour. - * @see #getColour() - */ - void setColour(int colour); - +public interface IPocketAccess extends PocketComputer { /** * Get the colour of this pocket computer's light as a RGB number. * @@ -92,7 +47,8 @@ public interface IPocketAccess { /** * Set the upgrade for this pocket computer, also updating the item stack. *

- * Note this method is not thread safe - it must be called from the server thread. + * This method can only be called from the main server thread, when this computer is {@linkplain #isActive() is + * active}. * * @param upgrade The new upgrade to set it to, may be {@code null}. * @see #getUpgrade() @@ -114,6 +70,9 @@ public interface IPocketAccess { /** * Update the upgrade-specific data. + *

+ * This method can only be called from the main server thread, when this computer is {@linkplain #isActive() is + * active}. * * @param data The new upgrade data. * @see #getUpgradeData() diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/pocket/IPocketUpgrade.java b/projects/common-api/src/main/java/dan200/computercraft/api/pocket/IPocketUpgrade.java index afa7198e3..4c7f24a87 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/pocket/IPocketUpgrade.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/pocket/IPocketUpgrade.java @@ -12,7 +12,7 @@ import dan200.computercraft.impl.ComputerCraftAPIService; import net.minecraft.core.Registry; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.level.Level; +import net.minecraft.server.level.ServerLevel; import org.jspecify.annotations.Nullable; /** @@ -71,7 +71,7 @@ public interface IPocketUpgrade extends UpgradeBase { /** * Called when the pocket computer is right clicked. * - * @param world The world the computer is in. + * @param level The world the computer is in. * @param access The access object for the pocket item stack. * @param peripheral The peripheral for this upgrade. * @return {@code true} to stop the GUI from opening, otherwise false. You should always provide some code path @@ -79,7 +79,7 @@ public interface IPocketUpgrade extends UpgradeBase { * access the GUI. * @see #createPeripheral(IPocketAccess) */ - default boolean onRightClick(Level world, IPocketAccess access, @Nullable IPeripheral peripheral) { + default boolean onRightClick(ServerLevel level, IPocketAccess access, @Nullable IPeripheral peripheral) { return false; } } diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/pocket/PocketComputer.java b/projects/common-api/src/main/java/dan200/computercraft/api/pocket/PocketComputer.java new file mode 100644 index 000000000..9b2d13bc5 --- /dev/null +++ b/projects/common-api/src/main/java/dan200/computercraft/api/pocket/PocketComputer.java @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.api.pocket; + +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +/** + * A pocket computer. + * + * @see IPocketAccess + * @see dan200.computercraft.api.component.ComputerComponents#POCKET + */ +@ApiStatus.NonExtendable +public interface PocketComputer { + /** + * Get the level in which the pocket computer exists. + * + * @return The pocket computer's level. + */ + ServerLevel getLevel(); + + /** + * Get the position of the pocket computer. + * + * @return The pocket computer's position. + */ + Vec3 getPosition(); + + /** + * Gets the entity holding this item. + *

+ * This must be called on the server thread. + * + * @return The holding entity, or {@code null} if none exists. + */ + @Nullable + Entity getEntity(); + + /** + * Check whether this pocket computer is currently being held by a player, lectern, or other valid entity. + *

+ * As pocket computers are backed by item stacks, you must check for validity before updating the computer. + *

+ * This must be called on the server thread. + * + * @return Whether this computer is active. + */ + boolean isActive(); + + /** + * Get the colour of this pocket computer as an RGB number. + * + *

+ * This method can only be called from the main server thread, when this computer is {@linkplain #isActive() is + * active}. + * + * @return The colour this pocket computer is. This will be a RGB colour between {@code 0x000000} and + * {@code 0xFFFFFF} or -1 if it has no colour. + * @see #setColour(int) + */ + int getColour(); + + /** + * Set the colour of the pocket computer to an RGB number. + *

+ * This method can only be called from the main server thread, when this computer is {@linkplain #isActive() is + * active}. + * + * @param colour The colour this pocket computer should be changed to. This should be a RGB colour between + * {@code 0x000000} and {@code 0xFFFFFF} or -1 to reset to the default colour. + * @see #getColour() + */ + void setColour(int colour); + +} diff --git a/projects/common/src/datagen/java/dan200/computercraft/data/LanguageProvider.java b/projects/common/src/datagen/java/dan200/computercraft/data/LanguageProvider.java index 06094aa4b..62e3d468b 100644 --- a/projects/common/src/datagen/java/dan200/computercraft/data/LanguageProvider.java +++ b/projects/common/src/datagen/java/dan200/computercraft/data/LanguageProvider.java @@ -100,8 +100,10 @@ public final class LanguageProvider implements DataProvider { add(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get(), "Pocket Computer"); add(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get().getDescriptionId() + ".upgraded", "%s Pocket Computer"); + add(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get().getDescriptionId() + ".upgraded_twice", "%s %s Pocket Computer"); add(ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get(), "Advanced Pocket Computer"); add(ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get().getDescriptionId() + ".upgraded", "Advanced %s Pocket Computer"); + add(ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get().getDescriptionId() + ".upgraded_twice", "Advanced %s %s Pocket Computer"); // Tags (for EMI) add(ComputerCraftTags.Items.COMPUTER, "Computers"); diff --git a/projects/common/src/datagen/java/dan200/computercraft/data/RecipeProvider.java b/projects/common/src/datagen/java/dan200/computercraft/data/RecipeProvider.java index 47bdce5e2..263031feb 100644 --- a/projects/common/src/datagen/java/dan200/computercraft/data/RecipeProvider.java +++ b/projects/common/src/datagen/java/dan200/computercraft/data/RecipeProvider.java @@ -141,7 +141,7 @@ final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider { registries.lookupOrThrow(IPocketUpgrade.REGISTRY).listElements().forEach(upgradeHolder -> { var upgrade = upgradeHolder.value(); - customShaped(RecipeCategory.REDSTONE, DataComponentUtil.createStack(pocket, ModRegistry.DataComponents.POCKET_UPGRADE.get(), UpgradeData.ofDefault(upgradeHolder))) + customShaped(RecipeCategory.REDSTONE, DataComponentUtil.createStack(pocket, ModRegistry.DataComponents.BACK_POCKET_UPGRADE.get(), UpgradeData.ofDefault(upgradeHolder))) .group(name.toString()) .pattern("#") .pattern("P") diff --git a/projects/common/src/generated/resources/assets/computercraft/lang/en_us.json b/projects/common/src/generated/resources/assets/computercraft/lang/en_us.json index 5789bf45b..dcf82714b 100644 --- a/projects/common/src/generated/resources/assets/computercraft/lang/en_us.json +++ b/projects/common/src/generated/resources/assets/computercraft/lang/en_us.json @@ -195,8 +195,10 @@ "item.computercraft.disk": "Floppy Disk", "item.computercraft.pocket_computer_advanced": "Advanced Pocket Computer", "item.computercraft.pocket_computer_advanced.upgraded": "Advanced %s Pocket Computer", + "item.computercraft.pocket_computer_advanced.upgraded_twice": "Advanced %s %s Pocket Computer", "item.computercraft.pocket_computer_normal": "Pocket Computer", "item.computercraft.pocket_computer_normal.upgraded": "%s Pocket Computer", + "item.computercraft.pocket_computer_normal.upgraded_twice": "%s %s Pocket Computer", "item.computercraft.printed_book": "Printed Book", "item.computercraft.printed_page": "Printed Page", "item.computercraft.printed_pages": "Printed Pages", diff --git a/projects/common/src/generated/resources/data/computercraft/recipe/pocket_advanced/computercraft/speaker.json b/projects/common/src/generated/resources/data/computercraft/recipe/pocket_advanced/computercraft/speaker.json index de0c09b5c..ee3e85b5c 100644 --- a/projects/common/src/generated/resources/data/computercraft/recipe/pocket_advanced/computercraft/speaker.json +++ b/projects/common/src/generated/resources/data/computercraft/recipe/pocket_advanced/computercraft/speaker.json @@ -5,7 +5,7 @@ "key": {"#": "computercraft:speaker", "P": "computercraft:pocket_computer_advanced"}, "pattern": ["#", "P"], "result": { - "components": {"computercraft:pocket_upgrade": {"id": "computercraft:speaker"}}, + "components": {"computercraft:back_pocket_upgrade": {"id": "computercraft:speaker"}}, "count": 1, "id": "computercraft:pocket_computer_advanced" } diff --git a/projects/common/src/generated/resources/data/computercraft/recipe/pocket_advanced/computercraft/wireless_modem_advanced.json b/projects/common/src/generated/resources/data/computercraft/recipe/pocket_advanced/computercraft/wireless_modem_advanced.json index d6798f026..d417ef6ff 100644 --- a/projects/common/src/generated/resources/data/computercraft/recipe/pocket_advanced/computercraft/wireless_modem_advanced.json +++ b/projects/common/src/generated/resources/data/computercraft/recipe/pocket_advanced/computercraft/wireless_modem_advanced.json @@ -5,7 +5,7 @@ "key": {"#": "computercraft:wireless_modem_advanced", "P": "computercraft:pocket_computer_advanced"}, "pattern": ["#", "P"], "result": { - "components": {"computercraft:pocket_upgrade": {"id": "computercraft:wireless_modem_advanced"}}, + "components": {"computercraft:back_pocket_upgrade": {"id": "computercraft:wireless_modem_advanced"}}, "count": 1, "id": "computercraft:pocket_computer_advanced" } diff --git a/projects/common/src/generated/resources/data/computercraft/recipe/pocket_advanced/computercraft/wireless_modem_normal.json b/projects/common/src/generated/resources/data/computercraft/recipe/pocket_advanced/computercraft/wireless_modem_normal.json index f86914d72..e14a2fd45 100644 --- a/projects/common/src/generated/resources/data/computercraft/recipe/pocket_advanced/computercraft/wireless_modem_normal.json +++ b/projects/common/src/generated/resources/data/computercraft/recipe/pocket_advanced/computercraft/wireless_modem_normal.json @@ -5,7 +5,7 @@ "key": {"#": "computercraft:wireless_modem_normal", "P": "computercraft:pocket_computer_advanced"}, "pattern": ["#", "P"], "result": { - "components": {"computercraft:pocket_upgrade": {"id": "computercraft:wireless_modem_normal"}}, + "components": {"computercraft:back_pocket_upgrade": {"id": "computercraft:wireless_modem_normal"}}, "count": 1, "id": "computercraft:pocket_computer_advanced" } diff --git a/projects/common/src/generated/resources/data/computercraft/recipe/pocket_normal/computercraft/speaker.json b/projects/common/src/generated/resources/data/computercraft/recipe/pocket_normal/computercraft/speaker.json index 3836d300c..a53c503a7 100644 --- a/projects/common/src/generated/resources/data/computercraft/recipe/pocket_normal/computercraft/speaker.json +++ b/projects/common/src/generated/resources/data/computercraft/recipe/pocket_normal/computercraft/speaker.json @@ -5,7 +5,7 @@ "key": {"#": "computercraft:speaker", "P": "computercraft:pocket_computer_normal"}, "pattern": ["#", "P"], "result": { - "components": {"computercraft:pocket_upgrade": {"id": "computercraft:speaker"}}, + "components": {"computercraft:back_pocket_upgrade": {"id": "computercraft:speaker"}}, "count": 1, "id": "computercraft:pocket_computer_normal" } diff --git a/projects/common/src/generated/resources/data/computercraft/recipe/pocket_normal/computercraft/wireless_modem_advanced.json b/projects/common/src/generated/resources/data/computercraft/recipe/pocket_normal/computercraft/wireless_modem_advanced.json index 90ca2f8bb..fa2a439b9 100644 --- a/projects/common/src/generated/resources/data/computercraft/recipe/pocket_normal/computercraft/wireless_modem_advanced.json +++ b/projects/common/src/generated/resources/data/computercraft/recipe/pocket_normal/computercraft/wireless_modem_advanced.json @@ -5,7 +5,7 @@ "key": {"#": "computercraft:wireless_modem_advanced", "P": "computercraft:pocket_computer_normal"}, "pattern": ["#", "P"], "result": { - "components": {"computercraft:pocket_upgrade": {"id": "computercraft:wireless_modem_advanced"}}, + "components": {"computercraft:back_pocket_upgrade": {"id": "computercraft:wireless_modem_advanced"}}, "count": 1, "id": "computercraft:pocket_computer_normal" } diff --git a/projects/common/src/generated/resources/data/computercraft/recipe/pocket_normal/computercraft/wireless_modem_normal.json b/projects/common/src/generated/resources/data/computercraft/recipe/pocket_normal/computercraft/wireless_modem_normal.json index 906bece74..6c27cac6e 100644 --- a/projects/common/src/generated/resources/data/computercraft/recipe/pocket_normal/computercraft/wireless_modem_normal.json +++ b/projects/common/src/generated/resources/data/computercraft/recipe/pocket_normal/computercraft/wireless_modem_normal.json @@ -5,7 +5,7 @@ "key": {"#": "computercraft:wireless_modem_normal", "P": "computercraft:pocket_computer_normal"}, "pattern": ["#", "P"], "result": { - "components": {"computercraft:pocket_upgrade": {"id": "computercraft:wireless_modem_normal"}}, + "components": {"computercraft:back_pocket_upgrade": {"id": "computercraft:wireless_modem_normal"}}, "count": 1, "id": "computercraft:pocket_computer_normal" } diff --git a/projects/common/src/main/java/dan200/computercraft/impl/UpgradeManager.java b/projects/common/src/main/java/dan200/computercraft/impl/UpgradeManager.java index 930eb7436..abafa87ad 100644 --- a/projects/common/src/main/java/dan200/computercraft/impl/UpgradeManager.java +++ b/projects/common/src/main/java/dan200/computercraft/impl/UpgradeManager.java @@ -16,6 +16,7 @@ import net.minecraft.core.HolderLookup; import net.minecraft.core.Registry; import net.minecraft.core.component.DataComponentPatch; import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.chat.Component; import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; import net.minecraft.resources.RegistryFixedCodec; @@ -95,6 +96,29 @@ public final class UpgradeManager { // TODO: Would be nice if we could use the registration info here. } + /** + * Determine our "creator mod" from a list of upgrades. + *

+ * We attempt to find the first non-vanilla/non-CC upgrade. + * + * @param first The first upgrade. + * @param second The second upgrade. + * @return The owning mod id of this item. + */ + public String getOwner(@Nullable UpgradeData first, @Nullable UpgradeData second) { + if (first != null) { + var mod = getOwner(first.holder()); + if (!mod.equals(ComputerCraftAPI.MOD_ID)) return mod; + } + + if (second != null) { + var mod = getOwner(second.holder()); + if (!mod.equals(ComputerCraftAPI.MOD_ID)) return mod; + } + + return ComputerCraftAPI.MOD_ID; + } + @Nullable public UpgradeData get(HolderLookup.Provider registries, ItemStack stack) { if (stack.isEmpty()) return null; @@ -109,4 +133,16 @@ public final class UpgradeManager { .map(x -> UpgradeData.of(x, x.value().getUpgradeData(stack))) .orElse(null); } + + public static Component getName(String baseString, @Nullable UpgradeBase first, @Nullable UpgradeBase second) { + if (first != null && second != null) { + return Component.translatable(baseString + ".upgraded_twice", second.getAdjective(), first.getAdjective()); + } else if (first != null) { + return Component.translatable(baseString + ".upgraded", first.getAdjective()); + } else if (second != null) { + return Component.translatable(baseString + ".upgraded", second.getAdjective()); + } else { + return Component.translatable(baseString); + } + } } diff --git a/projects/common/src/main/java/dan200/computercraft/mixin/DataFixersMixin.java b/projects/common/src/main/java/dan200/computercraft/mixin/DataFixersMixin.java index 747800701..bbb89ef05 100644 --- a/projects/common/src/main/java/dan200/computercraft/mixin/DataFixersMixin.java +++ b/projects/common/src/main/java/dan200/computercraft/mixin/DataFixersMixin.java @@ -8,6 +8,7 @@ import com.llamalad7.mixinextras.sugar.Local; import com.mojang.datafixers.DataFixUtils; import com.mojang.datafixers.DataFixerBuilder; import com.mojang.datafixers.schemas.Schema; +import dan200.computercraft.shared.datafix.RenamePocketComputerUpgradeFix; import dan200.computercraft.shared.datafix.TurtleUpgradeComponentizationFix; import net.minecraft.util.datafix.DataFixers; import net.minecraft.util.datafix.fixes.ItemStackComponentizationFix; @@ -42,6 +43,26 @@ abstract class DataFixersMixin { return schema; } + /** + * Register a {@link RenamePocketComputerUpgradeFix} fix. + * + * @param schema The {@code V4314} schema. + * @param builder The current datafixer builder. + * @return The input schema. + */ + @ModifyArg( + method = "addFixers", + at = @At(value = "INVOKE", target = "Lnet/minecraft/util/datafix/fixes/InlineBlockPosFormatFix;(Lcom/mojang/datafixers/schemas/Schema;)V"), + index = 0, + allow = 1 + ) + @SuppressWarnings("UnusedMethod") + private static Schema addRenamePocketComputerUpgradeFix(Schema schema, @Local DataFixerBuilder builder) { + assertSchemaVersion(schema, RenamePocketComputerUpgradeFix.SCHEMA_VERSION); + builder.addFixer(new RenamePocketComputerUpgradeFix(schema)); + return schema; + } + @Unique private static void assertSchemaVersion(Schema schema, int version) { if (schema.getVersionKey() != version) { diff --git a/projects/common/src/main/java/dan200/computercraft/mixin/V3818_3Mixin.java b/projects/common/src/main/java/dan200/computercraft/mixin/V3818_3Mixin.java index 9b0841a66..3ff9a4dba 100644 --- a/projects/common/src/main/java/dan200/computercraft/mixin/V3818_3Mixin.java +++ b/projects/common/src/main/java/dan200/computercraft/mixin/V3818_3Mixin.java @@ -25,7 +25,7 @@ class V3818_3Mixin { @ModifyReturnValue(method = "components", at = @At("TAIL")) @SuppressWarnings("UnusedMethod") private static SequencedMap> components(SequencedMap> types, Schema schema) { - ComponentizationFixers.addExtraTypes(types, schema); + ComponentizationFixers.addComponents(types, schema); return types; } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java b/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java index 79022143e..5446ef7c6 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java @@ -20,6 +20,7 @@ import dan200.computercraft.api.upgrades.UpgradeBase; import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.api.upgrades.UpgradeType; import dan200.computercraft.impl.PocketUpgrades; +import dan200.computercraft.impl.TurtleUpgrades; import dan200.computercraft.shared.command.UserLevel; import dan200.computercraft.shared.command.arguments.ComputerArgumentType; import dan200.computercraft.shared.command.arguments.RepeatArgumentType; @@ -71,6 +72,7 @@ import dan200.computercraft.shared.platform.PlatformHelper; import dan200.computercraft.shared.platform.RegistrationHelper; import dan200.computercraft.shared.platform.RegistryEntry; import dan200.computercraft.shared.pocket.apis.PocketAPI; +import dan200.computercraft.shared.pocket.core.PocketComputerInternal; import dan200.computercraft.shared.pocket.items.PocketComputerItem; import dan200.computercraft.shared.pocket.peripherals.PocketModem; import dan200.computercraft.shared.pocket.peripherals.PocketSpeaker; @@ -365,7 +367,7 @@ public final class ModRegistry { * @see TurtleItem */ public static final RegistryEntry>> LEFT_TURTLE_UPGRADE = register("left_turtle_upgrade", b -> b - .persistent(dan200.computercraft.impl.TurtleUpgrades.instance().upgradeDataCodec()).networkSynchronized(dan200.computercraft.impl.TurtleUpgrades.instance().upgradeDataStreamCodec()) + .persistent(TurtleUpgrades.instance().upgradeDataCodec()).networkSynchronized(TurtleUpgrades.instance().upgradeDataStreamCodec()) ); /** @@ -374,7 +376,7 @@ public final class ModRegistry { * @see TurtleItem */ public static final RegistryEntry>> RIGHT_TURTLE_UPGRADE = register("right_turtle_upgrade", b -> b - .persistent(dan200.computercraft.impl.TurtleUpgrades.instance().upgradeDataCodec()).networkSynchronized(dan200.computercraft.impl.TurtleUpgrades.instance().upgradeDataStreamCodec()) + .persistent(TurtleUpgrades.instance().upgradeDataCodec()).networkSynchronized(TurtleUpgrades.instance().upgradeDataStreamCodec()) ); /** @@ -396,7 +398,16 @@ public final class ModRegistry { * * @see PocketComputerItem */ - public static final RegistryEntry>> POCKET_UPGRADE = register("pocket_upgrade", b -> b + public static final RegistryEntry>> BACK_POCKET_UPGRADE = register("back_pocket_upgrade", b -> b + .persistent(PocketUpgrades.instance().upgradeDataCodec()).networkSynchronized(PocketUpgrades.instance().upgradeDataStreamCodec()) + ); + + /** + * The back upgrade of a pocket computer. + * + * @see PocketComputerItem + */ + public static final RegistryEntry>> BOTTOM_POCKET_UPGRADE = register("bottom_pocket_upgrade", b -> b .persistent(PocketUpgrades.instance().upgradeDataCodec()).networkSynchronized(PocketUpgrades.instance().upgradeDataStreamCodec()) ); @@ -649,7 +660,7 @@ public final class ModRegistry { ComputerCraftAPI.registerAPIFactory(computer -> { var pocket = computer.getComponent(ComputerComponents.POCKET); - return pocket == null ? null : new PocketAPI(pocket); + return pocket == null ? null : new PocketAPI((PocketComputerInternal) pocket); }); ComputerCraftAPI.registerAPIFactory(computer -> { @@ -751,7 +762,7 @@ public final class ModRegistry { out.accept(new ItemStack(pocket)); registries.lookupOrThrow(IPocketUpgrade.REGISTRY).listElements() .filter(ModRegistry::isOurUpgrade) - .map(x -> DataComponentUtil.createStack(pocket, DataComponents.POCKET_UPGRADE.get(), UpgradeData.ofDefault(x))).forEach(out::accept); + .map(x -> DataComponentUtil.createStack(pocket, DataComponents.BACK_POCKET_UPGRADE.get(), UpgradeData.ofDefault(x))).forEach(out::accept); } private static boolean isOurUpgrade(Holder.Reference upgrade) { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/datafix/ComponentizationFixers.java b/projects/common/src/main/java/dan200/computercraft/shared/datafix/ComponentizationFixers.java index ac62649ff..b41c4b3d0 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/datafix/ComponentizationFixers.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/datafix/ComponentizationFixers.java @@ -204,21 +204,30 @@ public class ComponentizationFixers { return newUpgrade; } + /** * Add our custom data components to the datafixer system. * * @param types The component type definition. * @param schema The current schema. * @see UpgradeManager#upgradeDataCodec() - * @see ModRegistry.DataComponents#POCKET_UPGRADE + * @see ModRegistry.DataComponents#BOTTOM_POCKET_UPGRADE + * @see ModRegistry.DataComponents#BACK_POCKET_UPGRADE * @see ModRegistry.DataComponents#LEFT_TURTLE_UPGRADE * @see ModRegistry.DataComponents#RIGHT_TURTLE_UPGRADE */ - public static void addExtraTypes(Map> types, Schema schema) { + public static void addComponents(Map> types, Schema schema) { // Create a codec for UpgradeData Supplier upgradeData = () -> DSL.optionalFields("components", References.DATA_COMPONENTS.in(schema)); - types.put("computercraft:pocket_upgrade", upgradeData); + if (schema.getVersionKey() < RenamePocketComputerUpgradeFix.SCHEMA_VERSION) { + types.put("computercraft:pocket_upgrade", upgradeData); + } else { + // Add extra upgrades on later versions. Really this should be done by overriding + types.put("computercraft:back_pocket_upgrade", upgradeData); + types.put("computercraft:bottom_pocket_upgrade", upgradeData); + } + types.put("computercraft:left_turtle_upgrade", upgradeData); types.put("computercraft:right_turtle_upgrade", upgradeData); } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/datafix/RenamePocketComputerUpgradeFix.java b/projects/common/src/main/java/dan200/computercraft/shared/datafix/RenamePocketComputerUpgradeFix.java new file mode 100644 index 000000000..9513c431c --- /dev/null +++ b/projects/common/src/main/java/dan200/computercraft/shared/datafix/RenamePocketComputerUpgradeFix.java @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.datafix; + +import com.mojang.datafixers.DataFix; +import com.mojang.datafixers.DataFixUtils; +import com.mojang.datafixers.TypeRewriteRule; +import com.mojang.datafixers.schemas.Schema; +import com.mojang.datafixers.types.Type; +import dan200.computercraft.shared.ModRegistry; +import net.minecraft.util.datafix.fixes.DataComponentRemainderFix; +import net.minecraft.util.datafix.fixes.FoodToConsumableFix; +import net.minecraft.util.datafix.fixes.References; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.function.Function; + +/** + * Renames {@code "computercraft:pocket_upgrade"} to {@link ModRegistry.DataComponents#BACK_POCKET_UPGRADE + * "computercraft:back_pocket_upgrade"}. + * + * @see ComponentizationFixers#addComponents(Map, Schema) + */ +public final class RenamePocketComputerUpgradeFix extends DataFix { + public static final int SCHEMA_VERSION = DataFixUtils.makeKey(4314, 0); + + public RenamePocketComputerUpgradeFix(Schema outputSchema) { + super(outputSchema, true); + } + + /** + * Make a rewrite rule to rename a component. + *

+ * We use {@link #writeFixAndRead(String, Type, Type, Function)} rather than + * {@link #fixTypeEverywhereTyped(String, Type, Function)}, as the types don't neatly line up. This is consistent + * with what {@link FoodToConsumableFix} does. + *

+ * {@link DataComponentRemainderFix} does use {@code fixTypeEverywhereTyped}. However, none of the + * components it references are in the component map, so don't cause the type to change! + * + * @return The constructed rewrite rule. + */ + @Override + protected TypeRewriteRule makeRule() { + return writeFixAndRead( + "Pocket upgrade rename", + getInputSchema().getType(References.DATA_COMPONENTS), + getOutputSchema().getType(References.DATA_COMPONENTS), + dynamic -> dynamic.renameField("computercraft:pocket_upgrade", "computercraft:back_pocket_upgrade") + ); + } + + private static final Logger LOG = LoggerFactory.getLogger(RenamePocketComputerUpgradeFix.class); +} diff --git a/projects/common/src/main/java/dan200/computercraft/shared/integration/RecipeModHelpers.java b/projects/common/src/main/java/dan200/computercraft/shared/integration/RecipeModHelpers.java index 036c7ef85..8003c4f26 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/integration/RecipeModHelpers.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/integration/RecipeModHelpers.java @@ -69,7 +69,7 @@ public final class RecipeModHelpers { for (var pocketSupplier : POCKET_COMPUTERS) { var pocket = pocketSupplier.get(); forEachRegistry(registries, IPocketUpgrade.REGISTRY, upgrade -> - upgradeItems.add(DataComponentUtil.createStack(pocket, ModRegistry.DataComponents.POCKET_UPGRADE.get(), UpgradeData.ofDefault(upgrade))) + upgradeItems.add(DataComponentUtil.createStack(pocket, ModRegistry.DataComponents.BACK_POCKET_UPGRADE.get(), UpgradeData.ofDefault(upgrade))) ); } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/integration/UpgradeRecipeGenerator.java b/projects/common/src/main/java/dan200/computercraft/shared/integration/UpgradeRecipeGenerator.java index a07d4089d..9f668ba80 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/integration/UpgradeRecipeGenerator.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/integration/UpgradeRecipeGenerator.java @@ -10,6 +10,7 @@ import dan200.computercraft.api.turtle.TurtleSide; import dan200.computercraft.api.upgrades.UpgradeBase; import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.shared.ModRegistry; +import dan200.computercraft.shared.pocket.core.PocketSide; import dan200.computercraft.shared.pocket.items.PocketComputerItem; import dan200.computercraft.shared.turtle.items.TurtleItem; import dan200.computercraft.shared.util.DataComponentUtil; @@ -134,14 +135,22 @@ public class UpgradeRecipeGenerator { return Collections.unmodifiableList(recipes); } else if (stack.getItem() instanceof PocketComputerItem) { // Suggest possible upgrades which can be applied to this turtle - var back = PocketComputerItem.getUpgrade(stack); - if (back != null) return List.of(); + var back = PocketComputerItem.getUpgradeWithData(stack, PocketSide.BACK); + var bottom = PocketComputerItem.getUpgradeWithData(stack, PocketSide.BOTTOM); + if (back != null && bottom != null) return List.of(); List recipes = new ArrayList<>(); var ingredient = new SlotDisplay.ItemStackSlotDisplay(stack); for (var upgrade : pocketUpgrades) { if (upgrade.pocket == null) throw new NullPointerException(); - recipes.add(pocket(upgrade.ingredient, ingredient, pocketWith(stack, UpgradeData.ofDefault(upgrade.pocket)))); + + if (back == null) { + recipes.add(pocket(upgrade.ingredient, ingredient, pocketWith(stack, UpgradeData.ofDefault(upgrade.pocket), bottom))); + } + + if (bottom == null) { + recipes.add(pocket(ingredient, upgrade.ingredient, pocketWith(stack, back, UpgradeData.ofDefault(upgrade.pocket)))); + } } return Collections.unmodifiableList(recipes); @@ -208,9 +217,22 @@ public class UpgradeRecipeGenerator { } else if (stack.getItem() instanceof PocketComputerItem) { List recipes = new ArrayList<>(0); - var back = PocketComputerItem.getUpgradeWithData(stack); + var back = PocketComputerItem.getUpgradeWithData(stack, PocketSide.BACK); + var bottom = PocketComputerItem.getUpgradeWithData(stack, PocketSide.BOTTOM); if (back != null) { - recipes.add(pocket(new SlotDisplay.ItemStackSlotDisplay(back.getUpgradeItem()), new SlotDisplay.ItemStackSlotDisplay(pocketWith(stack, null)), stack)); + recipes.add(pocket( + new SlotDisplay.ItemStackSlotDisplay(back.getUpgradeItem()), + new SlotDisplay.ItemStackSlotDisplay(pocketWith(stack, null, bottom)), + stack + )); + } + + if (bottom != null) { + recipes.add(pocket( + new SlotDisplay.ItemStackSlotDisplay(pocketWith(stack, back, null)), + new SlotDisplay.ItemStackSlotDisplay(bottom.getUpgradeItem()), + stack + )); } return Collections.unmodifiableList(recipes); @@ -226,15 +248,16 @@ public class UpgradeRecipeGenerator { return newStack; } - private static ItemStack pocketWith(ItemStack stack, @Nullable UpgradeData back) { + private static ItemStack pocketWith(ItemStack stack, @Nullable UpgradeData back, @Nullable UpgradeData bottom) { var newStack = stack.copyWithCount(1); - newStack.set(ModRegistry.DataComponents.POCKET_UPGRADE.get(), back); + newStack.set(ModRegistry.DataComponents.BACK_POCKET_UPGRADE.get(), back); + newStack.set(ModRegistry.DataComponents.BOTTOM_POCKET_UPGRADE.get(), bottom); return newStack; } - private T pocket(SlotDisplay upgrade, SlotDisplay pocketComputer, ItemStack result) { + private T pocket(SlotDisplay top, SlotDisplay bottom, ItemStack result) { return wrap.apply(new ShapedCraftingRecipeDisplay( - 1, 2, List.of(upgrade, pocketComputer), new SlotDisplay.ItemStackSlotDisplay(result), CRAFTING_STATION + 1, 2, List.of(top, bottom), new SlotDisplay.ItemStackSlotDisplay(result), CRAFTING_STATION )); } @@ -283,7 +306,7 @@ public class UpgradeRecipeGenerator { recipes.add(pocket( ingredient, new SlotDisplay.ItemSlotDisplay(pocketItem), - DataComponentUtil.createStack(pocketItem, ModRegistry.DataComponents.POCKET_UPGRADE.get(), UpgradeData.ofDefault(pocket)) + DataComponentUtil.createStack(pocketItem, ModRegistry.DataComponents.BACK_POCKET_UPGRADE.get(), UpgradeData.ofDefault(pocket)) )); } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/UpgradeSpeakerPeripheral.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/UpgradeSpeakerPeripheral.java index 8fb3abb7d..44c5c1eb5 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/UpgradeSpeakerPeripheral.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/UpgradeSpeakerPeripheral.java @@ -7,6 +7,7 @@ package dan200.computercraft.shared.peripheral.speaker; import dan200.computercraft.api.peripheral.IComputerAccess; import dan200.computercraft.shared.network.client.SpeakerStopClientMessage; import dan200.computercraft.shared.network.server.ServerNetworking; +import net.minecraft.server.level.ServerLevel; /** @@ -15,15 +16,15 @@ import dan200.computercraft.shared.network.server.ServerNetworking; public abstract class UpgradeSpeakerPeripheral extends SpeakerPeripheral { public static final String ADJECTIVE = "upgrade.computercraft.speaker.adjective"; + protected abstract ServerLevel getLevel(); + @Override public void detach(IComputerAccess computer) { super.detach(computer); // We could be in the process of shutting down the server, so we can't send packets in this case. - var level = getPosition().level(); - if (level == null) return; - var server = level.getServer(); - if (server == null || server.isStopped()) return; + var server = getLevel().getServer(); + if (server.isStopped()) return; ServerNetworking.sendToAllPlayers(new SpeakerStopClientMessage(getSource()), server); } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/pocket/apis/PocketAPI.java b/projects/common/src/main/java/dan200/computercraft/shared/pocket/apis/PocketAPI.java index a1117174f..43f2f367f 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/pocket/apis/PocketAPI.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/pocket/apis/PocketAPI.java @@ -6,10 +6,11 @@ package dan200.computercraft.shared.pocket.apis; import dan200.computercraft.api.lua.ILuaAPI; import dan200.computercraft.api.lua.LuaFunction; -import dan200.computercraft.api.pocket.IPocketAccess; import dan200.computercraft.api.pocket.IPocketUpgrade; import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.impl.PocketUpgrades; +import dan200.computercraft.shared.pocket.core.PocketComputerInternal; +import dan200.computercraft.shared.pocket.core.PocketSide; import net.minecraft.world.Container; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; @@ -41,9 +42,9 @@ import java.util.Objects; * @cc.module pocket */ public class PocketAPI implements ILuaAPI { - private final IPocketAccess pocket; + private final PocketComputerInternal pocket; - public PocketAPI(IPocketAccess pocket) { + public PocketAPI(PocketComputerInternal pocket) { this.pocket = pocket; } @@ -53,7 +54,7 @@ public class PocketAPI implements ILuaAPI { } /** - * Search the player's inventory for another upgrade, replacing the existing one with that item if found. + * Search the player's inventory for another upgrade, replacing the existing back upgrade with that item if found. *

* This inventory search starts from the player's currently selected slot, allowing you to prioritise upgrades. * @@ -63,10 +64,29 @@ public class PocketAPI implements ILuaAPI { */ @LuaFunction(mainThread = true) public final Object[] equipBack() { + return equip(PocketSide.BACK); + } + + /** + * Search the player's inventory for another upgrade, replacing the existing bottom upgrade with that item if found. + *

+ * This inventory search starts from the player's currently selected slot, allowing you to prioritise upgrades. + * + * @return The result of equipping. + * @cc.treturn boolean If an item was equipped. + * @cc.treturn string|nil The reason an item was not equipped. + */ + @LuaFunction(mainThread = true) + public final Object[] equipBottom() { + return equip(PocketSide.BOTTOM); + } + + private Object[] equip(PocketSide side) { var entity = pocket.getEntity(); if (!(entity instanceof Player player)) return new Object[]{ false, "Cannot find player" }; + var inventory = player.getInventory(); - var previousUpgrade = pocket.getUpgrade(); + var previousUpgrade = pocket.getUpgrade(side); // Attempt to find the upgrade, starting in the main segment, and then looking in the opposite // one. We start from the position the item is currently in and loop round to the start. @@ -82,13 +102,13 @@ public class PocketAPI implements ILuaAPI { if (previousUpgrade != null) storeItem(player, previousUpgrade.getUpgradeItem()); // Set the new upgrade - pocket.setUpgrade(newUpgrade); + pocket.setUpgrade(side, newUpgrade); return new Object[]{ true }; } /** - * Remove the pocket computer's current upgrade. + * Remove the pocket computer's back upgrade. * * @return The result of unequipping. * @cc.treturn boolean If the upgrade was unequipped. @@ -96,13 +116,29 @@ public class PocketAPI implements ILuaAPI { */ @LuaFunction(mainThread = true) public final Object[] unequipBack() { + return unequip(PocketSide.BACK); + } + + /** + * Remove the pocket computer's bottom upgrade. + * + * @return The result of unequipping. + * @cc.treturn boolean If the upgrade was unequipped. + * @cc.treturn string|nil The reason an upgrade was not unequipped. + */ + @LuaFunction(mainThread = true) + public final Object[] unequipBottom() { + return unequip(PocketSide.BOTTOM); + } + + private Object[] unequip(PocketSide side) { var entity = pocket.getEntity(); if (!(entity instanceof Player player)) return new Object[]{ false, "Cannot find player" }; - var previousUpgrade = pocket.getUpgrade(); + var previousUpgrade = pocket.getUpgrade(side); if (previousUpgrade == null) return new Object[]{ false, "Nothing to unequip" }; - pocket.setUpgrade(null); + pocket.setUpgrade(side, null); storeItem(player, previousUpgrade.getUpgradeItem()); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketBrain.java b/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketBrain.java index b619309f6..7398f444f 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketBrain.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketBrain.java @@ -8,46 +8,48 @@ import dan200.computercraft.api.pocket.IPocketAccess; import dan200.computercraft.api.pocket.IPocketUpgrade; import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.core.computer.ComputerSide; +import dan200.computercraft.core.util.Nullability; +import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.computer.core.ServerComputer; import dan200.computercraft.shared.network.client.PocketComputerDataMessage; import dan200.computercraft.shared.network.server.ServerNetworking; -import dan200.computercraft.shared.pocket.items.PocketComputerItem; import dan200.computercraft.shared.util.DataComponentUtil; import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.core.component.DataComponentType; import net.minecraft.core.component.DataComponents; import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.ARGB; import net.minecraft.world.entity.Entity; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.DyedItemColor; import net.minecraft.world.phys.Vec3; import org.jspecify.annotations.Nullable; +import java.util.EnumMap; +import java.util.Map; import java.util.Objects; /** * Holds additional state for a pocket computer. This includes pocket computer upgrade, * {@linkplain IPocketAccess#getLight() light colour} and {@linkplain IPocketAccess#getColour() colour}. *

- * This state is read when the brain is created, and written back to the holding item stack when the holding entity is - * ticked (see {@link #updateItem(ItemStack)}). + * This state is read when the brain is created, and then written back to the item whenever changed. */ -public final class PocketBrain implements IPocketAccess { +public final class PocketBrain implements PocketComputerInternal { private final PocketServerComputer computer; private PocketHolder holder; private Vec3 position; - private boolean dirty = false; - private @Nullable UpgradeData upgrade; - private int colour = -1; - private int lightColour = -1; + private final Map upgrades = new EnumMap<>(PocketSide.class); - public PocketBrain(PocketHolder holder, @Nullable UpgradeData upgrade, int colour, ServerComputer.Properties properties) { + public PocketBrain(PocketHolder holder, ServerComputer.Properties properties) { this.computer = new PocketServerComputer(this, holder, properties); this.holder = holder; this.position = holder.pos(); - this.upgrade = upgrade; - this.colour = colour; - invalidatePeripheral(); + + upgrades.put(PocketSide.BACK, new UpgradeAccess(ModRegistry.DataComponents.BACK_POCKET_UPGRADE.get(), ComputerSide.BACK)); + upgrades.put(PocketSide.BOTTOM, new UpgradeAccess(ModRegistry.DataComponents.BOTTOM_POCKET_UPGRADE.get(), ComputerSide.BOTTOM)); } /** @@ -83,25 +85,6 @@ public final class PocketBrain implements IPocketAccess { } } - /** - * Write back properties of the pocket brain to the item. - * - * @param stack The pocket computer stack to update. - * @return Whether the item was changed. - */ - public boolean updateItem(ItemStack stack) { - if (!dirty) return false; - this.dirty = false; - - if (colour == -1) { - stack.remove(DataComponents.DYED_COLOR); - } else { - DataComponentUtil.setDyeColour(stack, colour); - } - PocketComputerItem.setUpgrade(stack, upgrade); - return true; - } - @Override public ServerLevel getLevel() { return computer.getLevel(); @@ -114,71 +97,214 @@ public final class PocketBrain implements IPocketAccess { return position; } + private void requireMainThread() { + if (!computer.getLevel().getServer().isSameThread()) { + throw new IllegalStateException("Must be called from the main thread"); + } + } + + private ItemStack requireStack() { + requireMainThread(); + var stack = holder.getStack(computer); + if (stack.isEmpty()) throw new IllegalStateException("Pocket computer is not active"); + return stack; + } + @Override public @Nullable Entity getEntity() { + requireMainThread(); return holder instanceof PocketHolder.EntityHolder entity && holder.isValid(computer) ? entity.entity() : null; } + @Override + public boolean isActive() { + requireMainThread(); + return holder.isValid(computer); + } + @Override public int getColour() { - return colour; + return DyedItemColor.getOrDefault(requireStack(), -1); } @Override public void setColour(int colour) { - if (this.colour == colour) return; - dirty = true; - this.colour = colour; + var stack = requireStack(); + + if (DyedItemColor.getOrDefault(stack, -1) == colour) return; + + if (colour == -1) { + stack.remove(DataComponents.DYED_COLOR); + } else { + DataComponentUtil.setDyeColour(stack, colour); + } + holder.setChanged(); } - @Override public int getLight() { - return lightColour; + // Take the average of all upgrade lights. This is very naive, and just works in sRGB, rather than + // linear colour space. + int count = 0, totalR = 0, totalG = 0, totalB = 0; + for (var upgrade : upgrades.values()) { + var colour = upgrade.lightColour; + if (colour == -1) continue; + + count++; + totalR += ARGB.red(colour); + totalG += ARGB.green(colour); + totalB += ARGB.blue(colour); + } + + return count == 0 ? -1 : ARGB.color(totalR / count, totalG / count, totalB / count); + } + + public void tick() { + for (var holder : upgrades.values()) { + if (holder.upgrade == null) continue; + holder.upgrade.upgrade().update(holder, computer.getPeripheral(holder.side)); + } + } + + public boolean onRightClick(ServerLevel level) { + for (var holder : upgrades.values()) { + if (holder.upgrade == null) continue; + return holder.upgrade.upgrade().onRightClick(level, holder, computer.getPeripheral(holder.side)); + } + + return false; + } + + private UpgradeAccess getUpgradeAccess(PocketSide side) { + return Nullability.assertNonNull(upgrades.get(side)); } @Override - public void setLight(int colour) { - if (colour < 0 || colour > 0xFFFFFF) colour = -1; - lightColour = colour; + public @Nullable UpgradeData getUpgrade(PocketSide side) { + return getUpgradeAccess(side).getUpgrade(); } @Override - public DataComponentPatch getUpgradeData() { - var upgrade = this.upgrade; - return upgrade == null ? DataComponentPatch.EMPTY : upgrade.data(); + public void setUpgrade(PocketSide side, @Nullable UpgradeData upgrade) { + getUpgradeAccess(side).setUpgrade(upgrade); } - @Override - public void setUpgradeData(DataComponentPatch data) { - var upgrade = this.upgrade; - if (upgrade == null) return; - this.upgrade = UpgradeData.of(upgrade.holder(), data); + public void setUpgrades(@Nullable UpgradeData back, @Nullable UpgradeData bottom) { + getUpgradeAccess(PocketSide.BACK).setUpgradeDirect(back); + getUpgradeAccess(PocketSide.BOTTOM).setUpgradeDirect(bottom); } - @Override - public void invalidatePeripheral() { - var peripheral = upgrade == null ? null : upgrade.upgrade().createPeripheral(this); - computer.setPeripheral(ComputerSide.BACK, peripheral); - } + private final class UpgradeAccess implements IPocketAccess { + private final DataComponentType> component; + private final ComputerSide side; - @Override - public @Nullable UpgradeData getUpgrade() { - return upgrade; - } + private @Nullable UpgradeData upgrade; + private int lightColour = -1; - /** - * Set the upgrade for this pocket computer, also updating the item stack. - *

- * Note this method is not thread safe - it must be called from the server thread. - * - * @param upgrade The new upgrade to set it to, may be {@code null}. - */ - @Override - public void setUpgrade(@Nullable UpgradeData upgrade) { - if (Objects.equals(this.upgrade, upgrade)) return; + private UpgradeAccess(DataComponentType> component, ComputerSide side) { + this.component = component; + this.side = side; + } - this.upgrade = upgrade; - dirty = true; - invalidatePeripheral(); + @Override + public ServerLevel getLevel() { + return PocketBrain.this.getLevel(); + } + + @Override + public Vec3 getPosition() { + return PocketBrain.this.getPosition(); + } + + @Override + public @Nullable Entity getEntity() { + return PocketBrain.this.getEntity(); + } + + @Override + public boolean isActive() { + return PocketBrain.this.isActive(); + } + + @Override + public int getColour() { + return PocketBrain.this.getColour(); + } + + @Override + public void setColour(int colour) { + PocketBrain.this.setColour(colour); + } + + @Override + public int getLight() { + return lightColour; + } + + @Override + public void setLight(int colour) { + if (colour < 0 || colour > 0xFFFFFF) colour = -1; + lightColour = colour; + } + + @Override + public DataComponentPatch getUpgradeData() { + var upgrade = this.upgrade; + return upgrade == null ? DataComponentPatch.EMPTY : upgrade.data(); + } + + @Override + public void setUpgradeData(DataComponentPatch data) { + var stack = requireStack(); + + var upgrade = this.upgrade; + if (upgrade == null || upgrade.data().equals(data)) return; + + this.upgrade = UpgradeData.of(upgrade.holder(), data); + stack.set(component, upgrade); + holder.setChanged(); + } + + @Override + public void invalidatePeripheral() { + var peripheral = upgrade == null ? null : upgrade.upgrade().createPeripheral(this); + computer.setPeripheral(side, peripheral); + } + + @Override + public @Nullable UpgradeData getUpgrade() { + return upgrade; + } + + /** + * Set the upgrade for this pocket computer, also updating the item stack. + *

+ * Note this method is not thread safe - it must be called from the server thread. + * + * @param upgrade The new upgrade to set it to, may be {@code null}. + */ + @Override + public void setUpgrade(@Nullable UpgradeData upgrade) { + var stack = requireStack(); + + if (!setUpgradeDirect(upgrade)) return; + + stack.set(component, upgrade); + holder.setChanged(); + } + + /** + * Set an upgrade without writing it back to the stack. + * + * @param upgrade The upgrade to set. + * @return Whether the upgrade changed. + */ + private boolean setUpgradeDirect(@Nullable UpgradeData upgrade) { + if (Objects.equals(this.upgrade, upgrade)) return false; + + this.upgrade = upgrade; + lightColour = -1; + invalidatePeripheral(); + return true; + } } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketComputerInternal.java b/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketComputerInternal.java new file mode 100644 index 000000000..d0c7b440f --- /dev/null +++ b/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketComputerInternal.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.pocket.core; + +import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.api.pocket.PocketComputer; +import dan200.computercraft.api.upgrades.UpgradeData; +import org.jspecify.annotations.Nullable; + +/** + * An internal version of {@link PocketComputer}. + *

+ * This exposes additional functionality we don't want in the public API, but where we don't want access to the full + * {@link PocketBrain} interface. + */ +public interface PocketComputerInternal extends PocketComputer { + @Nullable + UpgradeData getUpgrade(PocketSide side); + + void setUpgrade(PocketSide side, @Nullable UpgradeData upgrade); +} diff --git a/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketHolder.java b/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketHolder.java index 6aa1b3787..1d5c9cdd9 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketHolder.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketHolder.java @@ -15,6 +15,7 @@ import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.phys.Vec3; /** @@ -42,13 +43,23 @@ public sealed interface PocketHolder { */ BlockPos blockPos(); + /** + * Get the current pocket computer stack. + * + * @param computer The owning computer. + * @return The found stack. This is empty if no valid stack is found. + */ + ItemStack getStack(ServerComputer computer); + /** * Determine if this holder is still valid for a particular computer. * * @param computer The current computer. * @return Whether this holder is valid. */ - boolean isValid(ServerComputer computer); + default boolean isValid(ServerComputer computer) { + return !getStack(computer).isEmpty(); + } /** * Mark the pocket computer item as having changed. @@ -99,8 +110,11 @@ public sealed interface PocketHolder { */ record PlayerHolder(ServerPlayer entity, int slot) implements EntityHolder { @Override - public boolean isValid(ServerComputer computer) { - return entity().isAlive() && PocketComputerItem.isServerComputer(computer, entity().getInventory().getItem(this.slot())); + public ItemStack getStack(ServerComputer computer) { + if (!entity().isAlive()) return ItemStack.EMPTY; + + var item = entity().getInventory().getItem(this.slot()); + return PocketComputerItem.isServerComputer(computer, item) ? item : ItemStack.EMPTY; } @Override @@ -117,8 +131,11 @@ public sealed interface PocketHolder { */ record LivingEntityHolder(LivingEntity entity, EquipmentSlot slot) implements EntityHolder { @Override - public boolean isValid(ServerComputer computer) { - return entity().isAlive() && PocketComputerItem.isServerComputer(computer, entity().getItemBySlot(slot())); + public ItemStack getStack(ServerComputer computer) { + if (!entity().isAlive()) return ItemStack.EMPTY; + + var item = entity().getItemBySlot(this.slot()); + return PocketComputerItem.isServerComputer(computer, item) ? item : ItemStack.EMPTY; } @Override @@ -134,8 +151,11 @@ public sealed interface PocketHolder { */ record ItemEntityHolder(ItemEntity entity) implements EntityHolder { @Override - public boolean isValid(ServerComputer computer) { - return entity().isAlive() && PocketComputerItem.isServerComputer(computer, this.entity().getItem()); + public ItemStack getStack(ServerComputer computer) { + if (!entity().isAlive()) return ItemStack.EMPTY; + + var item = entity().getItem(); + return PocketComputerItem.isServerComputer(computer, item) ? item : ItemStack.EMPTY; } @Override @@ -166,8 +186,11 @@ public sealed interface PocketHolder { } @Override - public boolean isValid(ServerComputer computer) { - return !lectern().isRemoved() && PocketComputerItem.isServerComputer(computer, lectern.getItem()); + public ItemStack getStack(ServerComputer computer) { + if (lectern.isRemoved()) return ItemStack.EMPTY; + + var item = lectern.getItem(); + return PocketComputerItem.isServerComputer(computer, item) ? item : ItemStack.EMPTY; } @Override diff --git a/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketSide.java b/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketSide.java new file mode 100644 index 000000000..fdb5e7701 --- /dev/null +++ b/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketSide.java @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.pocket.core; + +import dan200.computercraft.api.pocket.IPocketUpgrade; + +/** + * The side a {@linkplain IPocketUpgrade pocket upgrade} will be equipped on. + * + * @see PocketBrain + */ +public enum PocketSide { + BACK, + BOTTOM, +} diff --git a/projects/common/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItem.java b/projects/common/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItem.java index d3bf11ecb..7122cf012 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItem.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItem.java @@ -5,11 +5,10 @@ package dan200.computercraft.shared.pocket.items; import dan200.computercraft.annotations.ForgeOverride; -import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.pocket.IPocketUpgrade; import dan200.computercraft.api.upgrades.UpgradeData; -import dan200.computercraft.core.computer.ComputerSide; import dan200.computercraft.impl.PocketUpgrades; +import dan200.computercraft.impl.UpgradeManager; import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.core.ServerComputer; @@ -22,6 +21,7 @@ import dan200.computercraft.shared.platform.PlatformHelper; import dan200.computercraft.shared.pocket.core.PocketBrain; import dan200.computercraft.shared.pocket.core.PocketHolder; import dan200.computercraft.shared.pocket.core.PocketServerComputer; +import dan200.computercraft.shared.pocket.core.PocketSide; import dan200.computercraft.shared.util.*; import net.minecraft.core.HolderLookup; import net.minecraft.network.chat.Component; @@ -37,7 +37,6 @@ import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.component.DyedItemColor; import net.minecraft.world.level.Level; import org.jspecify.annotations.Nullable; @@ -69,9 +68,7 @@ public class PocketComputerItem extends Item { brain.computer().keepAlive(); } - // Update pocket upgrade - var upgrade = brain.getUpgrade(); - if (upgrade != null) upgrade.upgrade().update(brain, brain.computer().getPeripheral(ComputerSide.BACK)); + brain.tick(); if (updateItem(stack, brain)) holder.setChanged(); } @@ -84,7 +81,7 @@ public class PocketComputerItem extends Item { * @return Whether the item was changed. */ private boolean updateItem(ItemStack stack, PocketBrain brain) { - var changed = brain.updateItem(stack); + var changed = false; var computer = brain.computer(); // Sync label @@ -134,14 +131,7 @@ public class PocketComputerItem extends Item { var computer = brain.computer(); computer.turnOn(); - var stop = false; - var upgrade = getUpgrade(stack); - if (upgrade != null) { - stop = upgrade.onRightClick(world, brain, computer.getPeripheral(ComputerSide.BACK)); - // Sync back just in case. We don't need to setChanged, as we'll return the item anyway. - updateItem(stack, brain); - } - + var stop = brain.onRightClick((ServerLevel) world); if (!stop) openImpl(player, stack, holder, hand == InteractionHand.OFF_HAND, computer); } return InteractionResult.SUCCESS; @@ -172,20 +162,13 @@ public class PocketComputerItem extends Item { @Override public Component getName(ItemStack stack) { - var baseString = getDescriptionId(); - var upgrade = getUpgrade(stack); - if (upgrade != null) { - return Component.translatable(baseString + ".upgraded", upgrade.getAdjective()); - } else { - return super.getName(stack); - } + return UpgradeManager.getName(getDescriptionId(), getUpgrade(stack, PocketSide.BACK), getUpgrade(stack, PocketSide.BOTTOM)); } @Nullable @ForgeOverride public String getCreatorModId(HolderLookup.Provider registries, ItemStack stack) { - var upgrade = getUpgradeWithData(stack); - return upgrade != null ? PocketUpgrades.instance().getOwner(upgrade.holder()) : ComputerCraftAPI.MOD_ID; + return PocketUpgrades.instance().getOwner(getUpgradeWithData(stack, PocketSide.BACK), getUpgradeWithData(stack, PocketSide.BOTTOM)); } private PocketBrain getOrCreateBrain(ServerLevel level, PocketHolder holder, ItemStack stack) { @@ -200,12 +183,11 @@ public class PocketComputerItem extends Item { } var computerID = NonNegativeId.getOrCreate(level.getServer(), stack, ModRegistry.DataComponents.COMPUTER_ID.get(), NonNegativeId.Computer::new, IDAssigner.COMPUTER); - var brain = new PocketBrain( - holder, getUpgradeWithData(stack), DyedItemColor.getOrDefault(stack, -1), - ServerComputer.properties(computerID, getFamily()) - .label(getLabel(stack)) - .storageCapacity(StorageCapacity.getOrDefault(stack.get(ModRegistry.DataComponents.STORAGE_CAPACITY.get()), -1)) + var brain = new PocketBrain(holder, ServerComputer.properties(computerID, getFamily()) + .label(getLabel(stack)) + .storageCapacity(StorageCapacity.getOrDefault(stack.get(ModRegistry.DataComponents.STORAGE_CAPACITY.get()), -1)) ); + brain.setUpgrades(getUpgradeWithData(stack, PocketSide.BACK), getUpgradeWithData(stack, PocketSide.BOTTOM)); var computer = brain.computer(); stack.set(ModRegistry.DataComponents.COMPUTER.get(), new ServerComputerReference(registry.getSessionID(), computer.register())); @@ -245,9 +227,7 @@ public class PocketComputerItem extends Item { var computer = getServerComputer(server, stack); if (computer == null) return; - var brain = computer.getBrain(); - brain.setUpgrade(getUpgradeWithData(stack)); - brain.setColour(DyedItemColor.getOrDefault(stack, -1)); + computer.getBrain().setUpgrades(getUpgradeWithData(stack, PocketSide.BACK), getUpgradeWithData(stack, PocketSide.BOTTOM)); } public ComputerFamily getFamily() { @@ -264,16 +244,15 @@ public class PocketComputerItem extends Item { return stack.getOrDefault(ModRegistry.DataComponents.ON.get(), false); } - public static @Nullable IPocketUpgrade getUpgrade(ItemStack stack) { - var upgrade = getUpgradeWithData(stack); + public static @Nullable IPocketUpgrade getUpgrade(ItemStack stack, PocketSide side) { + var upgrade = getUpgradeWithData(stack, side); return upgrade == null ? null : upgrade.upgrade(); } - public static @Nullable UpgradeData getUpgradeWithData(ItemStack stack) { - return stack.get(ModRegistry.DataComponents.POCKET_UPGRADE.get()); - } - - public static void setUpgrade(ItemStack stack, @Nullable UpgradeData upgrade) { - stack.set(ModRegistry.DataComponents.POCKET_UPGRADE.get(), upgrade); + public static @Nullable UpgradeData getUpgradeWithData(ItemStack stack, PocketSide side) { + return stack.get(switch (side) { + case BACK -> ModRegistry.DataComponents.BACK_POCKET_UPGRADE.get(); + case BOTTOM -> ModRegistry.DataComponents.BOTTOM_POCKET_UPGRADE.get(); + }); } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketSpeakerPeripheral.java b/projects/common/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketSpeakerPeripheral.java index 6073b7046..9f2689e92 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketSpeakerPeripheral.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketSpeakerPeripheral.java @@ -8,15 +8,21 @@ import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.pocket.IPocketAccess; import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition; import dan200.computercraft.shared.peripheral.speaker.UpgradeSpeakerPeripheral; +import net.minecraft.server.level.ServerLevel; import org.jspecify.annotations.Nullable; -public class PocketSpeakerPeripheral extends UpgradeSpeakerPeripheral { +public final class PocketSpeakerPeripheral extends UpgradeSpeakerPeripheral { private final IPocketAccess access; public PocketSpeakerPeripheral(IPocketAccess access) { this.access = access; } + @Override + protected ServerLevel getLevel() { + return access.getLevel(); + } + @Override public SpeakerPosition getPosition() { var entity = access.getEntity(); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/pocket/recipes/PocketComputerUpgradeRecipe.java b/projects/common/src/main/java/dan200/computercraft/shared/pocket/recipes/PocketComputerUpgradeRecipe.java index 2ca44bd31..790923263 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/pocket/recipes/PocketComputerUpgradeRecipe.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/pocket/recipes/PocketComputerUpgradeRecipe.java @@ -8,6 +8,7 @@ import dan200.computercraft.api.pocket.IPocketUpgrade; import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.impl.PocketUpgrades; import dan200.computercraft.shared.ModRegistry; +import dan200.computercraft.shared.pocket.core.PocketSide; import dan200.computercraft.shared.pocket.items.PocketComputerItem; import net.minecraft.core.HolderLookup; import net.minecraft.world.item.ItemStack; @@ -48,29 +49,37 @@ public final class PocketComputerUpgradeRecipe extends CustomRecipe { if (computer.isEmpty()) return ItemStack.EMPTY; - if (PocketComputerItem.getUpgradeWithData(computer) != null) return ItemStack.EMPTY; - // Check for upgrades around the item - UpgradeData upgrade = null; + UpgradeData above = null, below = null; for (var y = 0; y < inventory.height(); y++) { for (var x = 0; x < inventory.width(); x++) { var item = inventory.getItem(x, y); - if (x == computerX && y == computerY) continue; + if (item.isEmpty() || (x == computerX && y == computerY)) continue; if (x == computerX && y == computerY - 1) { - upgrade = PocketUpgrades.instance().get(registryAccess, item); - if (upgrade == null) return ItemStack.EMPTY; - } else if (!item.isEmpty()) { + above = PocketUpgrades.instance().get(registryAccess, item); + if (above == null) return ItemStack.EMPTY; + } else if (x == computerX && y == computerY + 1) { + below = PocketUpgrades.instance().get(registryAccess, item); + if (below == null) return ItemStack.EMPTY; + } else { return ItemStack.EMPTY; } } } - if (upgrade == null) return ItemStack.EMPTY; + // Abort if we have no upgrades + if (above == null && below == null) return ItemStack.EMPTY; + // Or if we've already got an upgrade in that slot. + if ((above != null && PocketComputerItem.getUpgrade(computer, PocketSide.BACK) != null) + || (below != null && PocketComputerItem.getUpgrade(computer, PocketSide.BOTTOM) != null)) { + return ItemStack.EMPTY; + } // Construct the new stack var result = computer.copyWithCount(1); - result.set(ModRegistry.DataComponents.POCKET_UPGRADE.get(), upgrade); + if (above != null) result.set(ModRegistry.DataComponents.BACK_POCKET_UPGRADE.get(), above); + if (below != null) result.set(ModRegistry.DataComponents.BOTTOM_POCKET_UPGRADE.get(), below); return result; } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/items/TurtleItem.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/items/TurtleItem.java index 525cacd0b..95e9c4514 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/items/TurtleItem.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/items/TurtleItem.java @@ -5,11 +5,11 @@ package dan200.computercraft.shared.turtle.items; import dan200.computercraft.annotations.ForgeOverride; -import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.turtle.ITurtleUpgrade; import dan200.computercraft.api.turtle.TurtleSide; import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.impl.TurtleUpgrades; +import dan200.computercraft.impl.UpgradeManager; import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.turtle.TurtleOverlay; import dan200.computercraft.shared.turtle.blocks.TurtleBlock; @@ -30,39 +30,13 @@ public class TurtleItem extends BlockItem { @Override public Component getName(ItemStack stack) { - var baseString = descriptionId; - var left = getUpgrade(stack, TurtleSide.LEFT); - var right = getUpgrade(stack, TurtleSide.RIGHT); - if (left != null && right != null) { - return Component.translatable(baseString + ".upgraded_twice", right.getAdjective(), left.getAdjective()); - } else if (left != null) { - return Component.translatable(baseString + ".upgraded", left.getAdjective()); - } else if (right != null) { - return Component.translatable(baseString + ".upgraded", right.getAdjective()); - } else { - return Component.translatable(baseString); - } + return UpgradeManager.getName(getDescriptionId(), getUpgrade(stack, TurtleSide.LEFT), getUpgrade(stack, TurtleSide.RIGHT)); } @Nullable @ForgeOverride public String getCreatorModId(HolderLookup.Provider registries, ItemStack stack) { - // Determine our "creator mod" from the upgrades. We attempt to find the first non-vanilla/non-CC - // upgrade (starting from the left). - - var left = getUpgradeWithData(stack, TurtleSide.LEFT); - if (left != null) { - var mod = TurtleUpgrades.instance().getOwner(left.holder()); - if (!mod.equals(ComputerCraftAPI.MOD_ID)) return mod; - } - - var right = getUpgradeWithData(stack, TurtleSide.RIGHT); - if (right != null) { - var mod = TurtleUpgrades.instance().getOwner(right.holder()); - if (!mod.equals(ComputerCraftAPI.MOD_ID)) return mod; - } - - return ComputerCraftAPI.MOD_ID; + return TurtleUpgrades.instance().getOwner(getUpgradeWithData(stack, TurtleSide.LEFT), getUpgradeWithData(stack, TurtleSide.RIGHT)); } public static @Nullable ITurtleUpgrade getUpgrade(ItemStack stack, TurtleSide side) { @@ -71,7 +45,10 @@ public class TurtleItem extends BlockItem { } public static @Nullable UpgradeData getUpgradeWithData(ItemStack stack, TurtleSide side) { - return stack.get(side == TurtleSide.LEFT ? ModRegistry.DataComponents.LEFT_TURTLE_UPGRADE.get() : ModRegistry.DataComponents.RIGHT_TURTLE_UPGRADE.get()); + return stack.get(switch (side) { + case LEFT -> ModRegistry.DataComponents.LEFT_TURTLE_UPGRADE.get(); + case RIGHT -> ModRegistry.DataComponents.RIGHT_TURTLE_UPGRADE.get(); + }); } public static @Nullable TurtleOverlay getOverlay(ItemStack stack) { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSpeaker.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSpeaker.java index 67602d5bd..e6bc81664 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSpeaker.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSpeaker.java @@ -13,18 +13,24 @@ import dan200.computercraft.api.upgrades.UpgradeType; import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition; import dan200.computercraft.shared.peripheral.speaker.UpgradeSpeakerPeripheral; +import net.minecraft.server.level.ServerLevel; import net.minecraft.world.item.ItemStack; import net.minecraft.world.phys.Vec3; import org.jspecify.annotations.Nullable; public class TurtleSpeaker extends AbstractTurtleUpgrade { - private static class Peripheral extends UpgradeSpeakerPeripheral { + private static final class Peripheral extends UpgradeSpeakerPeripheral { final ITurtleAccess turtle; Peripheral(ITurtleAccess turtle) { this.turtle = turtle; } + @Override + protected ServerLevel getLevel() { + return (ServerLevel) turtle.getLevel(); + } + @Override public SpeakerPosition getPosition() { return SpeakerPosition.of(turtle.getLevel(), Vec3.atCenterOf(turtle.getPosition())); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/util/NonNegativeId.java b/projects/common/src/main/java/dan200/computercraft/shared/util/NonNegativeId.java index a550b34d0..5a7da5c40 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/util/NonNegativeId.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/util/NonNegativeId.java @@ -65,6 +65,12 @@ public abstract class NonNegativeId implements TooltipProvider { out.accept(Component.translatable(translation, id()).withStyle(ChatFormatting.GRAY)); } + @Override + public String toString() { + var className = getClass().getName(); + return className.substring(className.lastIndexOf('.') + 1) + "(" + id + ")"; + } + @Override @SuppressWarnings("EqualsGetClass") // We want to distinguish different subclasses. public final boolean equals(Object o) { diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Pocket_Computer_Test.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Pocket_Computer_Test.kt index 2bf4198ee..338844570 100644 --- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Pocket_Computer_Test.kt +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Pocket_Computer_Test.kt @@ -11,13 +11,14 @@ import dan200.computercraft.api.upgrades.UpgradeData import dan200.computercraft.client.pocket.ClientPocketComputers import dan200.computercraft.core.apis.TermAPI import dan200.computercraft.gametest.api.* -import dan200.computercraft.gametest.api.GameTest +import dan200.computercraft.impl.PocketUpgrades import dan200.computercraft.mixin.gametest.GameTestHelperAccessor import dan200.computercraft.shared.ModRegistry import dan200.computercraft.shared.computer.core.ComputerState import dan200.computercraft.shared.util.DataComponentUtil import dan200.computercraft.shared.util.NonNegativeId import dan200.computercraft.test.core.computer.getApi +import dan200.computercraft.test.shared.ItemStackMatcher.isStack import net.minecraft.core.BlockPos import net.minecraft.core.component.DataComponentPatch import net.minecraft.core.component.DataComponents @@ -26,6 +27,8 @@ import net.minecraft.gametest.framework.GameTestSequence import net.minecraft.network.chat.Component import net.minecraft.resources.ResourceLocation import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import org.hamcrest.MatcherAssert.assertThat import org.junit.jupiter.api.Assertions.assertEquals import kotlin.random.Random @@ -130,7 +133,7 @@ class Pocket_Computer_Test { it.applyComponents( DataComponentPatch.builder() .set(ModRegistry.DataComponents.COMPUTER_ID.get(), NonNegativeId.Computer(123)) - .set(ModRegistry.DataComponents.POCKET_UPGRADE.get(), UpgradeData.ofDefault(upgrade)) + .set(ModRegistry.DataComponents.BACK_POCKET_UPGRADE.get(), UpgradeData.ofDefault(upgrade)) .build(), ) }, @@ -138,4 +141,86 @@ class Pocket_Computer_Test { ) } } + + /** + * Test that turtles can be crafted with upgrades. + */ + @GameTest(template = Structures.DEFAULT) + fun Can_upgrades_be_crafted(helper: GameTestHelper) = helper.immediate { + fun pocket(back: UpgradeData? = null, bottom: UpgradeData? = null): ItemStack { + val item = ItemStack(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get()) + item.set(ModRegistry.DataComponents.BACK_POCKET_UPGRADE.get(), back) + item.set(ModRegistry.DataComponents.BOTTOM_POCKET_UPGRADE.get(), bottom) + return item + } + + val registries = helper.level.registryAccess() + val speaker = PocketUpgrades.instance().get(registries, ItemStack(ModRegistry.Items.SPEAKER.get()))!! + val modem = + PocketUpgrades.instance().get(registries, ItemStack(ModRegistry.Items.WIRELESS_MODEM_NORMAL.get()))!! + + // Check we can craft with upgrades + assertThat( + "Craft with item below", + helper.craftItem( + ItemStack.EMPTY, pocket(), ItemStack.EMPTY, + ItemStack.EMPTY, ItemStack(ModRegistry.Items.SPEAKER.get()), ItemStack.EMPTY, + ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY, + ), + isStack(pocket(bottom = speaker)), + ) + assertThat( + "Craft with item above", + helper.craftItem( + ItemStack.EMPTY, ItemStack(ModRegistry.Items.SPEAKER.get()), ItemStack.EMPTY, + ItemStack.EMPTY, pocket(), ItemStack.EMPTY, + ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY, + ), + isStack(pocket(back = speaker)), + ) + assertThat( + "Craft with two items", + helper.craftItem( + ItemStack.EMPTY, ItemStack(ModRegistry.Items.SPEAKER.get()), ItemStack.EMPTY, + ItemStack.EMPTY, pocket(), ItemStack.EMPTY, + ItemStack.EMPTY, ItemStack(ModRegistry.Items.WIRELESS_MODEM_NORMAL.get()), ItemStack.EMPTY, + ), + isStack(pocket(back = speaker, bottom = modem)), + ) + assertThat( + "Maintains upgrades", + helper.craftItem( + ItemStack.EMPTY, pocket(back = speaker), ItemStack.EMPTY, + ItemStack.EMPTY, ItemStack(ModRegistry.Items.WIRELESS_MODEM_NORMAL.get()), ItemStack.EMPTY, + ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY, + ), + isStack(pocket(back = speaker, bottom = modem)), + ) + + // Cannot craft when already have item + helper.assertNotCraftable( + ItemStack.EMPTY, ItemStack(ModRegistry.Items.SPEAKER.get()), ItemStack.EMPTY, + ItemStack.EMPTY, pocket(back = modem), ItemStack.EMPTY, + ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY, + ) + + // Cannot craft with an invalid upgrade + helper.assertNotCraftable( + ItemStack.EMPTY, pocket(), ItemStack.EMPTY, + ItemStack.EMPTY, ItemStack(Items.DIRT), ItemStack.EMPTY, + ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY, + ) + + // Cannot craft with extra items in the inventory + helper.assertNotCraftable( + ItemStack(Items.DIRT), ItemStack(ModRegistry.Items.SPEAKER.get()), ItemStack.EMPTY, + ItemStack.EMPTY, pocket(), ItemStack.EMPTY, + ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY, + ) + helper.assertNotCraftable( + ItemStack.EMPTY, ItemStack(ModRegistry.Items.SPEAKER.get()), ItemStack.EMPTY, + ItemStack.EMPTY, pocket(), ItemStack.EMPTY, + ItemStack.EMPTY, ItemStack.EMPTY, ItemStack(Items.DIRT), + ) + } } diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/pocket/equip.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/pocket/equip.lua index 7f60c7fc7..f1ac34972 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/pocket/equip.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/pocket/equip.lua @@ -7,9 +7,27 @@ if not pocket then return end -local ok, err = pocket.equipBack() -if not ok then - printError(err) -else - print("Item equipped") +if select('#', ...) > 1 then + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage: " .. programName .. " ") + return +end + +local function equip(fn) + local ok, err = fn() + if not ok then + printError(err) + else + print("Item equipped") + end +end + +local side = ... or "back" +if side == "back" then + equip(pocket.equipBack) +elseif side == "bottom" then + equip(pocket.equipBottom) +else + printError("Unknown side. Expected 'back' or 'bottom'.") + return end diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/pocket/unequip.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/pocket/unequip.lua index a857426d4..f5b3e95bc 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/pocket/unequip.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/pocket/unequip.lua @@ -7,9 +7,27 @@ if not pocket then return end -local ok, err = pocket.unequipBack() -if not ok then - printError(err) -else - print("Item unequipped") +if select('#', ...) > 1 then + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage: " .. programName .. " ") + return +end + +local function unequip(fn) + local ok, err = fn() + if not ok then + printError(err) + else + print("Item unequipped") + end +end + +local side = ... or "back" +if side == "back" then + unequip(pocket.unequipBack) +elseif side == "bottom" then + unequip(pocket.unequipBottom) +else + printError("Unknown side. Expected 'back' or 'bottom'.") + return end diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/integration/rei/REIComputerCraft.java b/projects/fabric/src/main/java/dan200/computercraft/shared/integration/rei/REIComputerCraft.java index 82041db82..c25dd1f94 100644 --- a/projects/fabric/src/main/java/dan200/computercraft/shared/integration/rei/REIComputerCraft.java +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/integration/rei/REIComputerCraft.java @@ -6,6 +6,7 @@ package dan200.computercraft.shared.integration.rei; import dan200.computercraft.api.turtle.TurtleSide; import dan200.computercraft.shared.ModRegistry; +import dan200.computercraft.shared.pocket.core.PocketSide; import dan200.computercraft.shared.pocket.items.PocketComputerItem; import dan200.computercraft.shared.turtle.items.TurtleItem; import me.shedaniel.rei.api.common.entry.comparison.ItemComparatorRegistry; @@ -30,8 +31,14 @@ public class REIComputerCraft implements REICommonPlugin { }, ModRegistry.Items.TURTLE_NORMAL.get(), ModRegistry.Items.TURTLE_ADVANCED.get()); registry.register((context, stack) -> { - var upgrade = PocketComputerItem.getUpgradeWithData(stack); - return upgrade == null ? 1 : upgrade.holder().key().location().hashCode(); + long hash = 1; + + var back = PocketComputerItem.getUpgradeWithData(stack, PocketSide.BACK); + var bottom = PocketComputerItem.getUpgradeWithData(stack, PocketSide.BOTTOM); + if (back != null) hash = hash * 31 + back.holder().key().location().hashCode(); + if (bottom != null) hash = hash * 31 + bottom.holder().key().location().hashCode(); + + return hash; }, ModRegistry.Items.POCKET_COMPUTER_NORMAL.get(), ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get()); registry.register((context, stack) -> DyedItemColor.getOrDefault(stack, -1), ModRegistry.Items.DISK.get());