From 924b8ef30f6dfd5cafd1600558fca6f0224e3747 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Fri, 23 Jul 2021 23:32:14 +0100 Subject: [PATCH 1/7] Do not access the current server from the client Just missing some guards in a few places. Fixes #867 --- .../dan200/computercraft/shared/command/ClientCommands.java | 5 +++++ .../dan200/computercraft/shared/network/NetworkHandler.java | 6 +----- .../shared/peripheral/speaker/TileSpeaker.java | 5 ++++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/dan200/computercraft/shared/command/ClientCommands.java b/src/main/java/dan200/computercraft/shared/command/ClientCommands.java index c16b3fdd1..0477c46ad 100644 --- a/src/main/java/dan200/computercraft/shared/command/ClientCommands.java +++ b/src/main/java/dan200/computercraft/shared/command/ClientCommands.java @@ -7,11 +7,13 @@ import dan200.computercraft.ComputerCraft; import dan200.computercraft.shared.util.IDAssigner; +import net.minecraft.server.MinecraftServer; import net.minecraft.util.Util; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.client.event.ClientChatEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.server.ServerLifecycleHooks; import java.io.File; @@ -35,6 +37,9 @@ public static void onClientSendMessage( ClientChatEvent event ) // Emulate the command on the client side if( event.getMessage().startsWith( OPEN_COMPUTER ) ) { + MinecraftServer server = ServerLifecycleHooks.getCurrentServer(); + if( server == null || server.isDedicatedServer() ) return; + event.setCanceled( true ); String idStr = event.getMessage().substring( OPEN_COMPUTER.length() ).trim(); diff --git a/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java b/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java index 3d4fd5143..c66465340 100644 --- a/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java +++ b/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java @@ -21,7 +21,6 @@ import net.minecraftforge.fml.network.NetworkRegistry; import net.minecraftforge.fml.network.PacketDistributor; import net.minecraftforge.fml.network.simple.SimpleChannel; -import net.minecraftforge.fml.server.ServerLifecycleHooks; import java.util.function.Function; import java.util.function.Supplier; @@ -68,10 +67,7 @@ public static void sendToPlayer( PlayerEntity player, NetworkMessage packet ) public static void sendToAllPlayers( NetworkMessage packet ) { - for( ServerPlayerEntity player : ServerLifecycleHooks.getCurrentServer().getPlayerList().getPlayers() ) - { - sendToPlayer( player, packet ); - } + network.send( PacketDistributor.ALL.noArg(), packet ); } public static void sendToServer( NetworkMessage packet ) diff --git a/src/main/java/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java b/src/main/java/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java index 600190867..89e928503 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java @@ -47,7 +47,10 @@ public void tick() public void setRemoved() { super.setRemoved(); - NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( source ) ); + if( level != null && !level.isClientSide ) + { + NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( source ) ); + } } @Nonnull From b31e66686dae20cf9c0612581cf951f72ed0d804 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Fri, 23 Jul 2021 23:53:40 +0100 Subject: [PATCH 2/7] Make rednet.run a little more strict Also add a test for rednet message sending. Hopefully gives some of the modem and networking code a little bit of coverage (which is clearly the same as being right :p). --- .../computercraft/lua/rom/apis/rednet.lua | 12 +- .../computercraft/ingame/CraftOsTest.kt | 13 + .../computers/computer/11/startup.lua | 2 + .../computers/computer/12/startup.lua | 2 + .../computers/computer/13/startup.lua | 14 + .../computers/computer/14/startup.lua | 7 + src/test/server-files/computers/ids.json | 2 +- ...t_os_test.sends_basic_rednet_messages.snbt | 555 ++++++++++++++++++ 8 files changed, 600 insertions(+), 7 deletions(-) create mode 100644 src/test/java/dan200/computercraft/ingame/CraftOsTest.kt create mode 100644 src/test/server-files/computers/computer/13/startup.lua create mode 100644 src/test/server-files/computers/computer/14/startup.lua create mode 100644 src/test/server-files/structures/craft_os_test.sends_basic_rednet_messages.snbt diff --git a/src/main/resources/data/computercraft/lua/rom/apis/rednet.lua b/src/main/resources/data/computercraft/lua/rom/apis/rednet.lua index 3a8f3a5c0..3ad2b6925 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/rednet.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/rednet.lua @@ -351,12 +351,12 @@ function run() -- Got a modem message, process it and add it to the rednet event queue local sModem, nChannel, nReplyChannel, tMessage = p1, p2, p3, p4 if isOpen(sModem) and (nChannel == os.getComputerID() or nChannel == CHANNEL_BROADCAST) then - if type(tMessage) == "table" and tMessage.nMessageID then - if not tReceivedMessages[tMessage.nMessageID] then - tReceivedMessages[tMessage.nMessageID] = true - tReceivedMessageTimeouts[os.startTimer(30)] = tMessage.nMessageID - os.queueEvent("rednet_message", nReplyChannel, tMessage.message, tMessage.sProtocol) - end + if type(tMessage) == "table" and type(tMessage.nMessageID) == "number" + and tMessage.nMessageID == tMessage.nMessageID and not tReceivedMessages[tMessage.nMessageID] + then + tReceivedMessages[tMessage.nMessageID] = true + tReceivedMessageTimeouts[os.startTimer(30)] = tMessage.nMessageID + os.queueEvent("rednet_message", nReplyChannel, tMessage.message, tMessage.sProtocol) end end diff --git a/src/test/java/dan200/computercraft/ingame/CraftOsTest.kt b/src/test/java/dan200/computercraft/ingame/CraftOsTest.kt new file mode 100644 index 000000000..8b3bef714 --- /dev/null +++ b/src/test/java/dan200/computercraft/ingame/CraftOsTest.kt @@ -0,0 +1,13 @@ +package dan200.computercraft.ingame + +import dan200.computercraft.ingame.api.GameTest +import dan200.computercraft.ingame.api.TestContext +import dan200.computercraft.ingame.api.checkComputerOk + +class CraftOsTest { + /** + * Sends a rednet message to another a computer and back again. + */ + @GameTest + suspend fun `Sends basic rednet messages`(context: TestContext) = context.checkComputerOk(13) +} diff --git a/src/test/server-files/computers/computer/11/startup.lua b/src/test/server-files/computers/computer/11/startup.lua index bcc6c8ee2..58cc42004 100644 --- a/src/test/server-files/computers/computer/11/startup.lua +++ b/src/test/server-files/computers/computer/11/startup.lua @@ -1,3 +1,5 @@ +-- TurtleTest.`Use compostors` + test.eq(true, turtle.dropDown(), "Drop items into compostor") test.ok() diff --git a/src/test/server-files/computers/computer/12/startup.lua b/src/test/server-files/computers/computer/12/startup.lua index d84bc4bdc..94513b754 100644 --- a/src/test/server-files/computers/computer/12/startup.lua +++ b/src/test/server-files/computers/computer/12/startup.lua @@ -1,3 +1,5 @@ +-- TurtleTest.`Cleaned with cauldrons` + local old_details = turtle.getItemDetail(1, true) test.assert(turtle.place(), "Dyed turtle") diff --git a/src/test/server-files/computers/computer/13/startup.lua b/src/test/server-files/computers/computer/13/startup.lua new file mode 100644 index 000000000..8c786ff4b --- /dev/null +++ b/src/test/server-files/computers/computer/13/startup.lua @@ -0,0 +1,14 @@ +-- CraftOsTest.`Sends basic rednet messages` + +rednet.open("top") + +rednet.send(14, "Test msg") + +local id, msg +repeat + id, msg = rednet.receive() + print(id, msg) +until id == 14 + +test.eq("Test msg", msg) +test.ok() diff --git a/src/test/server-files/computers/computer/14/startup.lua b/src/test/server-files/computers/computer/14/startup.lua new file mode 100644 index 000000000..dd81c8ecb --- /dev/null +++ b/src/test/server-files/computers/computer/14/startup.lua @@ -0,0 +1,7 @@ +-- CraftOsTest.`Sends basic rednet messages` + +rednet.open("top") +while true do + local id, msg, protocol = rednet.receive() + rednet.send(id, msg, protocol) +end diff --git a/src/test/server-files/computers/ids.json b/src/test/server-files/computers/ids.json index 6b4a94cbe..cbd91ed6c 100644 --- a/src/test/server-files/computers/ids.json +++ b/src/test/server-files/computers/ids.json @@ -1,3 +1,3 @@ { - "computer": 12 + "computer": 14 } \ No newline at end of file diff --git a/src/test/server-files/structures/craft_os_test.sends_basic_rednet_messages.snbt b/src/test/server-files/structures/craft_os_test.sends_basic_rednet_messages.snbt new file mode 100644 index 000000000..a6ce86eba --- /dev/null +++ b/src/test/server-files/structures/craft_os_test.sends_basic_rednet_messages.snbt @@ -0,0 +1,555 @@ +{ + size: [5, 5, 5], + entities: [], + blocks: [ + { + pos: [0, 0, 0], + state: 0 + }, + { + pos: [1, 0, 0], + state: 0 + }, + { + pos: [2, 0, 0], + state: 0 + }, + { + pos: [3, 0, 0], + state: 0 + }, + { + pos: [4, 0, 0], + state: 0 + }, + { + pos: [0, 0, 1], + state: 0 + }, + { + pos: [1, 0, 1], + state: 0 + }, + { + pos: [2, 0, 1], + state: 0 + }, + { + pos: [3, 0, 1], + state: 0 + }, + { + pos: [4, 0, 1], + state: 0 + }, + { + pos: [0, 0, 2], + state: 0 + }, + { + pos: [1, 0, 2], + state: 0 + }, + { + pos: [2, 0, 2], + state: 0 + }, + { + pos: [3, 0, 2], + state: 0 + }, + { + pos: [4, 0, 2], + state: 0 + }, + { + pos: [0, 0, 3], + state: 0 + }, + { + pos: [1, 0, 3], + state: 0 + }, + { + pos: [2, 0, 3], + state: 0 + }, + { + pos: [3, 0, 3], + state: 0 + }, + { + pos: [4, 0, 3], + state: 0 + }, + { + pos: [0, 0, 4], + state: 0 + }, + { + pos: [1, 0, 4], + state: 0 + }, + { + pos: [2, 0, 4], + state: 0 + }, + { + pos: [3, 0, 4], + state: 0 + }, + { + pos: [4, 0, 4], + state: 0 + }, + { + nbt: { + Label: "Echo", + id: "computercraft:computer_advanced", + ComputerId: 14, + On: 1b + }, + pos: [1, 1, 2], + state: 1 + }, + { + nbt: { + Label: "Main", + id: "computercraft:computer_advanced", + ComputerId: 13, + On: 1b + }, + pos: [3, 1, 2], + state: 2 + }, + { + nbt: { + id: "computercraft:wireless_modem_normal" + }, + pos: [1, 2, 2], + state: 3 + }, + { + nbt: { + id: "computercraft:wireless_modem_normal" + }, + pos: [3, 2, 2], + state: 3 + }, + { + pos: [0, 1, 0], + state: 4 + }, + { + pos: [1, 1, 0], + state: 4 + }, + { + pos: [2, 1, 0], + state: 4 + }, + { + pos: [3, 1, 0], + state: 4 + }, + { + pos: [4, 1, 0], + state: 4 + }, + { + pos: [0, 2, 0], + state: 4 + }, + { + pos: [1, 2, 0], + state: 4 + }, + { + pos: [2, 2, 0], + state: 4 + }, + { + pos: [3, 2, 0], + state: 4 + }, + { + pos: [4, 2, 0], + state: 4 + }, + { + pos: [0, 3, 0], + state: 4 + }, + { + pos: [1, 3, 0], + state: 4 + }, + { + pos: [2, 3, 0], + state: 4 + }, + { + pos: [3, 3, 0], + state: 4 + }, + { + pos: [4, 3, 0], + state: 4 + }, + { + pos: [0, 4, 0], + state: 4 + }, + { + pos: [1, 4, 0], + state: 4 + }, + { + pos: [2, 4, 0], + state: 4 + }, + { + pos: [3, 4, 0], + state: 4 + }, + { + pos: [4, 4, 0], + state: 4 + }, + { + pos: [0, 1, 1], + state: 4 + }, + { + pos: [1, 1, 1], + state: 4 + }, + { + pos: [2, 1, 1], + state: 4 + }, + { + pos: [3, 1, 1], + state: 4 + }, + { + pos: [4, 1, 1], + state: 4 + }, + { + pos: [0, 2, 1], + state: 4 + }, + { + pos: [1, 2, 1], + state: 4 + }, + { + pos: [2, 2, 1], + state: 4 + }, + { + pos: [3, 2, 1], + state: 4 + }, + { + pos: [4, 2, 1], + state: 4 + }, + { + pos: [0, 3, 1], + state: 4 + }, + { + pos: [1, 3, 1], + state: 4 + }, + { + pos: [2, 3, 1], + state: 4 + }, + { + pos: [3, 3, 1], + state: 4 + }, + { + pos: [4, 3, 1], + state: 4 + }, + { + pos: [0, 4, 1], + state: 4 + }, + { + pos: [1, 4, 1], + state: 4 + }, + { + pos: [2, 4, 1], + state: 4 + }, + { + pos: [3, 4, 1], + state: 4 + }, + { + pos: [4, 4, 1], + state: 4 + }, + { + pos: [0, 1, 2], + state: 4 + }, + { + pos: [2, 1, 2], + state: 4 + }, + { + pos: [4, 1, 2], + state: 4 + }, + { + pos: [0, 2, 2], + state: 4 + }, + { + pos: [2, 2, 2], + state: 4 + }, + { + pos: [4, 2, 2], + state: 4 + }, + { + pos: [0, 3, 2], + state: 4 + }, + { + pos: [1, 3, 2], + state: 4 + }, + { + pos: [2, 3, 2], + state: 4 + }, + { + pos: [3, 3, 2], + state: 4 + }, + { + pos: [4, 3, 2], + state: 4 + }, + { + pos: [0, 4, 2], + state: 4 + }, + { + pos: [1, 4, 2], + state: 4 + }, + { + pos: [2, 4, 2], + state: 4 + }, + { + pos: [3, 4, 2], + state: 4 + }, + { + pos: [4, 4, 2], + state: 4 + }, + { + pos: [0, 1, 3], + state: 4 + }, + { + pos: [1, 1, 3], + state: 4 + }, + { + pos: [2, 1, 3], + state: 4 + }, + { + pos: [3, 1, 3], + state: 4 + }, + { + pos: [4, 1, 3], + state: 4 + }, + { + pos: [0, 2, 3], + state: 4 + }, + { + pos: [1, 2, 3], + state: 4 + }, + { + pos: [2, 2, 3], + state: 4 + }, + { + pos: [3, 2, 3], + state: 4 + }, + { + pos: [4, 2, 3], + state: 4 + }, + { + pos: [0, 3, 3], + state: 4 + }, + { + pos: [1, 3, 3], + state: 4 + }, + { + pos: [2, 3, 3], + state: 4 + }, + { + pos: [3, 3, 3], + state: 4 + }, + { + pos: [4, 3, 3], + state: 4 + }, + { + pos: [0, 4, 3], + state: 4 + }, + { + pos: [1, 4, 3], + state: 4 + }, + { + pos: [2, 4, 3], + state: 4 + }, + { + pos: [3, 4, 3], + state: 4 + }, + { + pos: [4, 4, 3], + state: 4 + }, + { + pos: [0, 1, 4], + state: 4 + }, + { + pos: [1, 1, 4], + state: 4 + }, + { + pos: [2, 1, 4], + state: 4 + }, + { + pos: [3, 1, 4], + state: 4 + }, + { + pos: [4, 1, 4], + state: 4 + }, + { + pos: [0, 2, 4], + state: 4 + }, + { + pos: [1, 2, 4], + state: 4 + }, + { + pos: [2, 2, 4], + state: 4 + }, + { + pos: [3, 2, 4], + state: 4 + }, + { + pos: [4, 2, 4], + state: 4 + }, + { + pos: [0, 3, 4], + state: 4 + }, + { + pos: [1, 3, 4], + state: 4 + }, + { + pos: [2, 3, 4], + state: 4 + }, + { + pos: [3, 3, 4], + state: 4 + }, + { + pos: [4, 3, 4], + state: 4 + }, + { + pos: [0, 4, 4], + state: 4 + }, + { + pos: [1, 4, 4], + state: 4 + }, + { + pos: [2, 4, 4], + state: 4 + }, + { + pos: [3, 4, 4], + state: 4 + }, + { + pos: [4, 4, 4], + state: 4 + } + ], + palette: [ + { + Name: "minecraft:polished_andesite" + }, + { + Properties: { + facing: "north", + state: "on" + }, + Name: "computercraft:computer_advanced" + }, + { + Properties: { + facing: "north", + state: "blinking" + }, + Name: "computercraft:computer_advanced" + }, + { + Properties: { + waterlogged: "false", + facing: "down", + on: "true" + }, + Name: "computercraft:wireless_modem_normal" + }, + { + Name: "minecraft:air" + } + ], + DataVersion: 2230 +} From 0568c8662837f44b0213505215c50294dd563812 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 24 Jul 2021 00:00:37 +0100 Subject: [PATCH 3/7] Hopefully fix flakiness in rednet test --- src/test/server-files/computers/computer/13/startup.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/server-files/computers/computer/13/startup.lua b/src/test/server-files/computers/computer/13/startup.lua index 8c786ff4b..e8954964f 100644 --- a/src/test/server-files/computers/computer/13/startup.lua +++ b/src/test/server-files/computers/computer/13/startup.lua @@ -2,11 +2,11 @@ rednet.open("top") -rednet.send(14, "Test msg") - local id, msg repeat - id, msg = rednet.receive() + rednet.send(14, "Test msg") -- Keep sending, as other computer may not have started yet. + + id, msg = rednet.receive(nil, 1) print(id, msg) until id == 14 From 03396cf07aee870b10be3e0f167f4ff63b5eef6e Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 24 Jul 2021 22:58:23 +0100 Subject: [PATCH 4/7] Fix help crashing on terminal resize Closes #870. Woops. --- src/main/resources/data/computercraft/lua/rom/programs/help.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/data/computercraft/lua/rom/programs/help.lua b/src/main/resources/data/computercraft/lua/rom/programs/help.lua index 43ba258d4..9c7081ecc 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/help.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/help.lua @@ -265,7 +265,7 @@ while true do local new_width, new_height = term.getSize() if new_width ~= width then - lines = word_wrap(contents, new_width) + lines, fg, bg = word_wrap(contents, new_width) print_height = #lines end From 3a80b51a9fd0857b36185392d4f9a2f38a97155b Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 25 Jul 2021 14:18:07 +0100 Subject: [PATCH 5/7] Ensure monitors are well-formed when placed Closes #36 --- .../peripheral/monitor/MonitorState.java | 52 ++ .../peripheral/monitor/TileMonitor.java | 110 ++-- .../computercraft/ingame/MonitorTest.kt | 39 ++ .../ingame/api/TestExtensions.kt | 6 + .../monitor_test.ensures_valid_on_place.snbt | 515 ++++++++++++++++++ 5 files changed, 687 insertions(+), 35 deletions(-) create mode 100644 src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorState.java create mode 100644 src/test/java/dan200/computercraft/ingame/MonitorTest.kt create mode 100644 src/test/server-files/structures/monitor_test.ensures_valid_on_place.snbt diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorState.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorState.java new file mode 100644 index 000000000..791c425ad --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorState.java @@ -0,0 +1,52 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.peripheral.monitor; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +final class MonitorState +{ + public static final MonitorState UNLOADED = new MonitorState( State.UNLOADED, null ); + public static final MonitorState MISSING = new MonitorState( State.MISSING, null ); + + private final State state; + private final TileMonitor monitor; + + private MonitorState( @Nonnull State state, @Nullable TileMonitor monitor ) + { + this.state = state; + this.monitor = monitor; + } + + public static MonitorState present( @Nonnull TileMonitor monitor ) + { + return new MonitorState( State.PRESENT, monitor ); + } + + public boolean isPresent() + { + return state == State.PRESENT; + } + + public boolean isMissing() + { + return state == State.MISSING; + } + + @Nullable + public TileMonitor getMonitor() + { + return monitor; + } + + enum State + { + UNLOADED, + MISSING, + PRESENT, + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java index 0fc154e0e..637fd81e3 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java @@ -56,6 +56,7 @@ public class TileMonitor extends TileGeneric private final Set computers = new HashSet<>(); private boolean needsUpdate = false; + private boolean needsValidating = false; private boolean destroyed = false; private boolean visiting = false; @@ -78,6 +79,7 @@ public TileMonitor( TileEntityType type, boolean advanced public void onLoad() { super.onLoad(); + needsValidating = true; TickScheduler.schedule( this ); } @@ -148,6 +150,12 @@ public void load( @Nonnull CompoundNBT tag ) @Override public void blockTick() { + if( needsValidating ) + { + needsValidating = false; + validate(); + } + if( needsUpdate ) { needsUpdate = false; @@ -164,7 +172,7 @@ public void blockTick() { for( int y = 0; y < height; y++ ) { - TileMonitor monitor = getNeighbour( x, y ); + TileMonitor monitor = getNeighbour( x, y ).getMonitor(); if( monitor == null ) continue; for( IComputerAccess computer : monitor.computers ) @@ -208,7 +216,7 @@ private ServerMonitor getServerMonitor() { if( serverMonitor != null ) return serverMonitor; - TileMonitor origin = getOrigin(); + TileMonitor origin = getOrigin().getMonitor(); if( origin == null ) return null; return serverMonitor = origin.serverMonitor; @@ -229,7 +237,7 @@ private ServerMonitor createServerMonitor() { for( int y = 0; y < height; y++ ) { - TileMonitor monitor = getNeighbour( x, y ); + TileMonitor monitor = getNeighbour( x, y ).getMonitor(); if( monitor != null ) monitor.serverMonitor = serverMonitor; } } @@ -374,24 +382,24 @@ public int getYIndex() return yIndex; } - private TileMonitor getSimilarMonitorAt( BlockPos pos ) + @Nonnull + private MonitorState getSimilarMonitorAt( BlockPos pos ) { - if( pos.equals( getBlockPos() ) ) return this; + if( pos.equals( getBlockPos() ) ) return MonitorState.present( this ); - int y = pos.getY(); World world = getLevel(); - if( world == null || !world.isAreaLoaded( pos, 0 ) ) return null; + if( world == null || !world.isAreaLoaded( pos, 0 ) ) return MonitorState.UNLOADED; TileEntity tile = world.getBlockEntity( pos ); - if( !(tile instanceof TileMonitor) ) return null; + if( !(tile instanceof TileMonitor) ) return MonitorState.MISSING; TileMonitor monitor = (TileMonitor) tile; return !monitor.visiting && !monitor.destroyed && advanced == monitor.advanced && getDirection() == monitor.getDirection() && getOrientation() == monitor.getOrientation() - ? monitor : null; + ? MonitorState.present( monitor ) : MonitorState.MISSING; } - private TileMonitor getNeighbour( int x, int y ) + private MonitorState getNeighbour( int x, int y ) { BlockPos pos = getBlockPos(); Direction right = getRight(); @@ -401,7 +409,7 @@ private TileMonitor getNeighbour( int x, int y ) return getSimilarMonitorAt( pos.relative( right, xOffset ).relative( down, yOffset ) ); } - private TileMonitor getOrigin() + private MonitorState getOrigin() { return getNeighbour( 0, 0 ); } @@ -425,7 +433,7 @@ private void resize( int width, int height ) { for( int y = 0; y < height; y++ ) { - TileMonitor monitor = getNeighbour( x, y ); + TileMonitor monitor = getNeighbour( x, y ).getMonitor(); if( monitor != null && monitor.peripheral != null ) { needsTerminal = true; @@ -453,7 +461,7 @@ private void resize( int width, int height ) { for( int y = 0; y < height; y++ ) { - TileMonitor monitor = getNeighbour( x, y ); + TileMonitor monitor = getNeighbour( x, y ).getMonitor(); if( monitor == null ) continue; monitor.xIndex = x; @@ -469,13 +477,13 @@ private void resize( int width, int height ) private boolean mergeLeft() { - TileMonitor left = getNeighbour( -1, 0 ); + TileMonitor left = getNeighbour( -1, 0 ).getMonitor(); if( left == null || left.yIndex != 0 || left.height != height ) return false; int width = left.width + this.width; if( width > ComputerCraft.monitorWidth ) return false; - TileMonitor origin = left.getOrigin(); + TileMonitor origin = left.getOrigin().getMonitor(); if( origin != null ) origin.resize( width, height ); left.expand(); return true; @@ -483,13 +491,13 @@ private boolean mergeLeft() private boolean mergeRight() { - TileMonitor right = getNeighbour( width, 0 ); + TileMonitor right = getNeighbour( width, 0 ).getMonitor(); if( right == null || right.yIndex != 0 || right.height != height ) return false; int width = this.width + right.width; if( width > ComputerCraft.monitorWidth ) return false; - TileMonitor origin = getOrigin(); + TileMonitor origin = getOrigin().getMonitor(); if( origin != null ) origin.resize( width, height ); expand(); return true; @@ -497,13 +505,13 @@ private boolean mergeRight() private boolean mergeUp() { - TileMonitor above = getNeighbour( 0, height ); + TileMonitor above = getNeighbour( 0, height ).getMonitor(); if( above == null || above.xIndex != 0 || above.width != width ) return false; int height = above.height + this.height; if( height > ComputerCraft.monitorHeight ) return false; - TileMonitor origin = getOrigin(); + TileMonitor origin = getOrigin().getMonitor(); if( origin != null ) origin.resize( width, height ); expand(); return true; @@ -511,13 +519,13 @@ private boolean mergeUp() private boolean mergeDown() { - TileMonitor below = getNeighbour( 0, -1 ); + TileMonitor below = getNeighbour( 0, -1 ).getMonitor(); if( below == null || below.xIndex != 0 || below.width != width ) return false; int height = this.height + below.height; if( height > ComputerCraft.monitorHeight ) return false; - TileMonitor origin = below.getOrigin(); + TileMonitor origin = below.getOrigin().getMonitor(); if( origin != null ) origin.resize( width, height ); below.expand(); return true; @@ -546,22 +554,22 @@ void contractNeighbours() visiting = true; if( xIndex > 0 ) { - TileMonitor left = getNeighbour( xIndex - 1, yIndex ); + TileMonitor left = getNeighbour( xIndex - 1, yIndex ).getMonitor(); if( left != null ) left.contract(); } if( xIndex + 1 < width ) { - TileMonitor right = getNeighbour( xIndex + 1, yIndex ); + TileMonitor right = getNeighbour( xIndex + 1, yIndex ).getMonitor(); if( right != null ) right.contract(); } if( yIndex > 0 ) { - TileMonitor below = getNeighbour( xIndex, yIndex - 1 ); + TileMonitor below = getNeighbour( xIndex, yIndex - 1 ).getMonitor(); if( below != null ) below.contract(); } if( yIndex + 1 < height ) { - TileMonitor above = getNeighbour( xIndex, yIndex + 1 ); + TileMonitor above = getNeighbour( xIndex, yIndex + 1 ).getMonitor(); if( above != null ) above.contract(); } visiting = false; @@ -572,11 +580,11 @@ void contract() int height = this.height; int width = this.width; - TileMonitor origin = getOrigin(); + TileMonitor origin = getOrigin().getMonitor(); if( origin == null ) { - TileMonitor right = width > 1 ? getNeighbour( 1, 0 ) : null; - TileMonitor below = height > 1 ? getNeighbour( 0, 1 ) : null; + TileMonitor right = width > 1 ? getNeighbour( 1, 0 ).getMonitor() : null; + TileMonitor below = height > 1 ? getNeighbour( 0, 1 ).getMonitor() : null; if( right != null ) right.resize( width - 1, 1 ); if( below != null ) below.resize( width, height - 1 ); @@ -590,7 +598,7 @@ void contract() { for( int x = 0; x < width; x++ ) { - TileMonitor monitor = origin.getNeighbour( x, y ); + TileMonitor monitor = origin.getNeighbour( x, y ).getMonitor(); if( monitor != null ) continue; // Decompose @@ -606,17 +614,17 @@ void contract() } if( x > 0 ) { - left = origin.getNeighbour( 0, y ); + left = origin.getNeighbour( 0, y ).getMonitor(); left.resize( x, 1 ); } if( x + 1 < width ) { - right = origin.getNeighbour( x + 1, y ); + right = origin.getNeighbour( x + 1, y ).getMonitor(); right.resize( width - (x + 1), 1 ); } if( y + 1 < height ) { - below = origin.getNeighbour( 0, y + 1 ); + below = origin.getNeighbour( 0, y + 1 ).getMonitor(); below.resize( width, height - (y + 1) ); } @@ -630,6 +638,38 @@ void contract() } } + private boolean checkMonitorAt( int xIndex, int yIndex ) + { + BlockPos pos = getBlockPos(); + Direction right = getRight(); + Direction down = getDown(); + + MonitorState state = getSimilarMonitorAt( pos.relative( right, xIndex ).relative( down, yIndex ) ); + + if( state.isMissing() ) return false; + + TileMonitor monitor = state.getMonitor(); + if( monitor == null ) return true; + + return monitor.xIndex == xIndex && monitor.yIndex == yIndex && monitor.width == width && monitor.height == height; + } + + private void validate() + { + if( xIndex == 0 && yIndex == 0 && width == 1 || height == 1 ) return; + + if( checkMonitorAt( 0, 0 ) && checkMonitorAt( 0, height - 1 ) && + checkMonitorAt( width - 1, 0 ) && checkMonitorAt( width - 1, height - 1 ) ) + { + return; + } + + // Something in our monitor is invalid. For now, let's just reset ourselves and then try to integrate ourselves + // later. + resize( 1, 1 ); + needsUpdate = true; + } + private void monitorTouched( float xPos, float yPos, float zPos ) { XYPair pair = XYPair @@ -657,7 +697,7 @@ private void monitorTouched( float xPos, float yPos, float zPos ) { for( int x = 0; x < width; x++ ) { - TileMonitor monitor = getNeighbour( x, y ); + TileMonitor monitor = getNeighbour( x, y ).getMonitor(); if( monitor == null ) continue; for( IComputerAccess computer : monitor.computers ) @@ -683,8 +723,8 @@ void removeComputer( IComputerAccess computer ) @Override public AxisAlignedBB getRenderBoundingBox() { - TileMonitor start = getNeighbour( 0, 0 ); - TileMonitor end = getNeighbour( width - 1, height - 1 ); + TileMonitor start = getNeighbour( 0, 0 ).getMonitor(); + TileMonitor end = getNeighbour( width - 1, height - 1 ).getMonitor(); if( start != null && end != null ) { BlockPos startPos = start.getBlockPos(); diff --git a/src/test/java/dan200/computercraft/ingame/MonitorTest.kt b/src/test/java/dan200/computercraft/ingame/MonitorTest.kt new file mode 100644 index 000000000..6954f2522 --- /dev/null +++ b/src/test/java/dan200/computercraft/ingame/MonitorTest.kt @@ -0,0 +1,39 @@ +package dan200.computercraft.ingame + +import dan200.computercraft.ingame.api.* +import dan200.computercraft.shared.Registry +import dan200.computercraft.shared.peripheral.monitor.TileMonitor +import net.minecraft.block.Blocks +import net.minecraft.command.arguments.BlockStateInput +import net.minecraft.nbt.CompoundNBT +import net.minecraft.util.math.BlockPos +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.fail +import java.util.* + +class MonitorTest { + @GameTest + suspend fun `Ensures valid on place`(context: TestContext) { + val pos = BlockPos(2, 0, 2) + val tag = CompoundNBT() + tag.putInt("Width", 2) + tag.putInt("Height", 2) + + val toSet = BlockStateInput( + Registry.ModBlocks.MONITOR_ADVANCED.get().defaultBlockState(), + Collections.emptySet(), + tag + ) + + context.setBlock(pos, Blocks.AIR.defaultBlockState()) + context.setBlock(pos, toSet) + + context.sleep(2) + + val tile = context.getTile(pos) + if (tile !is TileMonitor) fail("Expected tile to be monitor, is $tile") + + assertEquals(1, tile.width, "Width should be 1") + assertEquals(1, tile.height, "Width should be 1") + } +} diff --git a/src/test/java/dan200/computercraft/ingame/api/TestExtensions.kt b/src/test/java/dan200/computercraft/ingame/api/TestExtensions.kt index aca550400..bac096f40 100644 --- a/src/test/java/dan200/computercraft/ingame/api/TestExtensions.kt +++ b/src/test/java/dan200/computercraft/ingame/api/TestExtensions.kt @@ -3,6 +3,7 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import net.minecraft.block.BlockState +import net.minecraft.command.arguments.BlockStateInput import net.minecraft.entity.Entity import net.minecraft.tileentity.TileEntity import net.minecraft.util.math.AxisAlignedBB @@ -54,6 +55,11 @@ tracker.level.setBlockAndUpdate(offset(pos), state) } +/** + * Set a block within the test structure. + */ +fun TestContext.setBlock(pos: BlockPos, state: BlockStateInput) = state.place(tracker.level, offset(pos), 3) + /** * Modify a block state within the test. */ diff --git a/src/test/server-files/structures/monitor_test.ensures_valid_on_place.snbt b/src/test/server-files/structures/monitor_test.ensures_valid_on_place.snbt new file mode 100644 index 000000000..eef6627f5 --- /dev/null +++ b/src/test/server-files/structures/monitor_test.ensures_valid_on_place.snbt @@ -0,0 +1,515 @@ +{ + size: [5, 5, 5], + entities: [], + blocks: [ + { + pos: [0, 0, 0], + state: 0 + }, + { + pos: [1, 0, 0], + state: 0 + }, + { + pos: [2, 0, 0], + state: 0 + }, + { + pos: [3, 0, 0], + state: 0 + }, + { + pos: [4, 0, 0], + state: 0 + }, + { + pos: [0, 0, 1], + state: 0 + }, + { + pos: [1, 0, 1], + state: 0 + }, + { + pos: [2, 0, 1], + state: 0 + }, + { + pos: [3, 0, 1], + state: 0 + }, + { + pos: [4, 0, 1], + state: 0 + }, + { + pos: [0, 0, 2], + state: 0 + }, + { + pos: [1, 0, 2], + state: 0 + }, + { + pos: [2, 0, 2], + state: 0 + }, + { + pos: [3, 0, 2], + state: 0 + }, + { + pos: [4, 0, 2], + state: 0 + }, + { + pos: [0, 0, 3], + state: 0 + }, + { + pos: [1, 0, 3], + state: 0 + }, + { + pos: [2, 0, 3], + state: 0 + }, + { + pos: [3, 0, 3], + state: 0 + }, + { + pos: [4, 0, 3], + state: 0 + }, + { + pos: [0, 0, 4], + state: 0 + }, + { + pos: [1, 0, 4], + state: 0 + }, + { + pos: [2, 0, 4], + state: 0 + }, + { + pos: [3, 0, 4], + state: 0 + }, + { + pos: [4, 0, 4], + state: 0 + }, + { + pos: [0, 1, 0], + state: 1 + }, + { + pos: [1, 1, 0], + state: 1 + }, + { + pos: [2, 1, 0], + state: 1 + }, + { + pos: [3, 1, 0], + state: 1 + }, + { + pos: [4, 1, 0], + state: 1 + }, + { + pos: [0, 2, 0], + state: 1 + }, + { + pos: [1, 2, 0], + state: 1 + }, + { + pos: [2, 2, 0], + state: 1 + }, + { + pos: [3, 2, 0], + state: 1 + }, + { + pos: [4, 2, 0], + state: 1 + }, + { + pos: [0, 3, 0], + state: 1 + }, + { + pos: [1, 3, 0], + state: 1 + }, + { + pos: [2, 3, 0], + state: 1 + }, + { + pos: [3, 3, 0], + state: 1 + }, + { + pos: [4, 3, 0], + state: 1 + }, + { + pos: [0, 4, 0], + state: 1 + }, + { + pos: [1, 4, 0], + state: 1 + }, + { + pos: [2, 4, 0], + state: 1 + }, + { + pos: [3, 4, 0], + state: 1 + }, + { + pos: [4, 4, 0], + state: 1 + }, + { + pos: [0, 1, 1], + state: 1 + }, + { + pos: [1, 1, 1], + state: 1 + }, + { + pos: [2, 1, 1], + state: 1 + }, + { + pos: [3, 1, 1], + state: 1 + }, + { + pos: [4, 1, 1], + state: 1 + }, + { + pos: [0, 2, 1], + state: 1 + }, + { + pos: [1, 2, 1], + state: 1 + }, + { + pos: [2, 2, 1], + state: 1 + }, + { + pos: [3, 2, 1], + state: 1 + }, + { + pos: [4, 2, 1], + state: 1 + }, + { + pos: [0, 3, 1], + state: 1 + }, + { + pos: [1, 3, 1], + state: 1 + }, + { + pos: [2, 3, 1], + state: 1 + }, + { + pos: [3, 3, 1], + state: 1 + }, + { + pos: [4, 3, 1], + state: 1 + }, + { + pos: [0, 4, 1], + state: 1 + }, + { + pos: [1, 4, 1], + state: 1 + }, + { + pos: [2, 4, 1], + state: 1 + }, + { + pos: [3, 4, 1], + state: 1 + }, + { + pos: [4, 4, 1], + state: 1 + }, + { + pos: [0, 1, 2], + state: 1 + }, + { + pos: [1, 1, 2], + state: 1 + }, + { + pos: [2, 1, 2], + state: 1 + }, + { + pos: [3, 1, 2], + state: 1 + }, + { + pos: [4, 1, 2], + state: 1 + }, + { + pos: [0, 2, 2], + state: 1 + }, + { + pos: [1, 2, 2], + state: 1 + }, + { + pos: [2, 2, 2], + state: 1 + }, + { + pos: [3, 2, 2], + state: 1 + }, + { + pos: [4, 2, 2], + state: 1 + }, + { + pos: [0, 3, 2], + state: 1 + }, + { + pos: [1, 3, 2], + state: 1 + }, + { + pos: [2, 3, 2], + state: 1 + }, + { + pos: [3, 3, 2], + state: 1 + }, + { + pos: [4, 3, 2], + state: 1 + }, + { + pos: [0, 4, 2], + state: 1 + }, + { + pos: [1, 4, 2], + state: 1 + }, + { + pos: [2, 4, 2], + state: 1 + }, + { + pos: [3, 4, 2], + state: 1 + }, + { + pos: [4, 4, 2], + state: 1 + }, + { + pos: [0, 1, 3], + state: 1 + }, + { + pos: [1, 1, 3], + state: 1 + }, + { + pos: [2, 1, 3], + state: 1 + }, + { + pos: [3, 1, 3], + state: 1 + }, + { + pos: [4, 1, 3], + state: 1 + }, + { + pos: [0, 2, 3], + state: 1 + }, + { + pos: [1, 2, 3], + state: 1 + }, + { + pos: [2, 2, 3], + state: 1 + }, + { + pos: [3, 2, 3], + state: 1 + }, + { + pos: [4, 2, 3], + state: 1 + }, + { + pos: [0, 3, 3], + state: 1 + }, + { + pos: [1, 3, 3], + state: 1 + }, + { + pos: [2, 3, 3], + state: 1 + }, + { + pos: [3, 3, 3], + state: 1 + }, + { + pos: [4, 3, 3], + state: 1 + }, + { + pos: [0, 4, 3], + state: 1 + }, + { + pos: [1, 4, 3], + state: 1 + }, + { + pos: [2, 4, 3], + state: 1 + }, + { + pos: [3, 4, 3], + state: 1 + }, + { + pos: [4, 4, 3], + state: 1 + }, + { + pos: [0, 1, 4], + state: 1 + }, + { + pos: [1, 1, 4], + state: 1 + }, + { + pos: [2, 1, 4], + state: 1 + }, + { + pos: [3, 1, 4], + state: 1 + }, + { + pos: [4, 1, 4], + state: 1 + }, + { + pos: [0, 2, 4], + state: 1 + }, + { + pos: [1, 2, 4], + state: 1 + }, + { + pos: [2, 2, 4], + state: 1 + }, + { + pos: [3, 2, 4], + state: 1 + }, + { + pos: [4, 2, 4], + state: 1 + }, + { + pos: [0, 3, 4], + state: 1 + }, + { + pos: [1, 3, 4], + state: 1 + }, + { + pos: [2, 3, 4], + state: 1 + }, + { + pos: [3, 3, 4], + state: 1 + }, + { + pos: [4, 3, 4], + state: 1 + }, + { + pos: [0, 4, 4], + state: 1 + }, + { + pos: [1, 4, 4], + state: 1 + }, + { + pos: [2, 4, 4], + state: 1 + }, + { + pos: [3, 4, 4], + state: 1 + }, + { + pos: [4, 4, 4], + state: 1 + } + ], + palette: [ + { + Name: "minecraft:polished_andesite" + }, + { + Name: "minecraft:air" + } + ], + DataVersion: 2230 +} From d50db8a6f38d5bf10837502400200f814206e631 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 25 Jul 2021 16:25:59 +0100 Subject: [PATCH 6/7] Add a fancy test system for async methods Written in order to ~avoid working on~ test #695. Sadly, no luck there. --- .../core/apis/IAPIEnvironment.java | 1 + .../computercraft/core/apis/AsyncRunner.kt | 121 ++++++++++++++++++ .../core/apis/http/options/TestHttpApi.kt | 51 ++++++++ 3 files changed, 173 insertions(+) create mode 100644 src/test/java/dan200/computercraft/core/apis/AsyncRunner.kt create mode 100644 src/test/java/dan200/computercraft/core/apis/http/options/TestHttpApi.kt diff --git a/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java b/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java index 25960c580..1f6ffe08a 100644 --- a/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java +++ b/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java @@ -62,6 +62,7 @@ interface IPeripheralChangeListener @Nullable IPeripheral getPeripheral( ComputerSide side ); + @Nullable String getLabel(); void setLabel( @Nullable String label ); diff --git a/src/test/java/dan200/computercraft/core/apis/AsyncRunner.kt b/src/test/java/dan200/computercraft/core/apis/AsyncRunner.kt new file mode 100644 index 000000000..936fb6c62 --- /dev/null +++ b/src/test/java/dan200/computercraft/core/apis/AsyncRunner.kt @@ -0,0 +1,121 @@ +package dan200.computercraft.core.apis + +import dan200.computercraft.ComputerCraft +import dan200.computercraft.api.lua.ILuaAPI +import dan200.computercraft.api.lua.MethodResult +import dan200.computercraft.api.peripheral.IPeripheral +import dan200.computercraft.api.peripheral.IWorkMonitor +import dan200.computercraft.core.computer.BasicEnvironment +import dan200.computercraft.core.computer.ComputerSide +import dan200.computercraft.core.computer.IComputerEnvironment +import dan200.computercraft.core.filesystem.FileSystem +import dan200.computercraft.core.terminal.Terminal +import dan200.computercraft.core.tracking.TrackingField +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import kotlin.time.Duration +import kotlin.time.ExperimentalTime +import kotlin.time.seconds + + +abstract class NullApiEnvironment : IAPIEnvironment { + private val computerEnv = BasicEnvironment() + + override fun getComputerID(): Int = 0 + override fun getComputerEnvironment(): IComputerEnvironment = computerEnv + override fun getMainThreadMonitor(): IWorkMonitor = throw IllegalStateException("Work monitor not available") + override fun getTerminal(): Terminal = throw IllegalStateException("Terminal not available") + override fun getFileSystem(): FileSystem = throw IllegalStateException("Terminal not available") + override fun shutdown() {} + override fun reboot() {} + override fun setOutput(side: ComputerSide?, output: Int) {} + override fun getOutput(side: ComputerSide?): Int = 0 + override fun getInput(side: ComputerSide?): Int = 0 + override fun setBundledOutput(side: ComputerSide?, output: Int) {} + override fun getBundledOutput(side: ComputerSide?): Int = 0 + override fun getBundledInput(side: ComputerSide?): Int = 0 + override fun setPeripheralChangeListener(listener: IAPIEnvironment.IPeripheralChangeListener?) {} + override fun getPeripheral(side: ComputerSide?): IPeripheral? = null + override fun getLabel(): String? = null + override fun setLabel(label: String?) {} + override fun startTimer(ticks: Long): Int = 0 + override fun cancelTimer(id: Int) {} + override fun addTrackingChange(field: TrackingField, change: Long) {} +} + +class EventResult(val name: String, val args: Array) + +class AsyncRunner : NullApiEnvironment() { + private val eventStream: Channel> = Channel(Int.MAX_VALUE) + private val apis: MutableList = mutableListOf() + + override fun queueEvent(event: String?, vararg args: Any?) { + ComputerCraft.log.debug("Queue event $event ${args.contentToString()}") + if (!eventStream.offer(arrayOf(event, *args))) { + throw IllegalStateException("Queue is full") + } + } + + override fun shutdown() { + super.shutdown() + eventStream.close() + apis.forEach { it.shutdown() } + } + + fun addApi(api: T): T { + apis.add(api) + api.startup() + return api + } + + suspend fun resultOf(toRun: MethodResult): Array { + var running = toRun + while (running.callback != null) running = runOnce(running) + return running.result ?: empty + } + + private suspend fun runOnce(obj: MethodResult): MethodResult { + val callback = obj.callback ?: throw NullPointerException("Callback cannot be null") + + val result = obj.result + val filter: String? = if (result.isNullOrEmpty() || result[0] !is String) { + null + } else { + result[0] as String + } + + return callback.resume(pullEventImpl(filter)) + } + + private suspend fun pullEventImpl(filter: String?): Array { + for (event in eventStream) { + ComputerCraft.log.debug("Pulled event ${event.contentToString()}") + val eventName = event[0] as String + if (filter == null || eventName == filter || eventName == "terminate") return event + } + + throw IllegalStateException("No more events") + } + + suspend fun pullEvent(filter: String? = null): EventResult { + val result = pullEventImpl(filter) + return EventResult(result[0] as String, result.copyOfRange(1, result.size)) + } + + companion object { + private val empty: Array = arrayOf() + + @OptIn(ExperimentalTime::class) + fun runTest(timeout: Duration = 5.seconds, fn: suspend AsyncRunner.() -> Unit) { + runBlocking { + val runner = AsyncRunner() + try { + withTimeout(timeout) { fn(runner) } + } finally { + runner.shutdown() + } + } + } + } +} diff --git a/src/test/java/dan200/computercraft/core/apis/http/options/TestHttpApi.kt b/src/test/java/dan200/computercraft/core/apis/http/options/TestHttpApi.kt new file mode 100644 index 000000000..206924cc0 --- /dev/null +++ b/src/test/java/dan200/computercraft/core/apis/http/options/TestHttpApi.kt @@ -0,0 +1,51 @@ +package dan200.computercraft.core.apis.http.options + +import dan200.computercraft.ComputerCraft +import dan200.computercraft.core.apis.AsyncRunner +import dan200.computercraft.core.apis.HTTPAPI +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import java.util.* + +@Disabled("Requires some setup locally.") +class TestHttpApi { + companion object { + private const val WS_ADDRESS = "ws://127.0.0.1:8080" + + @JvmStatic + @BeforeAll + fun before() { + ComputerCraft.httpRules = listOf(AddressRule.parse("*", null, Action.ALLOW.toPartial())) + } + + @JvmStatic + @AfterAll + fun after() { + ComputerCraft.httpRules = Collections.unmodifiableList( + listOf( + AddressRule.parse("\$private", null, Action.DENY.toPartial()), + AddressRule.parse("*", null, Action.ALLOW.toPartial()) + ) + ) + } + } + + @Test + fun `Connects to websocket`() { + AsyncRunner.runTest { + val httpApi = addApi(HTTPAPI(this)) + + val result = httpApi.websocket(WS_ADDRESS, Optional.empty()) + assertArrayEquals(arrayOf(true), result, "Should have created websocket") + + val event = pullEvent() + assertEquals("websocket_success", event.name) { + "Websocket failed to connect: ${event.args.contentToString()}" + } + } + } +} From 227b444d8141b63744abf7009d013c7b5f878362 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 25 Jul 2021 16:40:27 +0100 Subject: [PATCH 7/7] Accept client_no_context_takeover in websockets Doesn't fix #695, but Good Enough(TM). --- .../core/apis/http/websocket/Websocket.java | 3 +- .../WebsocketCompressionHandler.java | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketCompressionHandler.java diff --git a/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java b/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java index 3e54be9a1..155540560 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java +++ b/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java @@ -28,7 +28,6 @@ import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker; import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory; import io.netty.handler.codec.http.websocketx.WebSocketVersion; -import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler; import io.netty.handler.ssl.SslContext; import java.lang.ref.WeakReference; @@ -160,7 +159,7 @@ protected void initChannel( SocketChannel ch ) p.addLast( new HttpClientCodec(), new HttpObjectAggregator( 8192 ), - WebSocketClientCompressionHandler.INSTANCE, + WebsocketCompressionHandler.INSTANCE, new WebsocketHandler( Websocket.this, handshaker, options ) ); } diff --git a/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketCompressionHandler.java b/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketCompressionHandler.java new file mode 100644 index 000000000..e29a87837 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketCompressionHandler.java @@ -0,0 +1,38 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.core.apis.http.websocket; + +import io.netty.channel.ChannelHandler; +import io.netty.handler.codec.compression.ZlibCodecFactory; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtensionHandler; +import io.netty.handler.codec.http.websocketx.extensions.compression.DeflateFrameClientExtensionHandshaker; +import io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateClientExtensionHandshaker; +import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler; + +import static io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateServerExtensionHandshaker.MAX_WINDOW_SIZE; + +/** + * An alternative to {@link WebSocketClientCompressionHandler} which supports the {@literal client_no_context_takeover} + * extension. Makes CC slightly more flexible. + */ +@ChannelHandler.Sharable +final class WebsocketCompressionHandler extends WebSocketClientExtensionHandler +{ + public static final WebsocketCompressionHandler INSTANCE = new WebsocketCompressionHandler(); + + private WebsocketCompressionHandler() + { + super( + new PerMessageDeflateClientExtensionHandshaker( + 6, ZlibCodecFactory.isSupportingWindowSizeAndMemLevel(), MAX_WINDOW_SIZE, + true, false + ), + new DeflateFrameClientExtensionHandshaker( false ), + new DeflateFrameClientExtensionHandshaker( true ) + ); + + } +}