mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-02-02 20:29:13 +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:
parent
938eb38ad5
commit
94ad6dab0e
@ -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));
|
||||||
|
@ -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")
|
||||||
|
@ -71,11 +71,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,8 +109,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
|
||||||
@ -222,7 +219,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) {
|
||||||
|
@ -6,7 +6,7 @@ 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 describes the "shape" of both the client-and
|
* 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
|
* @see ServerComputer
|
||||||
*/
|
*/
|
||||||
public interface InputHandler {
|
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 keyDown(int key, boolean repeat);
|
||||||
|
|
||||||
void keyUp(int key);
|
void keyUp(int key);
|
||||||
|
|
||||||
|
void charTyped(byte chr);
|
||||||
|
|
||||||
|
void paste(ByteBuffer contents);
|
||||||
|
|
||||||
void mouseClick(int button, int x, int y);
|
void mouseClick(int button, int x, int y);
|
||||||
|
|
||||||
void mouseUp(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 mouseScroll(int direction, int x, int y);
|
||||||
|
|
||||||
|
void terminate();
|
||||||
|
|
||||||
void shutdown();
|
void shutdown();
|
||||||
|
|
||||||
void turnOn();
|
void turnOn();
|
||||||
|
@ -168,7 +168,7 @@ public class ServerComputer implements ComputerEnvironment, ComputerEvents.Recei
|
|||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ 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.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;
|
||||||
@ -22,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;
|
||||||
|
|
||||||
@ -49,11 +51,6 @@ 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);
|
||||||
@ -66,6 +63,23 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
|
|||||||
ComputerEvents.keyUp(owner.getComputer(), 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
|
||||||
public void mouseClick(int button, int x, int y) {
|
public void mouseClick(int button, int x, int y) {
|
||||||
lastMouseX = x;
|
lastMouseX = x;
|
||||||
@ -101,6 +115,11 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
|
|||||||
ComputerEvents.mouseScroll(owner.getComputer(), direction, x, y);
|
ComputerEvents.mouseScroll(owner.getComputer(), direction, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void terminate() {
|
||||||
|
owner.getComputer().queueEvent("terminate");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
owner.getComputer().shutdown();
|
owner.getComputer().shutdown();
|
||||||
|
@ -27,9 +27,9 @@ public final class NetworkMessages {
|
|||||||
private static final List<MessageType<? extends NetworkMessage<ClientNetworkContext>>> clientMessages = new ArrayList<>();
|
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<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(1, "key_event", KeyEventServerMessage.class, KeyEventServerMessage::new);
|
||||||
public static final MessageType<KeyEventServerMessage> KEY_EVENT = registerServerbound(2, "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<MouseEventServerMessage> MOUSE_EVENT = registerServerbound(3, "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<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);
|
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
|
@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();
|
||||||
@ -45,6 +46,7 @@ public class ComputerActionServerMessage extends ComputerServerMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public enum Action {
|
public enum Action {
|
||||||
|
TERMINATE,
|
||||||
TURN_ON,
|
TURN_ON,
|
||||||
SHUTDOWN,
|
SHUTDOWN,
|
||||||
REBOOT
|
REBOOT
|
||||||
|
@ -35,8 +35,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,10 +37,11 @@ public class KeyEventServerMessage extends ComputerServerMessage {
|
|||||||
@Override
|
@Override
|
||||||
protected void handle(ServerNetworkContext context, ComputerMenu container) {
|
protected void handle(ServerNetworkContext context, ComputerMenu container) {
|
||||||
var input = container.getInput();
|
var input = container.getInput();
|
||||||
if (type == Action.UP) {
|
switch (type) {
|
||||||
input.keyUp(key);
|
case UP -> input.keyUp(key);
|
||||||
} else {
|
case DOWN -> input.keyDown(key, false);
|
||||||
input.keyDown(key, type == Action.REPEAT);
|
case REPEAT -> input.keyDown(key, true);
|
||||||
|
case CHAR -> input.charTyped((byte) key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,6 +51,6 @@ public class KeyEventServerMessage extends ComputerServerMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public enum Action {
|
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();
|
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) {
|
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
|
||||||
|
@ -4,7 +4,10 @@
|
|||||||
|
|
||||||
package dan200.computercraft.core.computer;
|
package dan200.computercraft.core.computer;
|
||||||
|
|
||||||
|
import dan200.computercraft.core.util.StringUtil;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Built-in events that can be queued on a computer.
|
* 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 });
|
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) {
|
public static void mouseClick(Receiver receiver, int button, int x, int y) {
|
||||||
receiver.queueEvent("mouse_click", new Object[]{ button, x, y });
|
receiver.queueEvent("mouse_click", new Object[]{ button, x, y });
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,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) {
|
||||||
@ -69,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;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user