1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-08-04 12:58:07 +00:00

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

This commit is contained in:
Jonathan Coates 2025-01-31 21:41:10 +00:00
commit 9bb62b047a
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
31 changed files with 285 additions and 304 deletions

View File

@ -30,3 +30,5 @@ body:
Description of the bug. Please include the following: Description of the bug. Please include the following:
- Logs: These will be located in the `logs/` directory of your Minecraft instance. This is always useful, even if it doesn't include errors, so please upload this! - Logs: These will be located in the `logs/` directory of your Minecraft instance. This is always useful, even if it doesn't include errors, so please upload this!
- Detailed reproduction steps: sometimes I can spot a bug pretty easily, but often it's much more obscure. The more information I have to help reproduce it, the quicker it'll get fixed. - Detailed reproduction steps: sometimes I can spot a bug pretty easily, but often it's much more obscure. The more information I have to help reproduce it, the quicker it'll get fixed.
![A gif of burning text reading "Upload your logs!!!"](https://tweaked.cc/images/logs.gif)

View File

@ -10,8 +10,9 @@ kotlin.jvm.target.validation.mode=error
neogradle.subsystems.conventions.runs.enabled=false neogradle.subsystems.conventions.runs.enabled=false
# Mod properties
isUnstable=true isUnstable=true
modVersion=1.114.3 modVersion=1.114.4
# Minecraft properties: We want to configure this here so we can read it in settings.gradle # Minecraft properties: We want to configure this here so we can read it in settings.gradle
mcVersion=1.21.1 mcVersion=1.21.1

View File

@ -31,6 +31,7 @@ import dan200.computercraft.shared.computer.blocks.CommandComputerBlock;
import dan200.computercraft.shared.computer.blocks.ComputerBlock; import dan200.computercraft.shared.computer.blocks.ComputerBlock;
import dan200.computercraft.shared.computer.blocks.ComputerBlockEntity; import dan200.computercraft.shared.computer.blocks.ComputerBlockEntity;
import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory; import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory;
import dan200.computercraft.shared.computer.items.AbstractComputerItem; import dan200.computercraft.shared.computer.items.AbstractComputerItem;
import dan200.computercraft.shared.computer.items.CommandComputerItem; import dan200.computercraft.shared.computer.items.CommandComputerItem;
@ -90,7 +91,6 @@ import dan200.computercraft.shared.turtle.upgrades.TurtleCraftingTable;
import dan200.computercraft.shared.turtle.upgrades.TurtleModem; import dan200.computercraft.shared.turtle.upgrades.TurtleModem;
import dan200.computercraft.shared.turtle.upgrades.TurtleSpeaker; import dan200.computercraft.shared.turtle.upgrades.TurtleSpeaker;
import dan200.computercraft.shared.turtle.upgrades.TurtleTool; import dan200.computercraft.shared.turtle.upgrades.TurtleTool;
import dan200.computercraft.shared.util.ComponentMap;
import dan200.computercraft.shared.util.DataComponentUtil; import dan200.computercraft.shared.util.DataComponentUtil;
import dan200.computercraft.shared.util.NonNegativeId; import dan200.computercraft.shared.util.NonNegativeId;
import dan200.computercraft.shared.util.StorageCapacity; import dan200.computercraft.shared.util.StorageCapacity;
@ -615,7 +615,7 @@ public final class ModRegistry {
ComputerCraftAPI.registerAPIFactory(computer -> { ComputerCraftAPI.registerAPIFactory(computer -> {
var turtle = computer.getComponent(ComputerComponents.TURTLE); var turtle = computer.getComponent(ComputerComponents.TURTLE);
var metrics = Objects.requireNonNull(computer.getComponent(ComponentMap.METRICS)); var metrics = Objects.requireNonNull(computer.getComponent(ServerComputer.METRICS));
return turtle == null ? null : new TurtleAPI(metrics, (TurtleAccessInternal) turtle); return turtle == null ? null : new TurtleAPI(metrics, (TurtleAccessInternal) turtle);
}); });

View File

@ -24,15 +24,13 @@ import dan200.computercraft.shared.computer.metrics.basic.AggregatedMetric;
import dan200.computercraft.shared.computer.metrics.basic.BasicComputerMetricsObserver; import dan200.computercraft.shared.computer.metrics.basic.BasicComputerMetricsObserver;
import dan200.computercraft.shared.computer.metrics.basic.ComputerMetrics; import dan200.computercraft.shared.computer.metrics.basic.ComputerMetrics;
import dan200.computercraft.shared.network.container.ComputerContainerData; import dan200.computercraft.shared.network.container.ComputerContainerData;
import dan200.computercraft.shared.platform.PlatformHelper;
import net.minecraft.ChatFormatting; import net.minecraft.ChatFormatting;
import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.CommandSourceStack;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.RelativeMovement; import net.minecraft.world.entity.RelativeMovement;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
@ -259,18 +257,11 @@ public final class CommandComputerCraft {
* @return The constant {@code 1}. * @return The constant {@code 1}.
*/ */
private static int view(CommandSourceStack source, ServerComputer computer) throws CommandSyntaxException { private static int view(CommandSourceStack source, ServerComputer computer) throws CommandSyntaxException {
var player = source.getPlayerOrException(); PlatformHelper.get().openMenu(
new ComputerContainerData(computer, new ItemStack(ModRegistry.Items.COMPUTER_NORMAL.get())).open(player, new MenuProvider() { source.getPlayerOrException(), Component.translatable("gui.computercraft.view_computer"),
@Override (id, player, entity) -> new ComputerMenuWithoutInventory(ModRegistry.Menus.COMPUTER.get(), id, player, p -> true, computer),
public Component getDisplayName() { new ComputerContainerData(computer, new ItemStack(ModRegistry.Items.COMPUTER_NORMAL.get()))
return Component.translatable("gui.computercraft.view_computer"); );
}
@Override
public AbstractContainerMenu createMenu(int id, Inventory player, Player entity) {
return new ComputerMenuWithoutInventory(ModRegistry.Menus.COMPUTER.get(), id, player, p -> true, computer);
}
});
return 1; return 1;
} }

View File

@ -7,6 +7,7 @@ package dan200.computercraft.shared.computer.blocks;
import dan200.computercraft.annotations.ForgeOverride; import dan200.computercraft.annotations.ForgeOverride;
import dan200.computercraft.shared.common.IBundledRedstoneBlock; import dan200.computercraft.shared.common.IBundledRedstoneBlock;
import dan200.computercraft.shared.network.container.ComputerContainerData; import dan200.computercraft.shared.network.container.ComputerContainerData;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.platform.RegistryEntry; import dan200.computercraft.shared.platform.RegistryEntry;
import dan200.computercraft.shared.util.BlockEntityHelpers; import dan200.computercraft.shared.util.BlockEntityHelpers;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
@ -121,7 +122,7 @@ public abstract class AbstractComputerBlock<T extends AbstractComputerBlockEntit
var serverComputer = computer.createServerComputer(); var serverComputer = computer.createServerComputer();
serverComputer.turnOn(); serverComputer.turnOn();
new ComputerContainerData(serverComputer, getItem(computer)).open(player, computer); PlatformHelper.get().openMenu(player, computer.getName(), computer, new ComputerContainerData(serverComputer, getItem(computer)));
} }
return InteractionResult.sidedSuccess(level.isClientSide); return InteractionResult.sidedSuccess(level.isClientSide);
} }

