Merge branch 'mc-1.20.x' into mc-1.21.x
1
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -1,6 +1,7 @@
|
||||
name: Bug report
|
||||
description: Report some misbehaviour in the mod
|
||||
labels: [ bug ]
|
||||
type: bug
|
||||
body:
|
||||
- type: dropdown
|
||||
id: mc-version
|
||||
|
1
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -2,6 +2,7 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea or improvement
|
||||
labels: enhancement
|
||||
type: feature
|
||||
---
|
||||
|
||||
<!--
|
||||
|
@ -10,9 +10,7 @@ import org.gradle.api.GradleException
|
||||
import org.gradle.api.NamedDomainObjectProvider
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.Task
|
||||
import org.gradle.api.artifacts.Dependency
|
||||
import org.gradle.api.plugins.JavaPluginExtension
|
||||
import org.gradle.api.provider.ListProperty
|
||||
import org.gradle.api.provider.Provider
|
||||
import org.gradle.api.provider.SetProperty
|
||||
import org.gradle.api.tasks.SourceSet
|
||||
@ -33,6 +31,10 @@ import java.net.URI
|
||||
import java.util.regex.Pattern
|
||||
|
||||
abstract class CCTweakedExtension(private val project: Project) {
|
||||
/** Get the hash of the latest git commit. */
|
||||
val gitHash: Provider<String> =
|
||||
gitProvider("<no git commit>", listOf("rev-parse", "HEAD")) { it.trim() }
|
||||
|
||||
/** Get the current git branch. */
|
||||
val gitBranch: Provider<String> =
|
||||
gitProvider("<no git branch>", listOf("rev-parse", "--abbrev-ref", "HEAD")) { it.trim() }
|
||||
@ -164,6 +166,7 @@ abstract class CCTweakedExtension(private val project: Project) {
|
||||
jacoco.applyTo(this)
|
||||
|
||||
extensions.configure(JacocoTaskExtension::class.java) {
|
||||
includes = listOf("dan200.computercraft.*")
|
||||
excludes = listOf(
|
||||
"dan200.computercraft.mixin.*", // Exclude mixins, as they're not executed at runtime.
|
||||
"dan200.computercraft.shared.Capabilities$*", // Exclude capability tokens, as Forge rewrites them.
|
||||
|
@ -19,8 +19,8 @@ import dan200.computercraft.api.ComputerCraftAPI;
|
||||
*/
|
||||
public interface WiredElement extends WiredSender {
|
||||
/**
|
||||
* Called when objects on the network change. This may occur when network nodes are added or removed, or when
|
||||
* peripherals change.
|
||||
* Called when peripherals on the network change. This may occur when network nodes are added or removed, or when
|
||||
* peripherals are attached or detached from a modem.
|
||||
*
|
||||
* @param change The change which occurred.
|
||||
* @see WiredNetworkChange
|
||||
|
@ -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));
|
||||
|
@ -34,8 +34,8 @@ public final class GuiSprites extends TextureAtlasHolder {
|
||||
|
||||
private static ButtonTextures button(String name) {
|
||||
return new ButtonTextures(
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name),
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name + "_hover")
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "buttons/" + name),
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "buttons/" + name + "_hover")
|
||||
);
|
||||
}
|
||||
|
||||
@ -96,12 +96,8 @@ public final class GuiSprites extends TextureAtlasHolder {
|
||||
* @param active The texture for the button when it is active (hovered or focused).
|
||||
*/
|
||||
public record ButtonTextures(ResourceLocation normal, ResourceLocation active) {
|
||||
public TextureAtlasSprite get(boolean active) {
|
||||
return GuiSprites.get(active ? this.active : normal);
|
||||
}
|
||||
|
||||
public Stream<ResourceLocation> textures() {
|
||||
return Stream.of(normal, active);
|
||||
public ResourceLocation get(boolean isActive) {
|
||||
return isActive ? active : normal;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,9 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
|
||||
private static final ResourceLocation BACKGROUND_NORMAL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "textures/gui/turtle_normal.png");
|
||||
private static final ResourceLocation BACKGROUND_ADVANCED = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "textures/gui/turtle_advanced.png");
|
||||
|
||||
private static final ResourceLocation SELECTED_NORMAL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle_normal_selected_slot");
|
||||
private static final ResourceLocation SELECTED_ADVANCED = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle_advanced_selected_slot");
|
||||
|
||||
private static final int TEX_WIDTH = 278;
|
||||
private static final int TEX_HEIGHT = 217;
|
||||
|
||||
@ -54,9 +57,9 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
|
||||
if (slot >= 0) {
|
||||
var slotX = slot % 4;
|
||||
var slotY = slot / 4;
|
||||
graphics.blit(texture,
|
||||
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18, 0,
|
||||
0, 217, 24, 24, FULL_TEX_SIZE, FULL_TEX_SIZE
|
||||
graphics.blitSprite(
|
||||
advanced ? SELECTED_ADVANCED : SELECTED_NORMAL,
|
||||
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18, 0, 22, 22
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
@ -10,8 +10,8 @@ import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.Tooltip;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.function.Supplier;
|
||||
@ -21,11 +21,11 @@ import java.util.function.Supplier;
|
||||
* dynamically.
|
||||
*/
|
||||
public class DynamicImageButton extends Button {
|
||||
private final Boolean2ObjectFunction<TextureAtlasSprite> texture;
|
||||
private final Boolean2ObjectFunction<ResourceLocation> texture;
|
||||
private final Supplier<HintedMessage> message;
|
||||
|
||||
public DynamicImageButton(
|
||||
int x, int y, int width, int height, Boolean2ObjectFunction<TextureAtlasSprite> texture, OnPress onPress,
|
||||
int x, int y, int width, int height, Boolean2ObjectFunction<ResourceLocation> texture, OnPress onPress,
|
||||
HintedMessage message
|
||||
) {
|
||||
this(x, y, width, height, texture, onPress, () -> message);
|
||||
@ -33,7 +33,7 @@ public class DynamicImageButton extends Button {
|
||||
|
||||
public DynamicImageButton(
|
||||
int x, int y, int width, int height,
|
||||
Boolean2ObjectFunction<TextureAtlasSprite> texture,
|
||||
Boolean2ObjectFunction<ResourceLocation> texture,
|
||||
OnPress onPress, Supplier<HintedMessage> message
|
||||
) {
|
||||
super(x, y, width, height, Component.empty(), onPress, DEFAULT_NARRATION);
|
||||
@ -50,7 +50,7 @@ public class DynamicImageButton extends Button {
|
||||
var texture = this.texture.get(isHoveredOrFocused());
|
||||
|
||||
RenderSystem.disableDepthTest();
|
||||
graphics.blit(getX(), getY(), 0, width, height, texture);
|
||||
graphics.blitSprite(texture, getX(), getY(), 0, width, height);
|
||||
RenderSystem.enableDepthTest();
|
||||
}
|
||||
|
||||
|
@ -69,11 +69,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;
|
||||
}
|
||||
|
||||
@ -110,8 +107,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
|
||||
@ -220,7 +217,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) {
|
||||
|
@ -39,8 +39,8 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
|
||||
|
||||
int termWidth, termHeight;
|
||||
if (terminal == null) {
|
||||
termWidth = Config.pocketTermWidth;
|
||||
termHeight = Config.pocketTermHeight;
|
||||
termWidth = Config.DEFAULT_POCKET_TERM_WIDTH;
|
||||
termHeight = Config.DEFAULT_POCKET_TERM_HEIGHT;
|
||||
} else {
|
||||
termWidth = terminal.getWidth();
|
||||
termHeight = terminal.getHeight();
|
||||
|
@ -74,10 +74,6 @@ public final class DataProviders {
|
||||
LecternPrintoutModel.TEXTURE
|
||||
)));
|
||||
out.accept(GuiSprites.SPRITE_SHEET, makeSprites(
|
||||
// Buttons
|
||||
GuiSprites.TURNED_OFF.textures(),
|
||||
GuiSprites.TURNED_ON.textures(),
|
||||
GuiSprites.TERMINATE.textures(),
|
||||
// Computers
|
||||
GuiSprites.COMPUTER_NORMAL.textures(),
|
||||
GuiSprites.COMPUTER_ADVANCED.textures(),
|
||||
|
@ -1,11 +1,5 @@
|
||||
{
|
||||
"sources": [
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_off"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_off_hover"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_on"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_on_hover"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/terminate"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/terminate_hover"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/border_normal"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_normal"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/sidebar_normal"},
|
||||
|
@ -11,8 +11,7 @@ import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ComputerState;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory;
|
||||
import dan200.computercraft.shared.config.Config;
|
||||
import dan200.computercraft.shared.util.ComponentMap;
|
||||
import dan200.computercraft.shared.config.ConfigSpec;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
@ -33,10 +32,9 @@ public class ComputerBlockEntity extends AbstractComputerBlockEntity {
|
||||
|
||||
@Override
|
||||
protected ServerComputer createComputer(int id) {
|
||||
return new ServerComputer(
|
||||
(ServerLevel) getLevel(), getBlockPos(), id, label,
|
||||
getFamily(), Config.computerTermWidth, Config.computerTermHeight,
|
||||
ComponentMap.empty()
|
||||
return new ServerComputer((ServerLevel) getLevel(), getBlockPos(), ServerComputer.properties(id, getFamily())
|
||||
.label(getLabel())
|
||||
.terminalSize(ConfigSpec.computerTermWidth.get(), ConfigSpec.computerTermHeight.get())
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -6,44 +6,33 @@ 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 is used
|
||||
* Handles user-provided input, forwarding it to a computer. This describes the "shape" of both the client-and
|
||||
* server-side input handlers.
|
||||
*
|
||||
* @see ServerInputHandler
|
||||
* @see ServerComputer
|
||||
*/
|
||||
public interface InputHandler {
|
||||
void queueEvent(String event, @Nullable Object[] arguments);
|
||||
void keyDown(int key, boolean repeat);
|
||||
|
||||
default void queueEvent(String event) {
|
||||
queueEvent(event, null);
|
||||
}
|
||||
void keyUp(int key);
|
||||
|
||||
default void keyDown(int key, boolean repeat) {
|
||||
queueEvent("key", new Object[]{ key, repeat });
|
||||
}
|
||||
void charTyped(byte chr);
|
||||
|
||||
default void keyUp(int key) {
|
||||
queueEvent("key_up", new Object[]{ key });
|
||||
}
|
||||
void paste(ByteBuffer contents);
|
||||
|
||||
default void mouseClick(int button, int x, int y) {
|
||||
queueEvent("mouse_click", new Object[]{ button, x, y });
|
||||
}
|
||||
void mouseClick(int button, int x, int y);
|
||||
|
||||
default void mouseUp(int button, int x, int y) {
|
||||
queueEvent("mouse_up", new Object[]{ button, x, y });
|
||||
}
|
||||
void mouseUp(int button, int x, int y);
|
||||
|
||||
default void mouseDrag(int button, int x, int y) {
|
||||
queueEvent("mouse_drag", new Object[]{ button, x, y });
|
||||
}
|
||||
void mouseDrag(int button, int x, int y);
|
||||
|
||||
default void mouseScroll(int direction, int x, int y) {
|
||||
queueEvent("mouse_scroll", new Object[]{ direction, x, y });
|
||||
}
|
||||
void mouseScroll(int direction, int x, int y);
|
||||
|
||||
void terminate();
|
||||
|
||||
void shutdown();
|
||||
|
||||
|
@ -6,12 +6,14 @@ package dan200.computercraft.shared.computer.core;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.component.AdminComputer;
|
||||
import dan200.computercraft.api.component.ComputerComponent;
|
||||
import dan200.computercraft.api.component.ComputerComponents;
|
||||
import dan200.computercraft.api.filesystem.WritableMount;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.api.peripheral.WorkMonitor;
|
||||
import dan200.computercraft.core.computer.Computer;
|
||||
import dan200.computercraft.core.computer.ComputerEnvironment;
|
||||
import dan200.computercraft.core.computer.ComputerEvents;
|
||||
import dan200.computercraft.core.computer.ComputerSide;
|
||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||
import dan200.computercraft.impl.ApiFactories;
|
||||
@ -34,7 +36,7 @@ import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
public class ServerComputer implements ComputerEnvironment, ComputerEvents.Receiver {
|
||||
private final UUID instanceUUID = UUID.randomUUID();
|
||||
|
||||
private ServerLevel level;
|
||||
@ -49,16 +51,21 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
|
||||
private int ticksSincePing;
|
||||
|
||||
@Deprecated
|
||||
public ServerComputer(
|
||||
ServerLevel level, BlockPos position, int computerID, @Nullable String label, ComputerFamily family, int terminalWidth, int terminalHeight,
|
||||
ComponentMap baseComponents
|
||||
) {
|
||||
this(level, position, properties(computerID, family).label(label).terminalSize(terminalWidth, terminalHeight).addComponents(baseComponents));
|
||||
}
|
||||
|
||||
public ServerComputer(ServerLevel level, BlockPos position, Properties properties) {
|
||||
this.level = level;
|
||||
this.position = position;
|
||||
this.family = family;
|
||||
this.family = properties.family;
|
||||
|
||||
var context = ServerContext.get(level.getServer());
|
||||
terminal = new NetworkedTerminal(terminalWidth, terminalHeight, family != ComputerFamily.NORMAL, this::markTerminalChanged);
|
||||
terminal = new NetworkedTerminal(properties.terminalWidth, properties.terminalHeight, family != ComputerFamily.NORMAL, this::markTerminalChanged);
|
||||
metrics = context.metrics().createMetricObserver(this);
|
||||
|
||||
var componentBuilder = ComponentMap.builder();
|
||||
@ -67,11 +74,11 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
componentBuilder.add(ComputerComponents.ADMIN_COMPUTER, new AdminComputer() {
|
||||
});
|
||||
}
|
||||
componentBuilder.add(baseComponents);
|
||||
componentBuilder.add(properties.components.build());
|
||||
var components = componentBuilder.build();
|
||||
|
||||
computer = new Computer(context.computerContext(), this, terminal, computerID);
|
||||
computer.setLabel(label);
|
||||
computer = new Computer(context.computerContext(), this, terminal, properties.computerID);
|
||||
computer.setLabel(properties.label);
|
||||
|
||||
// Load in the externally registered APIs.
|
||||
for (var factory : ApiFactories.getAll()) {
|
||||
@ -84,24 +91,24 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
}
|
||||
}
|
||||
|
||||
public ComputerFamily getFamily() {
|
||||
public final ComputerFamily getFamily() {
|
||||
return family;
|
||||
}
|
||||
|
||||
public ServerLevel getLevel() {
|
||||
public final ServerLevel getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public BlockPos getPosition() {
|
||||
public final BlockPos getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public void setPosition(ServerLevel level, BlockPos pos) {
|
||||
public final void setPosition(ServerLevel level, BlockPos pos) {
|
||||
this.level = level;
|
||||
position = pos.immutable();
|
||||
}
|
||||
|
||||
protected void markTerminalChanged() {
|
||||
protected final void markTerminalChanged() {
|
||||
terminalChanged.set(true);
|
||||
}
|
||||
|
||||
@ -115,11 +122,11 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
sendToAllInteracting(c -> new ComputerTerminalClientMessage(c, getTerminalState()));
|
||||
}
|
||||
|
||||
public TerminalState getTerminalState() {
|
||||
public final TerminalState getTerminalState() {
|
||||
return TerminalState.create(terminal);
|
||||
}
|
||||
|
||||
public void keepAlive() {
|
||||
public final void keepAlive() {
|
||||
ticksSincePing = 0;
|
||||
}
|
||||
|
||||
@ -132,7 +139,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
*
|
||||
* @return What sides on the computer have changed.
|
||||
*/
|
||||
public int pollRedstoneChanges() {
|
||||
public final int pollRedstoneChanges() {
|
||||
return computer.pollRedstoneChanges();
|
||||
}
|
||||
|
||||
@ -145,7 +152,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
computer.unload();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
public final void close() {
|
||||
unload();
|
||||
ServerContext.get(level.getServer()).registry().remove(this);
|
||||
}
|
||||
@ -165,7 +172,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -174,93 +181,136 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
protected void onRemoved() {
|
||||
}
|
||||
|
||||
public UUID getInstanceUUID() {
|
||||
public final UUID getInstanceUUID() {
|
||||
return instanceUUID;
|
||||
}
|
||||
|
||||
public int getID() {
|
||||
public final int getID() {
|
||||
return computer.getID();
|
||||
}
|
||||
|
||||
public @Nullable String getLabel() {
|
||||
public final @Nullable String getLabel() {
|
||||
return computer.getLabel();
|
||||
}
|
||||
|
||||
public boolean isOn() {
|
||||
public final boolean isOn() {
|
||||
return computer.isOn();
|
||||
}
|
||||
|
||||
public ComputerState getState() {
|
||||
public final ComputerState getState() {
|
||||
if (!computer.isOn()) return ComputerState.OFF;
|
||||
return computer.isBlinking() ? ComputerState.BLINKING : ComputerState.ON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void turnOn() {
|
||||
public final void turnOn() {
|
||||
computer.turnOn();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
public final void shutdown() {
|
||||
computer.shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reboot() {
|
||||
public final void reboot() {
|
||||
computer.reboot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueEvent(String event, @Nullable Object[] arguments) {
|
||||
public final void queueEvent(String event, @Nullable Object[] arguments) {
|
||||
computer.queueEvent(event, arguments);
|
||||
}
|
||||
|
||||
public int getRedstoneOutput(ComputerSide side) {
|
||||
public final void queueEvent(String event) {
|
||||
queueEvent(event, null);
|
||||
}
|
||||
|
||||
public final int getRedstoneOutput(ComputerSide side) {
|
||||
return computer.isOn() ? computer.getRedstone().getExternalOutput(side) : 0;
|
||||
}
|
||||
|
||||
public void setRedstoneInput(ComputerSide side, int level, int bundledState) {
|
||||
public final void setRedstoneInput(ComputerSide side, int level, int bundledState) {
|
||||
computer.getRedstone().setInput(side, level, bundledState);
|
||||
}
|
||||
|
||||
public int getBundledRedstoneOutput(ComputerSide side) {
|
||||
public final int getBundledRedstoneOutput(ComputerSide side) {
|
||||
return computer.isOn() ? computer.getRedstone().getExternalBundledOutput(side) : 0;
|
||||
}
|
||||
|
||||
public void setPeripheral(ComputerSide side, @Nullable IPeripheral peripheral) {
|
||||
public final void setPeripheral(ComputerSide side, @Nullable IPeripheral peripheral) {
|
||||
computer.getEnvironment().setPeripheral(side, peripheral);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public IPeripheral getPeripheral(ComputerSide side) {
|
||||
public final IPeripheral getPeripheral(ComputerSide side) {
|
||||
return computer.getEnvironment().getPeripheral(side);
|
||||
}
|
||||
|
||||
public void setLabel(@Nullable String label) {
|
||||
public final void setLabel(@Nullable String label) {
|
||||
computer.setLabel(label);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getTimeOfDay() {
|
||||
public final double getTimeOfDay() {
|
||||
return (level.getDayTime() + 6000) % 24000 / 1000.0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDay() {
|
||||
public final int getDay() {
|
||||
return (int) ((level.getDayTime() + 6000) / 24000) + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MetricsObserver getMetrics() {
|
||||
public final MetricsObserver getMetrics() {
|
||||
return metrics;
|
||||
}
|
||||
|
||||
public WorkMonitor getMainThreadMonitor() {
|
||||
public final WorkMonitor getMainThreadMonitor() {
|
||||
return computer.getMainThreadMonitor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable WritableMount createRootMount() {
|
||||
public final WritableMount createRootMount() {
|
||||
return ComputerCraftAPI.createSaveDirMount(level.getServer(), "computer/" + computer.getID(), Config.computerSpaceLimit);
|
||||
}
|
||||
|
||||
public static Properties properties(int computerID, ComputerFamily family) {
|
||||
return new Properties(computerID, family);
|
||||
}
|
||||
|
||||
public static final class Properties {
|
||||
|
||||
private final int computerID;
|
||||
private @Nullable String label;
|
||||
private final ComputerFamily family;
|
||||
|
||||
private int terminalWidth = Config.DEFAULT_COMPUTER_TERM_WIDTH;
|
||||
private int terminalHeight = Config.DEFAULT_COMPUTER_TERM_HEIGHT;
|
||||
private final ComponentMap.Builder components = ComponentMap.builder();
|
||||
|
||||
private Properties(int computerID, ComputerFamily family) {
|
||||
this.computerID = computerID;
|
||||
this.family = family;
|
||||
}
|
||||
|
||||
public Properties label(@Nullable String label) {
|
||||
this.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Properties terminalSize(int width, int height) {
|
||||
if (width <= 0 || height <= 0) throw new IllegalArgumentException("Terminal size must be positive");
|
||||
this.terminalWidth = width;
|
||||
this.terminalHeight = height;
|
||||
return this;
|
||||
}
|
||||
|
||||
public <T> Properties addComponent(ComputerComponent<T> component, T value) {
|
||||
components.add(component, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
private Properties addComponents(ComponentMap components) {
|
||||
this.components.add(components);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,8 @@ package dan200.computercraft.shared.computer.menu;
|
||||
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;
|
||||
@ -21,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;
|
||||
|
||||
@ -48,21 +51,33 @@ 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);
|
||||
owner.getComputer().keyDown(key, repeat);
|
||||
ComputerEvents.keyDown(owner.getComputer(), key, repeat);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyUp(int key) {
|
||||
keysDown.remove(key);
|
||||
owner.getComputer().keyUp(key);
|
||||
ComputerEvents.keyUp(owner.getComputer(), key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void charTyped(byte chr) {
|
||||
if (StringUtil.isTypableChar(chr)) ComputerEvents.charTyped(owner.getComputer(), chr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paste(ByteBuffer contents) {
|
||||
if (contents.remaining() > 0 && isValidClipboard(contents)) ComputerEvents.paste(owner.getComputer(), contents);
|
||||
}
|
||||
|
||||
private static boolean isValidClipboard(ByteBuffer buffer) {
|
||||
for (int i = buffer.remaining(), max = buffer.limit(); i < max; i++) {
|
||||
if (!StringUtil.isTypableChar(buffer.get(i))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -71,7 +86,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
|
||||
lastMouseY = y;
|
||||
lastMouseDown = button;
|
||||
|
||||
owner.getComputer().mouseClick(button, x, y);
|
||||
ComputerEvents.mouseClick(owner.getComputer(), button, x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -80,7 +95,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
|
||||
lastMouseY = y;
|
||||
lastMouseDown = -1;
|
||||
|
||||
owner.getComputer().mouseUp(button, x, y);
|
||||
ComputerEvents.mouseUp(owner.getComputer(), button, x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -89,7 +104,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
|
||||
lastMouseY = y;
|
||||
lastMouseDown = button;
|
||||
|
||||
owner.getComputer().mouseDrag(button, x, y);
|
||||
ComputerEvents.mouseDrag(owner.getComputer(), button, x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -97,7 +112,12 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
|
||||
lastMouseX = x;
|
||||
lastMouseY = y;
|
||||
|
||||
owner.getComputer().mouseScroll(direction, x, y);
|
||||
ComputerEvents.mouseScroll(owner.getComputer(), direction, x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
owner.getComputer().queueEvent("terminate");
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -169,9 +189,9 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
|
||||
public void close() {
|
||||
var computer = owner.getComputer();
|
||||
var keys = keysDown.iterator();
|
||||
while (keys.hasNext()) computer.keyUp(keys.nextInt());
|
||||
while (keys.hasNext()) ComputerEvents.keyUp(computer, keys.nextInt());
|
||||
|
||||
if (lastMouseDown != -1) computer.mouseUp(lastMouseDown, lastMouseX, lastMouseY);
|
||||
if (lastMouseDown != -1) ComputerEvents.mouseUp(computer, lastMouseDown, lastMouseX, lastMouseY);
|
||||
|
||||
keysDown.clear();
|
||||
lastMouseDown = -1;
|
||||
|
@ -32,14 +32,14 @@ public final class Config {
|
||||
public static int advancedTurtleFuelLimit = 100000;
|
||||
public static boolean turtlesCanPush = true;
|
||||
|
||||
public static int computerTermWidth = 51;
|
||||
public static int computerTermHeight = 19;
|
||||
public static final int DEFAULT_COMPUTER_TERM_WIDTH = 51;
|
||||
public static final int DEFAULT_COMPUTER_TERM_HEIGHT = 19;
|
||||
|
||||
public static final int turtleTermWidth = 39;
|
||||
public static final int turtleTermHeight = 13;
|
||||
public static final int TURTLE_TERM_WIDTH = 39;
|
||||
public static final int TURTLE_TERM_HEIGHT = 13;
|
||||
|
||||
public static int pocketTermWidth = 26;
|
||||
public static int pocketTermHeight = 20;
|
||||
public static final int DEFAULT_POCKET_TERM_WIDTH = 26;
|
||||
public static final int DEFAULT_POCKET_TERM_HEIGHT = 20;
|
||||
|
||||
public static int monitorWidth = 8;
|
||||
public static int monitorHeight = 6;
|
||||
|
@ -344,13 +344,13 @@ public final class ConfigSpec {
|
||||
.push("term_sizes");
|
||||
|
||||
builder.comment("Terminal size of computers.").push("computer");
|
||||
computerTermWidth = builder.comment("Width of computer terminal").defineInRange("width", Config.computerTermWidth, 1, 255);
|
||||
computerTermHeight = builder.comment("Height of computer terminal").defineInRange("height", Config.computerTermHeight, 1, 255);
|
||||
computerTermWidth = builder.comment("Width of computer terminal").defineInRange("width", Config.DEFAULT_COMPUTER_TERM_WIDTH, 1, 255);
|
||||
computerTermHeight = builder.comment("Height of computer terminal").defineInRange("height", Config.DEFAULT_COMPUTER_TERM_HEIGHT, 1, 255);
|
||||
builder.pop();
|
||||
|
||||
builder.comment("Terminal size of pocket computers.").push("pocket_computer");
|
||||
pocketTermWidth = builder.comment("Width of pocket computer terminal").defineInRange("width", Config.pocketTermWidth, 1, 255);
|
||||
pocketTermHeight = builder.comment("Height of pocket computer terminal").defineInRange("height", Config.pocketTermHeight, 1, 255);
|
||||
pocketTermWidth = builder.comment("Width of pocket computer terminal").defineInRange("width", Config.DEFAULT_POCKET_TERM_WIDTH, 1, 255);
|
||||
pocketTermHeight = builder.comment("Height of pocket computer terminal").defineInRange("height", Config.DEFAULT_POCKET_TERM_HEIGHT, 1, 255);
|
||||
builder.pop();
|
||||
|
||||
builder.comment("Maximum size of monitors (in blocks).").push("monitor");
|
||||
@ -437,10 +437,6 @@ public final class ConfigSpec {
|
||||
Config.turtlesCanPush = turtlesCanPush.get();
|
||||
|
||||
// Terminal size
|
||||
Config.computerTermWidth = computerTermWidth.get();
|
||||
Config.computerTermHeight = computerTermHeight.get();
|
||||
Config.pocketTermWidth = pocketTermWidth.get();
|
||||
Config.pocketTermHeight = pocketTermHeight.get();
|
||||
Config.monitorWidth = monitorWidth.get();
|
||||
Config.monitorHeight = monitorHeight.get();
|
||||
}
|
||||
|
@ -26,9 +26,9 @@ public final class NetworkMessages {
|
||||
private static final List<CustomPacketPayload.TypeAndCodec<RegistryFriendlyByteBuf, ? extends NetworkMessage<ClientNetworkContext>>> clientMessages = new ArrayList<>();
|
||||
|
||||
public static final CustomPacketPayload.Type<ComputerActionServerMessage> COMPUTER_ACTION = registerServerbound("computer_action", ComputerActionServerMessage.STREAM_CODEC);
|
||||
public static final CustomPacketPayload.Type<QueueEventServerMessage> QUEUE_EVENT = register(serverMessages, "queue_event", QueueEventServerMessage.STREAM_CODEC);
|
||||
public static final CustomPacketPayload.Type<KeyEventServerMessage> KEY_EVENT = registerServerbound("key_event", KeyEventServerMessage.STREAM_CODEC);
|
||||
public static final CustomPacketPayload.Type<MouseEventServerMessage> MOUSE_EVENT = registerServerbound("mouse_event", MouseEventServerMessage.STREAM_CODEC);
|
||||
public static final CustomPacketPayload.Type<PasteEventComputerMessage> PASTE_EVENT = registerServerbound("paste_event", PasteEventComputerMessage.STREAM_CODEC);
|
||||
public static final CustomPacketPayload.Type<UploadFileMessage> UPLOAD_FILE = register(serverMessages, "upload_file", UploadFileMessage.STREAM_CODEC);
|
||||
|
||||
public static final CustomPacketPayload.Type<ChatTableClientMessage> CHAT_TABLE = registerClientbound("chat_table", ChatTableClientMessage.STREAM_CODEC);
|
||||
|
@ -83,25 +83,36 @@ public class MoreStreamCodecs {
|
||||
};
|
||||
|
||||
/**
|
||||
* Equivalent to {@link ByteBufCodecs#BYTE_ARRAY}, but into an immutable {@link ByteBuffer}.
|
||||
* Read a {@link ByteBuffer}, with a limit of the number of bytes to read.
|
||||
*
|
||||
* @param limit The maximum length of the received buffer.
|
||||
* @return A stream codec that reads {@link ByteBuffer}s.
|
||||
* @see #BYTE_BUFFER
|
||||
*/
|
||||
public static final StreamCodec<ByteBuf, ByteBuffer> BYTE_BUFFER = new StreamCodec<>() {
|
||||
@Override
|
||||
public ByteBuffer decode(ByteBuf buf) {
|
||||
var toRead = VarInt.read(buf);
|
||||
if (toRead > buf.readableBytes()) {
|
||||
throw new DecoderException("ByteArray with size " + toRead + " is bigger than allowed");
|
||||
public static StreamCodec<ByteBuf, ByteBuffer> byteBuffer(int limit) {
|
||||
return new StreamCodec<>() {
|
||||
@Override
|
||||
public ByteBuffer decode(ByteBuf buf) {
|
||||
var toRead = VarInt.read(buf);
|
||||
if (toRead > buf.readableBytes() || toRead >= limit) {
|
||||
throw new DecoderException("ByteArray with size " + toRead + " is bigger than allowed");
|
||||
}
|
||||
|
||||
var bytes = new byte[toRead];
|
||||
buf.readBytes(bytes);
|
||||
return ByteBuffer.wrap(bytes).asReadOnlyBuffer();
|
||||
}
|
||||
|
||||
var bytes = new byte[toRead];
|
||||
buf.readBytes(bytes);
|
||||
return ByteBuffer.wrap(bytes).asReadOnlyBuffer();
|
||||
}
|
||||
@Override
|
||||
public void encode(ByteBuf buf, ByteBuffer buffer) {
|
||||
VarInt.write(buf, buffer.remaining());
|
||||
buf.writeBytes(buffer.duplicate());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuf buf, ByteBuffer buffer) {
|
||||
VarInt.write(buf, buffer.remaining());
|
||||
buf.writeBytes(buffer.duplicate());
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Equivalent to {@link ByteBufCodecs#BYTE_ARRAY}, but into an immutable {@link ByteBuffer}.
|
||||
*/
|
||||
public static final StreamCodec<ByteBuf, ByteBuffer> BYTE_BUFFER = byteBuffer(Integer.MAX_VALUE);
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ public final 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();
|
||||
@ -49,6 +50,7 @@ public final class ComputerActionServerMessage extends ComputerServerMessage {
|
||||
}
|
||||
|
||||
public enum Action {
|
||||
TERMINATE,
|
||||
TURN_ON,
|
||||
SHUTDOWN,
|
||||
REBOOT
|
||||
|
@ -25,8 +25,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,6 +44,7 @@ public final class KeyEventServerMessage extends ComputerServerMessage {
|
||||
case UP -> input.keyUp(key);
|
||||
case DOWN -> input.keyDown(key, false);
|
||||
case REPEAT -> input.keyDown(key, true);
|
||||
case CHAR -> input.charTyped((byte) key);
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,6 +54,6 @@ public final class KeyEventServerMessage extends ComputerServerMessage {
|
||||
}
|
||||
|
||||
public enum Action {
|
||||
DOWN, REPEAT, UP
|
||||
DOWN, REPEAT, UP, CHAR
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,53 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.network.server;
|
||||
|
||||
import dan200.computercraft.core.util.StringUtil;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.computer.menu.ComputerMenu;
|
||||
import dan200.computercraft.shared.computer.menu.ServerInputHandler;
|
||||
import dan200.computercraft.shared.network.NetworkMessages;
|
||||
import dan200.computercraft.shared.network.codec.MoreStreamCodecs;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Paste a string on a {@link ServerComputer}.
|
||||
*
|
||||
* @see ServerInputHandler#paste(ByteBuffer)
|
||||
*/
|
||||
public class PasteEventComputerMessage extends ComputerServerMessage {
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, PasteEventComputerMessage> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.VAR_INT, PasteEventComputerMessage::containerId,
|
||||
MoreStreamCodecs.byteBuffer(StringUtil.MAX_PASTE_LENGTH), c -> c.text,
|
||||
PasteEventComputerMessage::new
|
||||
);
|
||||
|
||||
private final ByteBuffer text;
|
||||
|
||||
public PasteEventComputerMessage(AbstractContainerMenu menu, ByteBuffer text) {
|
||||
this(menu.containerId, text);
|
||||
}
|
||||
|
||||
private PasteEventComputerMessage(int id, ByteBuffer text) {
|
||||
super(id);
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handle(ServerNetworkContext context, ComputerMenu container) {
|
||||
container.getInput().paste(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomPacketPayload.Type<PasteEventComputerMessage> type() {
|
||||
return NetworkMessages.PASTE_EVENT;
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.network.server;
|
||||
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.computer.menu.ComputerMenu;
|
||||
import dan200.computercraft.shared.computer.menu.ServerInputHandler;
|
||||
import dan200.computercraft.shared.network.NetworkMessages;
|
||||
import dan200.computercraft.shared.util.NBTUtil;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Queue an event on a {@link ServerComputer}.
|
||||
*
|
||||
* @see ServerInputHandler#queueEvent(String)
|
||||
*/
|
||||
public final class QueueEventServerMessage extends ComputerServerMessage {
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, QueueEventServerMessage> STREAM_CODEC = StreamCodec.ofMember(QueueEventServerMessage::write, QueueEventServerMessage::new);
|
||||
|
||||
private final String event;
|
||||
private final @Nullable Object[] args;
|
||||
|
||||
public QueueEventServerMessage(AbstractContainerMenu menu, String event, @Nullable Object[] args) {
|
||||
super(menu.containerId);
|
||||
this.event = event;
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
private QueueEventServerMessage(FriendlyByteBuf buf) {
|
||||
super(buf.readVarInt());
|
||||
event = buf.readUtf(Short.MAX_VALUE);
|
||||
|
||||
var args = buf.readNbt();
|
||||
this.args = args == null ? null : NBTUtil.decodeObjects(args);
|
||||
}
|
||||
|
||||
private void write(RegistryFriendlyByteBuf buf) {
|
||||
buf.writeVarInt(containerId());
|
||||
buf.writeUtf(event);
|
||||
buf.writeNbt(args == null ? null : NBTUtil.encodeObjects(args));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handle(ServerNetworkContext context, ComputerMenu container) {
|
||||
container.getInput().queueEvent(event, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomPacketPayload.Type<QueueEventServerMessage> type() {
|
||||
return NetworkMessages.QUEUE_EVENT;
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ import dan200.computercraft.api.pocket.IPocketAccess;
|
||||
import dan200.computercraft.api.pocket.IPocketUpgrade;
|
||||
import dan200.computercraft.api.upgrades.UpgradeData;
|
||||
import dan200.computercraft.core.computer.ComputerSide;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
|
||||
import dan200.computercraft.shared.network.server.ServerNetworking;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
@ -41,8 +41,8 @@ public final class PocketBrain implements IPocketAccess {
|
||||
private int colour = -1;
|
||||
private int lightColour = -1;
|
||||
|
||||
public PocketBrain(PocketHolder holder, int computerID, @Nullable String label, ComputerFamily family, @Nullable UpgradeData<IPocketUpgrade> upgrade) {
|
||||
this.computer = new PocketServerComputer(this, holder, computerID, label, family);
|
||||
public PocketBrain(PocketHolder holder, @Nullable UpgradeData<IPocketUpgrade> upgrade, ServerComputer.Properties properties) {
|
||||
this.computer = new PocketServerComputer(this, holder, properties);
|
||||
this.holder = holder;
|
||||
this.position = holder.pos();
|
||||
this.upgrade = upgrade;
|
||||
|
@ -5,15 +5,13 @@
|
||||
package dan200.computercraft.shared.pocket.core;
|
||||
|
||||
import dan200.computercraft.api.component.ComputerComponents;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ComputerState;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.config.Config;
|
||||
import dan200.computercraft.shared.config.ConfigSpec;
|
||||
import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
|
||||
import dan200.computercraft.shared.network.client.PocketComputerDeletedClientMessage;
|
||||
import dan200.computercraft.shared.network.server.ServerNetworking;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
import dan200.computercraft.shared.util.ComponentMap;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
|
||||
@ -41,10 +39,10 @@ public final class PocketServerComputer extends ServerComputer {
|
||||
|
||||
private Set<ServerPlayer> tracking = Set.of();
|
||||
|
||||
PocketServerComputer(PocketBrain brain, PocketHolder holder, int computerID, @Nullable String label, ComputerFamily family) {
|
||||
super(
|
||||
holder.level(), holder.blockPos(), computerID, label, family, Config.pocketTermWidth, Config.pocketTermHeight,
|
||||
ComponentMap.builder().add(ComputerComponents.POCKET, brain).build()
|
||||
PocketServerComputer(PocketBrain brain, PocketHolder holder, ServerComputer.Properties properties) {
|
||||
super(holder.level(), holder.blockPos(), properties
|
||||
.terminalSize(ConfigSpec.pocketTermWidth.get(), ConfigSpec.pocketTermHeight.get())
|
||||
.addComponent(ComputerComponents.POCKET, brain)
|
||||
);
|
||||
this.brain = brain;
|
||||
}
|
||||
|
@ -196,7 +196,10 @@ public class PocketComputerItem extends Item implements IMedia {
|
||||
}
|
||||
|
||||
var computerID = NonNegativeId.getOrCreate(level.getServer(), stack, ModRegistry.DataComponents.COMPUTER_ID.get(), IDAssigner.COMPUTER);
|
||||
var brain = new PocketBrain(holder, computerID, getLabel(stack), getFamily(), getUpgradeWithData(stack));
|
||||
var brain = new PocketBrain(
|
||||
holder, getUpgradeWithData(stack),
|
||||
ServerComputer.properties(computerID, getFamily()).label(getLabel(stack))
|
||||
);
|
||||
var computer = brain.computer();
|
||||
|
||||
stack.set(ModRegistry.DataComponents.COMPUTER.get(), new ServerComputerReference(registry.getSessionID(), computer.register()));
|
||||
|
@ -24,7 +24,6 @@ import dan200.computercraft.shared.platform.PlatformHelper;
|
||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
|
||||
import dan200.computercraft.shared.turtle.core.TurtleBrain;
|
||||
import dan200.computercraft.shared.turtle.inventory.TurtleMenu;
|
||||
import dan200.computercraft.shared.util.ComponentMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
@ -80,10 +79,10 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
|
||||
|
||||
@Override
|
||||
protected ServerComputer createComputer(int id) {
|
||||
var computer = new ServerComputer(
|
||||
(ServerLevel) getLevel(), getBlockPos(), id, label,
|
||||
getFamily(), Config.turtleTermWidth, Config.turtleTermHeight,
|
||||
ComponentMap.builder().add(ComputerComponents.TURTLE, brain).build()
|
||||
var computer = new ServerComputer((ServerLevel) getLevel(), getBlockPos(), ServerComputer.properties(id, getFamily())
|
||||
.label(getLabel())
|
||||
.terminalSize(Config.TURTLE_TERM_WIDTH, Config.TURTLE_TERM_HEIGHT)
|
||||
.addComponent(ComputerComponents.TURTLE, brain)
|
||||
);
|
||||
brain.setupComputer(computer);
|
||||
return computer;
|
||||
|
@ -44,125 +44,33 @@ public final class NBTUtil {
|
||||
.ifPresent(x -> destination.put(key, x));
|
||||
}
|
||||
|
||||
private static @Nullable Tag toNBTTag(@Nullable Object object) {
|
||||
if (object == null) return null;
|
||||
if (object instanceof Boolean) return ByteTag.valueOf((byte) ((boolean) (Boolean) object ? 1 : 0));
|
||||
if (object instanceof Number) return DoubleTag.valueOf(((Number) object).doubleValue());
|
||||
if (object instanceof String) return StringTag.valueOf(object.toString());
|
||||
if (object instanceof Map<?, ?> m) {
|
||||
var nbt = new CompoundTag();
|
||||
var i = 0;
|
||||
for (Map.Entry<?, ?> entry : m.entrySet()) {
|
||||
var key = toNBTTag(entry.getKey());
|
||||
var value = toNBTTag(entry.getKey());
|
||||
if (key != null && value != null) {
|
||||
nbt.put("k" + i, key);
|
||||
nbt.put("v" + i, value);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
nbt.putInt("len", m.size());
|
||||
return nbt;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static @Nullable CompoundTag encodeObjects(@Nullable Object[] objects) {
|
||||
if (objects == null || objects.length == 0) return null;
|
||||
|
||||
var nbt = new CompoundTag();
|
||||
nbt.putInt("len", objects.length);
|
||||
for (var i = 0; i < objects.length; i++) {
|
||||
var child = toNBTTag(objects[i]);
|
||||
if (child != null) nbt.put(Integer.toString(i), child);
|
||||
}
|
||||
return nbt;
|
||||
}
|
||||
|
||||
private static @Nullable Object fromNBTTag(@Nullable Tag tag) {
|
||||
if (tag == null) return null;
|
||||
switch (tag.getId()) {
|
||||
case Tag.TAG_BYTE:
|
||||
return ((ByteTag) tag).getAsByte() > 0;
|
||||
case Tag.TAG_DOUBLE:
|
||||
return ((DoubleTag) tag).getAsDouble();
|
||||
default:
|
||||
case Tag.TAG_STRING:
|
||||
return tag.getAsString();
|
||||
case Tag.TAG_COMPOUND: {
|
||||
var c = (CompoundTag) tag;
|
||||
var len = c.getInt("len");
|
||||
Map<Object, Object> map = new HashMap<>(len);
|
||||
for (var i = 0; i < len; i++) {
|
||||
var key = fromNBTTag(c.get("k" + i));
|
||||
var value = fromNBTTag(c.get("v" + i));
|
||||
if (key != null && value != null) map.put(key, value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable Object toLua(@Nullable Tag tag) {
|
||||
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
|
||||
|
Before Width: | Height: | Size: 145 B After Width: | Height: | Size: 145 B |
Before Width: | Height: | Size: 144 B After Width: | Height: | Size: 144 B |
Before Width: | Height: | Size: 145 B After Width: | Height: | Size: 145 B |
Before Width: | Height: | Size: 145 B After Width: | Height: | Size: 145 B |
Before Width: | Height: | Size: 146 B After Width: | Height: | Size: 146 B |
Before Width: | Height: | Size: 146 B After Width: | Height: | Size: 146 B |
After Width: | Height: | Size: 461 B |
After Width: | Height: | Size: 476 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,51 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.mixin.gametest;
|
||||
|
||||
import com.mojang.datafixers.DataFixer;
|
||||
import dan200.computercraft.gametest.core.TestHooks;
|
||||
import net.minecraft.gametest.framework.GameTestServer;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.Services;
|
||||
import net.minecraft.server.WorldStem;
|
||||
import net.minecraft.server.level.progress.ChunkProgressListenerFactory;
|
||||
import net.minecraft.server.packs.repository.PackRepository;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Overwrite;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
import java.net.Proxy;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
@Mixin(GameTestServer.class)
|
||||
abstract class GameTestServerMixin extends MinecraftServer {
|
||||
GameTestServerMixin(Thread serverThread, LevelStorageSource.LevelStorageAccess storageSource, PackRepository packRepository, WorldStem worldStem, Proxy proxy, DataFixer fixerUpper, Services services, ChunkProgressListenerFactory progressListenerFactory) {
|
||||
super(serverThread, storageSource, packRepository, worldStem, proxy, fixerUpper, services, progressListenerFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrite {@link GameTestServer#waitUntilNextTick()} to wait for all computers to finish executing.
|
||||
* <p>
|
||||
* This is a little dangerous (breaks async behaviour of computers), but it forces tests to be deterministic.
|
||||
*
|
||||
* @reason See above. This is only in the test mod, so no risk of collision.
|
||||
* @author SquidDev.
|
||||
*/
|
||||
@Overwrite
|
||||
@Override
|
||||
public void waitUntilNextTick() {
|
||||
while (true) {
|
||||
runAllTasks();
|
||||
if (!haveTestsStarted() || TestHooks.areComputersIdle(this)) break;
|
||||
LockSupport.parkNanos(100_000);
|
||||
}
|
||||
}
|
||||
|
||||
@Shadow
|
||||
private boolean haveTestsStarted() {
|
||||
throw new AssertionError("Stub.");
|
||||
}
|
||||
}
|
@ -5,6 +5,8 @@
|
||||
package dan200.computercraft.gametest.core
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI
|
||||
import dan200.computercraft.core.ComputerContext
|
||||
import dan200.computercraft.core.computer.computerthread.ComputerThread
|
||||
import dan200.computercraft.gametest.*
|
||||
import dan200.computercraft.gametest.api.ClientGameTest
|
||||
import dan200.computercraft.gametest.api.TestTags
|
||||
@ -24,6 +26,8 @@ import net.minecraft.world.phys.Vec3
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
import java.lang.invoke.MethodHandle
|
||||
import java.lang.invoke.MethodHandles
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Modifier
|
||||
@ -91,6 +95,9 @@ object TestHooks {
|
||||
CCTestCommand.importFiles(server)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun areComputersIdle(server: MinecraftServer) = ComputerThreadReflection.isFullyIdle(ServerContext.get(server))
|
||||
|
||||
private val testClasses = listOf(
|
||||
Computer_Test::class.java,
|
||||
CraftOs_Test::class.java,
|
||||
@ -128,14 +135,6 @@ object TestHooks {
|
||||
}
|
||||
}
|
||||
|
||||
private val isCi = System.getenv("CI") != null
|
||||
|
||||
/**
|
||||
* Adjust the timeout of a test. This makes it 1.5 times longer when run under CI, as CI servers are less powerful
|
||||
* than our own.
|
||||
*/
|
||||
private fun adjustTimeout(timeout: Int): Int = if (isCi) timeout + (timeout / 2) else timeout
|
||||
|
||||
private fun registerTest(testClass: Class<*>, method: Method, fallbackRegister: Consumer<Method>) {
|
||||
val className = testClass.simpleName.lowercase()
|
||||
val testName = className + "." + method.name.lowercase()
|
||||
@ -147,7 +146,7 @@ object TestHooks {
|
||||
TestFunction(
|
||||
testInfo.batch, testName, testInfo.template.ifEmpty { testName },
|
||||
StructureUtils.getRotationForRotationSteps(testInfo.rotationSteps),
|
||||
adjustTimeout(testInfo.timeoutTicks),
|
||||
testInfo.timeoutTicks,
|
||||
testInfo.setupTicks,
|
||||
testInfo.required, testInfo.manualOnly,
|
||||
testInfo.attempts,
|
||||
@ -167,7 +166,7 @@ object TestHooks {
|
||||
testName,
|
||||
testName,
|
||||
testInfo.template.ifEmpty { testName },
|
||||
adjustTimeout(testInfo.timeoutTicks),
|
||||
testInfo.timeoutTicks,
|
||||
0,
|
||||
true,
|
||||
) { value -> safeInvoke(method, value) },
|
||||
@ -215,3 +214,31 @@ object TestHooks {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nasty reflection to determine if computers are fully idle.
|
||||
*
|
||||
* This is horribly nasty, and should not be used as a model for any production code!
|
||||
*
|
||||
* @see [ComputerThread.isFullyIdle]
|
||||
* @see [dan200.computercraft.mixin.gametest.GameTestServerMixin]
|
||||
*/
|
||||
private object ComputerThreadReflection {
|
||||
private val lookup = MethodHandles.lookup()
|
||||
|
||||
@JvmField
|
||||
val computerContext: MethodHandle = lookup.unreflectGetter(
|
||||
ServerContext::class.java.getDeclaredField("context").also { it.isAccessible = true },
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val isFullyIdle: MethodHandle = lookup.unreflect(
|
||||
ComputerThread::class.java.getDeclaredMethod("isFullyIdle").also { it.isAccessible = true },
|
||||
)
|
||||
|
||||
fun isFullyIdle(context: ServerContext): Boolean {
|
||||
val computerContext = computerContext.invokeExact(context) as ComputerContext
|
||||
val computerThread = computerContext.computerScheduler() as ComputerThread
|
||||
return isFullyIdle.invokeExact(computerThread) as Boolean
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ class MultiTestReporter(private val reporters: List<TestReporter>) : TestReporte
|
||||
* Reports tests to a JUnit XML file. This is equivalent to [JUnitLikeTestReporter], except it ensures the destination
|
||||
* directory exists.
|
||||
*/
|
||||
class JunitTestReporter constructor(destination: File) : JUnitLikeTestReporter(destination) {
|
||||
class JunitTestReporter(destination: File) : JUnitLikeTestReporter(destination) {
|
||||
override fun save(file: File) {
|
||||
try {
|
||||
Files.createDirectories(file.toPath().parent)
|
||||
|
@ -11,6 +11,7 @@
|
||||
"GameTestInfoAccessor",
|
||||
"GameTestSequenceAccessor",
|
||||
"GameTestSequenceMixin",
|
||||
"GameTestServerMixin",
|
||||
"StructureTemplateManagerMixin",
|
||||
"TestCommandAccessor"
|
||||
],
|
||||
|
@ -40,6 +40,8 @@ dependencies {
|
||||
}
|
||||
|
||||
tasks.processResources {
|
||||
inputs.property("gitHash", cct.gitHash)
|
||||
|
||||
var props = mapOf("gitContributors" to cct.gitContributors.get().joinToString("\n"))
|
||||
filesMatching("data/computercraft/lua/rom/help/credits.md") { expand(props) }
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
* <li>Passes main thread tasks to the {@link MainThreadScheduler.Executor}.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class Computer {
|
||||
public class Computer implements ComputerEvents.Receiver {
|
||||
private static final int START_DELAY = 50;
|
||||
|
||||
// Various properties of the computer
|
||||
@ -114,6 +114,7 @@ public class Computer {
|
||||
executor.queueStop(false, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueEvent(String event, @Nullable Object[] args) {
|
||||
executor.queueEvent(event, args);
|
||||
}
|
||||
|
@ -0,0 +1,72 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.computer;
|
||||
|
||||
import dan200.computercraft.core.util.StringUtil;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Built-in events that can be queued on a computer.
|
||||
*/
|
||||
public final class ComputerEvents {
|
||||
private ComputerEvents() {
|
||||
}
|
||||
|
||||
public static void keyDown(Receiver receiver, int key, boolean repeat) {
|
||||
receiver.queueEvent("key", new Object[]{ key, repeat });
|
||||
}
|
||||
|
||||
public static void keyUp(Receiver receiver, int key) {
|
||||
receiver.queueEvent("key_up", new Object[]{ key });
|
||||
}
|
||||
|
||||
/**
|
||||
* Type a character on the computer.
|
||||
*
|
||||
* @param receiver The computer to queue the event on.
|
||||
* @param chr The character to type.
|
||||
* @see StringUtil#isTypableChar(byte)
|
||||
*/
|
||||
public static void charTyped(Receiver receiver, byte chr) {
|
||||
receiver.queueEvent("char", new Object[]{ new byte[]{ chr } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste a string.
|
||||
*
|
||||
* @param receiver The computer to queue the event on.
|
||||
* @param contents The string to paste.
|
||||
* @see StringUtil#getClipboardString(String)
|
||||
*/
|
||||
public static void paste(Receiver receiver, ByteBuffer contents) {
|
||||
receiver.queueEvent("paste", new Object[]{ contents });
|
||||
}
|
||||
|
||||
public static void mouseClick(Receiver receiver, int button, int x, int y) {
|
||||
receiver.queueEvent("mouse_click", new Object[]{ button, x, y });
|
||||
}
|
||||
|
||||
public static void mouseUp(Receiver receiver, int button, int x, int y) {
|
||||
receiver.queueEvent("mouse_up", new Object[]{ button, x, y });
|
||||
}
|
||||
|
||||
public static void mouseDrag(Receiver receiver, int button, int x, int y) {
|
||||
receiver.queueEvent("mouse_drag", new Object[]{ button, x, y });
|
||||
}
|
||||
|
||||
public static void mouseScroll(Receiver receiver, int direction, int x, int y) {
|
||||
receiver.queueEvent("mouse_scroll", new Object[]{ direction, x, y });
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that can receive computer events.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface Receiver {
|
||||
void queueEvent(String event, @Nullable Object[] arguments);
|
||||
}
|
||||
}
|
@ -433,6 +433,16 @@ public final class ComputerThread implements ComputerScheduler {
|
||||
return computerQueueSize() > idleWorkers.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if no work is queued, and all workers are idle.
|
||||
*
|
||||
* @return If the threads are fully idle.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
boolean isFullyIdle() {
|
||||
return computerQueueSize() == 0 && idleWorkers.get() >= workerCount();
|
||||
}
|
||||
|
||||
private void workerFinished(WorkerThread worker) {
|
||||
// We should only shut down a worker once! This should only happen if we fail to abort a worker and then the
|
||||
// worker finishes normally.
|
||||
|
@ -4,52 +4,116 @@
|
||||
|
||||
package dan200.computercraft.core.util;
|
||||
|
||||
import dan200.computercraft.core.computer.ComputerEvents;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public final class StringUtil {
|
||||
public static final int MAX_PASTE_LENGTH = 512;
|
||||
|
||||
private StringUtil() {
|
||||
}
|
||||
|
||||
private static boolean isAllowed(char c) {
|
||||
return (c >= ' ' && c <= '~') || (c >= 161 && c <= 172) || (c >= 174 && c <= 255);
|
||||
}
|
||||
|
||||
private static String removeSpecialCharacters(String text, int length) {
|
||||
var builder = new StringBuilder(length);
|
||||
for (var i = 0; i < length; i++) {
|
||||
var c = text.charAt(i);
|
||||
builder.append(isAllowed(c) ? c : '?');
|
||||
/**
|
||||
* Convert a Unicode character to a terminal one.
|
||||
*
|
||||
* @param chr The Unicode character.
|
||||
* @return The terminal character.
|
||||
*/
|
||||
public static byte unicodeToTerminal(int chr) {
|
||||
// ASCII and latin1 map to themselves
|
||||
if (chr == 0 || chr == '\t' || chr == '\n' || chr == '\r' || (chr >= ' ' && chr <= '~') || (chr >= 160 && chr <= 255)) {
|
||||
return (byte) chr;
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
// Teletext block mosaics are *fairly* contiguous.
|
||||
if (chr >= 0x1FB00 && chr <= 0x1FB13) return (byte) (chr + (129 - 0x1fb00));
|
||||
if (chr >= 0x1FB14 && chr <= 0x1FB1D) return (byte) (chr + (150 - 0x1fb14));
|
||||
|
||||
// Everything else is just a manual lookup. For now, we just use a big switch statement, which we spin into a
|
||||
// separate function to hopefully avoid inlining it here.
|
||||
return unicodeToCraftOsFallback(chr);
|
||||
}
|
||||
|
||||
public static String normaliseLabel(String text) {
|
||||
return removeSpecialCharacters(text, Math.min(32, text.length()));
|
||||
private static byte unicodeToCraftOsFallback(int c) {
|
||||
return switch (c) {
|
||||
case 0x263A -> 1;
|
||||
case 0x263B -> 2;
|
||||
case 0x2665 -> 3;
|
||||
case 0x2666 -> 4;
|
||||
case 0x2663 -> 5;
|
||||
case 0x2660 -> 6;
|
||||
case 0x2022 -> 7;
|
||||
case 0x25D8 -> 8;
|
||||
case 0x2642 -> 11;
|
||||
case 0x2640 -> 12;
|
||||
case 0x266A -> 14;
|
||||
case 0x266B -> 15;
|
||||
case 0x25BA -> 16;
|
||||
case 0x25C4 -> 17;
|
||||
case 0x2195 -> 18;
|
||||
case 0x203C -> 19;
|
||||
case 0x25AC -> 22;
|
||||
case 0x21A8 -> 23;
|
||||
case 0x2191 -> 24;
|
||||
case 0x2193 -> 25;
|
||||
case 0x2192 -> 26;
|
||||
case 0x2190 -> 27;
|
||||
case 0x221F -> 28;
|
||||
case 0x2194 -> 29;
|
||||
case 0x25B2 -> 30;
|
||||
case 0x25BC -> 31;
|
||||
case 0x1FB99 -> 127;
|
||||
case 0x258C -> (byte) 149;
|
||||
default -> '?';
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise a string from the clipboard, suitable for pasting into a computer.
|
||||
* Check if a character is capable of being input and passed to a {@linkplain ComputerEvents#charTyped(ComputerEvents.Receiver, byte)
|
||||
* "char" event}.
|
||||
*
|
||||
* @param chr The character to check.
|
||||
* @return Whether this character can be typed.
|
||||
*/
|
||||
public static boolean isTypableChar(byte chr) {
|
||||
return chr != 0 && chr != '\r' && chr != '\n';
|
||||
}
|
||||
|
||||
private static boolean isAllowedInLabel(char c) {
|
||||
// Limit to ASCII and latin1, excluding '§' (Minecraft's formatting character).
|
||||
return (c >= ' ' && c <= '~') || (c >= 161 && c <= 255 && c != 167);
|
||||
}
|
||||
|
||||
public static String normaliseLabel(String text) {
|
||||
var length = Math.min(32, text.length());
|
||||
var builder = new StringBuilder(length);
|
||||
for (var i = 0; i < length; i++) {
|
||||
var c = text.charAt(i);
|
||||
builder.append(isAllowedInLabel(c) ? c : '?');
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Java string to a Lua one (using the terminal charset), suitable for pasting into a computer.
|
||||
* <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();
|
||||
}
|
||||
}
|
||||
|
@ -221,6 +221,8 @@ loom {
|
||||
}
|
||||
|
||||
tasks.processResources {
|
||||
inputs.property("modVersion", modVersion)
|
||||
|
||||
var props = mapOf("version" to modVersion)
|
||||
|
||||
filesMatching("fabric.mod.json") { expand(props) }
|
||||
|
@ -198,6 +198,9 @@ dependencies {
|
||||
// Compile tasks
|
||||
|
||||
tasks.processResources {
|
||||
inputs.property("modVersion", modVersion)
|
||||
inputs.property("neoVersion", libs.versions.neoForge)
|
||||
|
||||
var props = mapOf(
|
||||
"neoVersion" to libs.versions.neoForge.get(),
|
||||
"file" to mapOf("jarVersion" to modVersion),
|
||||
|
@ -8,6 +8,7 @@ import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
||||
import dan200.computercraft.core.apis.transfer.TransferredFile;
|
||||
import dan200.computercraft.core.apis.transfer.TransferredFiles;
|
||||
import dan200.computercraft.core.computer.Computer;
|
||||
import dan200.computercraft.core.computer.ComputerEvents;
|
||||
import dan200.computercraft.core.util.StringUtil;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
import org.lwjgl.glfw.GLFWDropCallback;
|
||||
@ -49,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) {
|
||||
@ -68,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;
|
||||
}
|
||||
@ -92,7 +91,7 @@ public class InputState {
|
||||
// Queue the "key" event and add to the down set
|
||||
var repeat = keysDown.get(key);
|
||||
keysDown.set(key);
|
||||
computer.queueEvent("key", new Object[]{ key, repeat });
|
||||
ComputerEvents.keyDown(computer, key, repeat);
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,7 +99,7 @@ public class InputState {
|
||||
// Queue the "key_up" event and remove from the down set
|
||||
if (key >= 0 && keysDown.get(key)) {
|
||||
keysDown.set(key, false);
|
||||
computer.queueEvent("key_up", new Object[]{ key });
|
||||
ComputerEvents.keyUp(computer, key);
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
@ -115,12 +114,12 @@ public class InputState {
|
||||
public void onMouseClick(int button, int action) {
|
||||
switch (action) {
|
||||
case GLFW.GLFW_PRESS -> {
|
||||
computer.queueEvent("mouse_click", new Object[]{ button + 1, lastMouseX + 1, lastMouseY + 1 });
|
||||
ComputerEvents.mouseClick(computer, button + 1, lastMouseX + 1, lastMouseY + 1);
|
||||
lastMouseButton = button;
|
||||
}
|
||||
case GLFW.GLFW_RELEASE -> {
|
||||
if (button == lastMouseButton) {
|
||||
computer.queueEvent("mouse_click", new Object[]{ button + 1, lastMouseX + 1, lastMouseY + 1 });
|
||||
ComputerEvents.mouseUp(computer, button + 1, lastMouseX + 1, lastMouseY + 1);
|
||||
lastMouseButton = -1;
|
||||
}
|
||||
}
|
||||
@ -133,13 +132,13 @@ public class InputState {
|
||||
lastMouseX = mouseX;
|
||||
lastMouseY = mouseY;
|
||||
if (lastMouseButton != -1) {
|
||||
computer.queueEvent("mouse_drag", new Object[]{ lastMouseButton + 1, mouseX + 1, mouseY + 1 });
|
||||
ComputerEvents.mouseDrag(computer, lastMouseButton + 1, mouseX + 1, mouseY + 1);
|
||||
}
|
||||
}
|
||||
|
||||
public void onMouseScroll(double yOffset) {
|
||||
if (yOffset != 0) {
|
||||
computer.queueEvent("mouse_scroll", new Object[]{ yOffset < 0 ? 1 : -1, lastMouseX + 1, lastMouseY + 1 });
|
||||
ComputerEvents.mouseScroll(computer, yOffset < 0 ? 1 : -1, lastMouseX + 1, lastMouseY + 1);
|
||||
}
|
||||
}
|
||||
|
||||
|