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); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|   | ||||
 Jonathan Coates
					Jonathan Coates