View File

@ -28,9 +28,10 @@ import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.world.Container; import net.minecraft.world.Container;
import net.minecraft.world.LockCode; import net.minecraft.world.LockCode;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.Nameable; import net.minecraft.world.Nameable;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.MenuConstructor;
import net.minecraft.world.level.block.GameMasterBlock;
import net.minecraft.world.level.block.entity.BaseContainerBlockEntity; import net.minecraft.world.level.block.entity.BaseContainerBlockEntity;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.entity.BlockEntityType;
@ -40,7 +41,7 @@ import javax.annotation.Nullable;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
public abstract class AbstractComputerBlockEntity extends BlockEntity implements Nameable, MenuProvider { public abstract class AbstractComputerBlockEntity extends BlockEntity implements Nameable, MenuConstructor {
private static final String NBT_ID = "ComputerId"; private static final String NBT_ID = "ComputerId";
private static final String NBT_LABEL = "Label"; private static final String NBT_LABEL = "Label";
private static final String NBT_ON = "On"; private static final String NBT_ON = "On";
@ -344,6 +345,10 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
return label; return label;
} }
public final boolean isAdminOnly() {
return getBlockState().getBlock() instanceof GameMasterBlock;
}
public final void setComputerID(int id) { public final void setComputerID(int id) {
if (getLevel().isClientSide || computerID == id) return; if (getLevel().isClientSide || computerID == id) return;
@ -454,4 +459,9 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
public Component getDisplayName() { public Component getDisplayName() {
return Nameable.super.getDisplayName(); return Nameable.super.getDisplayName();
} }
@Override
public boolean onlyOpCanSetNbt() {
return isAdminOnly();
}
} }

View File

@ -11,7 +11,6 @@ import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.apis.ComputerAccess; import dan200.computercraft.core.apis.ComputerAccess;
import dan200.computercraft.core.apis.IAPIEnvironment; import dan200.computercraft.core.apis.IAPIEnvironment;
import dan200.computercraft.core.computer.ApiLifecycle; import dan200.computercraft.core.computer.ApiLifecycle;
import dan200.computercraft.shared.util.ComponentMap;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
@ -26,11 +25,11 @@ import java.util.Map;
final class ComputerSystem extends ComputerAccess implements IComputerSystem, ApiLifecycle { final class ComputerSystem extends ComputerAccess implements IComputerSystem, ApiLifecycle {
private final ServerComputer computer; private final ServerComputer computer;
private final IAPIEnvironment environment; private final IAPIEnvironment environment;
private final ComponentMap components; private final Map<ComputerComponent<?>, Object> components;
private boolean active; private boolean active;
ComputerSystem(ServerComputer computer, IAPIEnvironment environment, ComponentMap components) { ComputerSystem(ServerComputer computer, IAPIEnvironment environment, Map<ComputerComponent<?>, Object> components) {
super(environment); super(environment);
this.computer = computer; this.computer = computer;
this.environment = environment; this.environment = environment;
@ -95,7 +94,8 @@ final class ComputerSystem extends ComputerAccess implements IComputerSystem, Ap
} }
@Override @Override
@SuppressWarnings("unchecked")
public <T> @Nullable T getComponent(ComputerComponent<T> component) { public <T> @Nullable T getComponent(ComputerComponent<T> component) {
return components.get(component); return (T) components.get(component);
} }
} }

View File

@ -26,18 +26,21 @@ import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.client.ClientNetworkContext; import dan200.computercraft.shared.network.client.ClientNetworkContext;
import dan200.computercraft.shared.network.client.ComputerTerminalClientMessage; import dan200.computercraft.shared.network.client.ComputerTerminalClientMessage;
import dan200.computercraft.shared.network.server.ServerNetworking; import dan200.computercraft.shared.network.server.ServerNetworking;
import dan200.computercraft.shared.util.ComponentMap;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.AbstractContainerMenu;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function; import java.util.function.Function;
public class ServerComputer implements ComputerEnvironment, ComputerEvents.Receiver { public class ServerComputer implements ComputerEnvironment, ComputerEvents.Receiver {
public static final ComputerComponent<MetricsObserver> METRICS = ComputerComponent.create("computercraft", "metrics");
private final UUID instanceUUID = UUID.randomUUID(); private final UUID instanceUUID = UUID.randomUUID();
private ServerLevel level; private ServerLevel level;
@ -65,14 +68,12 @@ public class ServerComputer implements ComputerEnvironment, ComputerEvents.Recei
storageCapacity = properties.storageCapacity; storageCapacity = properties.storageCapacity;
var componentBuilder = ComponentMap.builder(); properties.addComponent(METRICS, metrics);
componentBuilder.add(ComponentMap.METRICS, metrics);
if (family == ComputerFamily.COMMAND) { if (family == ComputerFamily.COMMAND) {
componentBuilder.add(ComputerComponents.ADMIN_COMPUTER, new AdminComputer() { properties.addComponent(ComputerComponents.ADMIN_COMPUTER, new AdminComputer() {
}); });
} }
componentBuilder.add(properties.components.build()); var components = Map.copyOf(properties.components);
var components = componentBuilder.build();
computer = new Computer(context.computerContext(), this, terminal, properties.computerID); computer = new Computer(context.computerContext(), this, terminal, properties.computerID);
computer.setLabel(properties.label); computer.setLabel(properties.label);
@ -282,7 +283,7 @@ public class ServerComputer implements ComputerEnvironment, ComputerEvents.Recei
private int terminalWidth = Config.DEFAULT_COMPUTER_TERM_WIDTH; private int terminalWidth = Config.DEFAULT_COMPUTER_TERM_WIDTH;
private int terminalHeight = Config.DEFAULT_COMPUTER_TERM_HEIGHT; private int terminalHeight = Config.DEFAULT_COMPUTER_TERM_HEIGHT;
private long storageCapacity = -1; private long storageCapacity = -1;
private final ComponentMap.Builder components = ComponentMap.builder(); private final Map<ComputerComponent<?>, Object> components = new HashMap<>();
private Properties(int computerID, ComputerFamily family) { private Properties(int computerID, ComputerFamily family) {
this.computerID = computerID; this.computerID = computerID;
@ -313,7 +314,8 @@ public class ServerComputer implements ComputerEnvironment, ComputerEvents.Recei
} }
public <T> Properties addComponent(ComputerComponent<T> component, T value) { public <T> Properties addComponent(ComputerComponent<T> component, T value) {
components.add(component, value); if (components.containsKey(component)) throw new IllegalArgumentException(component + " is already set");
components.put(component, value);
return this; return this;
} }
} }

View File

