mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-10-31 13:42:59 +00:00 
			
		
		
		
	Map Unicode to CC's charset for char/paste events
We now convert uncode characters from "char" and "paste" events to CC's charset[^1], rather than just leaving them unconverted. This means you can paste in special characters like "♠" or "🮙" and they will be converted correctly. Characters outside that range will be replaced with "?", as before. It would be nice to make this a bi-directional mapping, and do this for Lua methods too (e.g. os.setComputerLabel). However, that has much wider ramifications (and more likelyhood of breaking something), so avoiding that for now. - Remove the generic "queue event" client->server message, and replace it with separate char/terminate/paste messages. This allows us to delete a chunk of code (all the NBT<->Object conversion), and makes server-side validation of events possible. - Fix os.setComputerLabel accepting the section sign — this is treated as special by Minecraft's formatting code. Sorry, no fun allowed. - Convert paste/char codepoints to CC's charset. Sadly MC's char hook splits the codepoint into surrogate pairs, which we *don't* attempt to reconstruct, so you can't currently use unicode input for block characters — you can paste them though! [^1]: I'm referring this to the "terminal charset" within the code. I've flip-flopped between "CraftOS", "terminal", "ComputerCraft", but feel especially great.
This commit is contained in:
		| @@ -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)); | ||||
|   | ||||
| @@ -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") | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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); | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -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<T extends AbstractContainerMenu & ComputerMenu> im | ||||
|         this.owner = owner; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void queueEvent(String event, @Nullable Object[] arguments) { | ||||
|         owner.getComputer().queueEvent(event, arguments); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void keyDown(int key, boolean repeat) { | ||||
|         keysDown.add(key); | ||||
| @@ -66,6 +63,23 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> 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<T extends AbstractContainerMenu & ComputerMenu> im | ||||
|         ComputerEvents.mouseScroll(owner.getComputer(), direction, x, y); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void terminate() { | ||||
|         owner.getComputer().queueEvent("terminate"); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void shutdown() { | ||||
|         owner.getComputer().shutdown(); | ||||
|   | ||||
| @@ -27,9 +27,9 @@ public final class NetworkMessages { | ||||
|     private static final List<MessageType<? extends NetworkMessage<ClientNetworkContext>>> clientMessages = new ArrayList<>(); | ||||
| 
 | ||||
|     public static final MessageType<ComputerActionServerMessage> COMPUTER_ACTION = registerServerbound(0, "computer_action", ComputerActionServerMessage.class, ComputerActionServerMessage::new); | ||||
|     public static final MessageType<QueueEventServerMessage> QUEUE_EVENT = registerServerbound(1, "queue_event", QueueEventServerMessage.class, QueueEventServerMessage::new); | ||||
|     public static final MessageType<KeyEventServerMessage> KEY_EVENT = registerServerbound(2, "key_event", KeyEventServerMessage.class, KeyEventServerMessage::new); | ||||
|     public static final MessageType<MouseEventServerMessage> MOUSE_EVENT = registerServerbound(3, "mouse_event", MouseEventServerMessage.class, MouseEventServerMessage::new); | ||||
|     public static final MessageType<KeyEventServerMessage> KEY_EVENT = registerServerbound(1, "key_event", KeyEventServerMessage.class, KeyEventServerMessage::new); | ||||
|     public static final MessageType<MouseEventServerMessage> MOUSE_EVENT = registerServerbound(2, "mouse_event", MouseEventServerMessage.class, MouseEventServerMessage::new); | ||||
|     public static final MessageType<PasteEventComputerMessage> PASTE_EVENT = registerServerbound(3, "paste_event", PasteEventComputerMessage.class, PasteEventComputerMessage::new); | ||||
|     public static final MessageType<UploadFileMessage> UPLOAD_FILE = registerServerbound(4, "upload_file", UploadFileMessage.class, UploadFileMessage::new); | ||||
| 
 | ||||
|     public static final MessageType<ChatTableClientMessage> CHAT_TABLE = registerClientbound(10, "chat_table", ChatTableClientMessage.class, ChatTableClientMessage::new); | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -35,8 +35,8 @@ public abstract class ComputerServerMessage implements NetworkMessage<ServerNetw | ||||
|     @Override | ||||
|     public void handle(ServerNetworkContext context) { | ||||
|         Player player = context.getSender(); | ||||
|         if (player.containerMenu.containerId == containerId && player.containerMenu instanceof ComputerMenu) { | ||||
|             handle(context, (ComputerMenu) player.containerMenu); | ||||
|         if (player.containerMenu.containerId == containerId && player.containerMenu instanceof ComputerMenu menu) { | ||||
|             handle(context, menu); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|   | ||||
| @@ -37,10 +37,11 @@ public class KeyEventServerMessage extends ComputerServerMessage { | ||||
|     @Override | ||||
|     protected void handle(ServerNetworkContext context, ComputerMenu container) { | ||||
|         var input = container.getInput(); | ||||
|         if (type == Action.UP) { | ||||
|             input.keyUp(key); | ||||
|         } else { | ||||
|             input.keyDown(key, type == Action.REPEAT); | ||||
|         switch (type) { | ||||
|             case UP -> 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 | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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<PasteEventComputerMessage> type() { | ||||
|         return NetworkMessages.PASTE_EVENT; | ||||
|     } | ||||
| } | ||||
| @@ -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<QueueEventServerMessage> type() { | ||||
|         return NetworkMessages.QUEUE_EVENT; | ||||
|     } | ||||
| } | ||||
| @@ -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<Object, Object> map = new HashMap<>(len); | ||||
|                 for (var i = 0; i < len; i++) { | ||||
|                     var key = fromNBTTag(c.get("k" + i)); | ||||
|                     var value = fromNBTTag(c.get("v" + i)); | ||||
|                     if (key != null && value != null) map.put(key, value); | ||||
|                 } | ||||
|                 return map; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static @Nullable Object toLua(@Nullable Tag tag) { | ||||
|         if (tag == null) return null; | ||||
| 
 | ||||
|         switch (tag.getId()) { | ||||
|             case Tag.TAG_BYTE: | ||||
|             case Tag.TAG_SHORT: | ||||
|             case Tag.TAG_INT: | ||||
|             case Tag.TAG_LONG: | ||||
|                 return ((NumericTag) tag).getAsLong(); | ||||
|             case Tag.TAG_FLOAT: | ||||
|             case Tag.TAG_DOUBLE: | ||||
|                 return ((NumericTag) tag).getAsDouble(); | ||||
|             case Tag.TAG_STRING: // String | ||||
|                 return tag.getAsString(); | ||||
|             case Tag.TAG_COMPOUND: { // Compound | ||||
|         return switch (tag.getId()) { | ||||
|             case Tag.TAG_BYTE, Tag.TAG_SHORT, Tag.TAG_INT, Tag.TAG_LONG -> ((NumericTag) tag).getAsLong(); | ||||
|             case Tag.TAG_FLOAT, Tag.TAG_DOUBLE -> ((NumericTag) tag).getAsDouble(); | ||||
|             case Tag.TAG_STRING -> tag.getAsString(); | ||||
|             case Tag.TAG_COMPOUND -> { | ||||
|                 var compound = (CompoundTag) tag; | ||||
|                 Map<String, Object> map = new HashMap<>(compound.size()); | ||||
|                 for (var key : compound.getAllKeys()) { | ||||
|                     var value = toLua(compound.get(key)); | ||||
|                     if (value != null) map.put(key, value); | ||||
|                 } | ||||
|                 return map; | ||||
|                 yield map; | ||||
|             } | ||||
|             case Tag.TAG_LIST: { | ||||
|                 var list = (ListTag) tag; | ||||
|                 List<Object> map = new ArrayList<>(list.size()); | ||||
|                 for (var value : list) map.add(toLua(value)); | ||||
|                 return map; | ||||
|             } | ||||
|             case Tag.TAG_BYTE_ARRAY: { | ||||
|             case Tag.TAG_LIST -> ((ListTag) tag).stream().map(NBTUtil::toLua).toList(); | ||||
|             case Tag.TAG_BYTE_ARRAY -> { | ||||
|                 var array = ((ByteArrayTag) tag).getAsByteArray(); | ||||
|                 List<Byte> map = new ArrayList<>(array.length); | ||||
|                 for (var b : array) map.add(b); | ||||
|                 return map; | ||||
|                 yield map; | ||||
|             } | ||||
|             case Tag.TAG_INT_ARRAY: { | ||||
|                 var array = ((IntArrayTag) tag).getAsIntArray(); | ||||
|                 List<Integer> map = new ArrayList<>(array.length); | ||||
|                 for (var j : array) map.add(j); | ||||
|                 return map; | ||||
|             } | ||||
| 
 | ||||
|             default: | ||||
|                 return null; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static @Nullable Object[] decodeObjects(CompoundTag tag) { | ||||
|         var len = tag.getInt("len"); | ||||
|         if (len <= 0) return null; | ||||
| 
 | ||||
|         var objects = new Object[len]; | ||||
|         for (var i = 0; i < len; i++) { | ||||
|             var key = Integer.toString(i); | ||||
|             if (tag.contains(key)) { | ||||
|                 objects[i] = fromNBTTag(tag.get(key)); | ||||
|             } | ||||
|         } | ||||
|         return objects; | ||||
|             case Tag.TAG_INT_ARRAY -> Arrays.stream(((IntArrayTag) tag).getAsIntArray()).boxed().toList(); | ||||
|             case Tag.TAG_LONG_ARRAY -> Arrays.stream(((LongArrayTag) tag).getAsLongArray()).boxed().toList(); | ||||
|             default -> null; | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|   | ||||
| @@ -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 }); | ||||
|     } | ||||
|   | ||||
| @@ -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); | ||||
|     /** | ||||
|      * 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; | ||||
|         } | ||||
| 
 | ||||
|     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 : '?'); | ||||
|         // 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); | ||||
|     } | ||||
| 
 | ||||
|         return builder.toString(); | ||||
|     } | ||||
| 
 | ||||
|     public static String normaliseLabel(String text) { | ||||
|         return removeSpecialCharacters(text, Math.min(32, text.length())); | ||||
|     private static byte unicodeToCraftOsFallback(int c) { | ||||
|         return switch (c) { | ||||
|             case 0x263A -> 1; | ||||
|             case 0x263B -> 2; | ||||
|             case 0x2665 -> 3; | ||||
|             case 0x2666 -> 4; | ||||
|             case 0x2663 -> 5; | ||||
|             case 0x2660 -> 6; | ||||
|             case 0x2022 -> 7; | ||||
|             case 0x25D8 -> 8; | ||||
|             case 0x2642 -> 11; | ||||
|             case 0x2640 -> 12; | ||||
|             case 0x266A -> 14; | ||||
|             case 0x266B -> 15; | ||||
|             case 0x25BA -> 16; | ||||
|             case 0x25C4 -> 17; | ||||
|             case 0x2195 -> 18; | ||||
|             case 0x203C -> 19; | ||||
|             case 0x25AC -> 22; | ||||
|             case 0x21A8 -> 23; | ||||
|             case 0x2191 -> 24; | ||||
|             case 0x2193 -> 25; | ||||
|             case 0x2192 -> 26; | ||||
|             case 0x2190 -> 27; | ||||
|             case 0x221F -> 28; | ||||
|             case 0x2194 -> 29; | ||||
|             case 0x25B2 -> 30; | ||||
|             case 0x25BC -> 31; | ||||
|             case 0x1FB99 -> 127; | ||||
|             case 0x258C -> (byte) 149; | ||||
|             default -> '?'; | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Normalise a string from the clipboard, suitable for pasting into a computer. | ||||
|      * Check if a character is capable of being input and passed to a {@linkplain ComputerEvents#charTyped(ComputerEvents.Receiver, byte) | ||||
|      * "char" event}. | ||||
|      * | ||||
|      * @param chr The character to check. | ||||
|      * @return Whether this character can be typed. | ||||
|      */ | ||||
|     public static boolean isTypableChar(byte chr) { | ||||
|         return chr != 0 && chr != '\r' && chr != '\n'; | ||||
|     } | ||||
| 
 | ||||
|     private static boolean isAllowedInLabel(char c) { | ||||
|         // Limit to ASCII and latin1, excluding '§' (Minecraft's formatting character). | ||||
|         return (c >= ' ' && c <= '~') || (c >= 161 && c <= 255 && c != 167); | ||||
|     } | ||||
| 
 | ||||
|     public static String normaliseLabel(String text) { | ||||
|         var length = Math.min(32, text.length()); | ||||
|         var builder = new StringBuilder(length); | ||||
|         for (var i = 0; i < length; i++) { | ||||
|             var c = text.charAt(i); | ||||
|             builder.append(isAllowedInLabel(c) ? c : '?'); | ||||
|         } | ||||
|         return builder.toString(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convert a Java string to a Lua one (using the terminal charset), suitable for pasting into a computer. | ||||
|      * <p> | ||||
|      * This removes special characters and strips to the first line of text. | ||||
|      * | ||||
|      * @param clipboard The text from the clipboard. | ||||
|      * @return The normalised clipboard text. | ||||
|      * @return The encoded clipboard text. | ||||
|      */ | ||||
|     public static String normaliseClipboardString(String clipboard) { | ||||
|         // Clip to the first occurrence of \r or \n | ||||
|         var newLineIndex1 = clipboard.indexOf('\r'); | ||||
|         var newLineIndex2 = clipboard.indexOf('\n'); | ||||
|     public static ByteBuffer getClipboardString(String clipboard) { | ||||
|         var output = new byte[Math.min(MAX_PASTE_LENGTH, clipboard.length())]; | ||||
|         var idx = 0; | ||||
| 
 | ||||
|         int length; | ||||
|         if (newLineIndex1 >= 0 && newLineIndex2 >= 0) { | ||||
|             length = Math.min(newLineIndex1, newLineIndex2); | ||||
|         } else if (newLineIndex1 >= 0) { | ||||
|             length = newLineIndex1; | ||||
|         } else if (newLineIndex2 >= 0) { | ||||
|             length = newLineIndex2; | ||||
|         } else { | ||||
|             length = clipboard.length(); | ||||
|         var iterator = clipboard.codePoints().iterator(); | ||||
|         while (iterator.hasNext() && idx <= output.length) { | ||||
|             var chr = unicodeToTerminal(iterator.next()); | ||||
|             if (!isTypableChar(chr)) break; | ||||
|             output[idx++] = chr; | ||||
|         } | ||||
| 
 | ||||
|         return removeSpecialCharacters(clipboard, Math.min(length, 512)); | ||||
|         return ByteBuffer.wrap(output, 0, idx).asReadOnlyBuffer(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
|         } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Jonathan Coates
					Jonathan Coates