1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-04-10 04:36:40 +00:00

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

This commit is contained in:
Jonathan Coates 2025-01-20 22:22:09 +00:00
commit 53425c1e76
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
53 changed files with 599 additions and 398 deletions

View File

@ -1,6 +1,7 @@
name: Bug report
description: Report some misbehaviour in the mod
labels: [ bug ]
type: bug
body:
- type: dropdown
id: mc-version

View File

@ -2,6 +2,7 @@
name: Feature request
about: Suggest an idea or improvement
labels: enhancement
type: feature
---
<!--

View File

@ -10,9 +10,7 @@ import org.gradle.api.GradleException
import org.gradle.api.NamedDomainObjectProvider
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Dependency
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Provider
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.SourceSet
@ -33,6 +31,10 @@ import java.net.URI
import java.util.regex.Pattern
abstract class CCTweakedExtension(private val project: Project) {
/** Get the hash of the latest git commit. */
val gitHash: Provider<String> =
gitProvider("<no git commit>", listOf("rev-parse", "HEAD")) { it.trim() }
/** Get the current git branch. */
val gitBranch: Provider<String> =
gitProvider("<no git branch>", listOf("rev-parse", "--abbrev-ref", "HEAD")) { it.trim() }
@ -164,6 +166,7 @@ abstract class CCTweakedExtension(private val project: Project) {
jacoco.applyTo(this)
extensions.configure(JacocoTaskExtension::class.java) {
includes = listOf("dan200.computercraft.*")
excludes = listOf(
"dan200.computercraft.mixin.*", // Exclude mixins, as they're not executed at runtime.
"dan200.computercraft.shared.Capabilities$*", // Exclude capability tokens, as Forge rewrites them.

View File

@ -19,8 +19,8 @@ import dan200.computercraft.api.ComputerCraftAPI;
*/
public interface WiredElement extends WiredSender {
/**
* Called when objects on the network change. This may occur when network nodes are added or removed, or when
* peripherals change.
* Called when peripherals on the network change. This may occur when network nodes are added or removed, or when
* peripherals are attached or detached from a modem.
*
* @param change The change which occurred.
* @see WiredNetworkChange

View File

@ -10,10 +10,10 @@ import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.network.server.ComputerActionServerMessage;
import dan200.computercraft.shared.network.server.KeyEventServerMessage;
import dan200.computercraft.shared.network.server.MouseEventServerMessage;
import dan200.computercraft.shared.network.server.QueueEventServerMessage;
import dan200.computercraft.shared.network.server.PasteEventComputerMessage;
import net.minecraft.world.inventory.AbstractContainerMenu;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
/**
* An {@link InputHandler} for use on the client.
@ -27,6 +27,11 @@ public final class ClientInputHandler implements InputHandler {
this.menu = menu;
}
@Override
public void terminate() {
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TERMINATE));
}
@Override
public void turnOn() {
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TURN_ON));
@ -42,11 +47,6 @@ public final class ClientInputHandler implements InputHandler {
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.REBOOT));
}
@Override
public void queueEvent(String event, @Nullable Object[] arguments) {
ClientNetworking.sendToServer(new QueueEventServerMessage(menu, event, arguments));
}
@Override
public void keyDown(int key, boolean repeat) {
ClientNetworking.sendToServer(new KeyEventServerMessage(menu, repeat ? KeyEventServerMessage.Action.REPEAT : KeyEventServerMessage.Action.DOWN, key));
@ -57,6 +57,16 @@ public final class ClientInputHandler implements InputHandler {
ClientNetworking.sendToServer(new KeyEventServerMessage(menu, KeyEventServerMessage.Action.UP, key));
}
@Override
public void charTyped(byte chr) {
ClientNetworking.sendToServer(new KeyEventServerMessage(menu, KeyEventServerMessage.Action.CHAR, chr));
}
@Override
public void paste(ByteBuffer contents) {
ClientNetworking.sendToServer(new PasteEventComputerMessage(menu, contents));
}
@Override
public void mouseClick(int button, int x, int y) {
ClientNetworking.sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.Action.CLICK, button, x, y));

View File

@ -34,8 +34,8 @@ public final class GuiSprites extends TextureAtlasHolder {
private static ButtonTextures button(String name) {
return new ButtonTextures(
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name + "_hover")
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "buttons/" + name),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "buttons/" + name + "_hover")
);
}
@ -96,12 +96,8 @@ public final class GuiSprites extends TextureAtlasHolder {
* @param active The texture for the button when it is active (hovered or focused).
*/
public record ButtonTextures(ResourceLocation normal, ResourceLocation active) {
public TextureAtlasSprite get(boolean active) {
return GuiSprites.get(active ? this.active : normal);
}
public Stream<ResourceLocation> textures() {
return Stream.of(normal, active);
public ResourceLocation get(boolean isActive) {
return isActive ? active : normal;
}
}

View File

@ -26,6 +26,9 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
private static final ResourceLocation BACKGROUND_NORMAL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "textures/gui/turtle_normal.png");
private static final ResourceLocation BACKGROUND_ADVANCED = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "textures/gui/turtle_advanced.png");
private static final ResourceLocation SELECTED_NORMAL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle_normal_selected_slot");
private static final ResourceLocation SELECTED_ADVANCED = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle_advanced_selected_slot");
private static final int TEX_WIDTH = 278;
private static final int TEX_HEIGHT = 217;
@ -54,9 +57,9 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
if (slot >= 0) {
var slotX = slot % 4;
var slotY = slot / 4;
graphics.blit(texture,
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18, 0,
0, 217, 24, 24, FULL_TEX_SIZE, FULL_TEX_SIZE
graphics.blitSprite(
advanced ? SELECTED_ADVANCED : SELECTED_NORMAL,
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18, 0, 22, 22
);
}

View File

@ -55,7 +55,7 @@ public final class ComputerSidebar {
add.accept(new DynamicImageButton(
x, y, ICON_WIDTH, ICON_HEIGHT,
GuiSprites.TERMINATE::get,
b -> input.queueEvent("terminate"),
b -> input.terminate(),
new HintedMessage(
Component.translatable("gui.computercraft.tooltip.terminate"),
Component.translatable("gui.computercraft.tooltip.terminate.key")

View File

@ -10,8 +10,8 @@ import net.minecraft.ChatFormatting;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import javax.annotation.Nullable;
import java.util.function.Supplier;
@ -21,11 +21,11 @@ import java.util.function.Supplier;
* dynamically.
*/
public class DynamicImageButton extends Button {
private final Boolean2ObjectFunction<TextureAtlasSprite> texture;
private final Boolean2ObjectFunction<ResourceLocation> texture;
private final Supplier<HintedMessage> message;
public DynamicImageButton(
int x, int y, int width, int height, Boolean2ObjectFunction<TextureAtlasSprite> texture, OnPress onPress,
int x, int y, int width, int height, Boolean2ObjectFunction<ResourceLocation> texture, OnPress onPress,
HintedMessage message
) {
this(x, y, width, height, texture, onPress, () -> message);
@ -33,7 +33,7 @@ public class DynamicImageButton extends Button {
public DynamicImageButton(
int x, int y, int width, int height,
Boolean2ObjectFunction<TextureAtlasSprite> texture,
Boolean2ObjectFunction<ResourceLocation> texture,
OnPress onPress, Supplier<HintedMessage> message
) {
super(x, y, width, height, Component.empty(), onPress, DEFAULT_NARRATION);
@ -50,7 +50,7 @@ public class DynamicImageButton extends Button {
var texture = this.texture.get(isHoveredOrFocused());
RenderSystem.disableDepthTest();
graphics.blit(getX(), getY(), 0, width, height, texture);
graphics.blitSprite(texture, getX(), getY(), 0, width, height);
RenderSystem.enableDepthTest();
}

View File

@ -69,11 +69,8 @@ public class TerminalWidget extends AbstractWidget {
@Override
public boolean charTyped(char ch, int modifiers) {
if (ch >= 32 && ch <= 126 || ch >= 160 && ch <= 255) {
// Queue the char event for any printable chars in byte range
computer.queueEvent("char", new Object[]{ Character.toString(ch) });
}
var terminalChar = StringUtil.unicodeToTerminal(ch);
if (StringUtil.isTypableChar(terminalChar)) computer.charTyped(terminalChar);
return true;
}
@ -110,8 +107,8 @@ public class TerminalWidget extends AbstractWidget {
}
private void paste() {
var clipboard = StringUtil.normaliseClipboardString(Minecraft.getInstance().keyboardHandler.getClipboard());
if (!clipboard.isEmpty()) computer.queueEvent("paste", new Object[]{ clipboard });
var clipboard = StringUtil.getClipboardString(Minecraft.getInstance().keyboardHandler.getClipboard());
if (clipboard.remaining() > 0) computer.paste(clipboard);
}
@Override
@ -220,7 +217,7 @@ public class TerminalWidget extends AbstractWidget {
public void update() {
if (terminateTimer >= 0 && terminateTimer < TERMINATE_TIME && (terminateTimer += 0.05f) > TERMINATE_TIME) {
computer.queueEvent("terminate");
computer.terminate();
}
if (shutdownTimer >= 0 && shutdownTimer < TERMINATE_TIME && (shutdownTimer += 0.05f) > TERMINATE_TIME) {

View File

@ -39,8 +39,8 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
int termWidth, termHeight;
if (terminal == null) {
termWidth = Config.pocketTermWidth;
termHeight = Config.pocketTermHeight;
termWidth = Config.DEFAULT_POCKET_TERM_WIDTH;
termHeight = Config.DEFAULT_POCKET_TERM_HEIGHT;
} else {
termWidth = terminal.getWidth();
termHeight = terminal.getHeight();

View File

@ -74,10 +74,6 @@ public final class DataProviders {
LecternPrintoutModel.TEXTURE
)));
out.accept(GuiSprites.SPRITE_SHEET, makeSprites(
// Buttons
GuiSprites.TURNED_OFF.textures(),
GuiSprites.TURNED_ON.textures(),
GuiSprites.TERMINATE.textures(),
// Computers
GuiSprites.COMPUTER_NORMAL.textures(),
GuiSprites.COMPUTER_ADVANCED.textures(),

View File

@ -1,11 +1,5 @@
{
"sources": [
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_off"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_off_hover"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_on"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_on_hover"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/terminate"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/terminate_hover"},
{"type": "minecraft:single", "resource": "computercraft:gui/border_normal"},
{"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_normal"},
{"type": "minecraft:single", "resource": "computercraft:gui/sidebar_normal"},

View File

@ -11,8 +11,7 @@ import dan200.computercraft.shared.computer.core.ComputerFamily;
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 dan200.computercraft.shared.config.ConfigSpec;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
@ -33,10 +32,9 @@ public class ComputerBlockEntity extends AbstractComputerBlockEntity {
@Override
protected ServerComputer createComputer(int id) {
return new ServerComputer(
(ServerLevel) getLevel(), getBlockPos(), id, label,
getFamily(), Config.computerTermWidth, Config.computerTermHeight,
ComponentMap.empty()
return new ServerComputer((ServerLevel) getLevel(), getBlockPos(), ServerComputer.properties(id, getFamily())
.label(getLabel())
.terminalSize(ConfigSpec.computerTermWidth.get(), ConfigSpec.computerTermHeight.get())
);
}

View File

@ -6,44 +6,33 @@ package dan200.computercraft.shared.computer.core;
import dan200.computercraft.shared.computer.menu.ServerInputHandler;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
/**
* Handles user-provided input, forwarding it to a computer. This is used
* Handles user-provided input, forwarding it to a computer. This describes the "shape" of both the client-and
* server-side input handlers.
*
* @see ServerInputHandler
* @see ServerComputer
*/
public interface InputHandler {
void queueEvent(String event, @Nullable Object[] arguments);
void keyDown(int key, boolean repeat);
default void queueEvent(String event) {
queueEvent(event, null);
}
void keyUp(int key);
default void keyDown(int key, boolean repeat) {
queueEvent("key", new Object[]{ key, repeat });
}
void charTyped(byte chr);
default void keyUp(int key) {
queueEvent("key_up", new Object[]{ key });
}
void paste(ByteBuffer contents);
default void mouseClick(int button, int x, int y) {
queueEvent("mouse_click", new Object[]{ button, x, y });
}
void mouseClick(int button, int x, int y);
default void mouseUp(int button, int x, int y) {
queueEvent("mouse_up", new Object[]{ button, x, y });
}
void mouseUp(int button, int x, int y);
default void mouseDrag(int button, int x, int y) {
queueEvent("mouse_drag", new Object[]{ button, x, y });
}
void mouseDrag(int button, int x, int y);
default void mouseScroll(int direction, int x, int y) {
queueEvent("mouse_scroll", new Object[]{ direction, x, y });
}
void mouseScroll(int direction, int x, int y);
void terminate();
void shutdown();

View File

@ -6,12 +6,14 @@ package dan200.computercraft.shared.computer.core;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.component.AdminComputer;
import dan200.computercraft.api.component.ComputerComponent;
import dan200.computercraft.api.component.ComputerComponents;
import dan200.computercraft.api.filesystem.WritableMount;
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.ComputerEvents;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.impl.ApiFactories;
@ -34,7 +36,7 @@ import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
public class ServerComputer implements InputHandler, ComputerEnvironment {
public class ServerComputer implements ComputerEnvironment, ComputerEvents.Receiver {
private final UUID instanceUUID = UUID.randomUUID();
private ServerLevel level;
@ -49,16 +51,21 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
private int ticksSincePing;
@Deprecated
public ServerComputer(
ServerLevel level, BlockPos position, int computerID, @Nullable String label, ComputerFamily family, int terminalWidth, int terminalHeight,
ComponentMap baseComponents
) {
this(level, position, properties(computerID, family).label(label).terminalSize(terminalWidth, terminalHeight).addComponents(baseComponents));
}
public ServerComputer(ServerLevel level, BlockPos position, Properties properties) {
this.level = level;
this.position = position;
this.family = family;
this.family = properties.family;
var context = ServerContext.get(level.getServer());
terminal = new NetworkedTerminal(terminalWidth, terminalHeight, family != ComputerFamily.NORMAL, this::markTerminalChanged);
terminal = new NetworkedTerminal(properties.terminalWidth, properties.terminalHeight, family != ComputerFamily.NORMAL, this::markTerminalChanged);
metrics = context.metrics().createMetricObserver(this);
var componentBuilder = ComponentMap.builder();
@ -67,11 +74,11 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
componentBuilder.add(ComputerComponents.ADMIN_COMPUTER, new AdminComputer() {
});
}
componentBuilder.add(baseComponents);
componentBuilder.add(properties.components.build());
var components = componentBuilder.build();
computer = new Computer(context.computerContext(), this, terminal, computerID);
computer.setLabel(label);
computer = new Computer(context.computerContext(), this, terminal, properties.computerID);
computer.setLabel(properties.label);
// Load in the externally registered APIs.
for (var factory : ApiFactories.getAll()) {
@ -84,24 +91,24 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
}
}
public ComputerFamily getFamily() {
public final ComputerFamily getFamily() {
return family;
}
public ServerLevel getLevel() {
public final ServerLevel getLevel() {
return level;
}
public BlockPos getPosition() {
public final BlockPos getPosition() {
return position;
}
public void setPosition(ServerLevel level, BlockPos pos) {
public final void setPosition(ServerLevel level, BlockPos pos) {
this.level = level;
position = pos.immutable();
}
protected void markTerminalChanged() {
protected final void markTerminalChanged() {
terminalChanged.set(true);
}
@ -115,11 +122,11 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
sendToAllInteracting(c -> new ComputerTerminalClientMessage(c, getTerminalState()));
}
public TerminalState getTerminalState() {
public final TerminalState getTerminalState() {
return TerminalState.create(terminal);
}
public void keepAlive() {
public final void keepAlive() {
ticksSincePing = 0;
}
@ -132,7 +139,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
*
* @return What sides on the computer have changed.
*/
public int pollRedstoneChanges() {
public final int pollRedstoneChanges() {
return computer.pollRedstoneChanges();
}
@ -145,7 +152,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
computer.unload();
}
public void close() {
public final void close() {
unload();
ServerContext.get(level.getServer()).registry().remove(this);
}
@ -165,7 +172,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
var server = level.getServer();
for (var player : server.getPlayerList().getPlayers()) {
if (player.containerMenu instanceof ComputerMenu && ((ComputerMenu) player.containerMenu).getComputer() == this) {
if (player.containerMenu instanceof ComputerMenu menu && menu.getComputer() == this) {
ServerNetworking.sendToPlayer(createPacket.apply(player.containerMenu), player);
}
}
@ -174,93 +181,136 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
protected void onRemoved() {
}
public UUID getInstanceUUID() {
public final UUID getInstanceUUID() {
return instanceUUID;
}
public int getID() {
public final int getID() {
return computer.getID();
}
public @Nullable String getLabel() {
public final @Nullable String getLabel() {
return computer.getLabel();
}
public boolean isOn() {
public final boolean isOn() {
return computer.isOn();
}
public ComputerState getState() {
public final ComputerState getState() {
if (!computer.isOn()) return ComputerState.OFF;
return computer.isBlinking() ? ComputerState.BLINKING : ComputerState.ON;
}
@Override
public void turnOn() {
public final void turnOn() {
computer.turnOn();
}
@Override
public void shutdown() {
public final void shutdown() {
computer.shutdown();
}
@Override
public void reboot() {
public final void reboot() {
computer.reboot();
}
@Override
public void queueEvent(String event, @Nullable Object[] arguments) {
public final void queueEvent(String event, @Nullable Object[] arguments) {
computer.queueEvent(event, arguments);
}
public int getRedstoneOutput(ComputerSide side) {
public final void queueEvent(String event) {
queueEvent(event, null);
}
public final int getRedstoneOutput(ComputerSide side) {
return computer.isOn() ? computer.getRedstone().getExternalOutput(side) : 0;
}
public void setRedstoneInput(ComputerSide side, int level, int bundledState) {
public final void setRedstoneInput(ComputerSide side, int level, int bundledState) {
computer.getRedstone().setInput(side, level, bundledState);
}
public int getBundledRedstoneOutput(ComputerSide side) {
public final int getBundledRedstoneOutput(ComputerSide side) {
return computer.isOn() ? computer.getRedstone().getExternalBundledOutput(side) : 0;
}
public void setPeripheral(ComputerSide side, @Nullable IPeripheral peripheral) {
public final void setPeripheral(ComputerSide side, @Nullable IPeripheral peripheral) {
computer.getEnvironment().setPeripheral(side, peripheral);
}
@Nullable
public IPeripheral getPeripheral(ComputerSide side) {
public final IPeripheral getPeripheral(ComputerSide side) {
return computer.getEnvironment().getPeripheral(side);
}
public void setLabel(@Nullable String label) {
public final void setLabel(@Nullable String label) {
computer.setLabel(label);
}
@Override
public double getTimeOfDay() {
public final double getTimeOfDay() {
return (level.getDayTime() + 6000) % 24000 / 1000.0;
}
@Override
public int getDay() {
public final int getDay() {
return (int) ((level.getDayTime() + 6000) / 24000) + 1;
}
@Override
public MetricsObserver getMetrics() {
public final MetricsObserver getMetrics() {
return metrics;
}
public WorkMonitor getMainThreadMonitor() {
public final WorkMonitor getMainThreadMonitor() {
return computer.getMainThreadMonitor();
}
@Override
public @Nullable WritableMount createRootMount() {
public final WritableMount createRootMount() {
return ComputerCraftAPI.createSaveDirMount(level.getServer(), "computer/" + computer.getID(), Config.computerSpaceLimit);
}
public static Properties properties(int computerID, ComputerFamily family) {
return new Properties(computerID, family);
}
public static final class Properties {
private final int computerID;
private @Nullable String label;
private final ComputerFamily family;
private int terminalWidth = Config.DEFAULT_COMPUTER_TERM_WIDTH;
private int terminalHeight = Config.DEFAULT_COMPUTER_TERM_HEIGHT;
private final ComponentMap.Builder components = ComponentMap.builder();
private Properties(int computerID, ComputerFamily family) {
this.computerID = computerID;
this.family = family;
}
public Properties label(@Nullable String label) {
this.label = label;
return this;
}
public Properties terminalSize(int width, int height) {
if (width <= 0 || height <= 0) throw new IllegalArgumentException("Terminal size must be positive");
this.terminalWidth = width;
this.terminalHeight = height;
return this;
}
public <T> Properties addComponent(ComputerComponent<T> component, T value) {
components.add(component, value);
return this;
}
private Properties addComponents(ComponentMap components) {
this.components.add(components);
return this;
}
}
}

View File

@ -7,6 +7,8 @@ package dan200.computercraft.shared.computer.menu;
import dan200.computercraft.core.apis.handles.ByteBufferChannel;
import dan200.computercraft.core.apis.transfer.TransferredFile;
import dan200.computercraft.core.apis.transfer.TransferredFiles;
import dan200.computercraft.core.computer.ComputerEvents;
import dan200.computercraft.core.util.StringUtil;
import dan200.computercraft.shared.computer.upload.FileSlice;
import dan200.computercraft.shared.computer.upload.FileUpload;
import dan200.computercraft.shared.computer.upload.UploadResult;
@ -21,6 +23,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.UUID;
@ -48,21 +51,33 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
this.owner = owner;
}
@Override
public void queueEvent(String event, @Nullable Object[] arguments) {
owner.getComputer().queueEvent(event, arguments);
}
@Override
public void keyDown(int key, boolean repeat) {
keysDown.add(key);
owner.getComputer().keyDown(key, repeat);
ComputerEvents.keyDown(owner.getComputer(), key, repeat);
}
@Override
public void keyUp(int key) {
keysDown.remove(key);
owner.getComputer().keyUp(key);
ComputerEvents.keyUp(owner.getComputer(), key);
}
@Override
public void charTyped(byte chr) {
if (StringUtil.isTypableChar(chr)) ComputerEvents.charTyped(owner.getComputer(), chr);
}
@Override
public void paste(ByteBuffer contents) {
if (contents.remaining() > 0 && isValidClipboard(contents)) ComputerEvents.paste(owner.getComputer(), contents);
}
private static boolean isValidClipboard(ByteBuffer buffer) {
for (int i = buffer.remaining(), max = buffer.limit(); i < max; i++) {
if (!StringUtil.isTypableChar(buffer.get(i))) return false;
}
return true;
}
@Override
@ -71,7 +86,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
lastMouseY = y;
lastMouseDown = button;
owner.getComputer().mouseClick(button, x, y);
ComputerEvents.mouseClick(owner.getComputer(), button, x, y);
}
@Override
@ -80,7 +95,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
lastMouseY = y;
lastMouseDown = -1;
owner.getComputer().mouseUp(button, x, y);
ComputerEvents.mouseUp(owner.getComputer(), button, x, y);
}
@Override
@ -89,7 +104,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
lastMouseY = y;
lastMouseDown = button;
owner.getComputer().mouseDrag(button, x, y);
ComputerEvents.mouseDrag(owner.getComputer(), button, x, y);
}
@Override
@ -97,7 +112,12 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
lastMouseX = x;
lastMouseY = y;
owner.getComputer().mouseScroll(direction, x, y);
ComputerEvents.mouseScroll(owner.getComputer(), direction, x, y);
}
@Override
public void terminate() {
owner.getComputer().queueEvent("terminate");
}
@Override
@ -169,9 +189,9 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
public void close() {
var computer = owner.getComputer();
var keys = keysDown.iterator();
while (keys.hasNext()) computer.keyUp(keys.nextInt());
while (keys.hasNext()) ComputerEvents.keyUp(computer, keys.nextInt());
if (lastMouseDown != -1) computer.mouseUp(lastMouseDown, lastMouseX, lastMouseY);
if (lastMouseDown != -1) ComputerEvents.mouseUp(computer, lastMouseDown, lastMouseX, lastMouseY);
keysDown.clear();
lastMouseDown = -1;

View File

@ -32,14 +32,14 @@ public final class Config {
public static int advancedTurtleFuelLimit = 100000;
public static boolean turtlesCanPush = true;
public static int computerTermWidth = 51;
public static int computerTermHeight = 19;
public static final int DEFAULT_COMPUTER_TERM_WIDTH = 51;
public static final int DEFAULT_COMPUTER_TERM_HEIGHT = 19;
public static final int turtleTermWidth = 39;
public static final int turtleTermHeight = 13;
public static final int TURTLE_TERM_WIDTH = 39;
public static final int TURTLE_TERM_HEIGHT = 13;
public static int pocketTermWidth = 26;
public static int pocketTermHeight = 20;
public static final int DEFAULT_POCKET_TERM_WIDTH = 26;
public static final int DEFAULT_POCKET_TERM_HEIGHT = 20;
public static int monitorWidth = 8;
public static int monitorHeight = 6;

View File

@ -344,13 +344,13 @@ public final class ConfigSpec {
.push("term_sizes");
builder.comment("Terminal size of computers.").push("computer");
computerTermWidth = builder.comment("Width of computer terminal").defineInRange("width", Config.computerTermWidth, 1, 255);
computerTermHeight = builder.comment("Height of computer terminal").defineInRange("height", Config.computerTermHeight, 1, 255);
computerTermWidth = builder.comment("Width of computer terminal").defineInRange("width", Config.DEFAULT_COMPUTER_TERM_WIDTH, 1, 255);
computerTermHeight = builder.comment("Height of computer terminal").defineInRange("height", Config.DEFAULT_COMPUTER_TERM_HEIGHT, 1, 255);
builder.pop();
builder.comment("Terminal size of pocket computers.").push("pocket_computer");
pocketTermWidth = builder.comment("Width of pocket computer terminal").defineInRange("width", Config.pocketTermWidth, 1, 255);
pocketTermHeight = builder.comment("Height of pocket computer terminal").defineInRange("height", Config.pocketTermHeight, 1, 255);
pocketTermWidth = builder.comment("Width of pocket computer terminal").defineInRange("width", Config.DEFAULT_POCKET_TERM_WIDTH, 1, 255);
pocketTermHeight = builder.comment("Height of pocket computer terminal").defineInRange("height", Config.DEFAULT_POCKET_TERM_HEIGHT, 1, 255);
builder.pop();
builder.comment("Maximum size of monitors (in blocks).").push("monitor");
@ -437,10 +437,6 @@ public final class ConfigSpec {
Config.turtlesCanPush = turtlesCanPush.get();
// Terminal size
Config.computerTermWidth = computerTermWidth.get();
Config.computerTermHeight = computerTermHeight.get();
Config.pocketTermWidth = pocketTermWidth.get();
Config.pocketTermHeight = pocketTermHeight.get();
Config.monitorWidth = monitorWidth.get();
Config.monitorHeight = monitorHeight.get();
}

View File

@ -26,9 +26,9 @@ public final class NetworkMessages {
private static final List<CustomPacketPayload.TypeAndCodec<RegistryFriendlyByteBuf, ? extends NetworkMessage<ClientNetworkContext>>> clientMessages = new ArrayList<>();
public static final CustomPacketPayload.Type<ComputerActionServerMessage> COMPUTER_ACTION = registerServerbound("computer_action", ComputerActionServerMessage.STREAM_CODEC);
public static final CustomPacketPayload.Type<QueueEventServerMessage> QUEUE_EVENT = register(serverMessages, "queue_event", QueueEventServerMessage.STREAM_CODEC);
public static final CustomPacketPayload.Type<KeyEventServerMessage> KEY_EVENT = registerServerbound("key_event", KeyEventServerMessage.STREAM_CODEC);
public static final CustomPacketPayload.Type<MouseEventServerMessage> MOUSE_EVENT = registerServerbound("mouse_event", MouseEventServerMessage.STREAM_CODEC);
public static final CustomPacketPayload.Type<PasteEventComputerMessage> PASTE_EVENT = registerServerbound("paste_event", PasteEventComputerMessage.STREAM_CODEC);
public static final CustomPacketPayload.Type<UploadFileMessage> UPLOAD_FILE = register(serverMessages, "upload_file", UploadFileMessage.STREAM_CODEC);
public static final CustomPacketPayload.Type<ChatTableClientMessage> CHAT_TABLE = registerClientbound("chat_table", ChatTableClientMessage.STREAM_CODEC);

View File

@ -83,25 +83,36 @@ public class MoreStreamCodecs {
};
/**
* Equivalent to {@link ByteBufCodecs#BYTE_ARRAY}, but into an immutable {@link ByteBuffer}.
* Read a {@link ByteBuffer}, with a limit of the number of bytes to read.
*
* @param limit The maximum length of the received buffer.
* @return A stream codec that reads {@link ByteBuffer}s.
* @see #BYTE_BUFFER
*/
public static final StreamCodec<ByteBuf, ByteBuffer> BYTE_BUFFER = new StreamCodec<>() {
@Override
public ByteBuffer decode(ByteBuf buf) {
var toRead = VarInt.read(buf);
if (toRead > buf.readableBytes()) {
throw new DecoderException("ByteArray with size " + toRead + " is bigger than allowed");
public static StreamCodec<ByteBuf, ByteBuffer> byteBuffer(int limit) {
return new StreamCodec<>() {
@Override
public ByteBuffer decode(ByteBuf buf) {
var toRead = VarInt.read(buf);
if (toRead > buf.readableBytes() || toRead >= limit) {
throw new DecoderException("ByteArray with size " + toRead + " is bigger than allowed");
}
var bytes = new byte[toRead];
buf.readBytes(bytes);
return ByteBuffer.wrap(bytes).asReadOnlyBuffer();
}
var bytes = new byte[toRead];
buf.readBytes(bytes);
return ByteBuffer.wrap(bytes).asReadOnlyBuffer();
}
@Override
public void encode(ByteBuf buf, ByteBuffer buffer) {
VarInt.write(buf, buffer.remaining());
buf.writeBytes(buffer.duplicate());
}
};
}
@Override
public void encode(ByteBuf buf, ByteBuffer buffer) {
VarInt.write(buf, buffer.remaining());
buf.writeBytes(buffer.duplicate());
}
};
/**
* Equivalent to {@link ByteBufCodecs#BYTE_ARRAY}, but into an immutable {@link ByteBuffer}.
*/
public static final StreamCodec<ByteBuf, ByteBuffer> BYTE_BUFFER = byteBuffer(Integer.MAX_VALUE);
}

View File

@ -37,6 +37,7 @@ public final class ComputerActionServerMessage extends ComputerServerMessage {
@Override
protected void handle(ServerNetworkContext context, ComputerMenu container) {
switch (action) {
case TERMINATE -> container.getInput().terminate();
case TURN_ON -> container.getInput().turnOn();
case REBOOT -> container.getInput().reboot();
case SHUTDOWN -> container.getInput().shutdown();
@ -49,6 +50,7 @@ public final class ComputerActionServerMessage extends ComputerServerMessage {
}
public enum Action {
TERMINATE,
TURN_ON,
SHUTDOWN,
REBOOT

View File

@ -25,8 +25,8 @@ public abstract class ComputerServerMessage implements NetworkMessage<ServerNetw
@Override
public void handle(ServerNetworkContext context) {
Player player = context.getSender();
if (player.containerMenu.containerId == containerId && player.containerMenu instanceof ComputerMenu) {
handle(context, (ComputerMenu) player.containerMenu);
if (player.containerMenu.containerId == containerId && player.containerMenu instanceof ComputerMenu menu) {
handle(context, menu);
}
}

View File

@ -44,6 +44,7 @@ public final class KeyEventServerMessage extends ComputerServerMessage {
case UP -> input.keyUp(key);
case DOWN -> input.keyDown(key, false);
case REPEAT -> input.keyDown(key, true);
case CHAR -> input.charTyped((byte) key);
}
}
@ -53,6 +54,6 @@ public final class KeyEventServerMessage extends ComputerServerMessage {
}
public enum Action {
DOWN, REPEAT, UP
DOWN, REPEAT, UP, CHAR
}
}

View File

@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.network.server;
import dan200.computercraft.core.util.StringUtil;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.computer.menu.ServerInputHandler;
import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.network.codec.MoreStreamCodecs;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.world.inventory.AbstractContainerMenu;
import java.nio.ByteBuffer;
/**
* Paste a string on a {@link ServerComputer}.
*
* @see ServerInputHandler#paste(ByteBuffer)
*/
public class PasteEventComputerMessage extends ComputerServerMessage {
public static final StreamCodec<RegistryFriendlyByteBuf, PasteEventComputerMessage> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.VAR_INT, PasteEventComputerMessage::containerId,
MoreStreamCodecs.byteBuffer(StringUtil.MAX_PASTE_LENGTH), c -> c.text,
PasteEventComputerMessage::new
);
private final ByteBuffer text;
public PasteEventComputerMessage(AbstractContainerMenu menu, ByteBuffer text) {
this(menu.containerId, text);
}
private PasteEventComputerMessage(int id, ByteBuffer text) {
super(id);
this.text = text;
}
@Override
protected void handle(ServerNetworkContext context, ComputerMenu container) {
container.getInput().paste(text);
}
@Override
public CustomPacketPayload.Type<PasteEventComputerMessage> type() {
return NetworkMessages.PASTE_EVENT;
}
}

View File

@ -1,60 +0,0 @@
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.network.server;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.computer.menu.ServerInputHandler;
import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.util.NBTUtil;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.world.inventory.AbstractContainerMenu;
import javax.annotation.Nullable;
/**
* Queue an event on a {@link ServerComputer}.
*
* @see ServerInputHandler#queueEvent(String)
*/
public final class QueueEventServerMessage extends ComputerServerMessage {
public static final StreamCodec<RegistryFriendlyByteBuf, QueueEventServerMessage> STREAM_CODEC = StreamCodec.ofMember(QueueEventServerMessage::write, QueueEventServerMessage::new);
private final String event;
private final @Nullable Object[] args;
public QueueEventServerMessage(AbstractContainerMenu menu, String event, @Nullable Object[] args) {
super(menu.containerId);
this.event = event;
this.args = args;
}
private QueueEventServerMessage(FriendlyByteBuf buf) {
super(buf.readVarInt());
event = buf.readUtf(Short.MAX_VALUE);
var args = buf.readNbt();
this.args = args == null ? null : NBTUtil.decodeObjects(args);
}
private void write(RegistryFriendlyByteBuf buf) {
buf.writeVarInt(containerId());
buf.writeUtf(event);
buf.writeNbt(args == null ? null : NBTUtil.encodeObjects(args));
}
@Override
protected void handle(ServerNetworkContext context, ComputerMenu container) {
container.getInput().queueEvent(event, args);
}
@Override
public CustomPacketPayload.Type<QueueEventServerMessage> type() {
return NetworkMessages.QUEUE_EVENT;
}
}

View File

@ -8,7 +8,7 @@ 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.computer.core.ServerComputer;
import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
import dan200.computercraft.shared.network.server.ServerNetworking;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
@ -41,8 +41,8 @@ public final class PocketBrain implements IPocketAccess {
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);
public PocketBrain(PocketHolder holder, @Nullable UpgradeData<IPocketUpgrade> upgrade, ServerComputer.Properties properties) {
this.computer = new PocketServerComputer(this, holder, properties);
this.holder = holder;
this.position = holder.pos();
this.upgrade = upgrade;

View File

@ -5,15 +5,13 @@
package dan200.computercraft.shared.pocket.core;
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;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.config.ConfigSpec;
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 dan200.computercraft.shared.util.ComponentMap;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.ChunkPos;
@ -41,10 +39,10 @@ public final class PocketServerComputer extends ServerComputer {
private Set<ServerPlayer> tracking = Set.of();
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()
PocketServerComputer(PocketBrain brain, PocketHolder holder, ServerComputer.Properties properties) {
super(holder.level(), holder.blockPos(), properties
.terminalSize(ConfigSpec.pocketTermWidth.get(), ConfigSpec.pocketTermHeight.get())
.addComponent(ComputerComponents.POCKET, brain)
);
this.brain = brain;
}

View File

@ -196,7 +196,10 @@ public class PocketComputerItem extends Item implements IMedia {
}
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 brain = new PocketBrain(
holder, getUpgradeWithData(stack),
ServerComputer.properties(computerID, getFamily()).label(getLabel(stack))
);
var computer = brain.computer();
stack.set(ModRegistry.DataComponents.COMPUTER.get(), new ServerComputerReference(registry.getSessionID(), computer.register()));

View File

@ -24,7 +24,6 @@ import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.turtle.TurtleOverlay;
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;
@ -80,10 +79,10 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
@Override
protected ServerComputer createComputer(int id) {
var computer = new ServerComputer(
(ServerLevel) getLevel(), getBlockPos(), id, label,
getFamily(), Config.turtleTermWidth, Config.turtleTermHeight,
ComponentMap.builder().add(ComputerComponents.TURTLE, brain).build()
var computer = new ServerComputer((ServerLevel) getLevel(), getBlockPos(), ServerComputer.properties(id, getFamily())
.label(getLabel())
.terminalSize(Config.TURTLE_TERM_WIDTH, Config.TURTLE_TERM_HEIGHT)
.addComponent(ComputerComponents.TURTLE, brain)
);
brain.setupComputer(computer);
return computer;

View File

@ -44,125 +44,33 @@ public final class NBTUtil {
.ifPresent(x -> destination.put(key, x));
}
private static @Nullable Tag toNBTTag(@Nullable Object object) {
if (object == null) return null;
if (object instanceof Boolean) return ByteTag.valueOf((byte) ((boolean) (Boolean) object ? 1 : 0));
if (object instanceof Number) return DoubleTag.valueOf(((Number) object).doubleValue());
if (object instanceof String) return StringTag.valueOf(object.toString());
if (object instanceof Map<?, ?> m) {
var nbt = new CompoundTag();
var i = 0;
for (Map.Entry<?, ?> entry : m.entrySet()) {
var key = toNBTTag(entry.getKey());
var value = toNBTTag(entry.getKey());
if (key != null && value != null) {
nbt.put("k" + i, key);
nbt.put("v" + i, value);
i++;
}
}
nbt.putInt("len", m.size());
return nbt;
}
return null;
}
public static @Nullable CompoundTag encodeObjects(@Nullable Object[] objects) {
if (objects == null || objects.length == 0) return null;
var nbt = new CompoundTag();
nbt.putInt("len", objects.length);
for (var i = 0; i < objects.length; i++) {
var child = toNBTTag(objects[i]);
if (child != null) nbt.put(Integer.toString(i), child);
}
return nbt;
}
private static @Nullable Object fromNBTTag(@Nullable Tag tag) {
if (tag == null) return null;
switch (tag.getId()) {
case Tag.TAG_BYTE:
return ((ByteTag) tag).getAsByte() > 0;
case Tag.TAG_DOUBLE:
return ((DoubleTag) tag).getAsDouble();
default:
case Tag.TAG_STRING:
return tag.getAsString();
case Tag.TAG_COMPOUND: {
var c = (CompoundTag) tag;
var len = c.getInt("len");
Map<Object, Object> map = new HashMap<>(len);
for (var i = 0; i < len; i++) {
var key = fromNBTTag(c.get("k" + i));
var value = fromNBTTag(c.get("v" + i));
if (key != null && value != null) map.put(key, value);
}
return map;
}
}
}
public static @Nullable Object toLua(@Nullable Tag tag) {
if (tag == null) return null;
switch (tag.getId()) {
case Tag.TAG_BYTE:
case Tag.TAG_SHORT:
case Tag.TAG_INT:
case Tag.TAG_LONG:
return ((NumericTag) tag).getAsLong();
case Tag.TAG_FLOAT:
case Tag.TAG_DOUBLE:
return ((NumericTag) tag).getAsDouble();
case Tag.TAG_STRING: // String
return tag.getAsString();
case Tag.TAG_COMPOUND: { // Compound
return switch (tag.getId()) {
case Tag.TAG_BYTE, Tag.TAG_SHORT, Tag.TAG_INT, Tag.TAG_LONG -> ((NumericTag) tag).getAsLong();
case Tag.TAG_FLOAT, Tag.TAG_DOUBLE -> ((NumericTag) tag).getAsDouble();
case Tag.TAG_STRING -> tag.getAsString();
case Tag.TAG_COMPOUND -> {
var compound = (CompoundTag) tag;
Map<String, Object> map = new HashMap<>(compound.size());
for (var key : compound.getAllKeys()) {
var value = toLua(compound.get(key));
if (value != null) map.put(key, value);
}
return map;
yield map;
}
case Tag.TAG_LIST: {
var list = (ListTag) tag;
List<Object> map = new ArrayList<>(list.size());
for (var value : list) map.add(toLua(value));
return map;
}
case Tag.TAG_BYTE_ARRAY: {
case Tag.TAG_LIST -> ((ListTag) tag).stream().map(NBTUtil::toLua).toList();
case Tag.TAG_BYTE_ARRAY -> {
var array = ((ByteArrayTag) tag).getAsByteArray();
List<Byte> map = new ArrayList<>(array.length);
for (var b : array) map.add(b);
return map;
yield map;
}
case Tag.TAG_INT_ARRAY: {
var array = ((IntArrayTag) tag).getAsIntArray();
List<Integer> map = new ArrayList<>(array.length);
for (var j : array) map.add(j);
return map;
}
default:
return null;
}
}
public static @Nullable Object[] decodeObjects(CompoundTag tag) {
var len = tag.getInt("len");
if (len <= 0) return null;
var objects = new Object[len];
for (var i = 0; i < len; i++) {
var key = Integer.toString(i);
if (tag.contains(key)) {
objects[i] = fromNBTTag(tag.get(key));
}
}
return objects;
case Tag.TAG_INT_ARRAY -> Arrays.stream(((IntArrayTag) tag).getAsIntArray()).boxed().toList();
case Tag.TAG_LONG_ARRAY -> Arrays.stream(((LongArrayTag) tag).getAsLongArray()).boxed().toList();
default -> null;
};
}
@Nullable

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.mixin.gametest;
import com.mojang.datafixers.DataFixer;
import dan200.computercraft.gametest.core.TestHooks;
import net.minecraft.gametest.framework.GameTestServer;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.Services;
import net.minecraft.server.WorldStem;
import net.minecraft.server.level.progress.ChunkProgressListenerFactory;
import net.minecraft.server.packs.repository.PackRepository;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import java.net.Proxy;
import java.util.concurrent.locks.LockSupport;
@Mixin(GameTestServer.class)
abstract class GameTestServerMixin extends MinecraftServer {
GameTestServerMixin(Thread serverThread, LevelStorageSource.LevelStorageAccess storageSource, PackRepository packRepository, WorldStem worldStem, Proxy proxy, DataFixer fixerUpper, Services services, ChunkProgressListenerFactory progressListenerFactory) {
super(serverThread, storageSource, packRepository, worldStem, proxy, fixerUpper, services, progressListenerFactory);
}
/**
* Overwrite {@link GameTestServer#waitUntilNextTick()} to wait for all computers to finish executing.
* <p>
* This is a little dangerous (breaks async behaviour of computers), but it forces tests to be deterministic.
*
* @reason See above. This is only in the test mod, so no risk of collision.
* @author SquidDev.
*/
@Overwrite
@Override
public void waitUntilNextTick() {
while (true) {
runAllTasks();
if (!haveTestsStarted() || TestHooks.areComputersIdle(this)) break;
LockSupport.parkNanos(100_000);
}
}
@Shadow
private boolean haveTestsStarted() {
throw new AssertionError("Stub.");
}
}

View File

@ -5,6 +5,8 @@
package dan200.computercraft.gametest.core
import dan200.computercraft.api.ComputerCraftAPI
import dan200.computercraft.core.ComputerContext
import dan200.computercraft.core.computer.computerthread.ComputerThread
import dan200.computercraft.gametest.*
import dan200.computercraft.gametest.api.ClientGameTest
import dan200.computercraft.gametest.api.TestTags
@ -24,6 +26,8 @@ import net.minecraft.world.phys.Vec3
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.File
import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import java.lang.reflect.Modifier
@ -91,6 +95,9 @@ object TestHooks {
CCTestCommand.importFiles(server)
}
@JvmStatic
fun areComputersIdle(server: MinecraftServer) = ComputerThreadReflection.isFullyIdle(ServerContext.get(server))
private val testClasses = listOf(
Computer_Test::class.java,
CraftOs_Test::class.java,
@ -128,14 +135,6 @@ object TestHooks {
}
}
private val isCi = System.getenv("CI") != null
/**
* Adjust the timeout of a test. This makes it 1.5 times longer when run under CI, as CI servers are less powerful
* than our own.
*/
private fun adjustTimeout(timeout: Int): Int = if (isCi) timeout + (timeout / 2) else timeout
private fun registerTest(testClass: Class<*>, method: Method, fallbackRegister: Consumer<Method>) {
val className = testClass.simpleName.lowercase()
val testName = className + "." + method.name.lowercase()
@ -147,7 +146,7 @@ object TestHooks {
TestFunction(
testInfo.batch, testName, testInfo.template.ifEmpty { testName },
StructureUtils.getRotationForRotationSteps(testInfo.rotationSteps),
adjustTimeout(testInfo.timeoutTicks),
testInfo.timeoutTicks,
testInfo.setupTicks,
testInfo.required, testInfo.manualOnly,
testInfo.attempts,
@ -167,7 +166,7 @@ object TestHooks {
testName,
testName,
testInfo.template.ifEmpty { testName },
adjustTimeout(testInfo.timeoutTicks),
testInfo.timeoutTicks,
0,
true,
) { value -> safeInvoke(method, value) },
@ -215,3 +214,31 @@ object TestHooks {
return false
}
}
/**
* Nasty reflection to determine if computers are fully idle.
*
* This is horribly nasty, and should not be used as a model for any production code!
*
* @see [ComputerThread.isFullyIdle]
* @see [dan200.computercraft.mixin.gametest.GameTestServerMixin]
*/
private object ComputerThreadReflection {
private val lookup = MethodHandles.lookup()
@JvmField
val computerContext: MethodHandle = lookup.unreflectGetter(
ServerContext::class.java.getDeclaredField("context").also { it.isAccessible = true },
)
@JvmField
val isFullyIdle: MethodHandle = lookup.unreflect(
ComputerThread::class.java.getDeclaredMethod("isFullyIdle").also { it.isAccessible = true },
)
fun isFullyIdle(context: ServerContext): Boolean {
val computerContext = computerContext.invokeExact(context) as ComputerContext
val computerThread = computerContext.computerScheduler() as ComputerThread
return isFullyIdle.invokeExact(computerThread) as Boolean
}
}

View File

@ -35,7 +35,7 @@ class MultiTestReporter(private val reporters: List<TestReporter>) : TestReporte
* Reports tests to a JUnit XML file. This is equivalent to [JUnitLikeTestReporter], except it ensures the destination
* directory exists.
*/
class JunitTestReporter constructor(destination: File) : JUnitLikeTestReporter(destination) {
class JunitTestReporter(destination: File) : JUnitLikeTestReporter(destination) {
override fun save(file: File) {
try {
Files.createDirectories(file.toPath().parent)

View File

@ -11,6 +11,7 @@
"GameTestInfoAccessor",
"GameTestSequenceAccessor",
"GameTestSequenceMixin",
"GameTestServerMixin",
"StructureTemplateManagerMixin",
"TestCommandAccessor"
],

View File

@ -40,6 +40,8 @@ dependencies {
}
tasks.processResources {
inputs.property("gitHash", cct.gitHash)
var props = mapOf("gitContributors" to cct.gitContributors.get().joinToString("\n"))
filesMatching("data/computercraft/lua/rom/help/credits.md") { expand(props) }
}

View File

@ -31,7 +31,7 @@ import java.util.concurrent.atomic.AtomicLong;
* <li>Passes main thread tasks to the {@link MainThreadScheduler.Executor}.</li>
* </ul>
*/
public class Computer {
public class Computer implements ComputerEvents.Receiver {
private static final int START_DELAY = 50;
// Various properties of the computer
@ -114,6 +114,7 @@ public class Computer {
executor.queueStop(false, true);
}
@Override
public void queueEvent(String event, @Nullable Object[] args) {
executor.queueEvent(event, args);
}

View File

@ -0,0 +1,72 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.computer;
import dan200.computercraft.core.util.StringUtil;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
/**
* Built-in events that can be queued on a computer.
*/
public final class ComputerEvents {
private ComputerEvents() {
}
public static void keyDown(Receiver receiver, int key, boolean repeat) {
receiver.queueEvent("key", new Object[]{ key, repeat });
}
public static void keyUp(Receiver receiver, int key) {
receiver.queueEvent("key_up", new Object[]{ key });
}
/**
* Type a character on the computer.
*
* @param receiver The computer to queue the event on.
* @param chr The character to type.
* @see StringUtil#isTypableChar(byte)
*/
public static void charTyped(Receiver receiver, byte chr) {
receiver.queueEvent("char", new Object[]{ new byte[]{ chr } });
}
/**
* Paste a string.
*
* @param receiver The computer to queue the event on.
* @param contents The string to paste.
* @see StringUtil#getClipboardString(String)
*/
public static void paste(Receiver receiver, ByteBuffer contents) {
receiver.queueEvent("paste", new Object[]{ contents });
}
public static void mouseClick(Receiver receiver, int button, int x, int y) {
receiver.queueEvent("mouse_click", new Object[]{ button, x, y });
}
public static void mouseUp(Receiver receiver, int button, int x, int y) {
receiver.queueEvent("mouse_up", new Object[]{ button, x, y });
}
public static void mouseDrag(Receiver receiver, int button, int x, int y) {
receiver.queueEvent("mouse_drag", new Object[]{ button, x, y });
}
public static void mouseScroll(Receiver receiver, int direction, int x, int y) {
receiver.queueEvent("mouse_scroll", new Object[]{ direction, x, y });
}
/**
* An object that can receive computer events.
*/
@FunctionalInterface
public interface Receiver {
void queueEvent(String event, @Nullable Object[] arguments);
}
}

View File

@ -433,6 +433,16 @@ public final class ComputerThread implements ComputerScheduler {
return computerQueueSize() > idleWorkers.get();
}
/**
* Determine if no work is queued, and all workers are idle.
*
* @return If the threads are fully idle.
*/
@VisibleForTesting
boolean isFullyIdle() {
return computerQueueSize() == 0 && idleWorkers.get() >= workerCount();
}
private void workerFinished(WorkerThread worker) {
// We should only shut down a worker once! This should only happen if we fail to abort a worker and then the
// worker finishes normally.

View File

@ -4,52 +4,116 @@
package dan200.computercraft.core.util;
import dan200.computercraft.core.computer.ComputerEvents;
import java.nio.ByteBuffer;
public final class StringUtil {
public static final int MAX_PASTE_LENGTH = 512;
private StringUtil() {
}
private static boolean isAllowed(char c) {
return (c >= ' ' && c <= '~') || (c >= 161 && c <= 172) || (c >= 174 && c <= 255);
}
private static String removeSpecialCharacters(String text, int length) {
var builder = new StringBuilder(length);
for (var i = 0; i < length; i++) {
var c = text.charAt(i);
builder.append(isAllowed(c) ? c : '?');
/**
* Convert a Unicode character to a terminal one.
*
* @param chr The Unicode character.
* @return The terminal character.
*/
public static byte unicodeToTerminal(int chr) {
// ASCII and latin1 map to themselves
if (chr == 0 || chr == '\t' || chr == '\n' || chr == '\r' || (chr >= ' ' && chr <= '~') || (chr >= 160 && chr <= 255)) {
return (byte) chr;
}
return builder.toString();
// Teletext block mosaics are *fairly* contiguous.
if (chr >= 0x1FB00 && chr <= 0x1FB13) return (byte) (chr + (129 - 0x1fb00));
if (chr >= 0x1FB14 && chr <= 0x1FB1D) return (byte) (chr + (150 - 0x1fb14));
// Everything else is just a manual lookup. For now, we just use a big switch statement, which we spin into a
// separate function to hopefully avoid inlining it here.
return unicodeToCraftOsFallback(chr);
}
public static String normaliseLabel(String text) {
return removeSpecialCharacters(text, Math.min(32, text.length()));
private static byte unicodeToCraftOsFallback(int c) {
return switch (c) {
case 0x263A -> 1;
case 0x263B -> 2;
case 0x2665 -> 3;
case 0x2666 -> 4;
case 0x2663 -> 5;
case 0x2660 -> 6;
case 0x2022 -> 7;
case 0x25D8 -> 8;
case 0x2642 -> 11;
case 0x2640 -> 12;
case 0x266A -> 14;
case 0x266B -> 15;
case 0x25BA -> 16;
case 0x25C4 -> 17;
case 0x2195 -> 18;
case 0x203C -> 19;
case 0x25AC -> 22;
case 0x21A8 -> 23;
case 0x2191 -> 24;
case 0x2193 -> 25;
case 0x2192 -> 26;
case 0x2190 -> 27;
case 0x221F -> 28;
case 0x2194 -> 29;
case 0x25B2 -> 30;
case 0x25BC -> 31;
case 0x1FB99 -> 127;
case 0x258C -> (byte) 149;
default -> '?';
};
}
/**
* Normalise a string from the clipboard, suitable for pasting into a computer.
* Check if a character is capable of being input and passed to a {@linkplain ComputerEvents#charTyped(ComputerEvents.Receiver, byte)
* "char" event}.
*
* @param chr The character to check.
* @return Whether this character can be typed.
*/
public static boolean isTypableChar(byte chr) {
return chr != 0 && chr != '\r' && chr != '\n';
}
private static boolean isAllowedInLabel(char c) {
// Limit to ASCII and latin1, excluding '§' (Minecraft's formatting character).
return (c >= ' ' && c <= '~') || (c >= 161 && c <= 255 && c != 167);
}
public static String normaliseLabel(String text) {
var length = Math.min(32, text.length());
var builder = new StringBuilder(length);
for (var i = 0; i < length; i++) {
var c = text.charAt(i);
builder.append(isAllowedInLabel(c) ? c : '?');
}
return builder.toString();
}
/**
* Convert a Java string to a Lua one (using the terminal charset), suitable for pasting into a computer.
* <p>
* This removes special characters and strips to the first line of text.
*
* @param clipboard The text from the clipboard.
* @return The normalised clipboard text.
* @return The encoded clipboard text.
*/
public static String normaliseClipboardString(String clipboard) {
// Clip to the first occurrence of \r or \n
var newLineIndex1 = clipboard.indexOf('\r');
var newLineIndex2 = clipboard.indexOf('\n');
public static ByteBuffer getClipboardString(String clipboard) {
var output = new byte[Math.min(MAX_PASTE_LENGTH, clipboard.length())];
var idx = 0;
int length;
if (newLineIndex1 >= 0 && newLineIndex2 >= 0) {
length = Math.min(newLineIndex1, newLineIndex2);
} else if (newLineIndex1 >= 0) {
length = newLineIndex1;
} else if (newLineIndex2 >= 0) {
length = newLineIndex2;
} else {
length = clipboard.length();
var iterator = clipboard.codePoints().iterator();
while (iterator.hasNext() && idx <= output.length) {
var chr = unicodeToTerminal(iterator.next());
if (!isTypableChar(chr)) break;
output[idx++] = chr;
}
return removeSpecialCharacters(clipboard, Math.min(length, 512));
return ByteBuffer.wrap(output, 0, idx).asReadOnlyBuffer();
}
}

View File

@ -221,6 +221,8 @@ loom {
}
tasks.processResources {
inputs.property("modVersion", modVersion)
var props = mapOf("version" to modVersion)
filesMatching("fabric.mod.json") { expand(props) }

View File

@ -198,6 +198,9 @@ dependencies {
// Compile tasks
tasks.processResources {
inputs.property("modVersion", modVersion)
inputs.property("neoVersion", libs.versions.neoForge)
var props = mapOf(
"neoVersion" to libs.versions.neoForge.get(),
"file" to mapOf("jarVersion" to modVersion),

View File

@ -8,6 +8,7 @@ import dan200.computercraft.core.apis.handles.ArrayByteChannel;
import dan200.computercraft.core.apis.transfer.TransferredFile;
import dan200.computercraft.core.apis.transfer.TransferredFiles;
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.ComputerEvents;
import dan200.computercraft.core.util.StringUtil;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWDropCallback;
@ -49,10 +50,8 @@ public class InputState {
}
public void onCharEvent(int codepoint) {
if (codepoint >= 32 && codepoint <= 126 || codepoint >= 160 && codepoint <= 255) {
// Queue the char event for any printable chars in byte range
computer.queueEvent("char", new Object[]{ Character.toString(codepoint) });
}
var terminalChar = StringUtil.unicodeToTerminal(codepoint);
if (StringUtil.isTypableChar(terminalChar)) ComputerEvents.charTyped(computer, terminalChar);
}
public void onKeyEvent(long window, int key, int action, int modifiers) {
@ -68,8 +67,8 @@ public class InputState {
if (key == GLFW.GLFW_KEY_V && modifiers == GLFW.GLFW_MOD_CONTROL) {
var string = GLFW.glfwGetClipboardString(window);
if (string != null) {
var clipboard = StringUtil.normaliseClipboardString(string);
if (!clipboard.isEmpty()) computer.queueEvent("paste", new Object[]{ clipboard });
var clipboard = StringUtil.getClipboardString(string);
if (clipboard.remaining() > 0) ComputerEvents.paste(computer, clipboard);
}
return;
}
@ -92,7 +91,7 @@ public class InputState {
// Queue the "key" event and add to the down set
var repeat = keysDown.get(key);
keysDown.set(key);
computer.queueEvent("key", new Object[]{ key, repeat });
ComputerEvents.keyDown(computer, key, repeat);
}
}
@ -100,7 +99,7 @@ public class InputState {
// Queue the "key_up" event and remove from the down set
if (key >= 0 && keysDown.get(key)) {
keysDown.set(key, false);
computer.queueEvent("key_up", new Object[]{ key });
ComputerEvents.keyUp(computer, key);
}
switch (key) {
@ -115,12 +114,12 @@ public class InputState {
public void onMouseClick(int button, int action) {
switch (action) {
case GLFW.GLFW_PRESS -> {
computer.queueEvent("mouse_click", new Object[]{ button + 1, lastMouseX + 1, lastMouseY + 1 });
ComputerEvents.mouseClick(computer, button + 1, lastMouseX + 1, lastMouseY + 1);
lastMouseButton = button;
}
case GLFW.GLFW_RELEASE -> {
if (button == lastMouseButton) {
computer.queueEvent("mouse_click", new Object[]{ button + 1, lastMouseX + 1, lastMouseY + 1 });
ComputerEvents.mouseUp(computer, button + 1, lastMouseX + 1, lastMouseY + 1);
lastMouseButton = -1;
}
}
@ -133,13 +132,13 @@ public class InputState {
lastMouseX = mouseX;
lastMouseY = mouseY;
if (lastMouseButton != -1) {
computer.queueEvent("mouse_drag", new Object[]{ lastMouseButton + 1, mouseX + 1, mouseY + 1 });
ComputerEvents.mouseDrag(computer, lastMouseButton + 1, mouseX + 1, mouseY + 1);
}
}
public void onMouseScroll(double yOffset) {
if (yOffset != 0) {
computer.queueEvent("mouse_scroll", new Object[]{ yOffset < 0 ? 1 : -1, lastMouseX + 1, lastMouseY + 1 });
ComputerEvents.mouseScroll(computer, yOffset < 0 ? 1 : -1, lastMouseX + 1, lastMouseY + 1);
}
}