@ -8,9 +8,7 @@ import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.platform.PlatformHelper; import dan200.computercraft.shared.platform.PlatformHelper;
import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec; import net.minecraft.network.codec.StreamCodec;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuType; import net.minecraft.world.inventory.MenuType;
@ -20,16 +18,6 @@ import net.minecraft.world.inventory.MenuType;
public interface ContainerData { public interface ContainerData {
void toBytes(RegistryFriendlyByteBuf buf); void toBytes(RegistryFriendlyByteBuf buf);
/**
* Open a menu for a specific player using this data.
*
* @param player The player to open the menu for.
* @param menu The underlying menu provider.
*/
default void open(Player player, MenuProvider menu) {
PlatformHelper.get().openMenu(player, menu, this);
}
static <C extends AbstractContainerMenu, T extends ContainerData> MenuType<C> toType(StreamCodec<RegistryFriendlyByteBuf, T> codec, Factory<C, T> factory) { static <C extends AbstractContainerMenu, T extends ContainerData> MenuType<C> toType(StreamCodec<RegistryFriendlyByteBuf, T> codec, Factory<C, T> factory) {
return PlatformHelper.get().createMenuType(codec, factory); return PlatformHelper.get().createMenuType(codec, factory);
} }

View File

@ -18,6 +18,7 @@ import net.minecraft.core.Direction;
import net.minecraft.core.HolderLookup; import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
@ -237,7 +238,7 @@ public class MonitorBlockEntity extends BlockEntity {
getLevel().setBlock(getBlockPos(), getBlockState() getLevel().setBlock(getBlockPos(), getBlockState()
.setValue(MonitorBlock.STATE, MonitorEdgeState.fromConnections( .setValue(MonitorBlock.STATE, MonitorEdgeState.fromConnections(
yIndex < height - 1, yIndex > 0, yIndex < height - 1, yIndex > 0,
xIndex > 0, xIndex < width - 1)), 2); xIndex > 0, xIndex < width - 1)), Block.UPDATE_CLIENTS);
} }
// region Sizing and placement stuff // region Sizing and placement stuff

View File

@ -17,6 +17,7 @@ import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.core.Registry; import net.minecraft.core.Registry;
import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.network.codec.StreamCodec; import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
@ -24,10 +25,14 @@ import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ServerPlayerGameMode; import net.minecraft.server.level.ServerPlayerGameMode;
import net.minecraft.server.network.ServerGamePacketListenerImpl; import net.minecraft.server.network.ServerGamePacketListenerImpl;
import net.minecraft.tags.TagKey; import net.minecraft.tags.TagKey;
import net.minecraft.world.*; import net.minecraft.world.Container;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.WorldlyContainer;
import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuConstructor;
import net.minecraft.world.inventory.MenuType; import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.DyeColor; import net.minecraft.world.item.DyeColor;
@ -109,10 +114,11 @@ public interface PlatformHelper {
* Open a container using a specific {@link ContainerData}. * Open a container using a specific {@link ContainerData}.
* *
* @param player The player to open the menu for. * @param player The player to open the menu for.
* @param owner The underlying menu provider. * @param title The title for this menu.
* @param menu The menu data. * @param menu The underlying menu constructor.
* @param data The menu data.
*/ */
void openMenu(Player player, MenuProvider owner, ContainerData menu); void openMenu(Player player, Component title, MenuConstructor menu, ContainerData data);
/** /**
* Invalidate components on a block enitty. * Invalidate components on a block enitty.

View File

@ -1,56 +0,0 @@
// SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.pocket.inventory;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
import static dan200.computercraft.core.util.Nullability.assertNonNull;
public class PocketComputerMenuProvider implements MenuProvider {
private final ServerComputer computer;
private final Component name;
private final PocketComputerItem item;
private final InteractionHand hand;
private final boolean isTypingOnly;
public PocketComputerMenuProvider(ServerComputer computer, ItemStack stack, PocketComputerItem item, InteractionHand hand, boolean isTypingOnly) {
this.computer = computer;
name = stack.getHoverName();
this.item = item;
this.hand = hand;
this.isTypingOnly = isTypingOnly;
}
@Override
public Component getDisplayName() {
return name;
}
@Nullable
@Override
public AbstractContainerMenu createMenu(int id, Inventory inventory, Player entity) {
return new ComputerMenuWithoutInventory(
isTypingOnly ? ModRegistry.Menus.POCKET_COMPUTER_NO_TERM.get() : ModRegistry.Menus.COMPUTER.get(), id, inventory,
p -> {
var stack = p.getItemInHand(hand);
return stack.getItem() == item && PocketComputerItem.getServerComputer(assertNonNull(entity.level().getServer()), stack) == computer;
},
computer
);
}
}

View File

@ -17,13 +17,14 @@ import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer; import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.core.ServerComputerRegistry; import dan200.computercraft.shared.computer.core.ServerComputerRegistry;
import dan200.computercraft.shared.computer.core.ServerContext; import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory;
import dan200.computercraft.shared.computer.items.ServerComputerReference; import dan200.computercraft.shared.computer.items.ServerComputerReference;
import dan200.computercraft.shared.config.ConfigSpec; import dan200.computercraft.shared.config.ConfigSpec;
import dan200.computercraft.shared.network.container.ComputerContainerData; import dan200.computercraft.shared.network.container.ComputerContainerData;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.pocket.core.PocketBrain; import dan200.computercraft.shared.pocket.core.PocketBrain;
import dan200.computercraft.shared.pocket.core.PocketHolder; import dan200.computercraft.shared.pocket.core.PocketHolder;
import dan200.computercraft.shared.pocket.core.PocketServerComputer; import dan200.computercraft.shared.pocket.core.PocketServerComputer;
import dan200.computercraft.shared.pocket.inventory.PocketComputerMenuProvider;
import dan200.computercraft.shared.util.*; import dan200.computercraft.shared.util.*;
import net.minecraft.ChatFormatting; import net.minecraft.ChatFormatting;
import net.minecraft.core.HolderLookup; import net.minecraft.core.HolderLookup;
@ -147,8 +148,13 @@ public class PocketComputerItem extends Item implements IMedia {
} }
if (!stop) { if (!stop) {
var isTypingOnly = hand == InteractionHand.OFF_HAND; PlatformHelper.get().openMenu(
new ComputerContainerData(computer, stack).open(player, new PocketComputerMenuProvider(computer, stack, this, hand, isTypingOnly)); player, stack.getHoverName(),
(id, inventory, entity) -> new ComputerMenuWithoutInventory(
hand == InteractionHand.OFF_HAND ? ModRegistry.Menus.POCKET_COMPUTER_NO_TERM.get() : ModRegistry.Menus.COMPUTER.get(),
id, inventory, p -> isServerComputer(computer, p.getItemInHand(hand)), computer
),
new ComputerContainerData(computer, stack));
} }
} }
return new InteractionResultHolder<>(InteractionResult.sidedSuccess(world.isClientSide), stack); return new InteractionResultHolder<>(InteractionResult.sidedSuccess(world.isClientSide), stack);

View File

@ -40,6 +40,7 @@ import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.MoverType; import net.minecraft.world.entity.MoverType;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.material.PushReaction; import net.minecraft.world.level.material.PushReaction;
import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
@ -256,7 +257,7 @@ public class TurtleBrain implements TurtleAccessInternal {
try { try {
// We use Block.UPDATE_CLIENTS here to ensure that neighbour updates caused in Block.updateNeighbourShapes // We use Block.UPDATE_CLIENTS here to ensure that neighbour updates caused in Block.updateNeighbourShapes
// are sent to the client. We want to avoid doing a full block update until the turtle state is copied over. // are sent to the client. We want to avoid doing a full block update until the turtle state is copied over.
if (world.setBlock(pos, newState, 2)) { if (world.setBlock(pos, newState, Block.UPDATE_CLIENTS)) {
var block = world.getBlockState(pos).getBlock(); var block = world.getBlockState(pos).getBlock();
if (block == oldBlock.getBlock()) { if (block == oldBlock.getBlock()) {
var newTile = world.getBlockEntity(pos); var newTile = world.getBlockEntity(pos);
@ -669,7 +670,7 @@ public class TurtleBrain implements TurtleAccessInternal {
} }
var aabb = new AABB(minX, minY, minZ, maxX, maxY, maxZ); var aabb = new AABB(minX, minY, minZ, maxX, maxY, maxZ);
var list = world.getEntitiesOfClass(Entity.class, aabb, TurtleBrain::canPush); var list = world.getEntities((Entity) null, aabb, TurtleBrain::canPush);
if (!list.isEmpty()) { if (!list.isEmpty()) {
double pushStep = 1.0f / ANIM_DURATION; double pushStep = 1.0f / ANIM_DURATION;
var pushStepX = moveDir.getStepX() * pushStep; var pushStepX = moveDir.getStepX() * pushStep;

View File

@ -22,11 +22,10 @@ public class TurtleDetectCommand implements TurtleCommand {
var direction = this.direction.toWorldDir(turtle); var direction = this.direction.toWorldDir(turtle);
// Check if thing in front is air or not // Check if thing in front is air or not
var world = turtle.getLevel(); var level = turtle.getLevel();
var oldPosition = turtle.getPosition(); var pos = turtle.getPosition().relative(direction);
var newPosition = oldPosition.relative(direction);
return !WorldUtil.isLiquidBlock(world, newPosition) && !world.isEmptyBlock(newPosition) return !WorldUtil.isEmptyBlock(level.getBlockState(pos))
? TurtleCommandResult.success() ? TurtleCommandResult.success()
: TurtleCommandResult.failure(); : TurtleCommandResult.failure();
} }

View File

@ -13,9 +13,9 @@ import dan200.computercraft.shared.util.WorldUtil;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.material.PushReaction;
import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
public class TurtleMoveCommand implements TurtleCommand { public class TurtleMoveCommand implements TurtleCommand {
private final MoveDirection direction; private final MoveDirection direction;
@ -30,57 +30,32 @@ public class TurtleMoveCommand implements TurtleCommand {
var direction = this.direction.toWorldDir(turtle); var direction = this.direction.toWorldDir(turtle);
// Check if we can move // Check if we can move
var oldWorld = (ServerLevel) turtle.getLevel(); var level = (ServerLevel) turtle.getLevel();
var oldPosition = turtle.getPosition(); var oldPosition = turtle.getPosition();
var newPosition = oldPosition.relative(direction); var newPosition = oldPosition.relative(direction);
var turtlePlayer = TurtlePlayer.getWithPosition(turtle, oldPosition, direction); var turtlePlayer = TurtlePlayer.getWithPosition(turtle, oldPosition, direction);
var canEnterResult = canEnter(turtlePlayer, oldWorld, newPosition); var canEnterResult = canEnter(turtlePlayer, level, newPosition);
if (!canEnterResult.isSuccess()) { if (!canEnterResult.isSuccess()) return canEnterResult;
return canEnterResult;
}
// Check existing block is air or replaceable // Check existing block is air or replaceable.
var state = oldWorld.getBlockState(newPosition); var existingState = level.getBlockState(newPosition);
if (!oldWorld.isEmptyBlock(newPosition) && if (!(WorldUtil.isEmptyBlock(existingState) || existingState.canBeReplaced())) {
!WorldUtil.isLiquidBlock(oldWorld, newPosition) &&
!state.canBeReplaced()) {
return TurtleCommandResult.failure("Movement obstructed"); return TurtleCommandResult.failure("Movement obstructed");
} }
// Check there isn't anything in the way // Check there isn't an entity in the way.
var collision = state.getCollisionShape(oldWorld, oldPosition).move( var turtleShape = level.getBlockState(oldPosition).getCollisionShape(level, oldPosition)
newPosition.getX(), .move(newPosition.getX(), newPosition.getY(), newPosition.getZ());
newPosition.getY(), if (!level.isUnobstructed(null, turtleShape) && !canPushEntities(level, turtleShape.bounds())) {
newPosition.getZ() return TurtleCommandResult.failure("Movement obstructed");
);
if (!oldWorld.isUnobstructed(null, collision)) {
if (!Config.turtlesCanPush || this.direction == MoveDirection.UP || this.direction == MoveDirection.DOWN) {
return TurtleCommandResult.failure("Movement obstructed");
}
// Check there is space for all the pushable entities to be pushed
var list = oldWorld.getEntitiesOfClass(Entity.class, getBox(collision), x -> x != null && x.isAlive() && x.blocksBuilding);
for (var entity : list) {
var pushedBB = entity.getBoundingBox().move(
direction.getStepX(),
direction.getStepY(),
direction.getStepZ()
);
if (!oldWorld.isUnobstructed(null, Shapes.create(pushedBB))) {
return TurtleCommandResult.failure("Movement obstructed");
}
}
} }
// Check fuel level // Check fuel level
if (turtle.isFuelNeeded() && turtle.getFuelLevel() < 1) { if (turtle.isFuelNeeded() && turtle.getFuelLevel() < 1) return TurtleCommandResult.failure("Out of fuel");
return TurtleCommandResult.failure("Out of fuel");
}
// Move // Move
if (!turtle.teleportTo(oldWorld, newPosition)) return TurtleCommandResult.failure("Movement failed"); if (!turtle.teleportTo(level, newPosition)) return TurtleCommandResult.failure("Movement failed");
// Consume fuel // Consume fuel
turtle.consumeFuel(1); turtle.consumeFuel(1);
@ -114,9 +89,20 @@ public class TurtleMoveCommand implements TurtleCommand {
return TurtleCommandResult.success(); return TurtleCommandResult.success();
} }
private static AABB getBox(VoxelShape shape) {
return shape.isEmpty() ? EMPTY_BOX : shape.bounds();
}
private static final AABB EMPTY_BOX = new AABB(0, 0, 0, 0, 0, 0); /**
* Determine if all entities in the given bounds can be pushed by the turtle.
*
* @param level The current level.
* @param bounds The bounding box.
* @return Whether all entities can be pushed.
*/
private boolean canPushEntities(Level level, AABB bounds) {
if (!Config.turtlesCanPush) return false;
// Check there is space for all the pushable entities to be pushed
return level.getEntities((Entity) null, bounds, e -> e.isAlive()
&& !e.isSpectator() && e.blocksBuilding && e.getPistonPushReaction() == PushReaction.IGNORE
).isEmpty();
}
} }

