diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/ClientInputHandler.java b/projects/common/src/client/java/dan200/computercraft/client/gui/ClientInputHandler.java index a69fe39c0..7fd121a44 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/gui/ClientInputHandler.java +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/ClientInputHandler.java @@ -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)); diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/ComputerSidebar.java b/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/ComputerSidebar.java index 8ab38d9e8..13437076d 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/ComputerSidebar.java +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/ComputerSidebar.java @@ -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") diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/TerminalWidget.java b/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/TerminalWidget.java index f19221382..f57206205 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/TerminalWidget.java +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/TerminalWidget.java @@ -71,11 +71,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; } @@ -112,8 +109,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 @@ -222,7 +219,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) { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/InputHandler.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/InputHandler.java index deed8e42b..dbdd23ce5 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/InputHandler.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/InputHandler.java @@ -6,7 +6,7 @@ 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 describes the "shape" of both the client-and @@ -16,16 +16,14 @@ import javax.annotation.Nullable; * @see ServerComputer */ public interface InputHandler { - void queueEvent(String event, @Nullable Object[] arguments); - - default void queueEvent(String event) { - queueEvent(event, null); - } - void keyDown(int key, boolean repeat); void keyUp(int key); + void charTyped(byte chr); + + void paste(ByteBuffer contents); + void mouseClick(int button, int x, int y); void mouseUp(int button, int x, int y); @@ -34,6 +32,8 @@ public interface InputHandler { void mouseScroll(int direction, int x, int y); + void terminate(); + void shutdown(); void turnOn(); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java index a6a21dbcb..906d30bbe 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java @@ -168,7 +168,7 @@ public class ServerComputer implements ComputerEnvironment, ComputerEvents.Recei 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); } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputState.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputState.java index 08e6702b0..7a70e76f9 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputState.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputState.java @@ -8,6 +8,7 @@ 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; @@ -22,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; @@ -49,11 +51,6 @@ public class ServerInputState 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); @@ -66,6 +63,23 @@ public class ServerInputState im 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 public void mouseClick(int button, int x, int y) { lastMouseX = x; @@ -101,6 +115,11 @@ public class ServerInputState im ComputerEvents.mouseScroll(owner.getComputer(), direction, x, y); } + @Override + public void terminate() { + owner.getComputer().queueEvent("terminate"); + } + @Override public void shutdown() { owner.getComputer().shutdown(); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/network/NetworkMessages.java b/projects/common/src/main/java/dan200/computercraft/shared/network/NetworkMessages.java index bf4c1927c..914652310 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/network/NetworkMessages.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/network/NetworkMessages.java @@ -27,9 +27,9 @@ public final class NetworkMessages { private static final List>> clientMessages = new ArrayList<>(); public static final MessageType COMPUTER_ACTION = registerServerbound(0, "computer_action", ComputerActionServerMessage.class, ComputerActionServerMessage::new); - public static final MessageType QUEUE_EVENT = registerServerbound(1, "queue_event", QueueEventServerMessage.class, QueueEventServerMessage::new); - public static final MessageType KEY_EVENT = registerServerbound(2, "key_event", KeyEventServerMessage.class, KeyEventServerMessage::new); - public static final MessageType MOUSE_EVENT = registerServerbound(3, "mouse_event", MouseEventServerMessage.class, MouseEventServerMessage::new); + public static final MessageType KEY_EVENT = registerServerbound(1, "key_event", KeyEventServerMessage.class, KeyEventServerMessage::new); + public static final MessageType MOUSE_EVENT = registerServerbound(2, "mouse_event", MouseEventServerMessage.class, MouseEventServerMessage::new); + public static final MessageType PASTE_EVENT = registerServerbound(3, "paste_event", PasteEventComputerMessage.class, PasteEventComputerMessage::new); public static final MessageType UPLOAD_FILE = registerServerbound(4, "upload_file", UploadFileMessage.class, UploadFileMessage::new); public static final MessageType CHAT_TABLE = registerClientbound(10, "chat_table", ChatTableClientMessage.class, ChatTableClientMessage::new); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/network/server/ComputerActionServerMessage.java b/projects/common/src/main/java/dan200/computercraft/shared/network/server/ComputerActionServerMessage.java index e4811f8e3..4c985ec19 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/network/server/ComputerActionServerMessage.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/network/server/ComputerActionServerMessage.java @@ -33,6 +33,7 @@ public 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(); @@ -45,6 +46,7 @@ public class ComputerActionServerMessage extends ComputerServerMessage { } public enum Action { + TERMINATE, TURN_ON, SHUTDOWN, REBOOT diff --git a/projects/common/src/main/java/dan200/computercraft/shared/network/server/ComputerServerMessage.java b/projects/common/src/main/java/dan200/computercraft/shared/network/server/ComputerServerMessage.java index c7bfe2d10..0abd197f6 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/network/server/ComputerServerMessage.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/network/server/ComputerServerMessage.java @@ -35,8 +35,8 @@ public abstract class ComputerServerMessage implements NetworkMessage input.keyUp(key); + case DOWN -> input.keyDown(key, false); + case REPEAT -> input.keyDown(key, true); + case CHAR -> input.charTyped((byte) key); } } @@ -50,6 +51,6 @@ public class KeyEventServerMessage extends ComputerServerMessage { } public enum Action { - DOWN, REPEAT, UP + DOWN, REPEAT, UP, CHAR } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/network/server/PasteEventComputerMessage.java b/projects/common/src/main/java/dan200/computercraft/shared/network/server/PasteEventComputerMessage.java new file mode 100644 index 000000000..31f725cad --- /dev/null +++ b/projects/common/src/main/java/dan200/computercraft/shared/network/server/PasteEventComputerMessage.java @@ -0,0 +1,61 @@ +// 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.MessageType; +import dan200.computercraft.shared.network.NetworkMessages; +import io.netty.handler.codec.DecoderException; +import net.minecraft.network.FriendlyByteBuf; +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 { + private final ByteBuffer text; + + public PasteEventComputerMessage(AbstractContainerMenu menu, ByteBuffer text) { + super(menu); + this.text = text; + } + + public PasteEventComputerMessage(FriendlyByteBuf buf) { + super(buf); + + var length = buf.readVarInt(); + if (length > StringUtil.MAX_PASTE_LENGTH) { + throw new DecoderException("ByteArray with size " + length + " is bigger than allowed " + StringUtil.MAX_PASTE_LENGTH); + } + + var text = new byte[length]; + buf.readBytes(text); + this.text = ByteBuffer.wrap(text); + } + + @Override + public void write(FriendlyByteBuf buf) { + super.write(buf); + buf.writeVarInt(text.remaining()); + buf.writeBytes(text.duplicate()); + } + + @Override + protected void handle(ServerNetworkContext context, ComputerMenu container) { + container.getInput().paste(text); + } + + @Override + public MessageType type() { + return NetworkMessages.PASTE_EVENT; + } +} diff --git a/projects/common/src/main/java/dan200/computercraft/shared/network/server/QueueEventServerMessage.java b/projects/common/src/main/java/dan200/computercraft/shared/network/server/QueueEventServerMessage.java deleted file mode 100644 index 057718ef9..000000000 --- a/projects/common/src/main/java/dan200/computercraft/shared/network/server/QueueEventServerMessage.java +++ /dev/null @@ -1,57 +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.MessageType; -import dan200.computercraft.shared.network.NetworkMessages; -import dan200.computercraft.shared.util.NBTUtil; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.world.inventory.AbstractContainerMenu; - -import javax.annotation.Nullable; - -/** - * Queue an event on a {@link ServerComputer}. - * - * @see ServerInputHandler#queueEvent(String) - */ -public class QueueEventServerMessage extends ComputerServerMessage { - private final String event; - private final @Nullable Object[] args; - - public QueueEventServerMessage(AbstractContainerMenu menu, String event, @Nullable Object[] args) { - super(menu); - this.event = event; - this.args = args; - } - - public QueueEventServerMessage(FriendlyByteBuf buf) { - super(buf); - event = buf.readUtf(Short.MAX_VALUE); - - var args = buf.readNbt(); - this.args = args == null ? null : NBTUtil.decodeObjects(args); - } - - @Override - public void write(FriendlyByteBuf buf) { - super.write(buf); - 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 MessageType type() { - return NetworkMessages.QUEUE_EVENT; - } -} diff --git a/projects/common/src/main/java/dan200/computercraft/shared/util/NBTUtil.java b/projects/common/src/main/java/dan200/computercraft/shared/util/NBTUtil.java index 96cdc6194..17d2bf8cc 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/util/NBTUtil.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/util/NBTUtil.java @@ -62,125 +62,33 @@ public final class NBTUtil { return childTag != null && childTag.getId() == Tag.TAG_COMPOUND ? (CompoundTag) childTag : emptyTag(); } - 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 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 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 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 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 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 diff --git a/projects/core/src/main/java/dan200/computercraft/core/computer/ComputerEvents.java b/projects/core/src/main/java/dan200/computercraft/core/computer/ComputerEvents.java index ba82a28e0..f3ed14372 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/computer/ComputerEvents.java +++ b/projects/core/src/main/java/dan200/computercraft/core/computer/ComputerEvents.java @@ -4,7 +4,10 @@ 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. @@ -21,6 +24,28 @@ public final class ComputerEvents { 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 }); } diff --git a/projects/core/src/main/java/dan200/computercraft/core/util/StringUtil.java b/projects/core/src/main/java/dan200/computercraft/core/util/StringUtil.java index 689775a6f..27e443bc6 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/util/StringUtil.java +++ b/projects/core/src/main/java/dan200/computercraft/core/util/StringUtil.java @@ -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. *

* 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(); } } diff --git a/projects/standalone/src/main/java/cc/tweaked/standalone/InputState.java b/projects/standalone/src/main/java/cc/tweaked/standalone/InputState.java index 6b7d80eb6..2013a1fb3 100644 --- a/projects/standalone/src/main/java/cc/tweaked/standalone/InputState.java +++ b/projects/standalone/src/main/java/cc/tweaked/standalone/InputState.java @@ -50,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) { @@ -69,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; }