1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-09-06 12:27:56 +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:
Jonathan Coates
2025-01-19 10:54:02 +00:00
parent 938eb38ad5
commit 94ad6dab0e
16 changed files with 263 additions and 235 deletions

View File

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

View File

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

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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();

View File

@@ -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);

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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