diff --git a/projects/common-api/src/client/java/dan200/computercraft/api/client/turtle/TurtleUpgradeModeller.java b/projects/common-api/src/client/java/dan200/computercraft/api/client/turtle/TurtleUpgradeModeller.java index b1a1e72c6..76a57ecf3 100644 --- a/projects/common-api/src/client/java/dan200/computercraft/api/client/turtle/TurtleUpgradeModeller.java +++ b/projects/common-api/src/client/java/dan200/computercraft/api/client/turtle/TurtleUpgradeModeller.java @@ -11,6 +11,7 @@ import dan200.computercraft.api.turtle.TurtleSide; import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser; import net.minecraft.client.resources.model.ModelResourceLocation; +import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceLocation; import javax.annotation.Nullable; @@ -28,12 +29,27 @@ public interface TurtleUpgradeModeller { * When the current turtle is {@literal null}, this function should be constant for a given upgrade and side. * * @param upgrade The upgrade that you're getting the model for. - * @param turtle Access to the turtle that the upgrade resides on. This will be null when getting item models! + * @param turtle Access to the turtle that the upgrade resides on. This will be null when getting item models, unless + * {@link #getModel(ITurtleUpgrade, CompoundTag, TurtleSide)} is overriden. * @param side Which side of the turtle (left or right) the upgrade resides on. * @return The model that you wish to be used to render your upgrade. */ TransformedModel getModel(T upgrade, @Nullable ITurtleAccess turtle, TurtleSide side); + /** + * Obtain the model to be used when rendering a turtle peripheral. + *

+ * This is used when rendering the turtle's item model, and so no {@link ITurtleAccess} is available. + * + * @param upgrade The upgrade that you're getting the model for. + * @param data Upgrade data instance for current turtle side. + * @param side Which side of the turtle (left or right) the upgrade resides on. + * @return The model that you wish to be used to render your upgrade. + */ + default TransformedModel getModel(T upgrade, CompoundTag data, TurtleSide side) { + return getModel(upgrade, (ITurtleAccess) null, side); + } + /** * A basic {@link TurtleUpgradeModeller} which renders using the upgrade's {@linkplain ITurtleUpgrade#getCraftingItem() * crafting item}. 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 07cb8e768..99ac916b9 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 @@ -5,9 +5,11 @@ package dan200.computercraft.api.pocket; import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.upgrades.UpgradeBase; import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; import javax.annotation.Nullable; import java.util.Map; @@ -69,6 +71,8 @@ public interface IPocketAccess { * * @return The upgrade's NBT. * @see #updateUpgradeNBTData() + * @see UpgradeBase#getUpgradeItem(CompoundTag) + * @see UpgradeBase#getUpgradeData(ItemStack) */ CompoundTag getUpgradeNBTData(); diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java index 3ef11c7ab..a057ed46c 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java @@ -8,10 +8,13 @@ import dan200.computercraft.api.lua.ILuaCallback; import dan200.computercraft.api.lua.MethodResult; import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.upgrades.UpgradeBase; +import dan200.computercraft.api.upgrades.UpgradeData; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.nbt.CompoundTag; import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.ApiStatus; @@ -245,23 +248,51 @@ public interface ITurtleAccess { void playAnimation(TurtleAnimation animation); /** - * Returns the turtle on the specified side of the turtle, if there is one. + * Returns the upgrade on the specified side of the turtle, if there is one. * * @param side The side to get the upgrade from. * @return The upgrade on the specified side of the turtle, if there is one. - * @see #setUpgrade(TurtleSide, ITurtleUpgrade) + * @see #getUpgradeWithData(TurtleSide) + * @see #setUpgradeWithData(TurtleSide, UpgradeData) */ @Nullable ITurtleUpgrade getUpgrade(TurtleSide side); + /** + * Returns the upgrade on the specified side of the turtle, along with its {@linkplain #getUpgradeNBTData(TurtleSide) + * update data}. + * + * @param side The side to get the upgrade from. + * @return The upgrade on the specified side of the turtle, along with its upgrade data, if there is one. + * @see #getUpgradeWithData(TurtleSide) + * @see #setUpgradeWithData(TurtleSide, UpgradeData) + */ + default @Nullable UpgradeData getUpgradeWithData(TurtleSide side) { + var upgrade = getUpgrade(side); + return upgrade == null ? null : UpgradeData.of(upgrade, getUpgradeNBTData(side)); + } + /** * Set the upgrade for a given side, resetting peripherals and clearing upgrade specific data. * * @param side The side to set the upgrade on. * @param upgrade The upgrade to set, may be {@code null} to clear. * @see #getUpgrade(TurtleSide) + * @deprecated Use {@link #setUpgradeWithData(TurtleSide, UpgradeData)} */ - void setUpgrade(TurtleSide side, @Nullable ITurtleUpgrade upgrade); + @Deprecated + default void setUpgrade(TurtleSide side, @Nullable ITurtleUpgrade upgrade) { + setUpgradeWithData(side, upgrade == null ? null : UpgradeData.ofDefault(upgrade)); + } + + /** + * Set the upgrade for a given side and its upgrade data. + * + * @param side The side to set the upgrade on. + * @param upgrade The upgrade to set, may be {@code null} to clear. + * @see #getUpgradeWithData(TurtleSide) + */ + void setUpgradeWithData(TurtleSide side, @Nullable UpgradeData upgrade); /** * Returns the peripheral created by the upgrade on the specified side of the turtle, if there is one. @@ -281,6 +312,8 @@ public interface ITurtleAccess { * @param side The side to get the upgrade data for. * @return The upgrade-specific data. * @see #updateUpgradeNBTData(TurtleSide) + * @see UpgradeBase#getUpgradeItem(CompoundTag) + * @see UpgradeBase#getUpgradeData(ItemStack) */ CompoundTag getUpgradeNBTData(TurtleSide side); diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java index b39d8429c..2e4d7616b 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java @@ -7,6 +7,7 @@ import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.upgrades.UpgradeBase; import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; import javax.annotation.Nullable; @@ -79,4 +80,17 @@ default TurtleCommandResult useTool(ITurtleAccess turtle, TurtleSide side, Turtl */ default void update(ITurtleAccess turtle, TurtleSide side) { } + + /** + * Get upgrade data that should be persisted when the turtle was broken. + *

+ * This method should be overridden when you don't need to store all upgrade data by default. For instance, if you + * store peripheral state in the upgrade data, which should be lost when the turtle is broken. + * + * @param upgradeData Data that currently stored for this upgrade + * @return Filtered version of this data. + */ + default CompoundTag getPersistedData(CompoundTag upgradeData) { + return upgradeData; + } } diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeBase.java b/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeBase.java index ee83da691..e5e48fd4f 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeBase.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeBase.java @@ -4,10 +4,14 @@ package dan200.computercraft.api.upgrades; +import dan200.computercraft.api.pocket.IPocketAccess; import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.api.turtle.ITurtleAccess; import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.api.turtle.TurtleSide; import dan200.computercraft.impl.PlatformHelper; import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.ItemStack; @@ -50,6 +54,42 @@ public interface UpgradeBase { */ ItemStack getCraftingItem(); + /** + * Returns the item stack representing a currently equipped turtle upgrade. + *

+ * While upgrades can store upgrade data ({@link ITurtleAccess#getUpgradeNBTData(TurtleSide)} and + * {@link IPocketAccess#getUpgradeNBTData()}}, by default this data is discarded when an upgrade is unequipped, + * and the original item stack is returned. + *

+ * By overriding this method, you can create a new {@link ItemStack} which contains enough data to + * {@linkplain #getUpgradeData(ItemStack) re-create the upgrade data} if the item is re-equipped. + *

+ * When overriding this, you should override {@link #getUpgradeData(ItemStack)} and {@link #isItemSuitable(ItemStack)} + * at the same time, + * + * @param upgradeData The current upgrade data. This should NOT be mutated. + * @return The item stack returned when unequipping. + */ + default ItemStack getUpgradeItem(CompoundTag upgradeData) { + return getCraftingItem(); + } + + /** + * Extract upgrade data from an {@link ItemStack}. + *

+ * This upgrade data will be available with {@link ITurtleAccess#getUpgradeNBTData(TurtleSide)} or + * {@link IPocketAccess#getUpgradeNBTData()}. + *

+ * This should be an inverse to {@link #getUpgradeItem(CompoundTag)}. + * + * @param stack The stack that was equipped by the turtle or pocket computer. This will have the same item as + * {@link #getCraftingItem()}. + * @return The upgrade data that should be set on the turtle or pocket computer. + */ + default CompoundTag getUpgradeData(ItemStack stack) { + return new CompoundTag(); + } + /** * Determine if an item is suitable for being used for this upgrade. *

diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeData.java b/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeData.java new file mode 100644 index 000000000..27dd914f2 --- /dev/null +++ b/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeData.java @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.api.upgrades; + +import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Contract; + +import javax.annotation.Nullable; + +/** + * An upgrade (i.e. a {@link ITurtleUpgrade}) and its current upgrade data. + *

+ * IMPORTANT: The {@link #data()} in an upgrade data is often a reference to the original upgrade data. + * Be careful to take a {@linkplain #copy() defensive copy} if you plan to use the data in this upgrade. + * + * @param upgrade The current upgrade. + * @param data The upgrade's data. + * @param The type of upgrade, either {@link ITurtleUpgrade} or {@link IPocketUpgrade}. + */ +public record UpgradeData(T upgrade, CompoundTag data) { + /** + * A utility method to construct a new {@link UpgradeData} instance. + * + * @param upgrade An upgrade. + * @param data The upgrade's data. + * @param The type of upgrade. + * @return The new {@link UpgradeData} instance. + */ + public static UpgradeData of(T upgrade, CompoundTag data) { + return new UpgradeData<>(upgrade, data); + } + + /** + * Create an {@link UpgradeData} containing the default {@linkplain #data() data} for an upgrade. + * + * @param upgrade The upgrade instance. + * @param The type of upgrade. + * @return The default upgrade data. + */ + public static UpgradeData ofDefault(T upgrade) { + return of(upgrade, upgrade.getUpgradeData(upgrade.getCraftingItem())); + } + + /** + * Take a copy of a (possibly {@code null}) {@link UpgradeData} instance. + * + * @param upgrade The copied upgrade data. + * @param The type of upgrade. + * @return The newly created upgrade data. + */ + @Contract("!null -> !null; null -> null") + public static @Nullable UpgradeData copyOf(@Nullable UpgradeData upgrade) { + return upgrade == null ? null : upgrade.copy(); + } + + /** + * Get the {@linkplain UpgradeBase#getUpgradeItem(CompoundTag) upgrade item} for this upgrade. + *

+ * This returns a defensive copy of the item, to prevent accidental mutation of the upgrade data or original + * {@linkplain UpgradeBase#getCraftingItem() upgrade stack}. + * + * @return This upgrade's item. + */ + public ItemStack getUpgradeItem() { + return upgrade.getUpgradeItem(data).copy(); + } + + /** + * Take a copy of this {@link UpgradeData}. This returns a new instance with the same upgrade and a fresh copy of + * the upgrade data. + * + * @return A copy of the current instance. + */ + public UpgradeData copy() { + return new UpgradeData<>(upgrade(), data().copy()); + } +} diff --git a/projects/common/src/client/java/dan200/computercraft/client/model/turtle/TurtleModelParts.java b/projects/common/src/client/java/dan200/computercraft/client/model/turtle/TurtleModelParts.java index a473beac3..fec6d4d7d 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/model/turtle/TurtleModelParts.java +++ b/projects/common/src/client/java/dan200/computercraft/client/model/turtle/TurtleModelParts.java @@ -4,11 +4,13 @@ package dan200.computercraft.client.model.turtle; +import com.google.common.cache.CacheBuilder; import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.math.Transformation; import dan200.computercraft.api.client.TransformedModel; import dan200.computercraft.api.turtle.ITurtleUpgrade; import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.client.platform.ClientPlatformHelper; import dan200.computercraft.client.render.TurtleBlockEntityRenderer; import dan200.computercraft.client.turtle.TurtleUpgradeModellers; @@ -21,9 +23,9 @@ import javax.annotation.Nullable; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.function.Function; /** @@ -46,12 +48,19 @@ public final class TurtleModelParts { private record Combination( boolean colour, - @Nullable ITurtleUpgrade leftUpgrade, - @Nullable ITurtleUpgrade rightUpgrade, + @Nullable UpgradeData leftUpgrade, + @Nullable UpgradeData rightUpgrade, @Nullable ResourceLocation overlay, boolean christmas, boolean flip ) { + Combination copy() { + if (leftUpgrade == null && rightUpgrade == null) return this; + return new Combination( + colour, UpgradeData.copyOf(leftUpgrade), UpgradeData.copyOf(rightUpgrade), + overlay, christmas, flip + ); + } } private final BakedModel familyModel; @@ -63,12 +72,20 @@ private record Combination( * A cache of {@link TransformedModel} to the transformed {@link BakedModel}. This helps us pool the transformed * instances, reducing memory usage and hopefully ensuring their caches are hit more often! */ - private final Map transformCache = new HashMap<>(); + private final Map transformCache = CacheBuilder.newBuilder() + .concurrencyLevel(1) + .expireAfterAccess(30, TimeUnit.SECONDS) + .build() + .asMap(); /** * A cache of {@link Combination}s to the combined model. */ - private final Map modelCache = new HashMap<>(); + private final Map modelCache = CacheBuilder.newBuilder() + .concurrencyLevel(1) + .expireAfterAccess(30, TimeUnit.SECONDS) + .build() + .asMap(); public TurtleModelParts(BakedModel familyModel, BakedModel colourModel, ModelTransformer transformer, Function, T> combineModel) { this.familyModel = familyModel; @@ -78,7 +95,15 @@ public TurtleModelParts(BakedModel familyModel, BakedModel colourModel, ModelTra } public T getModel(ItemStack stack) { - return modelCache.computeIfAbsent(getCombination(stack), buildModel); + var combination = getCombination(stack); + var existing = modelCache.get(combination); + if (existing != null) return existing; + + // Take a defensive copy of the upgrade data, and add it to the cache. + var newCombination = combination.copy(); + var newModel = buildModel.apply(newCombination); + modelCache.put(newCombination, newModel); + return newModel; } private Combination getCombination(ItemStack stack) { @@ -89,8 +114,8 @@ private Combination getCombination(ItemStack stack) { } var colour = turtle.getColour(stack); - var leftUpgrade = turtle.getUpgrade(stack, TurtleSide.LEFT); - var rightUpgrade = turtle.getUpgrade(stack, TurtleSide.RIGHT); + var leftUpgrade = turtle.getUpgradeWithData(stack, TurtleSide.LEFT); + var rightUpgrade = turtle.getUpgradeWithData(stack, TurtleSide.RIGHT); var overlay = turtle.getOverlay(stack); var label = turtle.getLabel(stack); var flip = label != null && (label.equals("Dinnerbone") || label.equals("Grumm")); @@ -110,18 +135,19 @@ private List buildModel(Combination combo) { if (overlayModelLocation != null) { parts.add(transform(ClientPlatformHelper.get().getModel(modelManager, overlayModelLocation), transformation)); } - if (combo.leftUpgrade() != null) { - var model = TurtleUpgradeModellers.getModel(combo.leftUpgrade(), null, TurtleSide.LEFT); - parts.add(transform(model.getModel(), transformation.compose(model.getMatrix()))); - } - if (combo.rightUpgrade() != null) { - var model = TurtleUpgradeModellers.getModel(combo.rightUpgrade(), null, TurtleSide.RIGHT); - parts.add(transform(model.getModel(), transformation.compose(model.getMatrix()))); - } + + addUpgrade(parts, transformation, TurtleSide.LEFT, combo.leftUpgrade()); + addUpgrade(parts, transformation, TurtleSide.RIGHT, combo.rightUpgrade()); return parts; } + private void addUpgrade(List parts, Transformation transformation, TurtleSide side, @Nullable UpgradeData upgrade) { + if (upgrade == null) return; + var model = TurtleUpgradeModellers.getModel(upgrade.upgrade(), upgrade.data(), side); + parts.add(transform(model.getModel(), transformation.compose(model.getMatrix()))); + } + private BakedModel transform(BakedModel model, Transformation transformation) { if (transformation.equals(Transformation.identity())) return model; return transformCache.computeIfAbsent(new TransformedModel(model, transformation), transformer); diff --git a/projects/common/src/client/java/dan200/computercraft/client/turtle/TurtleUpgradeModellers.java b/projects/common/src/client/java/dan200/computercraft/client/turtle/TurtleUpgradeModellers.java index 9037748f9..49b08801c 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/turtle/TurtleUpgradeModellers.java +++ b/projects/common/src/client/java/dan200/computercraft/client/turtle/TurtleUpgradeModellers.java @@ -14,8 +14,8 @@ import dan200.computercraft.impl.TurtleUpgrades; import dan200.computercraft.impl.UpgradeManager; import net.minecraft.client.Minecraft; +import net.minecraft.nbt.CompoundTag; -import javax.annotation.Nullable; import java.util.Map; import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; @@ -52,12 +52,18 @@ public static void register(TurtleUpgradeSerialiser) modelCache.computeIfAbsent(upgrade, TurtleUpgradeModellers::getModeller); return modeller.getModel(upgrade, access, side); } + public static TransformedModel getModel(ITurtleUpgrade upgrade, CompoundTag data, TurtleSide side) { + @SuppressWarnings("unchecked") + var modeller = (TurtleUpgradeModeller) modelCache.computeIfAbsent(upgrade, TurtleUpgradeModellers::getModeller); + return modeller.getModel(upgrade, data, side); + } + private static TurtleUpgradeModeller getModeller(ITurtleUpgrade upgradeA) { var wrapper = TurtleUpgrades.instance().getWrapper(upgradeA); if (wrapper == null) return NULL_TURTLE_MODELLER; diff --git a/projects/common/src/main/java/dan200/computercraft/data/RecipeProvider.java b/projects/common/src/main/java/dan200/computercraft/data/RecipeProvider.java index 46854e04d..f23d6711c 100644 --- a/projects/common/src/main/java/dan200/computercraft/data/RecipeProvider.java +++ b/projects/common/src/main/java/dan200/computercraft/data/RecipeProvider.java @@ -8,6 +8,7 @@ import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.pocket.PocketUpgradeDataProvider; import dan200.computercraft.api.turtle.TurtleUpgradeDataProvider; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.core.util.Colour; import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.common.IColouredItem; @@ -110,7 +111,7 @@ private void turtleUpgrades(Consumer add) { var nameId = turtleItem.getFamily().name().toLowerCase(Locale.ROOT); for (var upgrade : turtleUpgrades.getGeneratedUpgrades()) { - var result = turtleItem.create(-1, null, -1, null, upgrade, -1, null); + var result = turtleItem.create(-1, null, -1, null, UpgradeData.ofDefault(upgrade), -1, null); ShapedRecipeBuilder .shaped(RecipeCategory.REDSTONE, result.getItem()) .group(String.format("%s:turtle_%s", ComputerCraftAPI.MOD_ID, nameId)) @@ -146,7 +147,7 @@ private void pocketUpgrades(Consumer add) { var nameId = pocket.getFamily().name().toLowerCase(Locale.ROOT); for (var upgrade : pocketUpgrades.getGeneratedUpgrades()) { - var result = pocket.create(-1, null, -1, upgrade); + var result = pocket.create(-1, null, -1, UpgradeData.ofDefault(upgrade)); ShapedRecipeBuilder .shaped(RecipeCategory.REDSTONE, result.getItem()) .group(String.format("%s:pocket_%s", ComputerCraftAPI.MOD_ID, nameId)) 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 440d4bd2b..a3e25eae5 100644 --- a/projects/common/src/main/java/dan200/computercraft/impl/UpgradeManager.java +++ b/projects/common/src/main/java/dan200/computercraft/impl/UpgradeManager.java @@ -7,6 +7,7 @@ import com.google.gson.*; import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.upgrades.UpgradeBase; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.api.upgrades.UpgradeSerialiser; import dan200.computercraft.shared.platform.PlatformHelper; import net.minecraft.core.Registry; @@ -74,13 +75,13 @@ public String getOwner(T upgrade) { } @Nullable - public T get(ItemStack stack) { + public UpgradeData get(ItemStack stack) { if (stack.isEmpty()) return null; for (var wrapper : current.values()) { var craftingStack = wrapper.upgrade().getCraftingItem(); if (!craftingStack.isEmpty() && craftingStack.getItem() == stack.getItem() && wrapper.upgrade().isItemSuitable(stack)) { - return wrapper.upgrade(); + return UpgradeData.of(wrapper.upgrade, wrapper.upgrade.getUpgradeData(stack)); } } 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 13ea6d0a6..a903eed6e 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java @@ -11,6 +11,7 @@ import dan200.computercraft.api.media.IMedia; import dan200.computercraft.api.pocket.PocketUpgradeSerialiser; import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.core.util.Colour; import dan200.computercraft.impl.PocketUpgrades; import dan200.computercraft.impl.TurtleUpgrades; @@ -446,12 +447,12 @@ public static CreativeModeTab.Builder registerCreativeTab(CreativeModeTab.Builde private static void addTurtle(CreativeModeTab.Output out, TurtleItem turtle) { out.accept(turtle.create(-1, null, -1, null, null, 0, null)); TurtleUpgrades.getVanillaUpgrades() - .map(x -> turtle.create(-1, null, -1, null, x, 0, null)) + .map(x -> turtle.create(-1, null, -1, null, UpgradeData.ofDefault(x), 0, null)) .forEach(out::accept); } private static void addPocket(CreativeModeTab.Output out, PocketComputerItem pocket) { out.accept(pocket.create(-1, null, -1, null)); - PocketUpgrades.getVanillaUpgrades().map(x -> pocket.create(-1, null, -1, x)).forEach(out::accept); + PocketUpgrades.getVanillaUpgrades().map(x -> pocket.create(-1, null, -1, UpgradeData.ofDefault(x))).forEach(out::accept); } } 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 1e93c30a9..e848accef 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 @@ -5,6 +5,7 @@ package dan200.computercraft.shared.integration; import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.impl.PocketUpgrades; import dan200.computercraft.impl.TurtleUpgrades; import dan200.computercraft.shared.ModRegistry; @@ -56,14 +57,14 @@ public static List getExtraStacks() { for (var turtleSupplier : TURTLES) { var turtle = turtleSupplier.get(); for (var upgrade : TurtleUpgrades.instance().getUpgrades()) { - upgradeItems.add(turtle.create(-1, null, -1, null, upgrade, 0, null)); + upgradeItems.add(turtle.create(-1, null, -1, null, UpgradeData.ofDefault(upgrade), 0, null)); } } for (var pocketSupplier : POCKET_COMPUTERS) { var pocket = pocketSupplier.get(); for (var upgrade : PocketUpgrades.instance().getUpgrades()) { - upgradeItems.add(pocket.create(-1, null, -1, upgrade)); + upgradeItems.add(pocket.create(-1, null, -1, 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 0dbe35b5d..253f76a28 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 @@ -9,6 +9,7 @@ import dan200.computercraft.api.turtle.ITurtleUpgrade; import dan200.computercraft.api.turtle.TurtleSide; import dan200.computercraft.api.upgrades.UpgradeBase; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.impl.PocketUpgrades; import dan200.computercraft.impl.TurtleUpgrades; import dan200.computercraft.shared.pocket.items.PocketComputerItem; @@ -111,20 +112,22 @@ public List findRecipesWithInput(ItemStack stack) { if (stack.getItem() instanceof TurtleItem item) { // Suggest possible upgrades which can be applied to this turtle - var left = item.getUpgrade(stack, TurtleSide.LEFT); - var right = item.getUpgrade(stack, TurtleSide.RIGHT); + var left = item.getUpgradeWithData(stack, TurtleSide.LEFT); + var right = item.getUpgradeWithData(stack, TurtleSide.RIGHT); if (left != null && right != null) return Collections.emptyList(); List recipes = new ArrayList<>(); var ingredient = Ingredient.of(stack); for (var upgrade : turtleUpgrades) { + if (upgrade.turtle == null) throw new NullPointerException(); + // The turtle is facing towards us, so upgrades on the left are actually crafted on the right. if (left == null) { - recipes.add(turtle(ingredient, upgrade.ingredient, turtleWith(stack, upgrade.turtle, right))); + recipes.add(turtle(ingredient, upgrade.ingredient, turtleWith(stack, UpgradeData.ofDefault(upgrade.turtle), right))); } if (right == null) { - recipes.add(turtle(upgrade.ingredient, ingredient, turtleWith(stack, left, upgrade.turtle))); + recipes.add(turtle(upgrade.ingredient, ingredient, turtleWith(stack, left, UpgradeData.ofDefault(upgrade.turtle)))); } } @@ -137,7 +140,8 @@ public List findRecipesWithInput(ItemStack stack) { List recipes = new ArrayList<>(); var ingredient = Ingredient.of(stack); for (var upgrade : pocketUpgrades) { - recipes.add(pocket(upgrade.ingredient, ingredient, pocketWith(stack, upgrade.pocket))); + if (upgrade.pocket == null) throw new NullPointerException(); + recipes.add(pocket(upgrade.ingredient, ingredient, pocketWith(stack, UpgradeData.ofDefault(upgrade.pocket)))); } return Collections.unmodifiableList(recipes); @@ -180,21 +184,21 @@ public List findRecipesWithOutput(ItemStack stack) { if (stack.getItem() instanceof TurtleItem item) { List recipes = new ArrayList<>(0); - var left = item.getUpgrade(stack, TurtleSide.LEFT); - var right = item.getUpgrade(stack, TurtleSide.RIGHT); + var left = item.getUpgradeWithData(stack, TurtleSide.LEFT); + var right = item.getUpgradeWithData(stack, TurtleSide.RIGHT); // The turtle is facing towards us, so upgrades on the left are actually crafted on the right. if (left != null) { recipes.add(turtle( Ingredient.of(turtleWith(stack, null, right)), - Ingredient.of(left.getCraftingItem()), + Ingredient.of(left.getUpgradeItem()), stack )); } if (right != null) { recipes.add(turtle( - Ingredient.of(right.getCraftingItem()), + Ingredient.of(right.getUpgradeItem()), Ingredient.of(turtleWith(stack, left, null)), stack )); @@ -204,9 +208,9 @@ public List findRecipesWithOutput(ItemStack stack) { } else if (stack.getItem() instanceof PocketComputerItem) { List recipes = new ArrayList<>(0); - var back = PocketComputerItem.getUpgrade(stack); + var back = PocketComputerItem.getUpgradeWithData(stack); if (back != null) { - recipes.add(pocket(Ingredient.of(back.getCraftingItem()), Ingredient.of(pocketWith(stack, null)), stack)); + recipes.add(pocket(Ingredient.of(back.getUpgradeItem()), Ingredient.of(pocketWith(stack, null)), stack)); } return Collections.unmodifiableList(recipes); @@ -215,7 +219,7 @@ public List findRecipesWithOutput(ItemStack stack) { } } - private static ItemStack turtleWith(ItemStack stack, @Nullable ITurtleUpgrade left, @Nullable ITurtleUpgrade right) { + private static ItemStack turtleWith(ItemStack stack, @Nullable UpgradeData left, @Nullable UpgradeData right) { var item = (TurtleItem) stack.getItem(); return item.create( item.getComputerID(stack), item.getLabel(stack), item.getColour(stack), @@ -223,7 +227,7 @@ private static ItemStack turtleWith(ItemStack stack, @Nullable ITurtleUpgrade le ); } - private static ItemStack pocketWith(ItemStack stack, @Nullable IPocketUpgrade back) { + private static ItemStack pocketWith(ItemStack stack, @Nullable UpgradeData back) { var item = (PocketComputerItem) stack.getItem(); return item.create( item.getComputerID(stack), item.getLabel(stack), item.getColour(stack), back @@ -272,7 +276,7 @@ List getRecipes() { recipes.add(turtle( ingredient, // Right upgrade, recipe on left Ingredient.of(turtleItem.create(-1, null, -1, null, null, 0, null)), - turtleItem.create(-1, null, -1, null, turtle, 0, null) + turtleItem.create(-1, null, -1, null, UpgradeData.ofDefault(turtle), 0, null) )); } } @@ -283,7 +287,7 @@ List getRecipes() { recipes.add(pocket( ingredient, Ingredient.of(pocketItem.create(-1, null, -1, null)), - pocketItem.create(-1, null, -1, pocket) + pocketItem.create(-1, null, -1, UpgradeData.ofDefault(pocket)) )); } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/platform/PlatformHelper.java b/projects/common/src/main/java/dan200/computercraft/shared/platform/PlatformHelper.java index faba05862..614b52446 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/platform/PlatformHelper.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/platform/PlatformHelper.java @@ -68,6 +68,13 @@ static PlatformHelper get() { return (PlatformHelper) dan200.computercraft.impl.PlatformHelper.get(); } + /** + * Check if we're running in a development environment. + * + * @return If we're running in a development environment. + */ + boolean isDevelopmentEnvironment(); + /** * Create a new config builder. * 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 e1b1cb0ad..a0749450d 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 @@ -7,6 +7,7 @@ import dan200.computercraft.api.lua.ILuaAPI; import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.impl.PocketUpgrades; import dan200.computercraft.shared.pocket.core.PocketServerComputer; import net.minecraft.core.NonNullList; @@ -14,6 +15,7 @@ import net.minecraft.world.item.ItemStack; import javax.annotation.Nullable; +import java.util.Objects; /** * Control the current pocket computer, adding or removing upgrades. @@ -68,7 +70,7 @@ public final Object[] equipBack() { if (newUpgrade == null) return new Object[]{ false, "Cannot find a valid upgrade" }; // Remove the current upgrade - if (previousUpgrade != null) storeItem(player, previousUpgrade.getCraftingItem().copy()); + if (previousUpgrade != null) storeItem(player, previousUpgrade.getUpgradeItem()); // Set the new upgrade computer.setUpgrade(newUpgrade); @@ -93,7 +95,7 @@ public final Object[] unequipBack() { computer.setUpgrade(null); - storeItem(player, previousUpgrade.getCraftingItem().copy()); + storeItem(player, previousUpgrade.getUpgradeItem()); return new Object[]{ true }; } @@ -105,13 +107,13 @@ private static void storeItem(Player player, ItemStack stack) { } } - private static @Nullable IPocketUpgrade findUpgrade(NonNullList inv, int start, @Nullable IPocketUpgrade previous) { + private static @Nullable UpgradeData findUpgrade(NonNullList inv, int start, @Nullable UpgradeData previous) { for (var i = 0; i < inv.size(); i++) { var invStack = inv.get((i + start) % inv.size()); if (!invStack.isEmpty()) { var newUpgrade = PocketUpgrades.instance().get(invStack); - if (newUpgrade != null && newUpgrade != previous) { + if (newUpgrade != null && !Objects.equals(newUpgrade, previous)) { // Consume an item from this stack and exit the loop invStack = invStack.copy(); invStack.shrink(1); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java b/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java index 937da3341..64cc6c73f 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java @@ -7,6 +7,7 @@ import dan200.computercraft.api.peripheral.IPeripheral; 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.shared.common.IColouredItem; import dan200.computercraft.shared.computer.core.ComputerFamily; @@ -109,8 +110,8 @@ public Map getUpgrades() { return upgrade == null ? Collections.emptyMap() : Collections.singletonMap(upgrade.getUpgradeID(), getPeripheral(ComputerSide.BACK)); } - public @Nullable IPocketUpgrade getUpgrade() { - return upgrade; + public @Nullable UpgradeData getUpgrade() { + return upgrade == null ? null : UpgradeData.of(upgrade, getUpgradeNBTData()); } /** @@ -120,13 +121,11 @@ public Map getUpgrades() { * * @param upgrade The new upgrade to set it to, may be {@code null}. */ - public void setUpgrade(@Nullable IPocketUpgrade upgrade) { - if (this.upgrade == upgrade) return; - + public void setUpgrade(@Nullable UpgradeData upgrade) { synchronized (this) { PocketComputerItem.setUpgrade(stack, upgrade); updateUpgradeNBTData(); - this.upgrade = upgrade; + this.upgrade = upgrade == null ? null : upgrade.upgrade(); invalidatePeripheral(); } } 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 a3648ba0c..1834288ab 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 @@ -10,6 +10,7 @@ import dan200.computercraft.api.filesystem.Mount; import dan200.computercraft.api.media.IMedia; 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.shared.ModRegistry; @@ -23,6 +24,7 @@ import dan200.computercraft.shared.pocket.core.PocketServerComputer; import dan200.computercraft.shared.pocket.inventory.PocketComputerMenuProvider; import dan200.computercraft.shared.util.IDAssigner; +import dan200.computercraft.shared.util.NBTUtil; import net.minecraft.ChatFormatting; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; @@ -58,7 +60,7 @@ public PocketComputerItem(Properties settings, ComputerFamily family) { this.family = family; } - public static ItemStack create(int id, @Nullable String label, int colour, ComputerFamily family, @Nullable IPocketUpgrade upgrade) { + public static ItemStack create(int id, @Nullable String label, int colour, ComputerFamily family, @Nullable UpgradeData upgrade) { return switch (family) { case NORMAL -> ModRegistry.Items.POCKET_COMPUTER_NORMAL.get().create(id, label, colour, upgrade); case ADVANCED -> ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get().create(id, label, colour, upgrade); @@ -66,11 +68,14 @@ public static ItemStack create(int id, @Nullable String label, int colour, Compu }; } - public ItemStack create(int id, @Nullable String label, int colour, @Nullable IPocketUpgrade upgrade) { + public ItemStack create(int id, @Nullable String label, int colour, @Nullable UpgradeData upgrade) { var result = new ItemStack(this); if (id >= 0) result.getOrCreateTag().putInt(NBT_ID, id); if (label != null) result.setHoverName(Component.literal(label)); - if (upgrade != null) result.getOrCreateTag().putString(NBT_UPGRADE, upgrade.getUpgradeID().toString()); + if (upgrade != null) { + result.getOrCreateTag().putString(NBT_UPGRADE, upgrade.upgrade().getUpgradeID().toString()); + if (!upgrade.data().isEmpty()) result.getOrCreateTag().put(NBT_UPGRADE_INFO, upgrade.data().copy()); + } if (colour != -1) result.getOrCreateTag().putInt(NBT_COLOUR, colour); return result; } @@ -207,7 +212,9 @@ public PocketServerComputer createServerComputer(ServerLevel level, Entity entit setInstanceID(stack, computer.register()); setSessionID(stack, registry.getSessionID()); - computer.updateValues(entity, stack, getUpgrade(stack)); + var upgrade = getUpgrade(stack); + + computer.updateValues(entity, stack, upgrade); computer.addAPI(new PocketAPI(computer)); // Only turn on when initially creating the computer, rather than each tick. @@ -244,7 +251,7 @@ public ComputerFamily getFamily() { public ItemStack withFamily(ItemStack stack, ComputerFamily family) { return create( getComputerID(stack), getLabel(stack), getColour(stack), - family, getUpgrade(stack) + family, getUpgradeWithData(stack) ); } @@ -294,20 +301,27 @@ private static boolean isMarkedOn(ItemStack stack) { public static @Nullable IPocketUpgrade getUpgrade(ItemStack stack) { var compound = stack.getTag(); - return compound != null && compound.contains(NBT_UPGRADE) - ? PocketUpgrades.instance().get(compound.getString(NBT_UPGRADE)) : null; + if (compound == null || !compound.contains(NBT_UPGRADE)) return null; + return PocketUpgrades.instance().get(compound.getString(NBT_UPGRADE)); } - public static void setUpgrade(ItemStack stack, @Nullable IPocketUpgrade upgrade) { + public static @Nullable UpgradeData getUpgradeWithData(ItemStack stack) { + var compound = stack.getTag(); + if (compound == null || !compound.contains(NBT_UPGRADE)) return null; + var upgrade = PocketUpgrades.instance().get(compound.getString(NBT_UPGRADE)); + return upgrade == null ? null : UpgradeData.of(upgrade, NBTUtil.getCompoundOrEmpty(compound, NBT_UPGRADE_INFO)); + } + + public static void setUpgrade(ItemStack stack, @Nullable UpgradeData upgrade) { var compound = stack.getOrCreateTag(); if (upgrade == null) { compound.remove(NBT_UPGRADE); + compound.remove(NBT_UPGRADE_INFO); } else { - compound.putString(NBT_UPGRADE, upgrade.getUpgradeID().toString()); + compound.putString(NBT_UPGRADE, upgrade.upgrade().getUpgradeID().toString()); + compound.put(NBT_UPGRADE_INFO, upgrade.data().copy()); } - - compound.remove(NBT_UPGRADE_INFO); } public static CompoundTag getUpgradeInfo(ItemStack stack) { 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 6c74ea7b6..b0b25bd87 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 @@ -5,6 +5,7 @@ package dan200.computercraft.shared.pocket.recipes; 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.items.PocketComputerItem; @@ -62,7 +63,7 @@ public ItemStack assemble(CraftingContainer inventory, RegistryAccess registryAc if (PocketComputerItem.getUpgrade(computer) != null) return ItemStack.EMPTY; // Check for upgrades around the item - IPocketUpgrade upgrade = null; + UpgradeData upgrade = null; for (var y = 0; y < inventory.getHeight(); y++) { for (var x = 0; x < inventory.getWidth(); x++) { var item = inventory.getItem(x + y * inventory.getWidth()); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/blocks/TurtleBlock.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/blocks/TurtleBlock.java index 7e058698e..157bb876b 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/blocks/TurtleBlock.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/blocks/TurtleBlock.java @@ -5,7 +5,9 @@ package dan200.computercraft.shared.turtle.blocks; import dan200.computercraft.annotations.ForgeOverride; +import dan200.computercraft.api.turtle.ITurtleUpgrade; import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.shared.computer.blocks.AbstractComputerBlock; import dan200.computercraft.shared.computer.blocks.AbstractComputerBlockEntity; import dan200.computercraft.shared.computer.core.ComputerFamily; @@ -128,7 +130,7 @@ public void setPlacedBy(Level world, BlockPos pos, BlockState state, @Nullable L if (stack.getItem() instanceof TurtleItem item) { // Set Upgrades for (var side : TurtleSide.values()) { - turtle.getAccess().setUpgrade(side, item.getUpgrade(stack, side)); + turtle.getAccess().setUpgradeWithData(side, item.getUpgradeWithData(stack, side)); } turtle.getAccess().setFuelLevel(item.getFuelLevel(stack)); @@ -161,11 +163,16 @@ protected ItemStack getItem(AbstractComputerBlockEntity tile) { var access = turtle.getAccess(); return TurtleItem.create( turtle.getComputerID(), turtle.getLabel(), access.getColour(), turtle.getFamily(), - access.getUpgrade(TurtleSide.LEFT), access.getUpgrade(TurtleSide.RIGHT), + withPersistedData(access.getUpgradeWithData(TurtleSide.LEFT)), + withPersistedData(access.getUpgradeWithData(TurtleSide.RIGHT)), access.getFuelLevel(), turtle.getOverlay() ); } + private static @Nullable UpgradeData withPersistedData(@Nullable UpgradeData upgrade) { + return upgrade == null ? null : UpgradeData.of(upgrade.upgrade(), upgrade.upgrade().getPersistedData(upgrade.data())); + } + @Override @Nullable public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java index ecfc1f787..a5aa15902 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java @@ -13,6 +13,7 @@ import dan200.computercraft.api.turtle.TurtleAnimation; import dan200.computercraft.api.turtle.TurtleCommand; import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.core.computer.ComputerSide; import dan200.computercraft.core.util.Colour; import dan200.computercraft.impl.TurtleUpgrades; @@ -141,17 +142,16 @@ private void readCommon(CompoundTag nbt) { overlay = nbt.contains(NBT_OVERLAY) ? new ResourceLocation(nbt.getString(NBT_OVERLAY)) : null; // Read upgrades - setUpgradeDirect(TurtleSide.LEFT, nbt.contains(NBT_LEFT_UPGRADE) ? TurtleUpgrades.instance().get(nbt.getString(NBT_LEFT_UPGRADE)) : null); - setUpgradeDirect(TurtleSide.RIGHT, nbt.contains(NBT_RIGHT_UPGRADE) ? TurtleUpgrades.instance().get(nbt.getString(NBT_RIGHT_UPGRADE)) : null); + setUpgradeDirect(TurtleSide.LEFT, readUpgrade(nbt, NBT_LEFT_UPGRADE, NBT_LEFT_UPGRADE_DATA)); + setUpgradeDirect(TurtleSide.RIGHT, readUpgrade(nbt, NBT_RIGHT_UPGRADE, NBT_RIGHT_UPGRADE_DATA)); + } - // NBT - upgradeNBTData.clear(); - if (nbt.contains(NBT_LEFT_UPGRADE_DATA)) { - upgradeNBTData.put(TurtleSide.LEFT, nbt.getCompound(NBT_LEFT_UPGRADE_DATA).copy()); - } - if (nbt.contains(NBT_RIGHT_UPGRADE_DATA)) { - upgradeNBTData.put(TurtleSide.RIGHT, nbt.getCompound(NBT_RIGHT_UPGRADE_DATA).copy()); - } + private @Nullable UpgradeData readUpgrade(CompoundTag tag, String upgradeKey, String dataKey) { + if (!tag.contains(upgradeKey)) return null; + var upgrade = TurtleUpgrades.instance().get(tag.getString(upgradeKey)); + if (upgrade == null) return null; + + return UpgradeData.of(upgrade, tag.getCompound(dataKey)); } private void writeCommon(CompoundTag nbt) { @@ -516,7 +516,7 @@ public GameProfile getOwningPlayer() { } @Override - public void setUpgrade(TurtleSide side, @Nullable ITurtleUpgrade upgrade) { + public void setUpgradeWithData(TurtleSide side, @Nullable UpgradeData upgrade) { if (!setUpgradeDirect(side, upgrade) || owner.getLevel() == null) return; // This is a separate function to avoid updating the block when reading the NBT. We don't need to do this as @@ -529,19 +529,18 @@ public void setUpgrade(TurtleSide side, @Nullable ITurtleUpgrade upgrade) { owner.updateInputsImmediately(); } - private boolean setUpgradeDirect(TurtleSide side, @Nullable ITurtleUpgrade upgrade) { + private boolean setUpgradeDirect(TurtleSide side, @Nullable UpgradeData upgrade) { // Remove old upgrade - if (upgrades.containsKey(side)) { - if (upgrades.get(side) == upgrade) return false; - upgrades.remove(side); - } else { - if (upgrade == null) return false; - } - - upgradeNBTData.remove(side); + var oldUpgrade = upgrades.remove(side); + if (oldUpgrade == null && upgrade == null) return false; // Set new upgrade - if (upgrade != null) upgrades.put(side, upgrade); + if (upgrade == null) { + upgradeNBTData.remove(side); + } else { + upgrades.put(side, upgrade.upgrade()); + upgradeNBTData.put(side, upgrade.data().copy()); + } // Notify clients and create peripherals if (owner.getLevel() != null && !owner.getLevel().isClientSide) { @@ -595,7 +594,7 @@ public Vec3 getRenderOffset(float f) { public float getToolRenderAngle(TurtleSide side, float f) { return (side == TurtleSide.LEFT && animation == TurtleAnimation.SWING_LEFT_TOOL) || - (side == TurtleSide.RIGHT && animation == TurtleAnimation.SWING_RIGHT_TOOL) + (side == TurtleSide.RIGHT && animation == TurtleAnimation.SWING_RIGHT_TOOL) ? 45.0f * (float) Math.sin(getAnimationFraction(f) * Math.PI) : 0.0f; } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleEquipCommand.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleEquipCommand.java index 1a15bb840..ae373f697 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleEquipCommand.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleEquipCommand.java @@ -5,6 +5,7 @@ package dan200.computercraft.shared.turtle.core; import dan200.computercraft.api.turtle.*; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.impl.TurtleUpgrades; import dan200.computercraft.shared.turtle.TurtleUtil; @@ -18,10 +19,10 @@ public TurtleEquipCommand(TurtleSide side) { @Override public TurtleCommandResult execute(ITurtleAccess turtle) { // Determine the upgrade to replace - var oldUpgrade = turtle.getUpgrade(side); + var oldUpgrade = turtle.getUpgradeWithData(side); // Determine the upgrade to equipLeft - ITurtleUpgrade newUpgrade; + UpgradeData newUpgrade; var selectedStack = turtle.getInventory().getItem(turtle.getSelectedSlot()); if (!selectedStack.isEmpty()) { newUpgrade = TurtleUpgrades.instance().get(selectedStack); @@ -32,8 +33,8 @@ public TurtleCommandResult execute(ITurtleAccess turtle) { // Do the swapping: if (newUpgrade != null) turtle.getInventory().removeItem(turtle.getSelectedSlot(), 1); - if (oldUpgrade != null) TurtleUtil.storeItemOrDrop(turtle, oldUpgrade.getCraftingItem().copy()); - turtle.setUpgrade(side, newUpgrade); + if (oldUpgrade != null) TurtleUtil.storeItemOrDrop(turtle, oldUpgrade.getUpgradeItem()); + turtle.setUpgradeWithData(side, newUpgrade); // Animate if (newUpgrade != null || oldUpgrade != null) { 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 22d918626..0dd06e896 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 @@ -7,6 +7,7 @@ import dan200.computercraft.api.turtle.ITurtleAccess; import dan200.computercraft.api.turtle.ITurtleUpgrade; import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.impl.TurtleUpgrades; import net.minecraft.core.NonNullList; import net.minecraft.world.Container; @@ -27,7 +28,7 @@ class UpgradeContainer implements Container { private final ITurtleAccess turtle; - private final List lastUpgrade = Arrays.asList(null, null); + private final List> lastUpgrade = Arrays.asList(null, null); private final NonNullList lastStack = NonNullList.withSize(2, ItemStack.EMPTY); UpgradeContainer(ITurtleAccess turtle) { @@ -44,22 +45,25 @@ private TurtleSide getSide(int slot) { @Override public ItemStack getItem(int slot) { - var upgrade = turtle.getUpgrade(getSide(slot)); + var side = getSide(slot); + var upgrade = turtle.getUpgrade(side); + if (upgrade == null) return ItemStack.EMPTY; // We don't want to return getCraftingItem directly here, as consumers may mutate the stack (they shouldn't!, // but if they do it's a pain to track down). To avoid recreating the stack each tick, we maintain a simple - // cache. - if (upgrade == lastUpgrade.get(slot)) return lastStack.get(slot); + // cache. We use an inlined getUpgradeData here to avoid the additional defensive copy. + var upgradeData = UpgradeData.of(upgrade, turtle.getUpgradeNBTData(side)); + if (upgradeData.equals(lastUpgrade.get(slot))) return lastStack.get(slot); - var stack = upgrade == null ? ItemStack.EMPTY : upgrade.getCraftingItem().copy(); - lastUpgrade.set(slot, upgrade); + var stack = upgradeData.getUpgradeItem(); + lastUpgrade.set(slot, upgradeData.copy()); lastStack.set(slot, stack); return stack; } @Override public void setItem(int slot, ItemStack itemStack) { - turtle.setUpgrade(getSide(slot), TurtleUpgrades.instance().get(itemStack)); + turtle.setUpgradeWithData(getSide(slot), TurtleUpgrades.instance().get(itemStack)); } @Override 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 91bc45717..640872b23 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 @@ -8,12 +8,14 @@ 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.shared.ModRegistry; import dan200.computercraft.shared.common.IColouredItem; import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.items.AbstractComputerItem; import dan200.computercraft.shared.turtle.blocks.TurtleBlock; +import dan200.computercraft.shared.util.NBTUtil; import net.minecraft.core.cauldron.CauldronInteraction; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; @@ -32,7 +34,7 @@ public TurtleItem(TurtleBlock block, Properties settings) { public static ItemStack create( int id, @Nullable String label, int colour, ComputerFamily family, - @Nullable ITurtleUpgrade leftUpgrade, @Nullable ITurtleUpgrade rightUpgrade, + @Nullable UpgradeData leftUpgrade, @Nullable UpgradeData rightUpgrade, int fuelLevel, @Nullable ResourceLocation overlay ) { return switch (family) { @@ -46,7 +48,7 @@ public static ItemStack create( public ItemStack create( int id, @Nullable String label, int colour, - @Nullable ITurtleUpgrade leftUpgrade, @Nullable ITurtleUpgrade rightUpgrade, + @Nullable UpgradeData leftUpgrade, @Nullable UpgradeData rightUpgrade, int fuelLevel, @Nullable ResourceLocation overlay ) { // Build the stack @@ -58,11 +60,15 @@ public ItemStack create( if (overlay != null) stack.getOrCreateTag().putString(NBT_OVERLAY, overlay.toString()); if (leftUpgrade != null) { - stack.getOrCreateTag().putString(NBT_LEFT_UPGRADE, leftUpgrade.getUpgradeID().toString()); + var tag = stack.getOrCreateTag(); + tag.putString(NBT_LEFT_UPGRADE, leftUpgrade.upgrade().getUpgradeID().toString()); + if (!leftUpgrade.data().isEmpty()) tag.put(NBT_LEFT_UPGRADE_DATA, leftUpgrade.data().copy()); } if (rightUpgrade != null) { - stack.getOrCreateTag().putString(NBT_RIGHT_UPGRADE, rightUpgrade.getUpgradeID().toString()); + var tag = stack.getOrCreateTag(); + tag.putString(NBT_RIGHT_UPGRADE, rightUpgrade.upgrade().getUpgradeID().toString()); + if (!rightUpgrade.data().isEmpty()) tag.put(NBT_RIGHT_UPGRADE_DATA, rightUpgrade.data().copy()); } return stack; @@ -117,7 +123,7 @@ public ItemStack withFamily(ItemStack stack, ComputerFamily family) { return create( getComputerID(stack), getLabel(stack), getColour(stack), family, - getUpgrade(stack, TurtleSide.LEFT), getUpgrade(stack, TurtleSide.RIGHT), + getUpgradeWithData(stack, TurtleSide.LEFT), getUpgradeWithData(stack, TurtleSide.RIGHT), getFuelLevel(stack), getOverlay(stack) ); } @@ -127,7 +133,20 @@ public ItemStack withFamily(ItemStack stack, ComputerFamily family) { if (tag == null) return null; var key = side == TurtleSide.LEFT ? NBT_LEFT_UPGRADE : NBT_RIGHT_UPGRADE; - return tag.contains(key) ? TurtleUpgrades.instance().get(tag.getString(key)) : null; + if (!tag.contains(key)) return null; + return TurtleUpgrades.instance().get(tag.getString(key)); + } + + public @Nullable UpgradeData getUpgradeWithData(ItemStack stack, TurtleSide side) { + var tag = stack.getTag(); + if (tag == null) return null; + + var key = side == TurtleSide.LEFT ? NBT_LEFT_UPGRADE : NBT_RIGHT_UPGRADE; + if (!tag.contains(key)) return null; + var upgrade = TurtleUpgrades.instance().get(tag.getString(key)); + if (upgrade == null) return null; + var dataKey = side == TurtleSide.LEFT ? NBT_LEFT_UPGRADE_DATA : NBT_RIGHT_UPGRADE_DATA; + return UpgradeData.of(upgrade, NBTUtil.getCompoundOrEmpty(tag, dataKey)); } public @Nullable ResourceLocation getOverlay(ItemStack stack) { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleOverlayRecipe.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleOverlayRecipe.java index 4076a3662..7f31bca95 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleOverlayRecipe.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleOverlayRecipe.java @@ -38,8 +38,8 @@ private static ItemStack make(ItemStack stack, ResourceLocation overlay) { turtle.getComputerID(stack), turtle.getLabel(stack), turtle.getColour(stack), - turtle.getUpgrade(stack, TurtleSide.LEFT), - turtle.getUpgrade(stack, TurtleSide.RIGHT), + turtle.getUpgradeWithData(stack, TurtleSide.LEFT), + turtle.getUpgradeWithData(stack, TurtleSide.RIGHT), turtle.getFuelLevel(stack), overlay ); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleUpgradeRecipe.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleUpgradeRecipe.java index 4dcd9e5a2..3ad657e47 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleUpgradeRecipe.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleUpgradeRecipe.java @@ -6,6 +6,7 @@ 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.shared.ModRegistry; import dan200.computercraft.shared.turtle.items.TurtleItem; @@ -104,9 +105,10 @@ public ItemStack assemble(CraftingContainer inventory, RegistryAccess registryAc // At this point we have a turtle + 1 or 2 items // Get the turtle we already have var itemTurtle = (TurtleItem) turtle.getItem(); - var upgrades = new ITurtleUpgrade[]{ - itemTurtle.getUpgrade(turtle, TurtleSide.LEFT), - itemTurtle.getUpgrade(turtle, TurtleSide.RIGHT), + @SuppressWarnings({ "unchecked", "rawtypes" }) + UpgradeData[] upgrades = new UpgradeData[]{ + itemTurtle.getUpgradeWithData(turtle, TurtleSide.LEFT), + itemTurtle.getUpgradeWithData(turtle, TurtleSide.RIGHT), }; // Get the upgrades for the new items diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleModem.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleModem.java index 4c514de2d..7235d4431 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleModem.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleModem.java @@ -9,6 +9,7 @@ import dan200.computercraft.shared.peripheral.modem.ModemState; import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemPeripheral; import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; @@ -77,4 +78,9 @@ public void update(ITurtleAccess turtle, TurtleSide side) { } } } + + @Override + public CompoundTag getPersistedData(CompoundTag upgradeData) { + return new CompoundTag(); + } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/util/NBTUtil.java b/projects/common/src/main/java/dan200/computercraft/shared/util/NBTUtil.java index 874375033..5fa6f7061 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/util/NBTUtil.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/util/NBTUtil.java @@ -7,6 +7,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.io.BaseEncoding; import dan200.computercraft.core.util.Nullability; +import dan200.computercraft.shared.platform.PlatformHelper; import net.minecraft.nbt.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,6 +20,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -27,9 +29,42 @@ public final class NBTUtil { @VisibleForTesting static final BaseEncoding ENCODING = BaseEncoding.base16().lowerCase(); + private static final CompoundTag EMPTY_TAG; + + static { + // If in a development environment, create a magic immutable compound tag. + // We avoid doing this in prod, as I fear it might mess up the JIT inlining things. + if (PlatformHelper.get().isDevelopmentEnvironment()) { + try { + var ctor = CompoundTag.class.getDeclaredConstructor(Map.class); + ctor.setAccessible(true); + EMPTY_TAG = ctor.newInstance(Collections.emptyMap()); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } else { + EMPTY_TAG = new CompoundTag(); + } + } + private NBTUtil() { } + /** + * Get a singleton empty {@link CompoundTag}. This tag should never be modified. + * + * @return The empty compound tag. + */ + public static CompoundTag emptyTag() { + if (EMPTY_TAG.size() != 0) LOG.error("The empty tag has been modified."); + return EMPTY_TAG; + } + + public static CompoundTag getCompoundOrEmpty(CompoundTag tag, String key) { + var childTag = tag.get(key); + return childTag != null && childTag.getId() == Tag.TAG_COMPOUND ? (CompoundTag) childTag : emptyTag(); + } + private static @Nullable Tag toNBTTag(@Nullable Object object) { if (object == null) return null; if (object instanceof Boolean) return ByteTag.valueOf((byte) ((boolean) (Boolean) object ? 1 : 0)); diff --git a/projects/common/src/test/java/dan200/computercraft/TestPlatformHelper.java b/projects/common/src/test/java/dan200/computercraft/TestPlatformHelper.java index 552e63756..d898d5928 100644 --- a/projects/common/src/test/java/dan200/computercraft/TestPlatformHelper.java +++ b/projects/common/src/test/java/dan200/computercraft/TestPlatformHelper.java @@ -58,6 +58,11 @@ @AutoService({ PlatformHelper.class, dan200.computercraft.impl.PlatformHelper.class, ComputerCraftAPIService.class }) public class TestPlatformHelper extends AbstractComputerCraftAPI implements PlatformHelper { + @Override + public boolean isDevelopmentEnvironment() { + return true; + } + @Override public ConfigFile.Builder createConfigBuilder() { throw new UnsupportedOperationException("Cannot create config file inside tests"); diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java index 677366b30..7f0f5fb30 100644 --- a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java @@ -33,6 +33,7 @@ import net.fabricmc.fabric.api.tag.convention.v1.ConventionalItemTags; import net.fabricmc.fabric.api.transfer.v1.item.InventoryStorage; import net.fabricmc.fabric.api.transfer.v1.item.ItemStorage; +import net.fabricmc.loader.api.FabricLoader; import net.minecraft.commands.synchronization.ArgumentTypeInfo; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; @@ -81,6 +82,11 @@ @AutoService(dan200.computercraft.impl.PlatformHelper.class) public class PlatformHelperImpl implements PlatformHelper { + @Override + public boolean isDevelopmentEnvironment() { + return FabricLoader.getInstance().isDevelopmentEnvironment(); + } + @Override public ConfigFile.Builder createConfigBuilder() { return new FabricConfigFile.Builder(); diff --git a/projects/forge/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java b/projects/forge/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java index 41154448c..02a644bb1 100644 --- a/projects/forge/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java +++ b/projects/forge/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java @@ -62,6 +62,7 @@ import net.minecraftforge.event.ForgeEventFactory; import net.minecraftforge.eventbus.api.Event; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.fml.loading.FMLLoader; import net.minecraftforge.items.wrapper.InvWrapper; import net.minecraftforge.items.wrapper.SidedInvWrapper; import net.minecraftforge.network.NetworkHooks; @@ -76,6 +77,11 @@ @AutoService(dan200.computercraft.impl.PlatformHelper.class) public class PlatformHelperImpl implements PlatformHelper { + @Override + public boolean isDevelopmentEnvironment() { + return !FMLLoader.isProduction(); + } + @Override public ConfigFile.Builder createConfigBuilder() { return new ForgeConfigFile.Builder();