1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-08-25 06:52:17 +00:00

Allow equipping pocket computers on the bottom

This allows equipping pocket computers on both the back (as before) and
bottom of a pocket computer. The asymmetry is a little unfortunate here,
but makes some sense with the crafting recipe (above goes behind, below
goes on the bottom).

 - Move some functionality from IPocketAccess into a PocketComputer
   interface (and PocketComputerInternal) interface, used by the pocket
   API.

   IPocketAccess preserves the same interface as before. Unlike
   ITurtleAccess, we /don't/ expose the PocketSide in the public API.

 - Several pocket-computer methods (e.g. setUpgradeData, setColour) are
   now required to be called on the main thread, and when the computer
   is being held. This allows us to write back changes to the item
   immediately, rather than the next time the item is ticked.

   Sadly this doesn't actually remove the need for onCraftedPostProcess
   as I'd originally hoped, but I think does make the code a little
   simpler.

 - Rename "computercraft:pocket_computer" component to
   "computercraft:back_pocket_computer".

 - And finally, support multiple upgrades on the pocket computer. This
   is actually quite an easy change, just tedious — there's lots of
   places to update!

Fixes #1406, and I think fixes #1148 — you can use a speaker to notify
you now.
This commit is contained in:
Jonathan Coates 2025-05-10 17:13:54 +01:00
parent c20336286b
commit 0a0c80db41
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
37 changed files with 806 additions and 266 deletions

View File

@ -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<IPocketAccess> POCKET = ComputerComponent.create(ComputerCraftAPI.MOD_ID, "pocket");
public static final ComputerComponent<PocketComputer> POCKET = ComputerComponent.create(ComputerCraftAPI.MOD_ID, "pocket");
/**
* This component is only present on "command computers", and other computers with admin capabilities.

View File

@ -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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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()

View File

@ -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;
}
}

View File

@ -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.
* <p>
* 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.
* <p>
* As pocket computers are backed by item stacks, you must check for validity before updating the computer.
* <p>
* 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.
*
* <p>
* 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.
* <p>
* 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);
}

View File

@ -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");

View File

@ -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")

View File

@ -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",

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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<T extends UpgradeBase> {
// TODO: Would be nice if we could use the registration info here.
}
/**
* Determine our "creator mod" from a list of upgrades.
* <p>
* 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<T> first, @Nullable UpgradeData<T> 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<T> get(HolderLookup.Provider registries, ItemStack stack) {
if (stack.isEmpty()) return null;
@ -109,4 +133,16 @@ public final class UpgradeManager<T extends UpgradeBase> {
.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);
}
}
}

View File

@ -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;<init>(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) {

View File

@ -25,7 +25,7 @@ class V3818_3Mixin {
@ModifyReturnValue(method = "components", at = @At("TAIL"))
@SuppressWarnings("UnusedMethod")
private static SequencedMap<String, Supplier<TypeTemplate>> components(SequencedMap<String, Supplier<TypeTemplate>> types, Schema schema) {
ComponentizationFixers.addExtraTypes(types, schema);
ComponentizationFixers.addComponents(types, schema);
return types;
}
}

View File

@ -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<DataComponentType<UpgradeData<ITurtleUpgrade>>> 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<DataComponentType<UpgradeData<ITurtleUpgrade>>> 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<DataComponentType<UpgradeData<IPocketUpgrade>>> POCKET_UPGRADE = register("pocket_upgrade", b -> b
public static final RegistryEntry<DataComponentType<UpgradeData<IPocketUpgrade>>> 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<DataComponentType<UpgradeData<IPocketUpgrade>>> 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<? extends UpgradeBase> upgrade) {

View File

@ -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<String, Supplier<TypeTemplate>> types, Schema schema) {
public static void addComponents(Map<String, Supplier<TypeTemplate>> types, Schema schema) {
// Create a codec for UpgradeData
Supplier<TypeTemplate> 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);
}

View File

@ -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.
* <p>
* 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.
* <p>
* {@link DataComponentRemainderFix} <em>does</em> 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);
}

View File

@ -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)))
);
}

View File

@ -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<T> {
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<T> 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<T> {
} else if (stack.getItem() instanceof PocketComputerItem) {
List<T> 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<T> {
return newStack;
}
private static ItemStack pocketWith(ItemStack stack, @Nullable UpgradeData<IPocketUpgrade> back) {
private static ItemStack pocketWith(ItemStack stack, @Nullable UpgradeData<IPocketUpgrade> back, @Nullable UpgradeData<IPocketUpgrade> 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<T> {
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))
));
}
}

View File

@ -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);
}

View File

@ -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.
* <p>
* 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.
* <p>
* 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());

View File

@ -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}.
* <p>
* 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<IPocketUpgrade> upgrade;
private int colour = -1;
private int lightColour = -1;
private final Map<PocketSide, UpgradeAccess> upgrades = new EnumMap<>(PocketSide.class);
public PocketBrain(PocketHolder holder, @Nullable UpgradeData<IPocketUpgrade> 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<IPocketUpgrade> 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<IPocketUpgrade> 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<IPocketUpgrade> back, @Nullable UpgradeData<IPocketUpgrade> 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<UpgradeData<IPocketUpgrade>> component;
private final ComputerSide side;
@Override
public @Nullable UpgradeData<IPocketUpgrade> getUpgrade() {
return upgrade;
}
private @Nullable UpgradeData<IPocketUpgrade> upgrade;
private int lightColour = -1;
/**
* Set the upgrade for this pocket computer, also updating the item stack.
* <p>
* 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<IPocketUpgrade> upgrade) {
if (Objects.equals(this.upgrade, upgrade)) return;
private UpgradeAccess(DataComponentType<UpgradeData<IPocketUpgrade>> 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<IPocketUpgrade> getUpgrade() {
return upgrade;
}
/**
* Set the upgrade for this pocket computer, also updating the item stack.
* <p>
* 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<IPocketUpgrade> 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<IPocketUpgrade> upgrade) {
if (Objects.equals(this.upgrade, upgrade)) return false;
this.upgrade = upgrade;
lightColour = -1;
invalidatePeripheral();
return true;
}
}
}

View File

@ -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}.
* <p>
* 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<IPocketUpgrade> getUpgrade(PocketSide side);
void setUpgrade(PocketSide side, @Nullable UpgradeData<IPocketUpgrade> upgrade);
}

View File

@ -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

View File

@ -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,
}

View File

@ -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<IPocketUpgrade> getUpgradeWithData(ItemStack stack) {
return stack.get(ModRegistry.DataComponents.POCKET_UPGRADE.get());
}
public static void setUpgrade(ItemStack stack, @Nullable UpgradeData<IPocketUpgrade> upgrade) {
stack.set(ModRegistry.DataComponents.POCKET_UPGRADE.get(), upgrade);
public static @Nullable UpgradeData<IPocketUpgrade> 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();
});
}
}

View File

@ -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();

View File

@ -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<IPocketUpgrade> upgrade = null;
UpgradeData<IPocketUpgrade> 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;
}

View File

@ -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<ITurtleUpgrade> 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) {

View File

@ -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()));

View File

@ -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) {

View File

@ -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<IPocketUpgrade>? = null, bottom: UpgradeData<IPocketUpgrade>? = 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),
)
}
}

View File

@ -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 .. " <side>")
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

View File

@ -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 .. " <side>")
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

View File

@ -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());