1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-11-03 15:13:07 +00:00

Merge branch 'mc-1.20.x' into mc-1.21.x

This commit is contained in:
Jonathan Coates
2024-07-31 07:34:49 +01:00
72 changed files with 1603 additions and 557 deletions

View File

@@ -45,6 +45,7 @@ dependencies {
compileOnly(libs.mixin)
compileOnly(libs.mixinExtra)
compileOnly(libs.bundles.externalMods.common)
compileOnly(variantOf(libs.create.forge) { classifier("slim") }) { isTransitive = false }
clientCompileOnly(variantOf(libs.emi) { classifier("api") })
annotationProcessorEverywhere(libs.autoService)

View File

@@ -14,7 +14,6 @@ import java.util.Objects;
/**
* The global factory for {@link ILuaAPIFactory}s.
*
* @see dan200.computercraft.core.ComputerContext.Builder#apiFactories(Collection)
* @see dan200.computercraft.api.ComputerCraftAPI#registerAPIFactory(ILuaAPIFactory)
*/
public final class ApiFactories {

View File

@@ -8,6 +8,7 @@ import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.component.ComputerComponents;
import dan200.computercraft.api.detail.DetailProvider;
import dan200.computercraft.api.detail.VanillaDetailRegistries;
import dan200.computercraft.api.media.IMedia;
@@ -26,6 +27,7 @@ import dan200.computercraft.shared.common.ClearColourRecipe;
import dan200.computercraft.shared.common.ColourableRecipe;
import dan200.computercraft.shared.common.DefaultBundledRedstoneProvider;
import dan200.computercraft.shared.common.HeldItemMenu;
import dan200.computercraft.shared.computer.apis.CommandAPI;
import dan200.computercraft.shared.computer.blocks.CommandComputerBlock;
import dan200.computercraft.shared.computer.blocks.ComputerBlock;
import dan200.computercraft.shared.computer.blocks.ComputerBlockEntity;
@@ -64,6 +66,7 @@ import dan200.computercraft.shared.peripheral.speaker.SpeakerBlockEntity;
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.items.PocketComputerItem;
import dan200.computercraft.shared.pocket.peripherals.PocketModem;
import dan200.computercraft.shared.pocket.peripherals.PocketSpeaker;
@@ -73,8 +76,10 @@ import dan200.computercraft.shared.recipe.function.CopyComponents;
import dan200.computercraft.shared.recipe.function.RecipeFunction;
import dan200.computercraft.shared.turtle.FurnaceRefuelHandler;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.shared.turtle.apis.TurtleAPI;
import dan200.computercraft.shared.turtle.blocks.TurtleBlock;
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
import dan200.computercraft.shared.turtle.core.TurtleAccessInternal;
import dan200.computercraft.shared.turtle.inventory.TurtleMenu;
import dan200.computercraft.shared.turtle.items.TurtleItem;
import dan200.computercraft.shared.turtle.recipes.TurtleUpgradeRecipe;
@@ -82,6 +87,7 @@ import dan200.computercraft.shared.turtle.upgrades.TurtleCraftingTable;
import dan200.computercraft.shared.turtle.upgrades.TurtleModem;
import dan200.computercraft.shared.turtle.upgrades.TurtleSpeaker;
import dan200.computercraft.shared.turtle.upgrades.TurtleTool;
import dan200.computercraft.shared.util.ComponentMap;
import dan200.computercraft.shared.util.DataComponentUtil;
import dan200.computercraft.shared.util.NonNegativeId;
import net.minecraft.commands.CommandSourceStack;
@@ -114,6 +120,7 @@ import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.material.MapColor;
import net.minecraft.world.level.storage.loot.predicates.LootItemConditionType;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
@@ -579,6 +586,22 @@ public final class ModRegistry {
return null;
});
ComputerCraftAPI.registerAPIFactory(computer -> {
var turtle = computer.getComponent(ComputerComponents.TURTLE);
var metrics = Objects.requireNonNull(computer.getComponent(ComponentMap.METRICS));
return turtle == null ? null : new TurtleAPI(metrics, (TurtleAccessInternal) turtle);
});
ComputerCraftAPI.registerAPIFactory(computer -> {
var pocket = computer.getComponent(ComputerComponents.POCKET);
return pocket == null ? null : new PocketAPI(pocket);
});
ComputerCraftAPI.registerAPIFactory(computer -> {
var admin = computer.getComponent(ComputerComponents.ADMIN_COMPUTER);
return admin == null ? null : new CommandAPI(computer, admin);
});
VanillaDetailRegistries.ITEM_STACK.addProvider(ItemDetails::fill);
VanillaDetailRegistries.BLOCK_IN_WORLD.addProvider(BlockDetails::fill);
}

View File