View File

@ -1,66 +0,0 @@
// 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

@ -28,9 +28,14 @@ import net.minecraft.world.phys.shapes.VoxelShape;
import javax.annotation.Nullable; import javax.annotation.Nullable;
public final class WorldUtil { public final class WorldUtil {
@SuppressWarnings("deprecation")
public static boolean isLiquidBlock(Level world, BlockPos pos) { public static boolean isLiquidBlock(Level world, BlockPos pos) {
if (!world.isInWorldBounds(pos)) return false; return world.isInWorldBounds(pos) && world.getBlockState(pos).liquid();
return world.getBlockState(pos).liquid(); }
@SuppressWarnings("deprecation")
public static boolean isEmptyBlock(BlockState state) {
return state.isAir() || state.liquid();
} }
public static boolean isVecInside(VoxelShape shape, Vec3 vec) { public static boolean isVecInside(VoxelShape shape, Vec3 vec) {

View File

@ -18,6 +18,7 @@ import net.minecraft.commands.synchronization.ArgumentTypeInfo;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.core.Registry; import net.minecraft.core.Registry;
import net.minecraft.network.chat.Component;
import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec; import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceKey;
@ -26,10 +27,10 @@ import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.TagKey; import net.minecraft.tags.TagKey;
import net.minecraft.world.Container; import net.minecraft.world.Container;
import net.minecraft.world.InteractionResult; import net.minecraft.world.InteractionResult;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuConstructor;
import net.minecraft.world.inventory.MenuType; import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
@ -77,7 +78,7 @@ public class TestPlatformHelper extends AbstractComputerCraftAPI implements Plat
} }
@Override @Override
public void openMenu(Player player, MenuProvider owner, ContainerData menu) { public void openMenu(Player player, Component title, MenuConstructor menu, ContainerData data) {
throw new UnsupportedOperationException("Cannot open menu inside tests"); throw new UnsupportedOperationException("Cannot open menu inside tests");
} }

View File

@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.gametest
import dan200.computercraft.gametest.api.assertNoPeripheral
import dan200.computercraft.gametest.api.assertPeripheral
import dan200.computercraft.gametest.api.immediate
import dan200.computercraft.shared.ModRegistry
import dan200.computercraft.shared.platform.ComponentAccess
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.gametest.framework.GameTest
import net.minecraft.gametest.framework.GameTestHelper
import java.util.*
/**
* Checks that we expose [ComponentAccess] for various blocks/block entities
*/
class Component_Test {
@GameTest(template = "default")
fun Peripheral(context: GameTestHelper) = context.immediate {
val pos = BlockPos(2, 2, 2)
// We fetch peripherals from the NORTH, as that is the default direction for modems. This is a bit of a hack,
// but avoids having to override the block state.
val side = Direction.NORTH
for ((block, type) in mapOf(
// Computers
ModRegistry.Blocks.COMPUTER_NORMAL to Optional.of("computer"),
ModRegistry.Blocks.COMPUTER_ADVANCED to Optional.of("computer"),
ModRegistry.Blocks.COMPUTER_COMMAND to Optional.empty(),
// Turtles
ModRegistry.Blocks.TURTLE_NORMAL to Optional.of("turtle"),
ModRegistry.Blocks.TURTLE_ADVANCED to Optional.of("turtle"),
// Peripherals
ModRegistry.Blocks.SPEAKER to Optional.of("speaker"),
ModRegistry.Blocks.DISK_DRIVE to Optional.of("drive"),
ModRegistry.Blocks.PRINTER to Optional.of("printer"),
ModRegistry.Blocks.MONITOR_NORMAL to Optional.of("monitor"),
ModRegistry.Blocks.MONITOR_ADVANCED to Optional.of("monitor"),
ModRegistry.Blocks.WIRELESS_MODEM_NORMAL to Optional.of("modem"),
ModRegistry.Blocks.WIRELESS_MODEM_ADVANCED to Optional.of("modem"),
ModRegistry.Blocks.WIRED_MODEM_FULL to Optional.of("modem"),
ModRegistry.Blocks.REDSTONE_RELAY to Optional.of("redstone_relay"),
)) {
context.setBlock(pos, block.get())
if (type.isPresent) {
context.assertPeripheral(pos, side, type.get())
} else {
context.assertNoPeripheral(pos, side)
}
}
}
}

View File

@ -95,17 +95,6 @@ class Computer_Test {
} }
} }
/**
* Check computers and turtles expose peripherals.
*/
@GameTest
fun Computer_peripheral(context: GameTestHelper) = context.sequence {
thenExecute {
context.assertPeripheral(BlockPos(3, 2, 2), type = "computer")
context.assertPeripheral(BlockPos(1, 2, 2), type = "turtle")
}
}
/** /**
* Check chest peripherals are reattached with a new size. * Check chest peripherals are reattached with a new size.
*/ */

