1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-07-22 11:52:53 +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 name: Bug report
description: Report some misbehaviour in the mod description: Report some misbehaviour in the mod
labels: [ bug ] labels: [ bug ]
type: bug
body: body:
- type: dropdown - type: dropdown
id: mc-version id: mc-version

View File

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

View File

@ -10,9 +10,7 @@ import org.gradle.api.GradleException
import org.gradle.api.NamedDomainObjectProvider import org.gradle.api.NamedDomainObjectProvider
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.Task import org.gradle.api.Task
import org.gradle.api.artifacts.Dependency
import org.gradle.api.plugins.JavaPluginExtension import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Provider import org.gradle.api.provider.Provider
import org.gradle.api.provider.SetProperty import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.SourceSet
@ -33,6 +31,10 @@ import java.net.URI
import java.util.regex.Pattern import java.util.regex.Pattern
abstract class CCTweakedExtension(private val project: Project) { 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. */ /** Get the current git branch. */
val gitBranch: Provider<String> = val gitBranch: Provider<String> =
gitProvider("<no git branch>", listOf("rev-parse", "--abbrev-ref", "HEAD")) { it.trim() } 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) jacoco.applyTo(this)
extensions.configure(JacocoTaskExtension::class.java) { extensions.configure(JacocoTaskExtension::class.java) {
includes = listOf("dan200.computercraft.*")
excludes = listOf( excludes = listOf(
"dan200.computercraft.mixin.*", // Exclude mixins, as they're not executed at runtime. "dan200.computercraft.mixin.*", // Exclude mixins, as they're not executed at runtime.
"dan200.computercraft.shared.Capabilities$*", // Exclude capability tokens, as Forge rewrites them. "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 { public interface WiredElement extends WiredSender {
/** /**
* Called when objects on the network change. This may occur when network nodes are added or removed, or when * Called when peripherals on the network change. This may occur when network nodes are added or removed, or when
* peripherals change. * peripherals are attached or detached from a modem.
* *
* @param change The change which occurred. * @param change The change which occurred.
* @see WiredNetworkChange * @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.ComputerActionServerMessage;
import dan200.computercraft.shared.network.server.KeyEventServerMessage; import dan200.computercraft.shared.network.server.KeyEventServerMessage;
import dan200.computercraft.shared.network.server.MouseEventServerMessage; 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 net.minecraft.world.inventory.AbstractContainerMenu;
import javax.annotation.Nullable; import java.nio.ByteBuffer;
/** /**
* An {@link InputHandler} for use on the client. * An {@link InputHandler} for use on the client.
@ -27,6 +27,11 @@ public final class ClientInputHandler implements InputHandler {
this.menu = menu; this.menu = menu;
} }
@Override
public void terminate() {
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TERMINATE));
}
@Override @Override
public void turnOn() { public void turnOn() {
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TURN_ON)); 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)); 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 @Override
public void keyDown(int key, boolean repeat) { public void keyDown(int key, boolean repeat) {
ClientNetworking.sendToServer(new KeyEventServerMessage(menu, repeat ? KeyEventServerMessage.Action.REPEAT : KeyEventServerMessage.Action.DOWN, key)); 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)); 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 @Override
public void mouseClick(int button, int x, int y) { public void mouseClick(int button, int x, int y) {
ClientNetworking.sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.Action.CLICK, button, x, 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) { private static ButtonTextures button(String name) {
return new ButtonTextures( return new ButtonTextures(
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name), ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "buttons/" + name),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name + "_hover") 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). * @param active The texture for the button when it is active (hovered or focused).
*/ */
public record ButtonTextures(ResourceLocation normal, ResourceLocation active) { public record ButtonTextures(ResourceLocation normal, ResourceLocation active) {
public TextureAtlasSprite get(boolean active) { public ResourceLocation get(boolean isActive) {
return GuiSprites.get(active ? this.active : normal); return isActive ? active : normal;
}
public Stream<ResourceLocation> textures() {
return Stream.of(normal, active);
} }
} }

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_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 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_WIDTH = 278;
private static final int TEX_HEIGHT = 217; private static final int TEX_HEIGHT = 217;
@ -54,9 +57,9 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
if (slot >= 0) { if (slot >= 0) {
var slotX = slot % 4; var slotX = slot % 4;
var slotY = slot / 4; var slotY = slot / 4;
graphics.blit(texture, graphics.blitSprite(
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18, 0, advanced ? SELECTED_ADVANCED : SELECTED_NORMAL,
0, 217, 24, 24, FULL_TEX_SIZE, FULL_TEX_SIZE 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( add.accept(new DynamicImageButton(
x, y, ICON_WIDTH, ICON_HEIGHT, x, y, ICON_WIDTH, ICON_HEIGHT,
GuiSprites.TERMINATE::get, GuiSprites.TERMINATE::get,
b -> input.queueEvent("terminate"), b -> input.terminate(),
new HintedMessage( new HintedMessage(
Component.translatable("gui.computercraft.tooltip.terminate"), Component.translatable("gui.computercraft.tooltip.terminate"),
Component.translatable("gui.computercraft.tooltip.terminate.key") 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.GuiGraphics;
import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -21,11 +21,11 @@ import java.util.function.Supplier;
* dynamically. * dynamically.
*/ */
public class DynamicImageButton extends Button { public class DynamicImageButton extends Button {
private final Boolean2ObjectFunction<TextureAtlasSprite> texture; private final Boolean2ObjectFunction<ResourceLocation> texture;
private final Supplier<HintedMessage> message; private final Supplier<HintedMessage> message;
public DynamicImageButton( 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 HintedMessage message
) { ) {
this(x, y, width, height, texture, onPress, () -> message); this(x, y, width, height, texture, onPress, () -> message);
@ -33,7 +33,7 @@ public class DynamicImageButton extends Button {
public DynamicImageButton( public DynamicImageButton(
int x, int y, int width, int height, int x, int y, int width, int height,
Boolean2ObjectFunction<TextureAtlasSprite> texture, Boolean2ObjectFunction<ResourceLocation> texture,
OnPress onPress, Supplier<HintedMessage> message OnPress onPress, Supplier<HintedMessage> message
) { ) {
super(x, y, width, height, Component.empty(), onPress, DEFAULT_NARRATION); 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()); var texture = this.texture.get(isHoveredOrFocused());
RenderSystem.disableDepthTest(); RenderSystem.disableDepthTest();
graphics.blit(getX(), getY(), 0, width, height, texture); graphics.blitSprite(texture, getX(), getY(), 0, width, height);
RenderSystem.enableDepthTest(); RenderSystem.enableDepthTest();
} }

View File

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

View File

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

View File

@ -1,11 +1,5 @@
{ {
"sources": [ "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/border_normal"},
{"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_normal"}, {"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_normal"},
{"type": "minecraft:single", "resource": "computercraft:gui/sidebar_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.ComputerState;
import dan200.computercraft.shared.computer.core.ServerComputer; 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.config.Config; import dan200.computercraft.shared.config.ConfigSpec;
import dan200.computercraft.shared.util.ComponentMap;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
@ -33,10 +32,9 @@ public class ComputerBlockEntity extends AbstractComputerBlockEntity {
@Override @Override
protected ServerComputer createComputer(int id) { protected ServerComputer createComputer(int id) {
return new ServerComputer( return new ServerComputer((ServerLevel) getLevel(), getBlockPos(), ServerComputer.properties(id, getFamily())
(ServerLevel) getLevel(), getBlockPos(), id, label, .label(getLabel())
getFamily(), Config.computerTermWidth, Config.computerTermHeight, .terminalSize(ConfigSpec.computerTermWidth.get(), ConfigSpec.computerTermHeight.get())
ComponentMap.empty()
); );
} }

View File

@ -6,44 +6,33 @@ package dan200.computercraft.shared.computer.core;
import dan200.computercraft.shared.computer.menu.ServerInputHandler; 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 ServerInputHandler
* @see ServerComputer * @see ServerComputer
*/ */
public interface InputHandler { public interface InputHandler {
void queueEvent(String event, @Nullable Object[] arguments); void keyDown(int key, boolean repeat);
default void queueEvent(String event) { void keyUp(int key);
queueEvent(event, null);
}
default void keyDown(int key, boolean repeat) { void charTyped(byte chr);
queueEvent("key", new Object[]{ key, repeat });
}
default void keyUp(int key) { void paste(ByteBuffer contents);
queueEvent("key_up", new Object[]{ key });
}
default void mouseClick(int button, int x, int y) { void mouseClick(int button, int x, int y);
queueEvent("mouse_click", new Object[]{ button, x, y });
}
default void mouseUp(int button, int x, int y) { void mouseUp(int button, int x, int y);
queueEvent("mouse_up", new Object[]{ button, x, y });
}
default void mouseDrag(int button, int x, int y) { void mouseDrag(int button, int x, int y);
queueEvent("mouse_drag", new Object[]{ button, x, y });
}
default void mouseScroll(int direction, int x, int y) { void mouseScroll(int direction, int x, int y);
queueEvent("mouse_scroll", new Object[]{ direction, x, y });
} void terminate();
void shutdown(); void shutdown();

View File

@ -6,12 +6,14 @@ package dan200.computercraft.shared.computer.core;
import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.component.AdminComputer; import dan200.computercraft.api.component.AdminComputer;
import dan200.computercraft.api.component.ComputerComponent;
import dan200.computercraft.api.component.ComputerComponents; import dan200.computercraft.api.component.ComputerComponents;
import dan200.computercraft.api.filesystem.WritableMount; import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.peripheral.WorkMonitor; import dan200.computercraft.api.peripheral.WorkMonitor;
import dan200.computercraft.core.computer.Computer; import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.ComputerEnvironment; import dan200.computercraft.core.computer.ComputerEnvironment;
import dan200.computercraft.core.computer.ComputerEvents;
import dan200.computercraft.core.computer.ComputerSide; import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.metrics.MetricsObserver; import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.impl.ApiFactories; import dan200.computercraft.impl.ApiFactories;
@ -34,7 +36,7 @@ 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 InputHandler, ComputerEnvironment { public class ServerComputer implements ComputerEnvironment, ComputerEvents.Receiver {
private final UUID instanceUUID = UUID.randomUUID(); private final UUID instanceUUID = UUID.randomUUID();
private ServerLevel level; private ServerLevel level;
@ -49,16 +51,21 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
private int ticksSincePing; private int ticksSincePing;
@Deprecated
public ServerComputer( public ServerComputer(
ServerLevel level, BlockPos position, int computerID, @Nullable String label, ComputerFamily family, int terminalWidth, int terminalHeight, ServerLevel level, BlockPos position, int computerID, @Nullable String label, ComputerFamily family, int terminalWidth, int terminalHeight,
ComponentMap baseComponents 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.level = level;
this.position = position; this.position = position;
this.family = family; this.family = properties.family;
var context = ServerContext.get(level.getServer()); 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); metrics = context.metrics().createMetricObserver(this);
var componentBuilder = ComponentMap.builder(); var componentBuilder = ComponentMap.builder();
@ -67,11 +74,11 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
componentBuilder.add(ComputerComponents.ADMIN_COMPUTER, new AdminComputer() { componentBuilder.add(ComputerComponents.ADMIN_COMPUTER, new AdminComputer() {
}); });
} }
componentBuilder.add(baseComponents); componentBuilder.add(properties.components.build());
var components = componentBuilder.build(); var components = componentBuilder.build();
computer = new Computer(context.computerContext(), this, terminal, computerID); computer = new Computer(context.computerContext(), this, terminal, properties.computerID);
computer.setLabel(label); computer.setLabel(properties.label);
// Load in the externally registered APIs. // Load in the externally registered APIs.
for (var factory : ApiFactories.getAll()) { for (var factory : ApiFactories.getAll()) {
@ -84,24 +91,24 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
} }
} }
public ComputerFamily getFamily() { public final ComputerFamily getFamily() {
return family; return family;
} }
public ServerLevel getLevel() { public final ServerLevel getLevel() {
return level; return level;
} }
public BlockPos getPosition() { public final BlockPos getPosition() {
return position; return position;
} }
public void setPosition(ServerLevel level, BlockPos pos) { public final void setPosition(ServerLevel level, BlockPos pos) {
this.level = level; this.level = level;
position = pos.immutable(); position = pos.immutable();
} }
protected void markTerminalChanged() { protected final void markTerminalChanged() {
terminalChanged.set(true); terminalChanged.set(true);
} }
@ -115,11 +122,11 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
sendToAllInteracting(c -> new ComputerTerminalClientMessage(c, getTerminalState())); sendToAllInteracting(c -> new ComputerTerminalClientMessage(c, getTerminalState()));
} }
public TerminalState getTerminalState() { public final TerminalState getTerminalState() {
return TerminalState.create(terminal); return TerminalState.create(terminal);
} }
public void keepAlive() { public final void keepAlive() {
ticksSincePing = 0; ticksSincePing = 0;
} }
@ -132,7 +139,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
* *
* @return What sides on the computer have changed. * @return What sides on the computer have changed.
*/ */
public int pollRedstoneChanges() { public final int pollRedstoneChanges() {
return computer.pollRedstoneChanges(); return computer.pollRedstoneChanges();
} }
@ -145,7 +152,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
computer.unload(); computer.unload();
} }
public void close() { public final void close() {
unload(); unload();
ServerContext.get(level.getServer()).registry().remove(this); ServerContext.get(level.getServer()).registry().remove(this);
} }
@ -165,7 +172,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
var server = level.getServer(); var server = level.getServer();
for (var player : server.getPlayerList().getPlayers()) { 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); ServerNetworking.sendToPlayer(createPacket.apply(player.containerMenu), player);
} }
} }
@ -174,93 +181,136 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
protected void onRemoved() { protected void onRemoved() {
} }
public UUID getInstanceUUID() { public final UUID getInstanceUUID() {
return instanceUUID; return instanceUUID;
} }
public int getID() { public final int getID() {
return computer.getID(); return computer.getID();
} }
public @Nullable String getLabel() { public final @Nullable String getLabel() {
return computer.getLabel(); return computer.getLabel();
} }
public boolean isOn() { public final boolean isOn() {
return computer.isOn(); return computer.isOn();
} }
public ComputerState getState() { public final ComputerState getState() {
if (!computer.isOn()) return ComputerState.OFF; if (!computer.isOn()) return ComputerState.OFF;
return computer.isBlinking() ? ComputerState.BLINKING : ComputerState.ON; return computer.isBlinking() ? ComputerState.BLINKING : ComputerState.ON;
} }
@Override public final void turnOn() {
public void turnOn() {
computer.turnOn(); computer.turnOn();
} }
@Override public final void shutdown() {
public void shutdown() {
computer.shutdown(); computer.shutdown();
} }
@Override public final void reboot() {
public void reboot() {
computer.reboot(); computer.reboot();
} }
@Override @Override
public void queueEvent(String event, @Nullable Object[] arguments) { public final void queueEvent(String event, @Nullable Object[] arguments) {
computer.queueEvent(event, 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; 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); 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; 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); computer.getEnvironment().setPeripheral(side, peripheral);
} }
@Nullable @Nullable
public IPeripheral getPeripheral(ComputerSide side) { public final IPeripheral getPeripheral(ComputerSide side) {
return computer.getEnvironment().getPeripheral(side); return computer.getEnvironment().getPeripheral(side);
} }
public void setLabel(@Nullable String label) { public final void setLabel(@Nullable String label) {
computer.setLabel(label); computer.setLabel(label);
} }
@Override @Override
public double getTimeOfDay() { public final double getTimeOfDay() {
return (level.getDayTime() + 6000) % 24000 / 1000.0; return (level.getDayTime() + 6000) % 24000 / 1000.0;
} }
@Override @Override
public int getDay() { public final int getDay() {
return (int) ((level.getDayTime() + 6000) / 24000) + 1; return (int) ((level.getDayTime() + 6000) / 24000) + 1;
} }
@Override @Override
public MetricsObserver getMetrics() { public final MetricsObserver getMetrics() {
return metrics; return metrics;
} }
public WorkMonitor getMainThreadMonitor() { public final WorkMonitor getMainThreadMonitor() {
return computer.getMainThreadMonitor(); return computer.getMainThreadMonitor();
} }
@Override @Override
public @Nullable WritableMount createRootMount() { public final WritableMount createRootMount() {
return ComputerCraftAPI.createSaveDirMount(level.getServer(), "computer/" + computer.getID(), Config.computerSpaceLimit); 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.handles.ByteBufferChannel;
import dan200.computercraft.core.apis.transfer.TransferredFile; import dan200.computercraft.core.apis.transfer.TransferredFile;
import dan200.computercraft.core.apis.transfer.TransferredFiles; 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.FileSlice;
import dan200.computercraft.shared.computer.upload.FileUpload; import dan200.computercraft.shared.computer.upload.FileUpload;
import dan200.computercraft.shared.computer.upload.UploadResult; import dan200.computercraft.shared.computer.upload.UploadResult;
@ -21,6 +23,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -48,21 +51,33 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
this.owner = owner; this.owner = owner;
} }
@Override
public void queueEvent(String event, @Nullable Object[] arguments) {
owner.getComputer().queueEvent(event, arguments);
}
@Override @Override
public void keyDown(int key, boolean repeat) { public void keyDown(int key, boolean repeat) {
keysDown.add(key); keysDown.add(key);
owner.getComputer().keyDown(key, repeat); ComputerEvents.keyDown(owner.getComputer(), key, repeat);
} }
@Override @Override
public void keyUp(int key) { public void keyUp(int key) {
keysDown.remove(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 @Override
@ -71,7 +86,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
lastMouseY = y; lastMouseY = y;
lastMouseDown = button; lastMouseDown = button;
owner.getComputer().mouseClick(button, x, y); ComputerEvents.mouseClick(owner.getComputer(), button, x, y);
} }
@Override @Override
@ -80,7 +95,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
lastMouseY = y; lastMouseY = y;
lastMouseDown = -1; lastMouseDown = -1;
owner.getComputer().mouseUp(button, x, y); ComputerEvents.mouseUp(owner.getComputer(), button, x, y);
} }
@Override @Override
@ -89,7 +104,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
lastMouseY = y; lastMouseY = y;
lastMouseDown = button; lastMouseDown = button;
owner.getComputer().mouseDrag(button, x, y); ComputerEvents.mouseDrag(owner.getComputer(), button, x, y);
} }
@Override @Override
@ -97,7 +112,12 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
lastMouseX = x; lastMouseX = x;
lastMouseY = y; lastMouseY = y;
owner.getComputer().mouseScroll(direction, x, y); ComputerEvents.mouseScroll(owner.getComputer(), direction, x, y);
}
@Override
public void terminate() {
owner.getComputer().queueEvent("terminate");
} }
@Override @Override
@ -169,9 +189,9 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
public void close() { public void close() {
var computer = owner.getComputer(); var computer = owner.getComputer();
var keys = keysDown.iterator(); 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(); keysDown.clear();
lastMouseDown = -1; lastMouseDown = -1;

View File

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

View File

@ -344,13 +344,13 @@ public final class ConfigSpec {
.push("term_sizes"); .push("term_sizes");
builder.comment("Terminal size of computers.").push("computer"); builder.comment("Terminal size of computers.").push("computer");
computerTermWidth = builder.comment("Width of computer terminal").defineInRange("width", Config.computerTermWidth, 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.computerTermHeight, 1, 255); computerTermHeight = builder.comment("Height of computer terminal").defineInRange("height", Config.DEFAULT_COMPUTER_TERM_HEIGHT, 1, 255);
builder.pop(); builder.pop();
builder.comment("Terminal size of pocket computers.").push("pocket_computer"); builder.comment("Terminal size of pocket computers.").push("pocket_computer");
pocketTermWidth = builder.comment("Width of pocket computer terminal").defineInRange("width", Config.pocketTermWidth, 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.pocketTermHeight, 1, 255); pocketTermHeight = builder.comment("Height of pocket computer terminal").defineInRange("height", Config.DEFAULT_POCKET_TERM_HEIGHT, 1, 255);
builder.pop(); builder.pop();
builder.comment("Maximum size of monitors (in blocks).").push("monitor"); builder.comment("Maximum size of monitors (in blocks).").push("monitor");
@ -437,10 +437,6 @@ public final class ConfigSpec {
Config.turtlesCanPush = turtlesCanPush.get(); Config.turtlesCanPush = turtlesCanPush.get();
// Terminal size // Terminal size
Config.computerTermWidth = computerTermWidth.get();
Config.computerTermHeight = computerTermHeight.get();
Config.pocketTermWidth = pocketTermWidth.get();
Config.pocketTermHeight = pocketTermHeight.get();
Config.monitorWidth = monitorWidth.get(); Config.monitorWidth = monitorWidth.get();
Config.monitorHeight = monitorHeight.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<>(); 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<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<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<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<UploadFileMessage> UPLOAD_FILE = register(serverMessages, "upload_file", UploadFileMessage.STREAM_CODEC);
public static final CustomPacketPayload.Type<ChatTableClientMessage> CHAT_TABLE = registerClientbound("chat_table", ChatTableClientMessage.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<>() { public static StreamCodec<ByteBuf, ByteBuffer> byteBuffer(int limit) {
@Override return new StreamCodec<>() {
public ByteBuffer decode(ByteBuf buf) { @Override
var toRead = VarInt.read(buf); public ByteBuffer decode(ByteBuf buf) {
if (toRead > buf.readableBytes()) { var toRead = VarInt.read(buf);
throw new DecoderException("ByteArray with size " + toRead + " is bigger than allowed"); 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]; @Override
buf.readBytes(bytes); public void encode(ByteBuf buf, ByteBuffer buffer) {
return ByteBuffer.wrap(bytes).asReadOnlyBuffer(); VarInt.write(buf, buffer.remaining());
} buf.writeBytes(buffer.duplicate());
}
};
}
@Override /**
public void encode(ByteBuf buf, ByteBuffer buffer) { * Equivalent to {@link ByteBufCodecs#BYTE_ARRAY}, but into an immutable {@link ByteBuffer}.
VarInt.write(buf, buffer.remaining()); */
buf.writeBytes(buffer.duplicate()); 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 @Override
protected void handle(ServerNetworkContext context, ComputerMenu container) { protected void handle(ServerNetworkContext context, ComputerMenu container) {
switch (action) { switch (action) {
case TERMINATE -> container.getInput().terminate();
case TURN_ON -> container.getInput().turnOn(); case TURN_ON -> container.getInput().turnOn();
case REBOOT -> container.getInput().reboot(); case REBOOT -> container.getInput().reboot();
case SHUTDOWN -> container.getInput().shutdown(); case SHUTDOWN -> container.getInput().shutdown();
@ -49,6 +50,7 @@ public final class ComputerActionServerMessage extends ComputerServerMessage {
} }
public enum Action { public enum Action {
TERMINATE,
TURN_ON, TURN_ON,
SHUTDOWN, SHUTDOWN,
REBOOT REBOOT

View File

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

View File

@ -44,6 +44,7 @@ public final class KeyEventServerMessage extends ComputerServerMessage {
case UP -> input.keyUp(key); case UP -> input.keyUp(key);
case DOWN -> input.keyDown(key, false); case DOWN -> input.keyDown(key, false);
case REPEAT -> input.keyDown(key, true); 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 { 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.pocket.IPocketUpgrade;
import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.core.computer.ComputerSide; 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.client.PocketComputerDataMessage;
import dan200.computercraft.shared.network.server.ServerNetworking; import dan200.computercraft.shared.network.server.ServerNetworking;
import dan200.computercraft.shared.pocket.items.PocketComputerItem; import dan200.computercraft.shared.pocket.items.PocketComputerItem;
@ -41,8 +41,8 @@ public final class PocketBrain implements IPocketAccess {
private int colour = -1; private int colour = -1;
private int lightColour = -1; private int lightColour = -1;
public PocketBrain(PocketHolder holder, int computerID, @Nullable String label, ComputerFamily family, @Nullable UpgradeData<IPocketUpgrade> upgrade) { public PocketBrain(PocketHolder holder, @Nullable UpgradeData<IPocketUpgrade> upgrade, ServerComputer.Properties properties) {
this.computer = new PocketServerComputer(this, holder, computerID, label, family); this.computer = new PocketServerComputer(this, holder, properties);
this.holder = holder; this.holder = holder;
this.position = holder.pos(); this.position = holder.pos();
this.upgrade = upgrade; this.upgrade = upgrade;

View File

@ -5,15 +5,13 @@
package dan200.computercraft.shared.pocket.core; package dan200.computercraft.shared.pocket.core;
import dan200.computercraft.api.component.ComputerComponents; 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.ComputerState;
import dan200.computercraft.shared.computer.core.ServerComputer; 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.PocketComputerDataMessage;
import dan200.computercraft.shared.network.client.PocketComputerDeletedClientMessage; import dan200.computercraft.shared.network.client.PocketComputerDeletedClientMessage;
import dan200.computercraft.shared.network.server.ServerNetworking; import dan200.computercraft.shared.network.server.ServerNetworking;
import dan200.computercraft.shared.pocket.items.PocketComputerItem; import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import dan200.computercraft.shared.util.ComponentMap;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.ChunkPos;
@ -41,10 +39,10 @@ public final class PocketServerComputer extends ServerComputer {
private Set<ServerPlayer> tracking = Set.of(); private Set<ServerPlayer> tracking = Set.of();
PocketServerComputer(PocketBrain brain, PocketHolder holder, int computerID, @Nullable String label, ComputerFamily family) { PocketServerComputer(PocketBrain brain, PocketHolder holder, ServerComputer.Properties properties) {
super( super(holder.level(), holder.blockPos(), properties
holder.level(), holder.blockPos(), computerID, label, family, Config.pocketTermWidth, Config.pocketTermHeight, .terminalSize(ConfigSpec.pocketTermWidth.get(), ConfigSpec.pocketTermHeight.get())
ComponentMap.builder().add(ComputerComponents.POCKET, brain).build() .addComponent(ComputerComponents.POCKET, brain)
); );
this.brain = 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 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(); var computer = brain.computer();
stack.set(ModRegistry.DataComponents.COMPUTER.get(), new ServerComputerReference(registry.getSessionID(), computer.register())); 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.TurtleOverlay;
import dan200.computercraft.shared.turtle.core.TurtleBrain; import dan200.computercraft.shared.turtle.core.TurtleBrain;
import dan200.computercraft.shared.turtle.inventory.TurtleMenu; import dan200.computercraft.shared.turtle.inventory.TurtleMenu;
import dan200.computercraft.shared.util.ComponentMap;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.core.HolderLookup; import net.minecraft.core.HolderLookup;
@ -80,10 +79,10 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
@Override @Override
protected ServerComputer createComputer(int id) { protected ServerComputer createComputer(int id) {
var computer = new ServerComputer( var computer = new ServerComputer((ServerLevel) getLevel(), getBlockPos(), ServerComputer.properties(id, getFamily())
(ServerLevel) getLevel(), getBlockPos(), id, label, .label(getLabel())
getFamily(), Config.turtleTermWidth, Config.turtleTermHeight, .terminalSize(Config.TURTLE_TERM_WIDTH, Config.TURTLE_TERM_HEIGHT)
ComponentMap.builder().add(ComputerComponents.TURTLE, brain).build() .addComponent(ComputerComponents.TURTLE, brain)
); );
brain.setupComputer(computer); brain.setupComputer(computer);
return computer; return computer;

View File

@ -44,125 +44,33 @@ public final class NBTUtil {
.ifPresent(x -> destination.put(key, x)); .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) { public static @Nullable Object toLua(@Nullable Tag tag) {
if (tag == null) return null; if (tag == null) return null;
switch (tag.getId()) { return switch (tag.getId()) {
case Tag.TAG_BYTE: case Tag.TAG_BYTE, Tag.TAG_SHORT, Tag.TAG_INT, Tag.TAG_LONG -> ((NumericTag) tag).getAsLong();
case Tag.TAG_SHORT: case Tag.TAG_FLOAT, Tag.TAG_DOUBLE -> ((NumericTag) tag).getAsDouble();
case Tag.TAG_INT: case Tag.TAG_STRING -> tag.getAsString();
case Tag.TAG_LONG: case Tag.TAG_COMPOUND -> {
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
var compound = (CompoundTag) tag; var compound = (CompoundTag) tag;
Map<String, Object> map = new HashMap<>(compound.size()); Map<String, Object> map = new HashMap<>(compound.size());
for (var key : compound.getAllKeys()) { for (var key : compound.getAllKeys()) {
var value = toLua(compound.get(key)); var value = toLua(compound.get(key));
if (value != null) map.put(key, value); if (value != null) map.put(key, value);
} }
return map; yield map;
} }
case Tag.TAG_LIST: { case Tag.TAG_LIST -> ((ListTag) tag).stream().map(NBTUtil::toLua).toList();
var list = (ListTag) tag; case Tag.TAG_BYTE_ARRAY -> {
List<Object> map = new ArrayList<>(list.size());
for (var value : list) map.add(toLua(value));
return map;
}
case Tag.TAG_BYTE_ARRAY: {
var array = ((ByteArrayTag) tag).getAsByteArray(); var array = ((ByteArrayTag) tag).getAsByteArray();
List<Byte> map = new ArrayList<>(array.length); List<Byte> map = new ArrayList<>(array.length);
for (var b : array) map.add(b); for (var b : array) map.add(b);
return map; yield map;
} }
case Tag.TAG_INT_ARRAY: { case Tag.TAG_INT_ARRAY -> Arrays.stream(((IntArrayTag) tag).getAsIntArray()).boxed().toList();
var array = ((IntArrayTag) tag).getAsIntArray(); case Tag.TAG_LONG_ARRAY -> Arrays.stream(((LongArrayTag) tag).getAsLongArray()).boxed().toList();
List<Integer> map = new ArrayList<>(array.length); default -> null;
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;
} }
@Nullable @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 package dan200.computercraft.gametest.core
import dan200.computercraft.api.ComputerCraftAPI 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.*
import dan200.computercraft.gametest.api.ClientGameTest import dan200.computercraft.gametest.api.ClientGameTest
import dan200.computercraft.gametest.api.TestTags import dan200.computercraft.gametest.api.TestTags
@ -24,6 +26,8 @@ import net.minecraft.world.phys.Vec3
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.io.File import java.io.File
import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method import java.lang.reflect.Method
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
@ -91,6 +95,9 @@ object TestHooks {
CCTestCommand.importFiles(server) CCTestCommand.importFiles(server)
} }
@JvmStatic
fun areComputersIdle(server: MinecraftServer) = ComputerThreadReflection.isFullyIdle(ServerContext.get(server))
private val testClasses = listOf( private val testClasses = listOf(
Computer_Test::class.java, Computer_Test::class.java,
CraftOs_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>) { private fun registerTest(testClass: Class<*>, method: Method, fallbackRegister: Consumer<Method>) {
val className = testClass.simpleName.lowercase() val className = testClass.simpleName.lowercase()
val testName = className + "." + method.name.lowercase() val testName = className + "." + method.name.lowercase()
@ -147,7 +146,7 @@ object TestHooks {
TestFunction( TestFunction(
testInfo.batch, testName, testInfo.template.ifEmpty { testName }, testInfo.batch, testName, testInfo.template.ifEmpty { testName },
StructureUtils.getRotationForRotationSteps(testInfo.rotationSteps), StructureUtils.getRotationForRotationSteps(testInfo.rotationSteps),
adjustTimeout(testInfo.timeoutTicks), testInfo.timeoutTicks,
testInfo.setupTicks, testInfo.setupTicks,
testInfo.required, testInfo.manualOnly, testInfo.required, testInfo.manualOnly,
testInfo.attempts, testInfo.attempts,
@ -167,7 +166,7 @@ object TestHooks {
testName, testName,
testName, testName,
testInfo.template.ifEmpty { testName }, testInfo.template.ifEmpty { testName },
adjustTimeout(testInfo.timeoutTicks), testInfo.timeoutTicks,
0, 0,
true, true,
) { value -> safeInvoke(method, value) }, ) { value -> safeInvoke(method, value) },
@ -215,3 +214,31 @@ object TestHooks {
return false 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 * Reports tests to a JUnit XML file. This is equivalent to [JUnitLikeTestReporter], except it ensures the destination
* directory exists. * directory exists.
*/ */
class JunitTestReporter constructor(destination: File) : JUnitLikeTestReporter(destination) { class JunitTestReporter(destination: File) : JUnitLikeTestReporter(destination) {
override fun save(file: File) { override fun save(file: File) {
try { try {
Files.createDirectories(file.toPath().parent) Files.createDirectories(file.toPath().parent)

View File

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

View File

@ -40,6 +40,8 @@ dependencies {
} }
tasks.processResources { tasks.processResources {
inputs.property("gitHash", cct.gitHash)
var props = mapOf("gitContributors" to cct.gitContributors.get().joinToString("\n")) var props = mapOf("gitContributors" to cct.gitContributors.get().joinToString("\n"))
filesMatching("data/computercraft/lua/rom/help/credits.md") { expand(props) } 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> * <li>Passes main thread tasks to the {@link MainThreadScheduler.Executor}.</li>
* </ul> * </ul>
*/ */
public class Computer { public class Computer implements ComputerEvents.Receiver {
private static final int START_DELAY = 50; private static final int START_DELAY = 50;
// Various properties of the computer // Various properties of the computer
@ -114,6 +114,7 @@ public class Computer {
executor.queueStop(false, true); executor.queueStop(false, true);
} }
@Override
public void queueEvent(String event, @Nullable Object[] args) { public void queueEvent(String event, @Nullable Object[] args) {
executor.queueEvent(event, 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(); 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) { 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 // 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. // worker finishes normally.

View File

@ -4,52 +4,116 @@
package dan200.computercraft.core.util; package dan200.computercraft.core.util;
import dan200.computercraft.core.computer.ComputerEvents;
import java.nio.ByteBuffer;
public final class StringUtil { public final class StringUtil {
public static final int MAX_PASTE_LENGTH = 512;
private StringUtil() { private StringUtil() {
} }
private static boolean isAllowed(char c) { /**
return (c >= ' ' && c <= '~') || (c >= 161 && c <= 172) || (c >= 174 && c <= 255); * Convert a Unicode character to a terminal one.
} *
* @param chr The Unicode character.
private static String removeSpecialCharacters(String text, int length) { * @return The terminal character.
var builder = new StringBuilder(length); */
for (var i = 0; i < length; i++) { public static byte unicodeToTerminal(int chr) {
var c = text.charAt(i); // ASCII and latin1 map to themselves
builder.append(isAllowed(c) ? c : '?'); 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) { private static byte unicodeToCraftOsFallback(int c) {
return removeSpecialCharacters(text, Math.min(32, text.length())); 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> * <p>
* This removes special characters and strips to the first line of text. * This removes special characters and strips to the first line of text.
* *
* @param clipboard The text from the clipboard. * @param clipboard The text from the clipboard.
* @return The normalised clipboard text. * @return The encoded clipboard text.
*/ */
public static String normaliseClipboardString(String clipboard) { public static ByteBuffer getClipboardString(String clipboard) {
// Clip to the first occurrence of \r or \n var output = new byte[Math.min(MAX_PASTE_LENGTH, clipboard.length())];
var newLineIndex1 = clipboard.indexOf('\r'); var idx = 0;
var newLineIndex2 = clipboard.indexOf('\n');
int length; var iterator = clipboard.codePoints().iterator();
if (newLineIndex1 >= 0 && newLineIndex2 >= 0) { while (iterator.hasNext() && idx <= output.length) {
length = Math.min(newLineIndex1, newLineIndex2); var chr = unicodeToTerminal(iterator.next());
} else if (newLineIndex1 >= 0) { if (!isTypableChar(chr)) break;
length = newLineIndex1; output[idx++] = chr;
} else if (newLineIndex2 >= 0) {
length = newLineIndex2;
} else {
length = clipboard.length();
} }
return removeSpecialCharacters(clipboard, Math.min(length, 512)); return ByteBuffer.wrap(output, 0, idx).asReadOnlyBuffer();
} }
} }

View File

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

View File

@ -198,6 +198,9 @@ dependencies {
// Compile tasks // Compile tasks
tasks.processResources { tasks.processResources {
inputs.property("modVersion", modVersion)
inputs.property("neoVersion", libs.versions.neoForge)
var props = mapOf( var props = mapOf(
"neoVersion" to libs.versions.neoForge.get(), "neoVersion" to libs.versions.neoForge.get(),
"file" to mapOf("jarVersion" to modVersion), "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.TransferredFile;
import dan200.computercraft.core.apis.transfer.TransferredFiles; import dan200.computercraft.core.apis.transfer.TransferredFiles;
import dan200.computercraft.core.computer.Computer; import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.ComputerEvents;
import dan200.computercraft.core.util.StringUtil; import dan200.computercraft.core.util.StringUtil;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWDropCallback; import org.lwjgl.glfw.GLFWDropCallback;
@ -49,10 +50,8 @@ public class InputState {
} }
public void onCharEvent(int codepoint) { public void onCharEvent(int codepoint) {
if (codepoint >= 32 && codepoint <= 126 || codepoint >= 160 && codepoint <= 255) { var terminalChar = StringUtil.unicodeToTerminal(codepoint);
// Queue the char event for any printable chars in byte range if (StringUtil.isTypableChar(terminalChar)) ComputerEvents.charTyped(computer, terminalChar);
computer.queueEvent("char", new Object[]{ Character.toString(codepoint) });
}
} }
public void onKeyEvent(long window, int key, int action, int modifiers) { 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) { if (key == GLFW.GLFW_KEY_V && modifiers == GLFW.GLFW_MOD_CONTROL) {
var string = GLFW.glfwGetClipboardString(window); var string = GLFW.glfwGetClipboardString(window);
if (string != null) { if (string != null) {
var clipboard = StringUtil.normaliseClipboardString(string); var clipboard = StringUtil.getClipboardString(string);
if (!clipboard.isEmpty()) computer.queueEvent("paste", new Object[]{ clipboard }); if (clipboard.remaining() > 0) ComputerEvents.paste(computer, clipboard);
} }
return; return;
} }
@ -92,7 +91,7 @@ public class InputState {
// Queue the "key" event and add to the down set // Queue the "key" event and add to the down set
var repeat = keysDown.get(key); var repeat = keysDown.get(key);
keysDown.set(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 // Queue the "key_up" event and remove from the down set
if (key >= 0 && keysDown.get(key)) { if (key >= 0 && keysDown.get(key)) {
keysDown.set(key, false); keysDown.set(key, false);
computer.queueEvent("key_up", new Object[]{ key }); ComputerEvents.keyUp(computer, key);
} }
switch (key) { switch (key) {
@ -115,12 +114,12 @@ public class InputState {
public void onMouseClick(int button, int action) { public void onMouseClick(int button, int action) {
switch (action) { switch (action) {
case GLFW.GLFW_PRESS -> { 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; lastMouseButton = button;
} }
case GLFW.GLFW_RELEASE -> { case GLFW.GLFW_RELEASE -> {
if (button == lastMouseButton) { 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; lastMouseButton = -1;
} }
} }
@ -133,13 +132,13 @@ public class InputState {
lastMouseX = mouseX; lastMouseX = mouseX;
lastMouseY = mouseY; lastMouseY = mouseY;
if (lastMouseButton != -1) { 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) { public void onMouseScroll(double yOffset) {
if (yOffset != 0) { 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);
} }
} }