@@ -6,11 +6,11 @@ package dan200.computercraft.shared.computer.apis;
import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.tree.LiteralCommandNode;
import dan200.computercraft.api.component.AdminComputer;
import dan200.computercraft.api.detail.BlockReference;
import dan200.computercraft.api.detail.VanillaDetailRegistries;
import dan200.computercraft.api.lua.*;
import dan200.computercraft.core.Logging;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.util.NBTUtil;
import net.minecraft.commands.CommandSource;
import net.minecraft.commands.CommandSourceStack;
@@ -35,11 +35,13 @@ import java.util.*;
public class CommandAPI implements ILuaAPI {
private static final Logger LOG = LoggerFactory.getLogger(CommandAPI.class);
private final ServerComputer computer;
private final IComputerSystem computer;
private final AdminComputer admin;
private final OutputReceiver receiver = new OutputReceiver();
public CommandAPI(ServerComputer computer) {
public CommandAPI(IComputerSystem computer, AdminComputer admin) {
this.computer = computer;
this.admin = admin;
}
@Override
@@ -295,7 +297,7 @@ public class CommandAPI implements ILuaAPI {
return new CommandSourceStack(receiver,
Vec3.atCenterOf(computer.getPosition()), Vec2.ZERO,
computer.getLevel(), 2,
computer.getLevel(), admin.permissionLevel(),
name, Component.literal(name),
computer.getLevel().getServer(), null
);

View File

@@ -39,7 +39,7 @@ import javax.annotation.Nullable;
import java.util.Objects;
import java.util.UUID;
public abstract class AbstractComputerBlockEntity extends BlockEntity implements IComputerBlockEntity, Nameable, MenuProvider {
public abstract class AbstractComputerBlockEntity extends BlockEntity implements Nameable, MenuProvider {
private static final String NBT_ID = "ComputerId";
private static final String NBT_LABEL = "Label";
private static final String NBT_ON = "On";
@@ -326,17 +326,14 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
for (var dir : DirectionUtil.FACINGS) updateRedstoneTo(dir);
}
@Override
public final int getComputerID() {
return computerID;
}
@Override
public final @Nullable String getLabel() {
return label;
}
@Override
public final void setComputerID(int id) {
if (getLevel().isClientSide || computerID == id) return;
@@ -344,7 +341,6 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
BlockEntityHelpers.updateBlock(this);
}
@Override
public final void setLabel(@Nullable String label) {
if (getLevel().isClientSide || Objects.equals(this.label, label)) return;
@@ -354,7 +350,6 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
BlockEntityHelpers.updateBlock(this);
}
@Override
public ComputerFamily getFamily() {
return family;
}

View File

@@ -12,6 +12,7 @@ import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.util.ComponentMap;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
@@ -34,7 +35,8 @@ public class ComputerBlockEntity extends AbstractComputerBlockEntity {
protected ServerComputer createComputer(int id) {
return new ServerComputer(
(ServerLevel) getLevel(), getBlockPos(), id, label,
getFamily(), Config.computerTermWidth, Config.computerTermHeight
getFamily(), Config.computerTermWidth, Config.computerTermHeight,
ComponentMap.empty()
);
}

View File

@@ -1,22 +0,0 @@
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
//
// SPDX-License-Identifier: LicenseRef-CCPL
package dan200.computercraft.shared.computer.blocks;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import javax.annotation.Nullable;
public interface IComputerBlockEntity {
int getComputerID();
void setComputerID(int id);
@Nullable
String getLabel();
void setLabel(@Nullable String label);
ComputerFamily getFamily();
}

View File

@@ -0,0 +1,101 @@
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.computer.core;
import dan200.computercraft.api.component.ComputerComponent;
import dan200.computercraft.api.lua.IComputerSystem;
import dan200.computercraft.api.lua.ILuaAPIFactory;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.apis.ComputerAccess;
import dan200.computercraft.core.apis.IAPIEnvironment;
import dan200.computercraft.core.computer.ApiLifecycle;
import dan200.computercraft.shared.util.ComponentMap;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import javax.annotation.Nullable;
import java.util.Map;
/**
* Implementation of {@link IComputerSystem} for usage by externally registered APIs.
*
* @see ILuaAPIFactory
*/
final class ComputerSystem extends ComputerAccess implements IComputerSystem, ApiLifecycle {
private final ServerComputer computer;
private final IAPIEnvironment environment;
private final ComponentMap components;
private boolean active;
ComputerSystem(ServerComputer computer, IAPIEnvironment environment, ComponentMap components) {
super(environment);
this.computer = computer;
this.environment = environment;
this.components = components;
}
void activate() {
active = true;
}
@Override
public void shutdown() {
unmountAll();
}
@Override
public String getAttachmentName() {
return "computer";
}
@Override
public ServerLevel getLevel() {
if (!active) {
throw new IllegalStateException("""
Cannot access level when constructing the API. Computers are not guaranteed to stay in one place and
APIs should not rely on the level remaining constant. Instead, call this method when needed.
""".replace('\n', ' ').strip()
);
}
return computer.getLevel();
}
@Override
public BlockPos getPosition() {
if (!active) {
throw new IllegalStateException("""
Cannot access computer position when constructing the API. Computers are not guaranteed to stay in one
place and APIs should not rely on the position remaining constant. Instead, call this method when
needed.
""".replace('\n', ' ').strip()
);
}
return computer.getPosition();
}
@Nullable
@Override
public String getLabel() {
return environment.getLabel();
}
@Override
public Map<String, IPeripheral> getAvailablePeripherals() {
// TODO: Should this return peripherals on the current computer?
return Map.of();
}
@Nullable
@Override
public IPeripheral getAvailablePeripheral(String name) {
return null;
}
@Override
public <T> @Nullable T getComponent(ComputerComponent<T> component) {
return components.get(component);
}
}

View File

@@ -5,15 +5,16 @@
package dan200.computercraft.shared.computer.core;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.component.AdminComputer;
import dan200.computercraft.api.component.ComputerComponents;
import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.peripheral.WorkMonitor;
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.ComputerEnvironment;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.shared.computer.apis.CommandAPI;
import dan200.computercraft.impl.ApiFactories;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
import dan200.computercraft.shared.computer.terminal.TerminalState;
@@ -22,6 +23,7 @@ import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.client.ClientNetworkContext;
import dan200.computercraft.shared.network.client.ComputerTerminalClientMessage;
import dan200.computercraft.shared.network.server.ServerNetworking;
import dan200.computercraft.shared.util.ComponentMap;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.player.Player;
@@ -48,7 +50,8 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
private int ticksSincePing;
public ServerComputer(
ServerLevel level, BlockPos position, int computerID, @Nullable String label, ComputerFamily family, int terminalWidth, int terminalHeight
ServerLevel level, BlockPos position, int computerID, @Nullable String label, ComputerFamily family, int terminalWidth, int terminalHeight,
ComponentMap baseComponents
) {
this.level = level;
this.position = position;
@@ -58,10 +61,27 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
terminal = new NetworkedTerminal(terminalWidth, terminalHeight, family != ComputerFamily.NORMAL, this::markTerminalChanged);
metrics = context.metrics().createMetricObserver(this);
var componentBuilder = ComponentMap.builder();
componentBuilder.add(ComponentMap.METRICS, metrics);
if (family == ComputerFamily.COMMAND) {
componentBuilder.add(ComputerComponents.ADMIN_COMPUTER, new AdminComputer() {
});
}
componentBuilder.add(baseComponents);
var components = componentBuilder.build();
computer = new Computer(context.computerContext(), this, terminal, computerID);
computer.setLabel(label);
if (family == ComputerFamily.COMMAND) addAPI(new CommandAPI(this));
// Load in the externally registered APIs.
for (var factory : ApiFactories.getAll()) {
var system = new ComputerSystem(this, computer.getAPIEnvironment(), components);
var api = factory.create(system);
if (api == null) continue;
system.activate();
computer.addApi(api, system);
}
}
public ComputerFamily getFamily() {
@@ -211,10 +231,6 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
computer.getEnvironment().setBundledRedstoneInput(side, combination);
}
public void addAPI(ILuaAPI api) {
computer.addApi(api);
}
public void setPeripheral(ComputerSide side, @Nullable IPeripheral peripheral) {
computer.getEnvironment().setPeripheral(side, peripheral);
}

View File

@@ -17,7 +17,6 @@ import dan200.computercraft.core.lua.ILuaMachine;
import dan200.computercraft.core.methods.MethodSupplier;
import dan200.computercraft.core.methods.PeripheralMethod;
import dan200.computercraft.impl.AbstractComputerCraftAPI;
import dan200.computercraft.impl.ApiFactories;
import dan200.computercraft.impl.GenericSources;
import dan200.computercraft.shared.CommonHooks;
import dan200.computercraft.shared.computer.metrics.GlobalMetrics;
@@ -74,7 +73,6 @@ public final class ServerContext {
.computerThreads(ConfigSpec.computerThreads.get())
.mainThreadScheduler(mainThread)
.luaFactory(luaMachine)
.apiFactories(ApiFactories.getAll())
.genericMethods(GenericSources.getAllMethods())
.build();
idAssigner = new IDAssigner(storageDir.resolve("ids.json"));

View File

@@ -5,7 +5,7 @@
package dan200.computercraft.shared.data;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.computer.blocks.IComputerBlockEntity;
import dan200.computercraft.shared.computer.blocks.AbstractComputerBlockEntity;
import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.level.storage.loot.parameters.LootContextParam;
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
@@ -27,7 +27,7 @@ public final class HasComputerIdLootCondition implements LootItemCondition {
@Override
public boolean test(LootContext lootContext) {
var tile = lootContext.getParamOrNull(LootContextParams.BLOCK_ENTITY);
return tile instanceof IComputerBlockEntity computer && computer.getComputerID() >= 0;
return tile instanceof AbstractComputerBlockEntity computer && computer.getComputerID() >= 0;
}
@Override

View File

@@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.integration;
import com.simibubi.create.content.contraptions.BlockMovementChecks;
import com.simibubi.create.content.contraptions.BlockMovementChecks.CheckResult;
import dan200.computercraft.shared.peripheral.modem.wired.CableBlock;
import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemBlock;
/**
* Integration with Create.
*/
public final class CreateIntegration {
public static final String ID = "create";
private CreateIntegration() {
}
public static void setup() {
// Allow modems to be treated as "attached" to their adjacent block.
BlockMovementChecks.registerAttachedCheck((state, world, pos, direction) -> {
var block = state.getBlock();
if (block instanceof WirelessModemBlock) {
return CheckResult.of(state.getValue(WirelessModemBlock.FACING) == direction);
} else if (block instanceof CableBlock) {
return CheckResult.of(state.getValue(CableBlock.MODEM).getFacing() == direction);
} else {
return CheckResult.PASS;
}
});
}
}

View File

@@ -8,6 +8,7 @@ import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.TagKey;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
/**
* Tags defined by external mods.
@@ -26,9 +27,9 @@ public final class ExternalModTags {
/**
* Create's "brittle" tag, used to determine if this block needs to be moved before its neighbours.
*
* @see <a href="https://github.com/Creators-of-Create/Create/blob/mc1.20.1/dev/src/main/java/com/simibubi/create/content/contraptions/BlockMovementChecks.java">{@code BlockMovementChecks}</a>
* @see com.simibubi.create.content.contraptions.BlockMovementChecks#isBrittle(BlockState)
*/
public static final TagKey<Block> CREATE_BRITTLE = make("create", "brittle");
public static final TagKey<Block> CREATE_BRITTLE = make(CreateIntegration.ID, "brittle");
private static TagKey<Block> make(String mod, String name) {
return TagKey.create(Registries.BLOCK, ResourceLocation.fromNamespaceAndPath(mod, name));

View File

@@ -43,7 +43,7 @@ public record PocketComputerDataMessage(
this(
computer.getInstanceUUID(),
computer.getState(),
computer.getLight(),
computer.getBrain().getLight(),
sendTerminal ? Optional.of(computer.getTerminalState()) : Optional.empty()
);
}

View File

@@ -276,16 +276,18 @@ public abstract class SpeakerPeripheral implements IPeripheral {
* Attempt to stream some audio data to the speaker.
* <p>
* This accepts a list of audio samples as amplitudes between -128 and 127. These are stored in an internal buffer
* and played back at 48kHz. If this buffer is full, this function will return {@literal false}. You should wait for
* a [`speaker_audio_empty`] event before trying again.
* and played back at 48kHz. If this buffer is full, this function will return {@literal false}. Programs should
* wait for a [`speaker_audio_empty`] event before trying to play audio again.
* <p>
* > [!NOTE]
* > The speaker only buffers a single call to {@link #playAudio} at once. This means if you try to play a small
* > number of samples, you'll have a lot of stutter. You should try to play as many samples in one call as possible
* > (up to 128×1024), as this reduces the chances of audio stuttering or halting, especially when the server or
* > computer is lagging.
* The speaker only buffers a single call to {@link #playAudio} at once. This means if you try to play a small
* number of samples, you'll have a lot of stutter. You should try to play as many samples in one call as possible
* (up to 128×1024), as this reduces the chances of audio stuttering or halting, especially when the server or
* computer is lagging.
* <p>
* [`speaker_audio`] provides a more complete guide to using speakers
* While the speaker accepts 8-bit PCM audio, the audio stream is re-encoded before being played. This means that
* the supplied samples may not be played out exactly.
* <p>
* [`speaker_audio`] provides a more complete guide to using speakers.
*
* @param context The Lua context.
* @param audio The audio data to play.

View File

@@ -6,10 +6,10 @@ 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.PocketServerComputer;
import net.minecraft.core.NonNullList;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
@@ -34,10 +34,10 @@ import java.util.Objects;
* @cc.module pocket
*/
public class PocketAPI implements ILuaAPI {
private final PocketServerComputer computer;
private final IPocketAccess pocket;
public PocketAPI(PocketServerComputer computer) {
this.computer = computer;
public PocketAPI(IPocketAccess pocket) {
this.pocket = pocket;
}
@Override
@@ -56,10 +56,10 @@ public class PocketAPI implements ILuaAPI {
*/
@LuaFunction(mainThread = true)
public final Object[] equipBack() {
var entity = computer.getEntity();
var entity = pocket.getEntity();
if (!(entity instanceof Player player)) return new Object[]{ false, "Cannot find player" };
var inventory = player.getInventory();
var previousUpgrade = computer.getUpgrade();
var previousUpgrade = pocket.getUpgrade();
// 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.
@@ -73,7 +73,7 @@ public class PocketAPI implements ILuaAPI {
if (previousUpgrade != null) storeItem(player, previousUpgrade.getUpgradeItem());
// Set the new upgrade
computer.setUpgrade(newUpgrade);
pocket.setUpgrade(newUpgrade);
return new Object[]{ true };
}
@@ -87,13 +87,13 @@ public class PocketAPI implements ILuaAPI {
*/
@LuaFunction(mainThread = true)
public final Object[] unequipBack() {
var entity = computer.getEntity();
var entity = pocket.getEntity();
if (!(entity instanceof Player player)) return new Object[]{ false, "Cannot find player" };
var previousUpgrade = computer.getUpgrade();
var previousUpgrade = pocket.getUpgrade();
if (previousUpgrade == null) return new Object[]{ false, "Nothing to unequip" };
computer.setUpgrade(null);
pocket.setUpgrade(null);
storeItem(player, previousUpgrade.getUpgradeItem());
@@ -111,7 +111,7 @@ public class PocketAPI implements ILuaAPI {
for (var i = 0; i < inv.size(); i++) {
var invStack = inv.get((i + start) % inv.size());
if (!invStack.isEmpty()) {
var newUpgrade = PocketUpgrades.instance().get(computer.getLevel().registryAccess(), invStack);
var newUpgrade = PocketUpgrades.instance().get(pocket.getLevel().registryAccess(), invStack);
if (newUpgrade != null && !Objects.equals(newUpgrade, previous)) {
// Consume an item from this stack and exit the loop

View File

@@ -0,0 +1,176 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.pocket.core;
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.computer.core.ComputerFamily;
import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
import dan200.computercraft.shared.network.server.ServerNetworking;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.server.level.ServerLevel;
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 javax.annotation.Nullable;
/**
* 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)}).
*/
public final class PocketBrain implements IPocketAccess {
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;
public PocketBrain(PocketHolder holder, int computerID, @Nullable String label, ComputerFamily family, @Nullable UpgradeData<IPocketUpgrade> upgrade) {
this.computer = new PocketServerComputer(this, holder, computerID, label, family);
this.holder = holder;
this.position = holder.pos();
this.upgrade = upgrade;
invalidatePeripheral();
}
/**
* Get the corresponding pocket computer for this brain.
*
* @return The pocket computer.
*/
public PocketServerComputer computer() {
return computer;
}
PocketHolder holder() {
return holder;
}
/**
* Update the position and holder for this computer.
*
* @param newHolder The new holder
*/
public void updateHolder(PocketHolder newHolder) {
position = newHolder.pos();
computer.setPosition(newHolder.level(), newHolder.blockPos());
var oldHolder = this.holder;
if (holder.equals(newHolder)) return;
holder = newHolder;
// If a new player has picked it up then rebroadcast the terminal to them
var oldPlayer = oldHolder instanceof PocketHolder.PlayerHolder p ? p.entity() : null;
if (newHolder instanceof PocketHolder.PlayerHolder player && player.entity() != oldPlayer) {
ServerNetworking.sendToPlayer(new PocketComputerDataMessage(computer, true), player.entity());
}
}
/**
* 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;
stack.set(DataComponents.DYED_COLOR, colour == -1 ? null : new DyedItemColor(colour, false));
PocketComputerItem.setUpgrade(stack, upgrade);
return true;
}
@Override
public ServerLevel getLevel() {
return computer.getLevel();
}
@Override
public Vec3 getPosition() {
// This method can be called from off-thread, and so we must use the cached position rather than rereading
// from the holder.
return position;
}
@Override
public @Nullable Entity getEntity() {
return holder instanceof PocketHolder.EntityHolder entity && holder.isValid(computer) ? entity.entity() : null;
}
@Override
public int getColour() {
return colour;
}
@Override
public void setColour(int colour) {
if (this.colour == colour) return;
dirty = true;
this.colour = 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 upgrade = this.upgrade;
if (upgrade == null) return;
this.upgrade = UpgradeData.of(upgrade.holder(), data);
}
@Override
public void invalidatePeripheral() {
var peripheral = upgrade == null ? null : upgrade.upgrade().createPeripheral(this);
computer.setPeripheral(ComputerSide.BACK, 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) {
this.upgrade = upgrade;
dirty = true;
invalidatePeripheral();
}
}

View File

@@ -0,0 +1,115 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.pocket.core;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.phys.Vec3;
/**
* An object that holds a pocket computer item.
*/
public sealed interface PocketHolder permits PocketHolder.EntityHolder {
/**
* The level this holder is in.
*
* @return The holder's level.
*/
ServerLevel level();
/**
* The position of this holder.
*
* @return The position of this holder.
*/
Vec3 pos();
/**
* The block position of this holder.
*
* @return The position of this holder.
*/
BlockPos blockPos();
/**
* 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);
/**
* Mark the pocket computer item as having changed.
*/
void setChanged();
/**
* An {@link Entity} holding a pocket computer.
*/
sealed interface EntityHolder extends PocketHolder permits PocketHolder.PlayerHolder, PocketHolder.ItemEntityHolder {
/**
* Get the entity holding this pocket computer.
*
* @return The holding entity.
*/
Entity entity();
@Override
default ServerLevel level() {
return (ServerLevel) entity().level();
}
@Override
default Vec3 pos() {
return entity().getEyePosition();
}
@Override
default BlockPos blockPos() {
return entity().blockPosition();
}
}
/**
* A pocket computer in a player's slot.
*
* @param entity The current player.
* @param slot The slot the pocket computer is in.
*/
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()));
}
@Override
public void setChanged() {
entity.getInventory().setChanged();
}
}
/**
* A pocket computer in an {@link ItemEntity}.
*
* @param entity The item entity.
*/
record ItemEntityHolder(ItemEntity entity) implements EntityHolder {
@Override
public boolean isValid(ServerComputer computer) {
return entity().isAlive() && PocketComputerItem.isServerComputer(computer, this.entity().getItem());
}
@Override
public void setChanged() {
entity.setItem(entity.getItem().copy());
}
}
}

View File

@@ -4,11 +4,7 @@
package dan200.computercraft.shared.pocket.core;
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.ModRegistry;
import dan200.computercraft.api.component.ComputerComponents;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.core.ServerComputer;
@@ -17,176 +13,81 @@ import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
import dan200.computercraft.shared.network.client.PocketComputerDeletedClientMessage;
import dan200.computercraft.shared.network.server.ServerNetworking;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import net.minecraft.core.BlockPos;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.server.level.ServerLevel;
import dan200.computercraft.shared.util.ComponentMap;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.DyedItemColor;
import net.minecraft.world.level.ChunkPos;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class PocketServerComputer extends ServerComputer implements IPocketAccess {
private @Nullable IPocketUpgrade upgrade;
private @Nullable Entity entity;
private ItemStack stack = ItemStack.EMPTY;
private int lightColour = -1;
/**
* A {@link ServerComputer}-subclass for {@linkplain PocketComputerItem pocket computers}.
* <p>
* This extends default {@link ServerComputer} behaviour by also syncing pocket computer state to nearby players, and
* syncing the terminal to the current player.
* <p>
* The actual pocket computer state (upgrade, light) is maintained in {@link PocketBrain}. The two classes are tightly
* coupled, and maintain a reference to each other.
*
* @see PocketComputerDataMessage
* @see PocketComputerDeletedClientMessage
*/
public final class PocketServerComputer extends ServerComputer {
private final PocketBrain brain;
// The state the previous tick, used to determine if the state needs to be sent to the client.
private int oldLightColour = -1;
private @Nullable ComputerState oldComputerState;
private final Set<ServerPlayer> tracking = new HashSet<>();
private Set<ServerPlayer> tracking = Set.of();
public PocketServerComputer(ServerLevel world, BlockPos position, int computerID, @Nullable String label, ComputerFamily family) {
super(world, position, computerID, label, family, Config.pocketTermWidth, Config.pocketTermHeight);
PocketServerComputer(PocketBrain brain, PocketHolder holder, int computerID, @Nullable String label, ComputerFamily family) {
super(
holder.level(), holder.blockPos(), computerID, label, family, Config.pocketTermWidth, Config.pocketTermHeight,
ComponentMap.builder().add(ComputerComponents.POCKET, brain).build()
);
this.brain = brain;
}
@Nullable
@Override
public Entity getEntity() {
var entity = this.entity;
if (entity == null || stack.isEmpty() || !entity.isAlive()) return null;
if (entity instanceof Player) {
var inventory = ((Player) entity).getInventory();
return inventory.items.contains(stack) || inventory.offhand.contains(stack) ? entity : null;
} else if (entity instanceof LivingEntity living) {
return living.getMainHandItem() == stack || living.getOffhandItem() == stack ? entity : null;
} else if (entity instanceof ItemEntity itemEntity) {
return itemEntity.getItem() == stack ? entity : null;
} else {
return null;
}
}
@Override
public int getColour() {
return DyedItemColor.getOrDefault(stack, -1);
}
@Override
public void setColour(int colour) {
stack.set(DataComponents.DYED_COLOR, colour == -1 ? null : new DyedItemColor(colour, false));
setItemChanged();
}
@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 = PocketComputerItem.getUpgradeWithData(stack);
return upgrade == null ? DataComponentPatch.EMPTY : upgrade.data();
}
@Override
public void setUpgradeData(DataComponentPatch data) {
var upgrade = PocketComputerItem.getUpgradeWithData(stack);
if (upgrade == null) return;
PocketComputerItem.setUpgrade(stack, new UpgradeData<>(upgrade.holder(), data));
setItemChanged();
}
private void setItemChanged() {
if (entity instanceof Player player) player.getInventory().setChanged();
}
@Override
public void invalidatePeripheral() {
var peripheral = upgrade == null ? null : upgrade.createPeripheral(this);
setPeripheral(ComputerSide.BACK, peripheral);
}
public @Nullable UpgradeData<IPocketUpgrade> getUpgrade() {
return PocketComputerItem.getUpgradeWithData(stack);
}
/**
* 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}.
*/
public void setUpgrade(@Nullable UpgradeData<IPocketUpgrade> upgrade) {
synchronized (this) {
stack.set(ModRegistry.DataComponents.POCKET_UPGRADE.get(), upgrade);
setItemChanged();
this.upgrade = upgrade == null ? null : upgrade.upgrade();
invalidatePeripheral();
}
}
public synchronized void updateValues(@Nullable Entity entity, ItemStack stack, @Nullable IPocketUpgrade upgrade) {
if (entity != null) setPosition((ServerLevel) entity.level(), entity.blockPosition());
// If a new entity has picked it up then rebroadcast the terminal to them
if (entity != this.entity && entity instanceof ServerPlayer) markTerminalChanged();
this.entity = entity;
this.stack = stack;
if (this.upgrade != upgrade) {
this.upgrade = upgrade;
invalidatePeripheral();
}
public PocketBrain getBrain() {
return brain;
}
@Override
protected void tickServer() {
super.tickServer();
// Find any players which have gone missing and remove them from the tracking list.
tracking.removeIf(player -> !player.isAlive() || player.level() != getLevel());
// Get the new set of players tracking the current position.
var newTracking = getLevel().getChunkSource().chunkMap.getPlayers(new ChunkPos(getPosition()), false);
var trackingChanged = tracking.size() != newTracking.size() || !tracking.containsAll(newTracking);
// And now find any new players, add them to the tracking list, and broadcast state where appropriate.
var state = getState();
if (oldLightColour != lightColour || oldComputerState != state) {
var light = brain.getLight();
if (oldLightColour != light || oldComputerState != state) {
oldComputerState = state;
oldLightColour = lightColour;
oldLightColour = light;
// Broadcast the state to all players
tracking.addAll(getLevel().players());
ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, false), tracking);
} else {
ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, false), newTracking);
} else if (trackingChanged) {
// Broadcast the state to new players.
List<ServerPlayer> added = new ArrayList<>();
for (var player : getLevel().players()) {
if (tracking.add(player)) added.add(player);
}
var added = newTracking.stream().filter(x -> !tracking.contains(x)).toList();
if (!added.isEmpty()) {
ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, false), added);
}
}
if (trackingChanged) tracking = Set.copyOf(newTracking);
}
@Override
protected void onTerminalChanged() {
super.onTerminalChanged();
if (entity instanceof ServerPlayer player && entity.isAlive()) {
if (brain.holder() instanceof PocketHolder.PlayerHolder holder && holder.isValid(this)) {
// Broadcast the terminal to the current player.
ServerNetworking.sendToPlayer(new PocketComputerDataMessage(this, true), player);
ServerNetworking.sendToPlayer(new PocketComputerDataMessage(this, true), holder.entity());
}
}

View File

@@ -14,22 +14,26 @@ import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.impl.PocketUpgrades;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.core.ServerComputerRegistry;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.computer.items.ServerComputerReference;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.network.container.ComputerContainerData;
import dan200.computercraft.shared.pocket.apis.PocketAPI;
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.inventory.PocketComputerMenuProvider;
import dan200.computercraft.shared.util.DataComponentUtil;
import dan200.computercraft.shared.util.IDAssigner;
import dan200.computercraft.shared.util.InventoryUtil;
import dan200.computercraft.shared.util.NonNegativeId;
import net.minecraft.ChatFormatting;
import net.minecraft.core.HolderLookup;
import net.minecraft.network.chat.Component;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.Container;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.InteractionResultHolder;
@@ -53,12 +57,33 @@ public class PocketComputerItem extends Item implements IMedia {
this.family = family;
}
private boolean tick(ItemStack stack, Entity entity, PocketServerComputer computer) {
var upgrade = getUpgrade(stack);
/**
* Tick a pocket computer.
*
* @param stack The current pocket computer stack.
* @param holder The entity holding the pocket item.
* @param brain The pocket computer brain.
*/
private void tick(ItemStack stack, PocketHolder holder, PocketBrain brain) {
brain.updateHolder(holder);
computer.updateValues(entity, stack, upgrade);
// Update pocket upgrade
var upgrade = brain.getUpgrade();
if (upgrade != null) upgrade.upgrade().update(brain, brain.computer().getPeripheral(ComputerSide.BACK));
var changed = false;
if (updateItem(stack, brain)) holder.setChanged();
}
/**
* Copy properties from the brain back to the item stack.
*
* @param stack The current pocket computer stack.
* @param brain The current pocket brain.
* @return Whether the item was changed.
*/
private boolean updateItem(ItemStack stack, PocketBrain brain) {
var changed = brain.updateItem(stack);
var computer = brain.computer();
// Sync label
var label = computer.getLabel();
@@ -73,21 +98,20 @@ public class PocketComputerItem extends Item implements IMedia {
stack.set(ModRegistry.DataComponents.ON.get(), on);
}
// Update pocket upgrade
if (upgrade != null) upgrade.update(computer, computer.getPeripheral(ComputerSide.BACK));
return changed;
}
@Override
public void inventoryTick(ItemStack stack, Level world, Entity entity, int slotNum, boolean selected) {
if (world.isClientSide) return;
Container inventory = entity instanceof Player player ? player.getInventory() : null;
var computer = createServerComputer((ServerLevel) world, entity, inventory, stack);
computer.keepAlive();
// This (in vanilla at least) is only called for players. Don't bother to handle other entities.
if (world.isClientSide || !(entity instanceof ServerPlayer player)) return;
var changed = tick(stack, entity, computer);
if (changed && inventory != null) inventory.setChanged();
// If we're in the inventory, create a computer and keep it alive.
var holder = new PocketHolder.PlayerHolder(player, slotNum);
var brain = getOrCreateBrain((ServerLevel) world, holder, stack);
brain.computer().keepAlive();
tick(stack, holder, brain);
}
@ForgeOverride
@@ -95,8 +119,11 @@ public class PocketComputerItem extends Item implements IMedia {
var level = entity.level();
if (level.isClientSide || level.getServer() == null) return false;
// If we're an item entity, tick an already existing computer (as to update the position), but do not keep the
// computer alive.
var computer = getServerComputer(level.getServer(), stack);
if (computer != null && tick(stack, entity, computer)) entity.setItem(stack.copy());
if (computer != null) tick(stack, new PocketHolder.ItemEntityHolder(entity), computer.getBrain());
return false;
}
@@ -104,14 +131,18 @@ public class PocketComputerItem extends Item implements IMedia {
public InteractionResultHolder<ItemStack> use(Level world, Player player, InteractionHand hand) {
var stack = player.getItemInHand(hand);
if (!world.isClientSide) {
var computer = createServerComputer((ServerLevel) world, player, player.getInventory(), stack);
var holder = new PocketHolder.PlayerHolder((ServerPlayer) player, InventoryUtil.getHandSlot(player, hand));
var brain = getOrCreateBrain((ServerLevel) world, holder, stack);
var computer = brain.computer();
computer.turnOn();
var stop = false;
var upgrade = getUpgrade(stack);
if (upgrade != null) {
computer.updateValues(player, stack, upgrade);
stop = upgrade.onRightClick(world, computer, computer.getPeripheral(ComputerSide.BACK));
brain.updateHolder(holder);
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);
}
if (!stop) {
@@ -153,34 +184,42 @@ public class PocketComputerItem extends Item implements IMedia {
}
public PocketServerComputer createServerComputer(ServerLevel level, Entity entity, @Nullable Container inventory, ItemStack stack) {
private PocketBrain getOrCreateBrain(ServerLevel level, PocketHolder holder, ItemStack stack) {
var registry = ServerContext.get(level.getServer()).registry();
var computer = (PocketServerComputer) ServerComputerReference.get(stack, registry);
if (computer == null) {
var computerID = NonNegativeId.getOrCreate(level.getServer(), stack, ModRegistry.DataComponents.COMPUTER_ID.get(), IDAssigner.COMPUTER);
computer = new PocketServerComputer(level, entity.blockPosition(), computerID, getLabel(stack), getFamily());
var instanceId = computer.register();
stack.set(ModRegistry.DataComponents.COMPUTER.get(), new ServerComputerReference(registry.getSessionID(), instanceId));
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.
if (isMarkedOn(stack) && entity instanceof Player) computer.turnOn();
if (inventory != null) inventory.setChanged();
{
var computer = getServerComputer(registry, stack);
if (computer != null) return computer.getBrain();
}
return computer;
var computerID = NonNegativeId.getOrCreate(level.getServer(), stack, ModRegistry.DataComponents.COMPUTER_ID.get(), IDAssigner.COMPUTER);
var brain = new PocketBrain(holder, computerID, getLabel(stack), getFamily(), getUpgradeWithData(stack));
var computer = brain.computer();
stack.set(ModRegistry.DataComponents.COMPUTER.get(), new ServerComputerReference(registry.getSessionID(), computer.register()));
// Only turn on when initially creating the computer, rather than each tick.
if (isMarkedOn(stack) && holder instanceof PocketHolder.PlayerHolder) computer.turnOn();
updateItem(stack, brain);
holder.setChanged();
return brain;
}
public static boolean isServerComputer(ServerComputer computer, ItemStack stack) {
return stack.getItem() instanceof PocketComputerItem
&& getServerComputer(computer.getLevel().getServer(), stack) == computer;
}
@Nullable
public static PocketServerComputer getServerComputer(ServerComputerRegistry registry, ItemStack stack) {
return (PocketServerComputer) ServerComputerReference.get(stack, registry);
}
@Nullable
public static PocketServerComputer getServerComputer(MinecraftServer server, ItemStack stack) {
return (PocketServerComputer) ServerComputerReference.get(stack, ServerContext.get(server).registry());
return getServerComputer(ServerContext.get(server).registry(), stack);
}
public ComputerFamily getFamily() {

View File

@@ -41,8 +41,6 @@ public class PocketModem extends AbstractPocketUpgrade {
public void update(IPocketAccess access, @Nullable IPeripheral peripheral) {
if (!(peripheral instanceof PocketModemPeripheral modem)) return;
modem.setLocation(access);
var state = modem.getModemState();
if (state.pollChanged()) access.setLight(state.isOpen() ? 0xBA0000 : -1);
}

View File

@@ -14,31 +14,21 @@ import net.minecraft.world.phys.Vec3;
import javax.annotation.Nullable;
public class PocketModemPeripheral extends WirelessModemPeripheral {
private @Nullable Level level = null;
private Vec3 position = Vec3.ZERO;
private final IPocketAccess access;
public PocketModemPeripheral(boolean advanced, IPocketAccess access) {
super(new ModemState(), advanced);
setLocation(access);
}
void setLocation(IPocketAccess access) {
var entity = access.getEntity();
if (entity != null) {
level = entity.level();
position = entity.getEyePosition(1);
}
this.access = access;
}
@Override
public Level getLevel() {
if (level == null) throw new IllegalStateException("Using modem before position has been defined");
return level;
return access.getLevel();
}
@Override
public Vec3 getPosition() {
return position;
return access.getPosition();
}
@Override

View File

@@ -8,15 +8,11 @@ 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.world.level.Level;
import net.minecraft.world.phys.Vec3;
import javax.annotation.Nullable;
public class PocketSpeakerPeripheral extends UpgradeSpeakerPeripheral {
private final IPocketAccess access;
private @Nullable Level level;
private Vec3 position = Vec3.ZERO;
public PocketSpeakerPeripheral(IPocketAccess access) {
this.access = access;
@@ -25,7 +21,7 @@ public class PocketSpeakerPeripheral extends UpgradeSpeakerPeripheral {
@Override
public SpeakerPosition getPosition() {
var entity = access.getEntity();
return entity == null ? SpeakerPosition.of(level, position) : SpeakerPosition.of(entity);
return entity == null ? SpeakerPosition.of(access.getLevel(), access.getPosition()) : SpeakerPosition.of(entity);
}
@Override
@@ -35,12 +31,6 @@ public class PocketSpeakerPeripheral extends UpgradeSpeakerPeripheral {
@Override
public void update() {
var entity = access.getEntity();
if (entity != null) {
level = entity.level();
position = entity.position();
}
super.update();
access.setLight(madeSound() ? 0x3320fc : -1);

View File

@@ -11,7 +11,6 @@ import dan200.computercraft.api.turtle.TurtleCommandResult;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.peripheral.generic.methods.AbstractInventoryMethods;
import dan200.computercraft.shared.turtle.core.*;
@@ -68,8 +67,8 @@ public class TurtleAPI implements ILuaAPI {
private final MetricsObserver metrics;
private final TurtleAccessInternal turtle;
public TurtleAPI(ServerComputer computer, TurtleAccessInternal turtle) {
this.metrics = computer.getMetrics();
public TurtleAPI(MetricsObserver metrics, TurtleAccessInternal turtle) {
this.metrics = metrics;
this.turtle = turtle;
}

View File

@@ -5,6 +5,7 @@
package dan200.computercraft.shared.turtle.blocks;
import com.mojang.authlib.GameProfile;
import dan200.computercraft.api.component.ComputerComponents;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
@@ -21,9 +22,9 @@ import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.container.BasicContainer;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.shared.turtle.apis.TurtleAPI;
import dan200.computercraft.shared.turtle.core.TurtleBrain;
import dan200.computercraft.shared.turtle.inventory.TurtleMenu;
import dan200.computercraft.shared.util.ComponentMap;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderLookup;
@@ -81,10 +82,9 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
protected ServerComputer createComputer(int id) {
var computer = new ServerComputer(
(ServerLevel) getLevel(), getBlockPos(), id, label,
getFamily(), Config.turtleTermWidth,
Config.turtleTermHeight
getFamily(), Config.turtleTermWidth, Config.turtleTermHeight,
ComponentMap.builder().add(ComputerComponents.TURTLE, brain).build()
);
computer.addAPI(new TurtleAPI(computer, brain));
brain.setupComputer(computer);
return computer;
}

View File

@@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.util;
import dan200.computercraft.api.component.ComputerComponent;
import dan200.computercraft.core.metrics.MetricsObserver;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
/**
* An immutable map of components.
*/
public final class ComponentMap {
public static final ComputerComponent<MetricsObserver> METRICS = ComputerComponent.create("computercraft", "metrics");
private static final ComponentMap EMPTY = new ComponentMap(Map.of());
private final Map<ComputerComponent<?>, Object> components;
private ComponentMap(Map<ComputerComponent<?>, Object> components) {
this.components = components;
}
@SuppressWarnings("unchecked")
public <T> @Nullable T get(ComputerComponent<T> component) {
return (T) components.get(component);
}
public static ComponentMap empty() {
return EMPTY;
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private final Map<ComputerComponent<?>, Object> components = new HashMap<>();
private Builder() {
}
public <T> Builder add(ComputerComponent<T> component, T value) {
addImpl(component, value);
return this;
}
public Builder add(ComponentMap components) {
for (var component : components.components.entrySet()) addImpl(component.getKey(), component.getValue());
return this;
}
private void addImpl(ComputerComponent<?> component, Object value) {
if (components.containsKey(component)) throw new IllegalArgumentException(component + " is already set");
components.put(component, value);
}
public ComponentMap build() {
return new ComponentMap(Map.copyOf(components));
}
}
}

View File

@@ -8,6 +8,9 @@ import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.Container;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.world.phys.Vec3;
@@ -18,6 +21,20 @@ public final class InventoryUtil {
private InventoryUtil() {
}
/**
* Get the inventory slot for a given hand.
*
* @param player The player to get the slot from.
* @param hand The hand to get.
* @return The current slot.
*/
public static int getHandSlot(Player player, InteractionHand hand) {
return switch (hand) {
case MAIN_HAND -> player.getInventory().selected;
case OFF_HAND -> Inventory.SLOT_OFFHAND;
};
}
public static @Nullable Container getEntityContainer(ServerLevel level, BlockPos pos, Direction side) {
var vecStart = new Vec3(
pos.getX() + 0.5 + 0.6 * side.getStepX(),

View File

@@ -29,6 +29,7 @@ import dan200.computercraft.shared.util.WaterloggableHelpers
import dan200.computercraft.test.core.assertArrayEquals
import dan200.computercraft.test.core.computer.LuaTaskContext
import dan200.computercraft.test.core.computer.getApi
import dan200.computercraft.test.shared.ItemStackMatcher.isStack
import net.minecraft.core.BlockPos
import net.minecraft.core.registries.Registries
import net.minecraft.gametest.framework.GameTest
@@ -44,8 +45,7 @@ import net.minecraft.world.level.block.FenceBlock
import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.world.level.block.state.properties.BlockStateProperties
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.array
import org.hamcrest.Matchers.instanceOf
import org.hamcrest.Matchers.*
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
import java.util.*
@@ -693,6 +693,47 @@ class Turtle_Test {
}
}
/**
* `turtle.craft` works as expected
*/
@GameTest
fun Craft(helper: GameTestHelper) = helper.sequence {
thenOnComputer {
callPeripheral("left", "craft", 1).assertArrayEquals(true)
}
thenExecute {
val turtle = helper.getBlockEntity(BlockPos(2, 2, 2), ModRegistry.BlockEntities.TURTLE_NORMAL.get())
assertThat(
"Inventory is as expected.",
turtle.items,
contains(
isStack(Items.DIAMOND, 1), isStack(Items.DIAMOND, 1), isStack(Items.DIAMOND, 1), isStack(Items.DIAMOND_PICKAXE, 1),
isStack(ItemStack.EMPTY), isStack(Items.STICK, 1), isStack(ItemStack.EMPTY), isStack(ItemStack.EMPTY),
isStack(ItemStack.EMPTY), isStack(Items.STICK, 1), isStack(ItemStack.EMPTY), isStack(ItemStack.EMPTY),
isStack(ItemStack.EMPTY), isStack(ItemStack.EMPTY), isStack(ItemStack.EMPTY), isStack(ItemStack.EMPTY),
),
)
}
}
/**
* `turtle.equipLeft` equips a tool.
*/
@GameTest
fun Equip_tool(helper: GameTestHelper) = helper.sequence {
thenOnComputer {
turtle.equipLeft().await().assertArrayEquals(true)
}
thenExecute {
val turtle = helper.getBlockEntity(BlockPos(2, 2, 2), ModRegistry.BlockEntities.TURTLE_NORMAL.get())
assertEquals(
helper.level.registryAccess().registryOrThrow(ITurtleUpgrade.REGISTRY)
.get(ResourceLocation.withDefaultNamespace("diamond_pickaxe")),
turtle.getUpgrade(TurtleSide.LEFT),
)
}
}
/**
* Render turtles as an item.
*/

View File

@@ -179,7 +179,6 @@ fun <T : Comparable<T>> GameTestHelper.assertBlockHas(pos: BlockPos, property: P
fun GameTestHelper.getContainerAt(pos: BlockPos): Container =
when (val container: BlockEntity = getBlockEntity(pos)) {
is Container -> container
null -> failVerbose("Expected a container at $pos, found nothing", pos)
else -> failVerbose("Expected a container at $pos, found ${getName(container.type)}", pos)
}

View File

@@ -0,0 +1,137 @@
{
DataVersion: 3465,
size: [5, 5, 5],
data: [
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
{pos: [0, 0, 1], state: "minecraft:polished_andesite"},
{pos: [0, 0, 2], state: "minecraft:polished_andesite"},
{pos: [0, 0, 3], state: "minecraft:polished_andesite"},
{pos: [0, 0, 4], state: "minecraft:polished_andesite"},
{pos: [1, 0, 0], state: "minecraft:polished_andesite"},
{pos: [1, 0, 1], state: "minecraft:polished_andesite"},
{pos: [1, 0, 2], state: "minecraft:polished_andesite"},
{pos: [1, 0, 3], state: "minecraft:polished_andesite"},
{pos: [1, 0, 4], state: "minecraft:polished_andesite"},
{pos: [2, 0, 0], state: "minecraft:polished_andesite"},
{pos: [2, 0, 1], state: "minecraft:polished_andesite"},
{pos: [2, 0, 2], state: "minecraft:polished_andesite"},
{pos: [2, 0, 3], state: "minecraft:polished_andesite"},
{pos: [2, 0, 4], state: "minecraft:polished_andesite"},
{pos: [3, 0, 0], state: "minecraft:polished_andesite"},
{pos: [3, 0, 1], state: "minecraft:polished_andesite"},
{pos: [3, 0, 2], state: "minecraft:polished_andesite"},
{pos: [3, 0, 3], state: "minecraft:polished_andesite"},
{pos: [3, 0, 4], state: "minecraft:polished_andesite"},
{pos: [4, 0, 0], state: "minecraft:polished_andesite"},
{pos: [4, 0, 1], state: "minecraft:polished_andesite"},
{pos: [4, 0, 2], state: "minecraft:polished_andesite"},
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
{pos: [0, 1, 0], state: "minecraft:air"},
{pos: [0, 1, 1], state: "minecraft:air"},
{pos: [0, 1, 2], state: "minecraft:air"},
{pos: [0, 1, 3], state: "minecraft:air"},
{pos: [0, 1, 4], state: "minecraft:air"},
{pos: [1, 1, 0], state: "minecraft:air"},
{pos: [1, 1, 1], state: "minecraft:air"},
{pos: [1, 1, 2], state: "minecraft:air"},
{pos: [1, 1, 3], state: "minecraft:air"},
{pos: [1, 1, 4], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "minecraft:air"},
{pos: [2, 1, 2], state: "computercraft:turtle_normal{facing:north,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [{Count: 2b, Slot: 0b, id: "minecraft:diamond"}, {Count: 2b, Slot: 1b, id: "minecraft:diamond"}, {Count: 2b, Slot: 2b, id: "minecraft:diamond"}, {Count: 2b, Slot: 5b, id: "minecraft:stick"}, {Count: 2b, Slot: 9b, id: "minecraft:stick"}], Label: "turtle_test.craft", LeftUpgrade: "minecraft:crafting_table", LeftUpgradeNbt: {}, On: 1b, Slot: 0, id: "computercraft:turtle_normal"}},
{pos: [2, 1, 3], state: "minecraft:air"},
{pos: [2, 1, 4], state: "minecraft:air"},
{pos: [3, 1, 0], state: "minecraft:air"},
{pos: [3, 1, 1], state: "minecraft:air"},
{pos: [3, 1, 2], state: "minecraft:air"},
{pos: [3, 1, 3], state: "minecraft:air"},
{pos: [3, 1, 4], state: "minecraft:air"},
{pos: [4, 1, 0], state: "minecraft:air"},
{pos: [4, 1, 1], state: "minecraft:air"},
{pos: [4, 1, 2], state: "minecraft:air"},
{pos: [4, 1, 3], state: "minecraft:air"},
{pos: [4, 1, 4], state: "minecraft:air"},
{pos: [0, 2, 0], state: "minecraft:air"},
{pos: [0, 2, 1], state: "minecraft:air"},
{pos: [0, 2, 2], state: "minecraft:air"},
{pos: [0, 2, 3], state: "minecraft:air"},
{pos: [0, 2, 4], state: "minecraft:air"},
{pos: [1, 2, 0], state: "minecraft:air"},
{pos: [1, 2, 1], state: "minecraft:air"},
{pos: [1, 2, 2], state: "minecraft:air"},
{pos: [1, 2, 3], state: "minecraft:air"},
{pos: [1, 2, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:air"},
{pos: [2, 2, 2], state: "minecraft:air"},
{pos: [2, 2, 3], state: "minecraft:air"},
{pos: [2, 2, 4], state: "minecraft:air"},
{pos: [3, 2, 0], state: "minecraft:air"},
{pos: [3, 2, 1], state: "minecraft:air"},
{pos: [3, 2, 2], state: "minecraft:air"},
{pos: [3, 2, 3], state: "minecraft:air"},
{pos: [3, 2, 4], state: "minecraft:air"},
{pos: [4, 2, 0], state: "minecraft:air"},
{pos: [4, 2, 1], state: "minecraft:air"},
{pos: [4, 2, 2], state: "minecraft:air"},
{pos: [4, 2, 3], state: "minecraft:air"},
{pos: [4, 2, 4], state: "minecraft:air"},
{pos: [0, 3, 0], state: "minecraft:air"},
{pos: [0, 3, 1], state: "minecraft:air"},
{pos: [0, 3, 2], state: "minecraft:air"},
{pos: [0, 3, 3], state: "minecraft:air"},
{pos: [0, 3, 4], state: "minecraft:air"},
{pos: [1, 3, 0], state: "minecraft:air"},
{pos: [1, 3, 1], state: "minecraft:air"},
{pos: [1, 3, 2], state: "minecraft:air"},
{pos: [1, 3, 3], state: "minecraft:air"},
{pos: [1, 3, 4], state: "minecraft:air"},
{pos: [2, 3, 0], state: "minecraft:air"},
{pos: [2, 3, 1], state: "minecraft:air"},
{pos: [2, 3, 2], state: "minecraft:air"},
{pos: [2, 3, 3], state: "minecraft:air"},
{pos: [2, 3, 4], state: "minecraft:air"},
{pos: [3, 3, 0], state: "minecraft:air"},
{pos: [3, 3, 1], state: "minecraft:air"},
{pos: [3, 3, 2], state: "minecraft:air"},
{pos: [3, 3, 3], state: "minecraft:air"},
{pos: [3, 3, 4], state: "minecraft:air"},
{pos: [4, 3, 0], state: "minecraft:air"},
{pos: [4, 3, 1], state: "minecraft:air"},
{pos: [4, 3, 2], state: "minecraft:air"},
{pos: [4, 3, 3], state: "minecraft:air"},
{pos: [4, 3, 4], state: "minecraft:air"},
{pos: [0, 4, 0], state: "minecraft:air"},
{pos: [0, 4, 1], state: "minecraft:air"},
{pos: [0, 4, 2], state: "minecraft:air"},
{pos: [0, 4, 3], state: "minecraft:air"},
{pos: [0, 4, 4], state: "minecraft:air"},
{pos: [1, 4, 0], state: "minecraft:air"},
{pos: [1, 4, 1], state: "minecraft:air"},
{pos: [1, 4, 2], state: "minecraft:air"},
{pos: [1, 4, 3], state: "minecraft:air"},
{pos: [1, 4, 4], state: "minecraft:air"},
{pos: [2, 4, 0], state: "minecraft:air"},
{pos: [2, 4, 1], state: "minecraft:air"},
{pos: [2, 4, 2], state: "minecraft:air"},
{pos: [2, 4, 3], state: "minecraft:air"},
{pos: [2, 4, 4], state: "minecraft:air"},
{pos: [3, 4, 0], state: "minecraft:air"},
{pos: [3, 4, 1], state: "minecraft:air"},
{pos: [3, 4, 2], state: "minecraft:air"},
{pos: [3, 4, 3], state: "minecraft:air"},
{pos: [3, 4, 4], state: "minecraft:air"},
{pos: [4, 4, 0], state: "minecraft:air"},
{pos: [4, 4, 1], state: "minecraft:air"},
{pos: [4, 4, 2], state: "minecraft:air"},
{pos: [4, 4, 3], state: "minecraft:air"},
{pos: [4, 4, 4], state: "minecraft:air"}
],
entities: [],
palette: [
"minecraft:polished_andesite",
"minecraft:air",
"computercraft:turtle_normal{facing:north,waterlogged:false}"
]
}

View File

@@ -0,0 +1,137 @@
{
DataVersion: 3465,
size: [5, 5, 5],
data: [
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
{pos: [0, 0, 1], state: "minecraft:polished_andesite"},
{pos: [0, 0, 2], state: "minecraft:polished_andesite"},
{pos: [0, 0, 3], state: "minecraft:polished_andesite"},
{pos: [0, 0, 4], state: "minecraft:polished_andesite"},
{pos: [1, 0, 0], state: "minecraft:polished_andesite"},
{pos: [1, 0, 1], state: "minecraft:polished_andesite"},
{pos: [1, 0, 2], state: "minecraft:polished_andesite"},
{pos: [1, 0, 3], state: "minecraft:polished_andesite"},
{pos: [1, 0, 4], state: "minecraft:polished_andesite"},
{pos: [2, 0, 0], state: "minecraft:polished_andesite"},
{pos: [2, 0, 1], state: "minecraft:polished_andesite"},
{pos: [2, 0, 2], state: "minecraft:polished_andesite"},
{pos: [2, 0, 3], state: "minecraft:polished_andesite"},
{pos: [2, 0, 4], state: "minecraft:polished_andesite"},
{pos: [3, 0, 0], state: "minecraft:polished_andesite"},
{pos: [3, 0, 1], state: "minecraft:polished_andesite"},
{pos: [3, 0, 2], state: "minecraft:polished_andesite"},
{pos: [3, 0, 3], state: "minecraft:polished_andesite"},
{pos: [3, 0, 4], state: "minecraft:polished_andesite"},
{pos: [4, 0, 0], state: "minecraft:polished_andesite"},
{pos: [4, 0, 1], state: "minecraft:polished_andesite"},
{pos: [4, 0, 2], state: "minecraft:polished_andesite"},
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
{pos: [0, 1, 0], state: "minecraft:air"},
{pos: [0, 1, 1], state: "minecraft:air"},
{pos: [0, 1, 2], state: "minecraft:air"},
{pos: [0, 1, 3], state: "minecraft:air"},
{pos: [0, 1, 4], state: "minecraft:air"},
{pos: [1, 1, 0], state: "minecraft:air"},
{pos: [1, 1, 1], state: "minecraft:air"},
{pos: [1, 1, 2], state: "minecraft:air"},
{pos: [1, 1, 3], state: "minecraft:air"},
{pos: [1, 1, 4], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "minecraft:air"},
{pos: [2, 1, 2], state: "computercraft:turtle_normal{facing:north,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [{Count: 1b, Slot: 0b, id: "minecraft:diamond_pickaxe"}], Label: "turtle_test.equip_tool", On: 1b, Slot: 0, id: "computercraft:turtle_normal"}},
{pos: [2, 1, 3], state: "minecraft:air"},
{pos: [2, 1, 4], state: "minecraft:air"},
{pos: [3, 1, 0], state: "minecraft:air"},
{pos: [3, 1, 1], state: "minecraft:air"},
{pos: [3, 1, 2], state: "minecraft:air"},
{pos: [3, 1, 3], state: "minecraft:air"},
{pos: [3, 1, 4], state: "minecraft:air"},
{pos: [4, 1, 0], state: "minecraft:air"},
{pos: [4, 1, 1], state: "minecraft:air"},
{pos: [4, 1, 2], state: "minecraft:air"},
{pos: [4, 1, 3], state: "minecraft:air"},
{pos: [4, 1, 4], state: "minecraft:air"},
{pos: [0, 2, 0], state: "minecraft:air"},
{pos: [0, 2, 1], state: "minecraft:air"},
{pos: [0, 2, 2], state: "minecraft:air"},
{pos: [0, 2, 3], state: "minecraft:air"},
{pos: [0, 2, 4], state: "minecraft:air"},
{pos: [1, 2, 0], state: "minecraft:air"},
{pos: [1, 2, 1], state: "minecraft:air"},
{pos: [1, 2, 2], state: "minecraft:air"},
{pos: [1, 2, 3], state: "minecraft:air"},
{pos: [1, 2, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:air"},
{pos: [2, 2, 2], state: "minecraft:air"},
{pos: [2, 2, 3], state: "minecraft:air"},
{pos: [2, 2, 4], state: "minecraft:air"},
{pos: [3, 2, 0], state: "minecraft:air"},
{pos: [3, 2, 1], state: "minecraft:air"},
{pos: [3, 2, 2], state: "minecraft:air"},
{pos: [3, 2, 3], state: "minecraft:air"},
{pos: [3, 2, 4], state: "minecraft:air"},
{pos: [4, 2, 0], state: "minecraft:air"},
{pos: [4, 2, 1], state: "minecraft:air"},
{pos: [4, 2, 2], state: "minecraft:air"},
{pos: [4, 2, 3], state: "minecraft:air"},
{pos: [4, 2, 4], state: "minecraft:air"},
{pos: [0, 3, 0], state: "minecraft:air"},
{pos: [0, 3, 1], state: "minecraft:air"},
{pos: [0, 3, 2], state: "minecraft:air"},
{pos: [0, 3, 3], state: "minecraft:air"},
{pos: [0, 3, 4], state: "minecraft:air"},
{pos: [1, 3, 0], state: "minecraft:air"},
{pos: [1, 3, 1], state: "minecraft:air"},
{pos: [1, 3, 2], state: "minecraft:air"},
{pos: [1, 3, 3], state: "minecraft:air"},
{pos: [1, 3, 4], state: "minecraft:air"},
{pos: [2, 3, 0], state: "minecraft:air"},
{pos: [2, 3, 1], state: "minecraft:air"},
{pos: [2, 3, 2], state: "minecraft:air"},
{pos: [2, 3, 3], state: "minecraft:air"},
{pos: [2, 3, 4], state: "minecraft:air"},
{pos: [3, 3, 0], state: "minecraft:air"},
{pos: [3, 3, 1], state: "minecraft:air"},
{pos: [3, 3, 2], state: "minecraft:air"},
{pos: [3, 3, 3], state: "minecraft:air"},
{pos: [3, 3, 4], state: "minecraft:air"},
{pos: [4, 3, 0], state: "minecraft:air"},
{pos: [4, 3, 1], state: "minecraft:air"},
{pos: [4, 3, 2], state: "minecraft:air"},
{pos: [4, 3, 3], state: "minecraft:air"},
{pos: [4, 3, 4], state: "minecraft:air"},
{pos: [0, 4, 0], state: "minecraft:air"},
{pos: [0, 4, 1], state: "minecraft:air"},
{pos: [0, 4, 2], state: "minecraft:air"},
{pos: [0, 4, 3], state: "minecraft:air"},
{pos: [0, 4, 4], state: "minecraft:air"},
{pos: [1, 4, 0], state: "minecraft:air"},
{pos: [1, 4, 1], state: "minecraft:air"},
{pos: [1, 4, 2], state: "minecraft:air"},
{pos: [1, 4, 3], state: "minecraft:air"},
{pos: [1, 4, 4], state: "minecraft:air"},
{pos: [2, 4, 0], state: "minecraft:air"},
{pos: [2, 4, 1], state: "minecraft:air"},
{pos: [2, 4, 2], state: "minecraft:air"},
{pos: [2, 4, 3], state: "minecraft:air"},
{pos: [2, 4, 4], state: "minecraft:air"},
{pos: [3, 4, 0], state: "minecraft:air"},
{pos: [3, 4, 1], state: "minecraft:air"},
{pos: [3, 4, 2], state: "minecraft:air"},
{pos: [3, 4, 3], state: "minecraft:air"},
{pos: [3, 4, 4], state: "minecraft:air"},
{pos: [4, 4, 0], state: "minecraft:air"},
{pos: [4, 4, 1], state: "minecraft:air"},
{pos: [4, 4, 2], state: "minecraft:air"},
{pos: [4, 4, 3], state: "minecraft:air"},
{pos: [4, 4, 4], state: "minecraft:air"}
],
entities: [],
palette: [
"minecraft:polished_andesite",
"minecraft:air",
"computercraft:turtle_normal{facing:north,waterlogged:false}"
]
}