View File

@ -658,6 +658,27 @@ class Turtle_Test {
} }
} }
/**
* Test turtles can push entities.
*/
@GameTest
fun Move_push_entity(helper: GameTestHelper) = helper.sequence {
thenOnComputer { turtle.up().await().assertArrayEquals(true) }
thenIdle(9)
thenExecute {
// The turtle has moved up
helper.assertBlockPresent(ModRegistry.Blocks.TURTLE_NORMAL.get(), BlockPos(2, 3, 2))
// As has the villager
val pos = BlockPos(2, 4, 2)
helper.assertEntityPresent(EntityType.VILLAGER, pos)
val villager = helper.getEntity(EntityType.VILLAGER)
val expectedY = helper.absolutePos(pos).y - 0.125
if (villager.y < expectedY) helper.fail("Expected villager at y>=$expectedY, but at ${villager.y}", pos)
}
}
/** /**
* Test a turtle can attack an entity and capture its drops. * Test a turtle can attack an entity and capture its drops.
*/ */

View File

@ -128,6 +128,14 @@ fun GameTestHelper.sequence(run: GameTestSequence.() -> Unit) {
sequence.thenSucceed() sequence.thenSucceed()
} }
/**
* Run a function immediately, and then succeed.
*/
fun GameTestHelper.immediate(run: () -> Unit) {
run()
succeed()
}
/** /**
* A custom instance of [GameTestAssertPosException] which allows for longer error messages. * A custom instance of [GameTestAssertPosException] which allows for longer error messages.
*/ */
@ -237,15 +245,17 @@ private fun GameTestHelper.getPeripheralAt(pos: BlockPos, direction: Direction):
fun GameTestHelper.assertPeripheral(pos: BlockPos, direction: Direction = Direction.UP, type: String) { fun GameTestHelper.assertPeripheral(pos: BlockPos, direction: Direction = Direction.UP, type: String) {
val peripheral = getPeripheralAt(pos, direction) val peripheral = getPeripheralAt(pos, direction)
val block = getBlockState(pos).block.name.string
when { when {
peripheral == null -> fail("No peripheral at position", pos) peripheral == null -> fail("No peripheral for '$block'", pos)
peripheral.type != type -> fail("Peripheral is of type ${peripheral.type}, expected $type", pos) peripheral.type != type -> fail("Peripheral for '$block' is of type ${peripheral.type}, expected $type", pos)
} }
} }
fun GameTestHelper.assertNoPeripheral(pos: BlockPos, direction: Direction = Direction.UP) { fun GameTestHelper.assertNoPeripheral(pos: BlockPos, direction: Direction = Direction.UP) {
val peripheral = getPeripheralAt(pos, direction) val peripheral = getPeripheralAt(pos, direction)
if (peripheral != null) fail("Expected no peripheral, got a ${peripheral.type}", pos) val block = getBlockState(pos).block.name
if (peripheral != null) fail("Expected no peripheral for '$block', got a ${peripheral.type}", pos)
} }
fun GameTestHelper.assertExactlyItems(vararg expected: ItemStack, message: String? = null) { fun GameTestHelper.assertExactlyItems(vararg expected: ItemStack, message: String? = null) {

View File

@ -36,6 +36,10 @@ object ManagedComputers : ILuaMachine.Factory {
private val LOGGER = LoggerFactory.getLogger(ManagedComputers::class.java) private val LOGGER = LoggerFactory.getLogger(ManagedComputers::class.java)
private val computers: MutableMap<String, Queue<suspend LuaTaskContext.() -> Unit>> = mutableMapOf() private val computers: MutableMap<String, Queue<suspend LuaTaskContext.() -> Unit>> = mutableMapOf()
internal fun reset() {
computers.clear()
}
internal fun enqueue(test: GameTestInfo, label: String, task: suspend LuaTaskContext.() -> Unit): Monitor { internal fun enqueue(test: GameTestInfo, label: String, task: suspend LuaTaskContext.() -> Unit): Monitor {
val monitor = Monitor(test, label) val monitor = Monitor(test, label)
computers.computeIfAbsent(label) { ConcurrentLinkedDeque() }.add { computers.computeIfAbsent(label) { ConcurrentLinkedDeque() }.add {

View File

@ -84,6 +84,8 @@ object TestHooks {
StructureUtils.clearSpaceForStructure(StructureUtils.getStructureBoundingBox(structure), level) StructureUtils.clearSpaceForStructure(StructureUtils.getStructureBoundingBox(structure), level)
} }
ManagedComputers.reset()
// Delete server context and add one with a mutable machine factory. This allows us to set the factory for // Delete server context and add one with a mutable machine factory. This allows us to set the factory for
// specific test batches without having to reset all computers. // specific test batches without having to reset all computers.
for (computer in ServerContext.get(server).registry().computers) { for (computer in ServerContext.get(server).registry().computers) {
@ -99,6 +101,7 @@ object TestHooks {
fun areComputersIdle(server: MinecraftServer) = ComputerThreadReflection.isFullyIdle(ServerContext.get(server)) fun areComputersIdle(server: MinecraftServer) = ComputerThreadReflection.isFullyIdle(ServerContext.get(server))
private val testClasses = listOf( private val testClasses = listOf(
Component_Test::class.java,
Computer_Test::class.java, Computer_Test::class.java,
CraftOs_Test::class.java, CraftOs_Test::class.java,
Details_Test::class.java, Details_Test::class.java,

View File

@ -1,5 +1,5 @@
{ {
DataVersion: 2975, DataVersion: 3465,
size: [5, 5, 5], size: [5, 5, 5],
data: [ data: [
{pos: [0, 0, 0], state: "minecraft:polished_andesite"}, {pos: [0, 0, 0], state: "minecraft:polished_andesite"},
@ -33,19 +33,19 @@
{pos: [0, 1, 3], state: "minecraft:air"}, {pos: [0, 1, 3], state: "minecraft:air"},
{pos: [0, 1, 4], state: "minecraft:air"}, {pos: [0, 1, 4], state: "minecraft:air"},
{pos: [1, 1, 0], state: "minecraft:air"}, {pos: [1, 1, 0], state: "minecraft:air"},
{pos: [1, 1, 1], state: "minecraft:air"}, {pos: [1, 1, 1], state: "minecraft:white_stained_glass"},
{pos: [1, 1, 2], state: "computercraft:turtle_advanced{facing:south,waterlogged:false}", nbt: {Fuel: 0, Items: [], On: 0b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_advanced"}}, {pos: [1, 1, 2], state: "minecraft:white_stained_glass"},
{pos: [1, 1, 3], state: "minecraft:air"}, {pos: [1, 1, 3], state: "minecraft:white_stained_glass"},
{pos: [1, 1, 4], state: "minecraft:air"}, {pos: [1, 1, 4], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"}, {pos: [2, 1, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "minecraft:air"}, {pos: [2, 1, 1], state: "minecraft:white_stained_glass"},
{pos: [2, 1, 2], state: "minecraft:air"}, {pos: [2, 1, 2], state: "computercraft:turtle_normal{facing:west,waterlogged:false}", nbt: {ComputerId: 1, Label: "turtle_test.move_push_entity", Fuel: 80, Items: [], On: 1b, Slot: 0, id: "computercraft:turtle_normal"}},
{pos: [2, 1, 3], state: "minecraft:air"}, {pos: [2, 1, 3], state: "minecraft:white_stained_glass"},
{pos: [2, 1, 4], state: "minecraft:air"}, {pos: [2, 1, 4], state: "minecraft:air"},
{pos: [3, 1, 0], state: "minecraft:air"}, {pos: [3, 1, 0], state: "minecraft:air"},
{pos: [3, 1, 1], state: "minecraft:air"}, {pos: [3, 1, 1], state: "minecraft:white_stained_glass"},
{pos: [3, 1, 2], state: "computercraft:computer_advanced{facing:north,state:off}", nbt: {On: 0b, id: "computercraft:computer_advanced"}}, {pos: [3, 1, 2], state: "minecraft:white_stained_glass"},
{pos: [3, 1, 3], state: "minecraft:air"}, {pos: [3, 1, 3], state: "minecraft:white_stained_glass"},
{pos: [3, 1, 4], state: "minecraft:air"}, {pos: [3, 1, 4], state: "minecraft:air"},
{pos: [4, 1, 0], state: "minecraft:air"}, {pos: [4, 1, 0], state: "minecraft:air"},
{pos: [4, 1, 1], state: "minecraft:air"}, {pos: [4, 1, 1], state: "minecraft:air"},
@ -58,19 +58,19 @@
{pos: [0, 2, 3], state: "minecraft:air"}, {pos: [0, 2, 3], state: "minecraft:air"},
{pos: [0, 2, 4], state: "minecraft:air"}, {pos: [0, 2, 4], state: "minecraft:air"},
{pos: [1, 2, 0], state: "minecraft:air"}, {pos: [1, 2, 0], state: "minecraft:air"},
{pos: [1, 2, 1], state: "minecraft:air"}, {pos: [1, 2, 1], state: "minecraft:white_stained_glass"},
{pos: [1, 2, 2], state: "minecraft:air"}, {pos: [1, 2, 2], state: "minecraft:white_stained_glass"},
{pos: [1, 2, 3], state: "minecraft:air"}, {pos: [1, 2, 3], state: "minecraft:white_stained_glass"},
{pos: [1, 2, 4], state: "minecraft:air"}, {pos: [1, 2, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"}, {pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:air"}, {pos: [2, 2, 1], state: "minecraft:white_stained_glass"},
{pos: [2, 2, 2], state: "minecraft:air"}, {pos: [2, 2, 2], state: "minecraft:air"},
{pos: [2, 2, 3], state: "minecraft:air"}, {pos: [2, 2, 3], state: "minecraft:white_stained_glass"},
{pos: [2, 2, 4], state: "minecraft:air"}, {pos: [2, 2, 4], state: "minecraft:air"},
{pos: [3, 2, 0], state: "minecraft:air"}, {pos: [3, 2, 0], state: "minecraft:air"},
{pos: [3, 2, 1], state: "minecraft:air"}, {pos: [3, 2, 1], state: "minecraft:white_stained_glass"},
{pos: [3, 2, 2], state: "minecraft:air"}, {pos: [3, 2, 2], state: "minecraft:white_stained_glass"},
{pos: [3, 2, 3], state: "minecraft:air"}, {pos: [3, 2, 3], state: "minecraft:white_stained_glass"},
{pos: [3, 2, 4], state: "minecraft:air"}, {pos: [3, 2, 4], state: "minecraft:air"},
{pos: [4, 2, 0], state: "minecraft:air"}, {pos: [4, 2, 0], state: "minecraft:air"},
{pos: [4, 2, 1], state: "minecraft:air"}, {pos: [4, 2, 1], state: "minecraft:air"},
@ -83,19 +83,19 @@
{pos: [0, 3, 3], state: "minecraft:air"}, {pos: [0, 3, 3], state: "minecraft:air"},
{pos: [0, 3, 4], state: "minecraft:air"}, {pos: [0, 3, 4], state: "minecraft:air"},
{pos: [1, 3, 0], state: "minecraft:air"}, {pos: [1, 3, 0], state: "minecraft:air"},
{pos: [1, 3, 1], state: "minecraft:air"}, {pos: [1, 3, 1], state: "minecraft:white_stained_glass"},
{pos: [1, 3, 2], state: "minecraft:air"}, {pos: [1, 3, 2], state: "minecraft:white_stained_glass"},
{pos: [1, 3, 3], state: "minecraft:air"}, {pos: [1, 3, 3], state: "minecraft:white_stained_glass"},
{pos: [1, 3, 4], state: "minecraft:air"}, {pos: [1, 3, 4], state: "minecraft:air"},
{pos: [2, 3, 0], state: "minecraft:air"}, {pos: [2, 3, 0], state: "minecraft:air"},
{pos: [2, 3, 1], state: "minecraft:air"}, {pos: [2, 3, 1], state: "minecraft:white_stained_glass"},
{pos: [2, 3, 2], state: "minecraft:air"}, {pos: [2, 3, 2], state: "minecraft:air"},
{pos: [2, 3, 3], state: "minecraft:air"}, {pos: [2, 3, 3], state: "minecraft:white_stained_glass"},
{pos: [2, 3, 4], state: "minecraft:air"}, {pos: [2, 3, 4], state: "minecraft:air"},
{pos: [3, 3, 0], state: "minecraft:air"}, {pos: [3, 3, 0], state: "minecraft:air"},
{pos: [3, 3, 1], state: "minecraft:air"}, {pos: [3, 3, 1], state: "minecraft:white_stained_glass"},
{pos: [3, 3, 2], state: "minecraft:air"}, {pos: [3, 3, 2], state: "minecraft:white_stained_glass"},
{pos: [3, 3, 3], state: "minecraft:air"}, {pos: [3, 3, 3], state: "minecraft:white_stained_glass"},
{pos: [3, 3, 4], state: "minecraft:air"}, {pos: [3, 3, 4], state: "minecraft:air"},
{pos: [4, 3, 0], state: "minecraft:air"}, {pos: [4, 3, 0], state: "minecraft:air"},
{pos: [4, 3, 1], state: "minecraft:air"}, {pos: [4, 3, 1], state: "minecraft:air"},
@ -108,19 +108,19 @@
{pos: [0, 4, 3], state: "minecraft:air"}, {pos: [0, 4, 3], state: "minecraft:air"},
{pos: [0, 4, 4], state: "minecraft:air"}, {pos: [0, 4, 4], state: "minecraft:air"},
{pos: [1, 4, 0], state: "minecraft:air"}, {pos: [1, 4, 0], state: "minecraft:air"},
{pos: [1, 4, 1], state: "minecraft:air"}, {pos: [1, 4, 1], state: "minecraft:white_stained_glass"},
{pos: [1, 4, 2], state: "minecraft:air"}, {pos: [1, 4, 2], state: "minecraft:white_stained_glass"},
{pos: [1, 4, 3], state: "minecraft:air"}, {pos: [1, 4, 3], state: "minecraft:white_stained_glass"},
{pos: [1, 4, 4], state: "minecraft:air"}, {pos: [1, 4, 4], state: "minecraft:air"},
{pos: [2, 4, 0], state: "minecraft:air"}, {pos: [2, 4, 0], state: "minecraft:air"},
{pos: [2, 4, 1], state: "minecraft:air"}, {pos: [2, 4, 1], state: "minecraft:white_stained_glass"},
{pos: [2, 4, 2], state: "minecraft:air"}, {pos: [2, 4, 2], state: "minecraft:air"},
{pos: [2, 4, 3], state: "minecraft:air"}, {pos: [2, 4, 3], state: "minecraft:white_stained_glass"},
{pos: [2, 4, 4], state: "minecraft:air"}, {pos: [2, 4, 4], state: "minecraft:air"},
{pos: [3, 4, 0], state: "minecraft:air"}, {pos: [3, 4, 0], state: "minecraft:air"},
{pos: [3, 4, 1], state: "minecraft:air"}, {pos: [3, 4, 1], state: "minecraft:white_stained_glass"},
{pos: [3, 4, 2], state: "minecraft:air"}, {pos: [3, 4, 2], state: "minecraft:white_stained_glass"},
{pos: [3, 4, 3], state: "minecraft:air"}, {pos: [3, 4, 3], state: "minecraft:white_stained_glass"},
{pos: [3, 4, 4], state: "minecraft:air"}, {pos: [3, 4, 4], state: "minecraft:air"},
{pos: [4, 4, 0], state: "minecraft:air"}, {pos: [4, 4, 0], state: "minecraft:air"},
{pos: [4, 4, 1], state: "minecraft:air"}, {pos: [4, 4, 1], state: "minecraft:air"},
@ -128,11 +128,13 @@
{pos: [4, 4, 3], state: "minecraft:air"}, {pos: [4, 4, 3], state: "minecraft:air"},
{pos: [4, 4, 4], state: "minecraft:air"} {pos: [4, 4, 4], state: "minecraft:air"}
], ],
entities: [], entities: [
{blockPos: [2, 1, 2], pos: [2.5d, 1.875d, 2.5d], nbt: {AbsorptionAmount: 0.0f, Age: 0, Air: 300s, ArmorDropChances: [0.085f, 0.085f, 0.085f, 0.085f], ArmorItems: [{}, {}, {}, {}], Attributes: [{Base: 0.5d, Name: "minecraft:generic.movement_speed"}, {Base: 48.0d, Modifiers: [{Amount: -0.01165046535152748d, Name: "Random spawn bonus", Operation: 1, UUID: [I; 1412502412, 1522745411, -1211155694, 2103054347]}], Name: "minecraft:generic.follow_range"}], Brain: {memories: {}}, CanPickUpLoot: 1b, DeathTime: 0s, FallDistance: 0.0f, FallFlying: 0b, Fire: -1s, FoodLevel: 0b, ForcedAge: 0, Gossips: [], HandDropChances: [0.085f, 0.085f], HandItems: [{}, {}], Health: 20.0f, HurtByTimestamp: 0, HurtTime: 0s, Inventory: [], Invulnerable: 0b, LastGossipDecay: 52357L, LastRestock: 0L, LeftHanded: 0b, Motion: [0.0d, -0.0784000015258789d, 0.0d], OnGround: 1b, PersistenceRequired: 0b, PortalCooldown: 0, Pos: [-33.5d, 58.875d, -21.5d], RestocksToday: 0, Rotation: [-102.704926f, 0.0f], UUID: [I; 164071932, -867285780, -1817215456, -2129864016], VillagerData: {level: 1, profession: "minecraft:none", type: "minecraft:desert"}, Xp: 0, id: "minecraft:villager"}}
],
palette: [ palette: [
"minecraft:polished_andesite", "minecraft:polished_andesite",
"minecraft:white_stained_glass",
"minecraft:air", "minecraft:air",
"computercraft:turtle_advanced{facing:south,waterlogged:false}", "computercraft:turtle_normal{facing:west,waterlogged:false}"
"computercraft:computer_advanced{facing:north,state:off}"
] ]
} }

View File

@ -47,12 +47,22 @@ local function sortCoords(startX, startY, endX, endY)
return minX, maxX, minY, maxY return minX, maxX, minY, maxY
end end
--- Parses an image from a multi-line string --[=[- Parses an image from a multi-line string
--
-- @tparam string image The string containing the raw-image data. @tparam string image The string containing the raw-image data.
-- @treturn table The parsed image data, suitable for use with @treturn table The parsed image data, suitable for use with [`paintutils.drawImage`].
-- [`paintutils.drawImage`]. @usage Parse an image from a string, and draw it.
-- @since 1.80pr1
local image = paintutils.parseImage([[
e e
e e
eeee
]])
paintutils.drawImage(image, term.getCursorPos())
@since 1.80pr1
]=]
function parseImage(image) function parseImage(image)
expect(1, image, "string") expect(1, image, "string")
local tImage = {} local tImage = {}

View File

@ -1,3 +1,12 @@
# New features in CC: Tweaked 1.114.4
* Allow typing/pasting any character in the CC charset.
* Add a new `computercraft:storage_capacity` to override a disk or computer's capacity.
Several bug fixes:
* Fix command computers having NBT set when placed in a Create contraption.
* Use correct bounding box when checking for entities in turtle movement.
# New features in CC: Tweaked 1.114.3 # New features in CC: Tweaked 1.114.3
* `wget` now prints the error that occurred, rather than a generic "Failed" (tizu69). * `wget` now prints the error that occurred, rather than a generic "Failed" (tizu69).

View File

@ -1,10 +1,10 @@
New features in CC: Tweaked 1.114.3 New features in CC: Tweaked 1.114.4
* `wget` now prints the error that occurred, rather than a generic "Failed" (tizu69). * Allow typing/pasting any character in the CC charset.
* Update several translations. * Add a new `computercraft:storage_capacity` to override a disk or computer's capacity.
Several bug fixes: Several bug fixes:
* Fix `fs.isDriveRoot` returning true for non-existent files. * Fix command computers having NBT set when placed in a Create contraption.
* Fix possible memory leak when sending terminal contents. * Use correct bounding box when checking for entities in turtle movement.
Type "help changelog" to see the full version history. Type "help changelog" to see the full version history.

View File

@ -47,11 +47,11 @@ import net.minecraft.tags.TagKey;
import net.minecraft.world.Container; import net.minecraft.world.Container;
import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult; import net.minecraft.world.InteractionResult;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuConstructor;
import net.minecraft.world.inventory.MenuType; import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.item.*; import net.minecraft.world.item.*;
import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.world.item.crafting.Ingredient;
@ -105,8 +105,8 @@ public class PlatformHelperImpl implements PlatformHelper {
} }
@Override @Override
public void openMenu(Player player, MenuProvider owner, ContainerData menu) { public void openMenu(Player player, Component title, MenuConstructor menu, ContainerData data) {
player.openMenu(new WrappedMenuProvider<>(owner, menu)); player.openMenu(new WrappedMenuProvider<>(title, menu, data));
} }
@Override @Override
@ -277,22 +277,22 @@ public class PlatformHelperImpl implements PlatformHelper {
} }
private record WrappedMenuProvider<T extends ContainerData>( private record WrappedMenuProvider<T extends ContainerData>(
MenuProvider owner, T menu Component title, MenuConstructor menu, T data
) implements ExtendedScreenHandlerFactory<T> { ) implements ExtendedScreenHandlerFactory<T> {
@Nullable @Nullable
@Override @Override
public AbstractContainerMenu createMenu(int id, Inventory inventory, Player player) { public AbstractContainerMenu createMenu(int id, Inventory inventory, Player player) {
return owner.createMenu(id, inventory, player); return menu.createMenu(id, inventory, player);
} }
@Override @Override
public Component getDisplayName() { public Component getDisplayName() {
return owner.getDisplayName(); return title;
} }
@Override @Override
public T getScreenOpeningData(ServerPlayer player) { public T getScreenOpeningData(ServerPlayer player) {
return menu; return data;
} }
} }

View File

@ -23,19 +23,18 @@ import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.core.Registry; import net.minecraft.core.Registry;
import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.network.codec.StreamCodec; import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.TagKey; import net.minecraft.tags.TagKey;
import net.minecraft.world.Container; import net.minecraft.world.*;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuConstructor;
import net.minecraft.world.inventory.MenuType; import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
@ -95,8 +94,8 @@ public class PlatformHelperImpl implements PlatformHelper {
} }
@Override @Override
public void openMenu(Player player, MenuProvider owner, ContainerData menu) { public void openMenu(Player player, Component title, MenuConstructor menu, ContainerData data) {
player.openMenu(owner, menu::toBytes); player.openMenu(new SimpleMenuProvider(menu, title), data::toBytes);
} }
@Override @Override