From 95794fdaf33ba7c83f93d4b51267a0ae245ad138 Mon Sep 17 00:00:00 2001 From: Jummit Date: Sun, 16 May 2021 07:32:25 +0200 Subject: [PATCH 01/34] Add back command computer block drops This has been broken for almost a year (28th Jan 2020), and I never noticed. Good job me. Fixes #641, closes #648 (basically the same, but targetting 1.15.x) --- patchwork.md | 43 ++++++++++++++++++- .../loot_tables/blocks/computer_command.json | 39 +++++++++++++---- 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/patchwork.md b/patchwork.md index 97d1ecd5d..e124e5c73 100644 --- a/patchwork.md +++ b/patchwork.md @@ -536,4 +536,45 @@ e4b0a5b3ce035eb23feb4191432fc49af5772c5b 2020 -> 2021 ``` -A huge amount of changes. \ No newline at end of file +A huge amount of changes. + +``` +542b66c79a9b08e080c39c9a73d74ffe71c0106a + +Add back command computer block drops +``` +Didn't port some forge-related changes, but it works. + +``` +dd6f97622e6c18ce0d8988da6a5bede45c94ca5d + +Prevent reflection errors crashing the game +``` + +``` +92be0126df63927d07fc695945f8b98e328f945a + +Fix disk recipes +``` +Dye recipes actually work now. + +``` +1edb7288b974aec3764b0a820edce7e9eee38e66 + +Merge branch 'mc-1.15.x' into mc-1.16.x +``` +New version: 1.95.1. + +``` +41226371f3b5fd35f48b6d39c2e8e0c277125b21 + +Add isReadOnly to fs.attributes (#639) +``` +Also changed some lua test files, but made the changes anyway. + +``` +b2e54014869fac4b819b01b6c24e550ca113ce8a + +Added Numpad Enter Support in rom lua programs. (#657) +``` +Just lua changes. diff --git a/src/main/resources/data/computercraft/loot_tables/blocks/computer_command.json b/src/main/resources/data/computercraft/loot_tables/blocks/computer_command.json index 7e55bfbab..6ca0e4761 100644 --- a/src/main/resources/data/computercraft/loot_tables/blocks/computer_command.json +++ b/src/main/resources/data/computercraft/loot_tables/blocks/computer_command.json @@ -1,11 +1,34 @@ { - "type": "minecraft:block", - "pools": [ + "type": "minecraft:block", + "pools": [ + { + "name": "main", + "rolls": 1, + "entries": [ { - "rolls": 1, - "entries": [ - { "type": "minecraft:dynamic", "name": "computercraft:computer" } - ] + "type": "minecraft:dynamic", + "name": "computercraft:computer" } - ] -} + ], + "conditions": [ + { + "condition": "minecraft:alternative", + "terms": [ + { + "condition": "computercraft:block_named" + }, + { + "condition": "computercraft:has_id" + }, + { + "condition": "minecraft:inverted", + "term": { + "condition": "computercraft:player_creative" + } + } + ] + } + ] + } + ] +} \ No newline at end of file From eb2d9482a21026fba518f4cd2744a6785b124a3c Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 6 Jan 2021 18:21:03 +0000 Subject: [PATCH 02/34] Prevent reflection errors crashing the game .getMethods() may throw if a method references classes which don't exist (such as client-only classes on a server). This is an Error, and so is unchecked - hence us not handling it before. Fixes #645 --- .../computercraft/core/asm/Generator.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/java/dan200/computercraft/core/asm/Generator.java b/src/main/java/dan200/computercraft/core/asm/Generator.java index 7dd5e7e6e..e600880c0 100644 --- a/src/main/java/dan200/computercraft/core/asm/Generator.java +++ b/src/main/java/dan200/computercraft/core/asm/Generator.java @@ -64,9 +64,9 @@ public final class Generator { private final Function wrap; private final LoadingCache> methodCache = CacheBuilder.newBuilder() - .build(CacheLoader.from(this::build)); + .build(CacheLoader.from(catching(this::build, Optional.empty()))); private final LoadingCache, List>> classCache = CacheBuilder.newBuilder() - .build(CacheLoader.from(this::build)); + .build(CacheLoader.from(catching(this::build, Collections.emptyList()))); Generator(Class base, List> context, Function wrap) { this.base = base; @@ -374,4 +374,22 @@ public final class Generator { method.getName()); return null; } + + @SuppressWarnings( "Guava" ) + private static com.google.common.base.Function catching( Function function, U def ) + { + return x -> { + try + { + return function.apply( x ); + } + catch( Exception | LinkageError e ) + { + // LinkageError due to possible codegen bugs and NoClassDefFoundError. The latter occurs when fetching + // methods on a class which references non-existent (i.e. client-only) types. + ComputerCraft.log.error( "Error generating @LuaFunctions", e ); + return def; + } + }; + } } From a72a5e6deb2cf77c3a4238bf6ce2bb5fbe55fde3 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 6 Jan 2021 21:17:26 +0000 Subject: [PATCH 03/34] Fix disk recipes Closes #652. This has been broken since the 1.13 update. Not filling myself with confidence here. --- .../shared/common/ColourableRecipe.java | 8 +------- .../shared/media/recipes/DiskRecipe.java | 16 +++++++--------- .../computercraft/shared/util/ColourTracker.java | 13 +++++++++++-- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/main/java/dan200/computercraft/shared/common/ColourableRecipe.java b/src/main/java/dan200/computercraft/shared/common/ColourableRecipe.java index 3006954a0..7d47ccdb3 100644 --- a/src/main/java/dan200/computercraft/shared/common/ColourableRecipe.java +++ b/src/main/java/dan200/computercraft/shared/common/ColourableRecipe.java @@ -8,7 +8,6 @@ package dan200.computercraft.shared.common; import javax.annotation.Nonnull; -import dan200.computercraft.shared.util.Colour; import dan200.computercraft.shared.util.ColourTracker; import dan200.computercraft.shared.util.ColourUtils; @@ -71,12 +70,7 @@ public final class ColourableRecipe extends SpecialCraftingRecipe { colourable = stack; } else { DyeColor dye = ColourUtils.getStackColour(stack); - if (dye == null) { - continue; - } - - Colour colour = Colour.fromInt(15 - dye.getId()); - tracker.addColour(colour.getR(), colour.getG(), colour.getB()); + if( dye != null ) tracker.addColour( dye ); } } diff --git a/src/main/java/dan200/computercraft/shared/media/recipes/DiskRecipe.java b/src/main/java/dan200/computercraft/shared/media/recipes/DiskRecipe.java index 615985082..24006f1ee 100644 --- a/src/main/java/dan200/computercraft/shared/media/recipes/DiskRecipe.java +++ b/src/main/java/dan200/computercraft/shared/media/recipes/DiskRecipe.java @@ -53,7 +53,9 @@ public class DiskRecipe extends SpecialCraftingRecipe { return false; } redstoneFound = true; - } else if (ColourUtils.getStackColour(stack) != null) { + } + else if( ColourUtils.getStackColour( stack ) == null ) + { return false; } } @@ -74,14 +76,10 @@ public class DiskRecipe extends SpecialCraftingRecipe { continue; } - if (!this.paper.test(stack) && !this.redstone.test(stack)) { - DyeColor dye = ColourUtils.getStackColour(stack); - if (dye == null) { - continue; - } - - Colour colour = Colour.VALUES[dye.getId()]; - tracker.addColour(colour.getR(), colour.getG(), colour.getB()); + if( !paper.test( stack ) && !redstone.test( stack ) ) + { + DyeColor dye = ColourUtils.getStackColour( stack ); + if( dye != null ) tracker.addColour( dye ); } } diff --git a/src/main/java/dan200/computercraft/shared/util/ColourTracker.java b/src/main/java/dan200/computercraft/shared/util/ColourTracker.java index 55b4571f7..25daa65e2 100644 --- a/src/main/java/dan200/computercraft/shared/util/ColourTracker.java +++ b/src/main/java/dan200/computercraft/shared/util/ColourTracker.java @@ -6,6 +6,8 @@ package dan200.computercraft.shared.util; +import net.minecraft.util.DyeColor; + /** * A reimplementation of the colour system in {@link ArmorDyeRecipe}, but bundled together as an object. */ @@ -28,8 +30,15 @@ public class ColourTracker { this.count++; } - public boolean hasColour() { - return this.count > 0; + public void addColour( DyeColor dye ) + { + Colour colour = Colour.VALUES[15 - dye.getId()]; + addColour( colour.getR(), colour.getG(), colour.getB() ); + } + + public boolean hasColour() + { + return count > 0; } public int getColour() { From 19054684c615244c0ed1335734a38b3e6ffa1526 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 6 Jan 2021 22:39:54 +0000 Subject: [PATCH 04/34] Merge branch 'mc-1.15.x' into mc-1.16.x --- gradle.properties | 2 +- .../computercraft/api/IUpgradeBase.java | 5 ++++ .../shared/common/ColourableRecipe.java | 8 +++--- .../data/computercraft/lua/rom/apis/gps.lua | 6 ++--- .../computercraft/lua/rom/help/changelog.txt | 8 ++++++ .../computercraft/lua/rom/help/whatsnew.txt | 26 +++++-------------- 6 files changed, 26 insertions(+), 29 deletions(-) diff --git a/gradle.properties b/gradle.properties index 96b57d2a6..8be827870 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ org.gradle.jvmargs=-Xmx1G # Mod properties -mod_version=1.95.0-beta +mod_version=1.95.1-beta # Minecraft properties mc_version=1.16.2 diff --git a/src/main/java/dan200/computercraft/api/IUpgradeBase.java b/src/main/java/dan200/computercraft/api/IUpgradeBase.java index c659ea5f5..06675b7ed 100644 --- a/src/main/java/dan200/computercraft/api/IUpgradeBase.java +++ b/src/main/java/dan200/computercraft/api/IUpgradeBase.java @@ -1,3 +1,8 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ package dan200.computercraft.api; import dan200.computercraft.api.pocket.IPocketUpgrade; diff --git a/src/main/java/dan200/computercraft/shared/common/ColourableRecipe.java b/src/main/java/dan200/computercraft/shared/common/ColourableRecipe.java index 7d47ccdb3..87f84404e 100644 --- a/src/main/java/dan200/computercraft/shared/common/ColourableRecipe.java +++ b/src/main/java/dan200/computercraft/shared/common/ColourableRecipe.java @@ -65,11 +65,9 @@ public final class ColourableRecipe extends SpecialCraftingRecipe { if (stack.isEmpty()) { continue; } - - if (stack.getItem() instanceof IColouredItem) { - colourable = stack; - } else { - DyeColor dye = ColourUtils.getStackColour(stack); + else + { + DyeColor dye = ColourUtils.getStackColour( stack ); if( dye != null ) tracker.addColour( dye ); } } diff --git a/src/main/resources/data/computercraft/lua/rom/apis/gps.lua b/src/main/resources/data/computercraft/lua/rom/apis/gps.lua index 3be8906a5..8cbc63a78 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/gps.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/gps.lua @@ -83,9 +83,9 @@ end --- Tries to retrieve the computer or turtles own location. -- --- @tparam[opt] number timeout The maximum time taken to establish our --- position. Defaults to 2 seconds if not specified. --- @tparam[opt] boolean debug Print debugging messages +-- @tparam[opt=2] number timeout The maximum time in seconds taken to establish our +-- position. +-- @tparam[opt=false] boolean debug Print debugging messages -- @treturn[1] number This computer's `x` position. -- @treturn[1] number This computer's `y` position. -- @treturn[1] number This computer's `z` position. diff --git a/src/main/resources/data/computercraft/lua/rom/help/changelog.txt b/src/main/resources/data/computercraft/lua/rom/help/changelog.txt index 0c19ff76c..e08f993ff 100644 --- a/src/main/resources/data/computercraft/lua/rom/help/changelog.txt +++ b/src/main/resources/data/computercraft/lua/rom/help/changelog.txt @@ -1,3 +1,11 @@ +# New features in CC: Restitched 1.95.1 + +Several bug fixes: +* Command computers now drop items again. +* Restore crafting of disks with dyes. +* Fix CraftTweaker integrations for damageable items. +* Catch reflection errors in the generic peripheral system, resolving crashes with Botania. + # New features in CC: Restitched 1.95.0 * Optimise the paint program's initial render. diff --git a/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt b/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt index 802edff6f..1fdb9aa79 100644 --- a/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt +++ b/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt @@ -1,23 +1,9 @@ -New features in CC: Restitched 1.95.0 +New features in CC: Restitched 1.95.1 -* Optimise the paint program's initial render. -* Several documentation improvments (Gibbo3771, MCJack123). -* `fs.combine` now accepts multiple arguments. -* Add a setting (`bios.strict_globals`) to error when accidentally declaring a global. (Lupus590). -* Add an improved help viewer which allows scrolling up and down (MCJack123). -* Add `cc.strings` module, with utilities for wrapping text (Lupus590). -* The `clear` program now allows resetting the palette too (Luca0208). - -And several bug fixes: -* Fix memory leak in generic peripherals. -* Fix crash when a turtle is broken while being ticked. -* `textutils.*tabulate` now accepts strings _or_ numbers. -* We now deny _all_ local IPs, using the magic `$private` host. Previously the IPv6 loopback interface was not blocked. -* Fix crash when rendering monitors if the block has not yet been synced. You will need to regenerate the config file to apply this change. -* `read` now supports numpad enter (TheWireLord) -* Correctly handle HTTP redirects to URLs containing escape characters. -* Fix integer overflow in `os.epoch`. -* Allow using pickaxes (and other items) for turtle upgrades which have mod-specific NBT. -* Fix duplicate turtle/pocket upgrade recipes appearing in JEI. +Several bug fixes: +* Command computers now drop items again. +* Restore crafting of disks with dyes. +* Fix CraftTweaker integrations for damageable items. +* Catch reflection errors in the generic peripheral system, resolving crashes with Botania. Type "help changelog" to see the full version history. From 0b6dbe777879906dd1e6d17cb6d3af8e0d77d251 Mon Sep 17 00:00:00 2001 From: Lupus590 Date: Thu, 7 Jan 2021 16:36:25 +0000 Subject: [PATCH 05/34] Add isReadOnly to fs.attributes (#639) --- .../dan200/computercraft/core/apis/FSAPI.java | 16 +- .../resources/test-rom/spec/apis/fs_spec.lua | 208 ++++++++++++++++++ 2 files changed, 217 insertions(+), 7 deletions(-) diff --git a/src/main/java/dan200/computercraft/core/apis/FSAPI.java b/src/main/java/dan200/computercraft/core/apis/FSAPI.java index 52a2bcbbe..c2156a069 100644 --- a/src/main/java/dan200/computercraft/core/apis/FSAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/FSAPI.java @@ -396,7 +396,8 @@ public class FSAPI implements ILuaAPI { /** * Get attributes about a specific file or folder. * - * The returned attributes table contains information about the size of the file, whether it is a directory, and when it was created and last modified. + * The returned attributes table contains information about the size of the file, whether it is a directory, + * when it was created and last modified, and whether it is read only. * * The creation and modification times are given as the number of milliseconds since the UNIX epoch. This may be given to {@link OSAPI#date} in order to * convert it to more usable form. @@ -404,7 +405,7 @@ public class FSAPI implements ILuaAPI { * @param path The path to get attributes for. * @return The resulting attributes. * @throws LuaException If the path does not exist. - * @cc.treturn { size = number, isDir = boolean, created = number, modified = number } The resulting attributes. + * @cc.treturn { size = number, isDir = boolean, isReadOnly = boolean, created = number, modified = number } The resulting attributes. * @see #getSize If you only care about the file's size. * @see #isDir If you only care whether a path is a directory or not. */ @@ -413,11 +414,12 @@ public class FSAPI implements ILuaAPI { try { BasicFileAttributes attributes = this.fileSystem.getAttributes(path); Map result = new HashMap<>(); - result.put("modification", getFileTime(attributes.lastModifiedTime())); - result.put("modified", getFileTime(attributes.lastModifiedTime())); - result.put("created", getFileTime(attributes.creationTime())); - result.put("size", attributes.isDirectory() ? 0 : attributes.size()); - result.put("isDir", attributes.isDirectory()); + result.put( "modification", getFileTime( attributes.lastModifiedTime() ) ); + result.put( "modified", getFileTime( attributes.lastModifiedTime() ) ); + result.put( "created", getFileTime( attributes.creationTime() ) ); + result.put( "size", attributes.isDirectory() ? 0 : attributes.size() ); + result.put( "isDir", attributes.isDirectory() ); + result.put( "isReadOnly", fileSystem.isReadOnly( path ) ); return result; } catch (FileSystemException e) { throw new LuaException(e.getMessage()); diff --git a/src/test/resources/test-rom/spec/apis/fs_spec.lua b/src/test/resources/test-rom/spec/apis/fs_spec.lua index 883b8c575..32503598e 100644 --- a/src/test/resources/test-rom/spec/apis/fs_spec.lua +++ b/src/test/resources/test-rom/spec/apis/fs_spec.lua @@ -11,4 +11,212 @@ describe("The fs library", function() expect.error(fs.complete, "", "", true, 1):eq("bad argument #4 (expected boolean, got number)") end) end) + + describe("fs.isDriveRoot", function() + it("validates arguments", function() + fs.isDriveRoot("") + + expect.error(fs.isDriveRoot, nil):eq("bad argument #1 (expected string, got nil)") + end) + + it("correctly identifies drive roots", function() + expect(fs.isDriveRoot("/rom")):eq(true) + expect(fs.isDriveRoot("/")):eq(true) + expect(fs.isDriveRoot("/rom/startup.lua")):eq(false) + expect(fs.isDriveRoot("/rom/programs/delete.lua")):eq(false) + end) + end) + + describe("fs.list", function() + it("fails on files", function() + expect.error(fs.list, "rom/startup.lua"):eq("/rom/startup.lua: Not a directory") + expect.error(fs.list, "startup.lua"):eq("/startup.lua: Not a directory") + end) + + it("fails on non-existent nodes", function() + expect.error(fs.list, "rom/x"):eq("/rom/x: Not a directory") + expect.error(fs.list, "x"):eq("/x: Not a directory") + end) + end) + + describe("fs.combine", function() + it("removes . and ..", function() + expect(fs.combine("./a/b")):eq("a/b") + expect(fs.combine("a/b", "../c")):eq("a/c") + expect(fs.combine("a", "../c")):eq("c") + expect(fs.combine("a", "../../c")):eq("../c") + end) + + it("combines empty paths", function() + expect(fs.combine("a")):eq("a") + expect(fs.combine("a", "")):eq("a") + expect(fs.combine("", "a")):eq("a") + expect(fs.combine("a", "", "b", "c")):eq("a/b/c") + end) + end) + + describe("fs.getSize", function() + it("fails on non-existent nodes", function() + expect.error(fs.getSize, "rom/x"):eq("/rom/x: No such file") + expect.error(fs.getSize, "x"):eq("/x: No such file") + end) + end) + + describe("fs.open", function() + describe("reading", function() + it("fails on directories", function() + expect { fs.open("rom", "r") }:same { nil, "/rom: No such file" } + expect { fs.open("", "r") }:same { nil, "/: No such file" } + end) + + it("fails on non-existent nodes", function() + expect { fs.open("rom/x", "r") }:same { nil, "/rom/x: No such file" } + expect { fs.open("x", "r") }:same { nil, "/x: No such file" } + end) + + it("errors when closing twice", function() + local handle = fs.open("rom/startup.lua", "r") + handle.close() + expect.error(handle.close):eq("attempt to use a closed file") + end) + end) + + describe("reading in binary mode", function() + it("errors when closing twice", function() + local handle = fs.open("rom/startup.lua", "rb") + handle.close() + expect.error(handle.close):eq("attempt to use a closed file") + end) + end) + + describe("writing", function() + it("fails on directories", function() + expect { fs.open("", "w") }:same { nil, "/: Cannot write to directory" } + end) + + it("fails on read-only mounts", function() + expect { fs.open("rom/x", "w") }:same { nil, "/rom/x: Access denied" } + end) + + it("errors when closing twice", function() + local handle = fs.open("test-files/out.txt", "w") + handle.close() + expect.error(handle.close):eq("attempt to use a closed file") + end) + + it("fails gracefully when opening 'CON' on Windows", function() + local ok, err = fs.open("test-files/con", "w") + if ok then fs.delete("test-files/con") return end + + -- On my Windows/Java version the message appears to be "Incorrect function.". It may not be + -- consistent though, and honestly doesn't matter too much. + expect(err):str_match("^/test%-files/con: .*") + end) + end) + + describe("writing in binary mode", function() + it("errors when closing twice", function() + local handle = fs.open("test-files/out.txt", "wb") + handle.close() + expect.error(handle.close):eq("attempt to use a closed file") + end) + end) + + describe("appending", function() + it("fails on directories", function() + expect { fs.open("", "a") }:same { nil, "/: Cannot write to directory" } + end) + + it("fails on read-only mounts", function() + expect { fs.open("rom/x", "a") }:same { nil, "/rom/x: Access denied" } + end) + end) + end) + + describe("fs.makeDir", function() + it("fails on files", function() + expect.error(fs.makeDir, "startup.lua"):eq("/startup.lua: File exists") + end) + + it("fails on read-only mounts", function() + expect.error(fs.makeDir, "rom/x"):eq("/rom/x: Access denied") + end) + end) + + describe("fs.delete", function() + it("fails on read-only mounts", function() + expect.error(fs.delete, "rom/x"):eq("/rom/x: Access denied") + end) + end) + + describe("fs.copy", function() + it("fails on read-only mounts", function() + expect.error(fs.copy, "rom", "rom/startup"):eq("/rom/startup: Access denied") + end) + + it("fails to copy a folder inside itself", function() + fs.makeDir("some-folder") + expect.error(fs.copy, "some-folder", "some-folder/x"):eq("/some-folder: Can't copy a directory inside itself") + expect.error(fs.copy, "some-folder", "Some-Folder/x"):eq("/some-folder: Can't copy a directory inside itself") + end) + + it("copies folders", function() + fs.delete("some-folder") + fs.delete("another-folder") + + fs.makeDir("some-folder") + fs.copy("some-folder", "another-folder") + expect(fs.isDir("another-folder")):eq(true) + end) + end) + + describe("fs.move", function() + it("fails on read-only mounts", function() + expect.error(fs.move, "rom", "rom/move"):eq("Access denied") + expect.error(fs.move, "test-files", "rom/move"):eq("Access denied") + expect.error(fs.move, "rom", "test-files"):eq("Access denied") + end) + end) + + describe("fs.getCapacity", function() + it("returns nil on read-only mounts", function() + expect(fs.getCapacity("rom")):eq(nil) + end) + + it("returns the capacity on the root mount", function() + expect(fs.getCapacity("")):eq(10000000) + end) + end) + + describe("fs.attributes", function() + it("errors on non-existent files", function() + expect.error(fs.attributes, "xuxu_nao_existe"):eq("/xuxu_nao_existe: No such file") + end) + + it("returns information about read-only mounts", function() + expect(fs.attributes("rom")):matches { isDir = true, size = 0, isReadOnly = true } + end) + + it("returns information about files", function() + local now = os.epoch("utc") + + fs.delete("/tmp/basic-file") + local h = fs.open("/tmp/basic-file", "w") + h.write("A reasonably sized string") + h.close() + + local attributes = fs.attributes("tmp/basic-file") + expect(attributes):matches { isDir = false, size = 25, isReadOnly = false } + + if attributes.created - now >= 1000 then + fail(("Expected created time (%d) to be within 1000ms of now (%d"):format(attributes.created, now)) + end + + if attributes.modified - now >= 1000 then + fail(("Expected modified time (%d) to be within 1000ms of now (%d"):format(attributes.modified, now)) + end + + expect(attributes.modification):eq(attributes.modified) + end) + end) end) From 515ccfebd38bfdae4c1b720e1842c8693ad43806 Mon Sep 17 00:00:00 2001 From: Wojbie Date: Thu, 7 Jan 2021 22:41:04 +0100 Subject: [PATCH 06/34] Added Numpad Enter Support in rom lua programs. (#657) --- .../resources/data/computercraft/lua/rom/programs/edit.lua | 4 ++-- .../computercraft/lua/rom/programs/fun/advanced/paint.lua | 2 +- .../data/computercraft/lua/rom/programs/fun/worm.lua | 4 ++-- .../data/computercraft/lua/rom/programs/pocket/falling.lua | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/resources/data/computercraft/lua/rom/programs/edit.lua b/src/main/resources/data/computercraft/lua/rom/programs/edit.lua index 8f0af9356..8656798df 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/edit.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/edit.lua @@ -667,8 +667,8 @@ while bRunning do end end - elseif param == keys.enter then - -- Enter + elseif param == keys.enter or param == keys.numPadEnter then + -- Enter/Numpad Enter if not bMenu and not bReadOnly then -- Newline local sLine = tLines[y] diff --git a/src/main/resources/data/computercraft/lua/rom/programs/fun/advanced/paint.lua b/src/main/resources/data/computercraft/lua/rom/programs/fun/advanced/paint.lua index 9839d1077..91667464b 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/fun/advanced/paint.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/fun/advanced/paint.lua @@ -349,7 +349,7 @@ local function accessMenu() selection = #mChoices end - elseif key == keys.enter then + elseif key == keys.enter or key == keys.numPadEnter then -- Select an option return menu_choices[mChoices[selection]]() elseif key == keys.leftCtrl or keys == keys.rightCtrl then diff --git a/src/main/resources/data/computercraft/lua/rom/programs/fun/worm.lua b/src/main/resources/data/computercraft/lua/rom/programs/fun/worm.lua index eaaa54869..9d98ac2fd 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/fun/worm.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/fun/worm.lua @@ -199,8 +199,8 @@ while true do drawMenu() drawFrontend() end - elseif key == keys.enter then - -- Enter + elseif key == keys.enter or key == keys.numPadEnter then + -- Enter/Numpad Enter break end end diff --git a/src/main/resources/data/computercraft/lua/rom/programs/pocket/falling.lua b/src/main/resources/data/computercraft/lua/rom/programs/pocket/falling.lua index deb01c380..62e8e7eae 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/pocket/falling.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/pocket/falling.lua @@ -546,7 +546,7 @@ local function playGame() msgBox("Game Over!") while true do local _, k = os.pullEvent("key") - if k == keys.space or k == keys.enter then + if k == keys.space or k == keys.enter or k == keys.numPadEnter then break end end @@ -627,7 +627,7 @@ local function runMenu() elseif key == keys.down or key == keys.s then selected = selected % 2 + 1 drawMenu() - elseif key == keys.enter or key == keys.space then + elseif key == keys.enter or key == keys.numPadEnter or key == keys.space then break --begin play! end end From 64f3aa2dba17ca3cf5d44d620d6b6e5fbae7e91a Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Fri, 8 Jan 2021 17:49:31 +0000 Subject: [PATCH 07/34] Fix problem with RepeatArgumentType The whole "flatten" thing can probably be dropped TBH. We don't use it anywhere. Fixes #661 --- patchwork.md | 6 ++++++ .../shared/command/arguments/RepeatArgumentType.java | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/patchwork.md b/patchwork.md index e124e5c73..67dcabdd4 100644 --- a/patchwork.md +++ b/patchwork.md @@ -578,3 +578,9 @@ b2e54014869fac4b819b01b6c24e550ca113ce8a Added Numpad Enter Support in rom lua programs. (#657) ``` Just lua changes. + +``` +247c05305d106af430fcdaee41371a152bf7c38c + +Fix problem with RepeatArgumentType +``` \ No newline at end of file diff --git a/src/main/java/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java b/src/main/java/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java index 2a66975f9..36a6f1009 100644 --- a/src/main/java/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java +++ b/src/main/java/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java @@ -55,8 +55,9 @@ public final class RepeatArgumentType implements ArgumentType> { this.some = some; } - public static RepeatArgumentType some(ArgumentType appender, SimpleCommandExceptionType missing) { - return new RepeatArgumentType<>(appender, List::add, true, missing); + public static RepeatArgumentType some( ArgumentType appender, SimpleCommandExceptionType missing ) + { + return new RepeatArgumentType<>( appender, List::add, false, missing ); } public static RepeatArgumentType> someFlat(ArgumentType> appender, SimpleCommandExceptionType missing) { From bcc0effd00ca2e6c11c0cd33ad2f9d6dbc75dbb2 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 9 Jan 2021 18:30:07 +0000 Subject: [PATCH 08/34] Fix impostor recipes for disks Well, this is embarrassing. See #652 --- patchwork.md | 9 ++++++++- .../resources/data/computercraft/recipes/disk_1.json | 2 +- .../resources/data/computercraft/recipes/disk_10.json | 2 +- .../resources/data/computercraft/recipes/disk_11.json | 2 +- .../resources/data/computercraft/recipes/disk_12.json | 2 +- .../resources/data/computercraft/recipes/disk_13.json | 2 +- .../resources/data/computercraft/recipes/disk_14.json | 2 +- .../resources/data/computercraft/recipes/disk_15.json | 2 +- .../resources/data/computercraft/recipes/disk_16.json | 2 +- .../resources/data/computercraft/recipes/disk_2.json | 2 +- .../resources/data/computercraft/recipes/disk_3.json | 2 +- .../resources/data/computercraft/recipes/disk_4.json | 2 +- .../resources/data/computercraft/recipes/disk_5.json | 2 +- .../resources/data/computercraft/recipes/disk_6.json | 2 +- .../resources/data/computercraft/recipes/disk_7.json | 2 +- .../resources/data/computercraft/recipes/disk_8.json | 2 +- .../resources/data/computercraft/recipes/disk_9.json | 2 +- 17 files changed, 24 insertions(+), 17 deletions(-) diff --git a/patchwork.md b/patchwork.md index 67dcabdd4..ccda58db5 100644 --- a/patchwork.md +++ b/patchwork.md @@ -583,4 +583,11 @@ Just lua changes. 247c05305d106af430fcdaee41371a152bf7c38c Fix problem with RepeatArgumentType -``` \ No newline at end of file +``` + +``` +c864576619751077a0d8ac1a18123e14b095ec03 + +Fix impostor recipes for disks +``` +[TODO] [JUMT-03] REI still shows white disks, probably because it doesn' show nbt items. \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/disk_1.json b/src/main/resources/data/computercraft/recipes/disk_1.json index b365f90db..f58c4524d 100644 --- a/src/main/resources/data/computercraft/recipes/disk_1.json +++ b/src/main/resources/data/computercraft/recipes/disk_1.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:1118481}" + "nbt": "{Color:1118481}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_10.json b/src/main/resources/data/computercraft/recipes/disk_10.json index 7fdec5607..be3c706d6 100644 --- a/src/main/resources/data/computercraft/recipes/disk_10.json +++ b/src/main/resources/data/computercraft/recipes/disk_10.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:15905484}" + "nbt": "{Color:15905484}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_11.json b/src/main/resources/data/computercraft/recipes/disk_11.json index 5e20d91f3..b16467e7a 100644 --- a/src/main/resources/data/computercraft/recipes/disk_11.json +++ b/src/main/resources/data/computercraft/recipes/disk_11.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:8375321}" + "nbt": "{Color:8375321}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_12.json b/src/main/resources/data/computercraft/recipes/disk_12.json index 42f2962e6..c0ff791a8 100644 --- a/src/main/resources/data/computercraft/recipes/disk_12.json +++ b/src/main/resources/data/computercraft/recipes/disk_12.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:14605932}" + "nbt": "{Color:14605932}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_13.json b/src/main/resources/data/computercraft/recipes/disk_13.json index 02ce61e52..8cf914c0c 100644 --- a/src/main/resources/data/computercraft/recipes/disk_13.json +++ b/src/main/resources/data/computercraft/recipes/disk_13.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:10072818}" + "nbt": "{Color:10072818}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_14.json b/src/main/resources/data/computercraft/recipes/disk_14.json index f741e4faa..a63ed9a0c 100644 --- a/src/main/resources/data/computercraft/recipes/disk_14.json +++ b/src/main/resources/data/computercraft/recipes/disk_14.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:15040472}" + "nbt": "{Color:15040472}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_15.json b/src/main/resources/data/computercraft/recipes/disk_15.json index 07f131bb7..96e20a117 100644 --- a/src/main/resources/data/computercraft/recipes/disk_15.json +++ b/src/main/resources/data/computercraft/recipes/disk_15.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:15905331}" + "nbt": "{Color:15905331}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_16.json b/src/main/resources/data/computercraft/recipes/disk_16.json index cc80c50b6..b3e31354f 100644 --- a/src/main/resources/data/computercraft/recipes/disk_16.json +++ b/src/main/resources/data/computercraft/recipes/disk_16.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:15790320}" + "nbt": "{Color:15790320}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_2.json b/src/main/resources/data/computercraft/recipes/disk_2.json index f073b67b9..b211373ca 100644 --- a/src/main/resources/data/computercraft/recipes/disk_2.json +++ b/src/main/resources/data/computercraft/recipes/disk_2.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:13388876}" + "nbt": "{Color:13388876}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_3.json b/src/main/resources/data/computercraft/recipes/disk_3.json index 902f563ac..311e7fc28 100644 --- a/src/main/resources/data/computercraft/recipes/disk_3.json +++ b/src/main/resources/data/computercraft/recipes/disk_3.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:5744206}" + "nbt": "{Color:5744206}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_4.json b/src/main/resources/data/computercraft/recipes/disk_4.json index 21e0e4f63..dad728d09 100644 --- a/src/main/resources/data/computercraft/recipes/disk_4.json +++ b/src/main/resources/data/computercraft/recipes/disk_4.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:8349260}" + "nbt": "{Color:8349260}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_5.json b/src/main/resources/data/computercraft/recipes/disk_5.json index efcddcdd3..52eca6cf8 100644 --- a/src/main/resources/data/computercraft/recipes/disk_5.json +++ b/src/main/resources/data/computercraft/recipes/disk_5.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:3368652}" + "nbt": "{Color:3368652}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_6.json b/src/main/resources/data/computercraft/recipes/disk_6.json index e099b73e1..f21b7d509 100644 --- a/src/main/resources/data/computercraft/recipes/disk_6.json +++ b/src/main/resources/data/computercraft/recipes/disk_6.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:11691749}" + "nbt": "{Color:11691749}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_7.json b/src/main/resources/data/computercraft/recipes/disk_7.json index 2232c6305..6c5d9335f 100644 --- a/src/main/resources/data/computercraft/recipes/disk_7.json +++ b/src/main/resources/data/computercraft/recipes/disk_7.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:5020082}" + "nbt": "{Color:5020082}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_8.json b/src/main/resources/data/computercraft/recipes/disk_8.json index 1319f7fbf..5670d5fdb 100644 --- a/src/main/resources/data/computercraft/recipes/disk_8.json +++ b/src/main/resources/data/computercraft/recipes/disk_8.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:10066329}" + "nbt": "{Color:10066329}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_9.json b/src/main/resources/data/computercraft/recipes/disk_9.json index 3552f4418..b1b28a606 100644 --- a/src/main/resources/data/computercraft/recipes/disk_9.json +++ b/src/main/resources/data/computercraft/recipes/disk_9.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:5000268}" + "nbt": "{Color:5000268}" } } From e12ce95b2de8f497f56841148f8d812b4d4d8e8e Mon Sep 17 00:00:00 2001 From: Jummit Date: Sun, 16 May 2021 09:18:02 +0200 Subject: [PATCH 09/34] Update to 1.16.4 --- build.gradle | 4 +-- gradle.properties | 8 +++--- patchwork.md | 9 ++++++- .../computercraft/api/turtle/FakePlayer.java | 8 ++---- .../computercraft/mixin/MixinWorld.java | 2 +- .../computercraft/shared/BundledRedstone.java | 4 +-- .../computercraft/shared/Peripherals.java | 4 +-- .../shared/command/CommandUtils.java | 2 +- .../shared/computer/apis/CommandAPI.java | 4 +-- .../shared/computer/core/ServerComputer.java | 2 +- .../shared/turtle/core/TurtleMoveCommand.java | 4 +-- .../turtle/core/TurtlePlaceCommand.java | 2 +- .../shared/turtle/core/TurtlePlayer.java | 7 +----- .../shared/util/FakeNetHandler.java | 25 ------------------- .../computercraft/shared/util/WorldUtil.java | 2 +- 15 files changed, 29 insertions(+), 58 deletions(-) diff --git a/build.gradle b/build.gradle index 0be21ae8c..c6287584c 100644 --- a/build.gradle +++ b/build.gradle @@ -55,8 +55,8 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-params:5.7.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' - modRuntime "me.shedaniel:RoughlyEnoughItems-api:5.2.10" - modRuntime "me.shedaniel:RoughlyEnoughItems:5.2.10" + modRuntime "me.shedaniel:RoughlyEnoughItems-api:5.8.9" + modRuntime "me.shedaniel:RoughlyEnoughItems:5.8.9" } sourceSets { diff --git a/gradle.properties b/gradle.properties index 8be827870..430289d0e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,13 +5,13 @@ org.gradle.jvmargs=-Xmx1G mod_version=1.95.1-beta # Minecraft properties -mc_version=1.16.2 -mappings_version=31 +mc_version=1.16.4 +mappings_version=9 # Dependencies cloth_config_version=4.8.1 -fabric_api_version=0.19.0+build.398-1.16 -fabric_loader_version=0.9.2+build.206 +fabric_api_version=0.34.2+1.16 +fabric_loader_version=0.11.3 jankson_version=1.2.0 modmenu_version=1.14.6+ cloth_api_version=1.4.5 diff --git a/patchwork.md b/patchwork.md index ccda58db5..c413af097 100644 --- a/patchwork.md +++ b/patchwork.md @@ -590,4 +590,11 @@ c864576619751077a0d8ac1a18123e14b095ec03 Fix impostor recipes for disks ``` -[TODO] [JUMT-03] REI still shows white disks, probably because it doesn' show nbt items. \ No newline at end of file +[TODO] [JUMT-03] REI still shows white disks, probably because it doesn' show nbt items. + +``` +c5694ea9661c7a40021ebd280c378bd7bdc56988 + +Merge branch 'mc-1.15.x' into mc-1.16.x +``` +Update to 1.16.4. \ No newline at end of file diff --git a/src/main/java/dan200/computercraft/api/turtle/FakePlayer.java b/src/main/java/dan200/computercraft/api/turtle/FakePlayer.java index 76656fa78..4eac506e4 100644 --- a/src/main/java/dan200/computercraft/api/turtle/FakePlayer.java +++ b/src/main/java/dan200/computercraft/api/turtle/FakePlayer.java @@ -48,7 +48,7 @@ import net.minecraft.util.Hand; import net.minecraft.util.collection.DefaultedList; import net.minecraft.util.math.ChunkPos; import net.minecraft.util.math.Vec3d; -import net.minecraft.village.TraderOfferList; +import net.minecraft.village.TradeOfferList; import net.minecraft.world.GameMode; /** @@ -105,7 +105,7 @@ public class FakePlayer extends ServerPlayerEntity { } @Override - public void sendTradeOffers(int id, TraderOfferList list, int level, int experience, boolean levelled, boolean refreshable) { } + public void sendTradeOffers(int id, TradeOfferList list, int level, int experience, boolean levelled, boolean refreshable) { } @Override public void openHorseInventory(HorseBaseEntity horse, Inventory inventory) { } @@ -251,10 +251,6 @@ public class FakePlayer extends ServerPlayerEntity { public void disconnect(Text message) { } - @Override - public void setupEncryption(SecretKey key) { - } - @Override public void disableAutoRead() { } diff --git a/src/main/java/dan200/computercraft/mixin/MixinWorld.java b/src/main/java/dan200/computercraft/mixin/MixinWorld.java index 04b45e796..be271fca2 100644 --- a/src/main/java/dan200/computercraft/mixin/MixinWorld.java +++ b/src/main/java/dan200/computercraft/mixin/MixinWorld.java @@ -32,7 +32,7 @@ public class MixinWorld { @Inject (method = "setBlockEntity", at = @At ("HEAD")) public void setBlockEntity(BlockPos pos, @Nullable BlockEntity entity, CallbackInfo info) { - if (!World.isHeightInvalid(pos) && entity != null && !entity.isRemoved() && this.iteratingTickingBlockEntities) { + if (!World.isOutOfBuildLimitVertically(pos) && entity != null && !entity.isRemoved() && this.iteratingTickingBlockEntities) { setWorld(entity, this); } } diff --git a/src/main/java/dan200/computercraft/shared/BundledRedstone.java b/src/main/java/dan200/computercraft/shared/BundledRedstone.java index dec3f0bf7..2e53b20b0 100644 --- a/src/main/java/dan200/computercraft/shared/BundledRedstone.java +++ b/src/main/java/dan200/computercraft/shared/BundledRedstone.java @@ -31,7 +31,7 @@ public final class BundledRedstone { } public static int getDefaultOutput(@Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side) { - return World.method_24794(pos) ? DefaultBundledRedstoneProvider.getDefaultBundledRedstoneOutput(world, pos, side) : -1; + return World.isValid(pos) ? DefaultBundledRedstoneProvider.getDefaultBundledRedstoneOutput(world, pos, side) : -1; } public static int getOutput(World world, BlockPos pos, Direction side) { @@ -40,7 +40,7 @@ public final class BundledRedstone { } private static int getUnmaskedOutput(World world, BlockPos pos, Direction side) { - if (!World.method_24794(pos)) { + if (!World.isValid(pos)) { return -1; } diff --git a/src/main/java/dan200/computercraft/shared/Peripherals.java b/src/main/java/dan200/computercraft/shared/Peripherals.java index 3357d264b..9f7b9e179 100644 --- a/src/main/java/dan200/computercraft/shared/Peripherals.java +++ b/src/main/java/dan200/computercraft/shared/Peripherals.java @@ -35,13 +35,11 @@ public final class Peripherals { @Nullable public static IPeripheral getPeripheral(World world, BlockPos pos, Direction side) { - return World.method_24794(pos) && !world.isClient ? getPeripheralAt(world, pos, side) : null; + return World.isValid(pos) && !world.isClient ? getPeripheralAt(world, pos, side) : null; } @Nullable private static IPeripheral getPeripheralAt(World world, BlockPos pos, Direction side) { - BlockEntity block = world.getBlockEntity(pos); - // Try the handlers in order: for (IPeripheralProvider peripheralProvider : providers) { try { diff --git a/src/main/java/dan200/computercraft/shared/command/CommandUtils.java b/src/main/java/dan200/computercraft/shared/command/CommandUtils.java index 26754058b..375dd4402 100644 --- a/src/main/java/dan200/computercraft/shared/command/CommandUtils.java +++ b/src/main/java/dan200/computercraft/shared/command/CommandUtils.java @@ -17,7 +17,7 @@ import com.mojang.brigadier.suggestion.SuggestionsBuilder; import dan200.computercraft.api.turtle.FakePlayer; import net.minecraft.entity.Entity; -import net.minecraft.server.command.CommandSource; +import net.minecraft.command.CommandSource; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.network.ServerPlayerEntity; diff --git a/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java b/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java index fe87d15dd..fa7cf62cd 100644 --- a/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java +++ b/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java @@ -207,7 +207,7 @@ public class CommandAPI implements ILuaAPI { World world = this.computer.getWorld(); BlockPos min = new BlockPos(Math.min(minX, maxX), Math.min(minY, maxY), Math.min(minZ, maxZ)); BlockPos max = new BlockPos(Math.max(minX, maxX), Math.max(minY, maxY), Math.max(minZ, maxZ)); - if (!World.method_24794(min) || !World.method_24794(max)) { + if (!World.isValid(min) || !World.isValid(max)) { throw new LuaException("Co-ordinates out of range"); } @@ -284,7 +284,7 @@ public class CommandAPI implements ILuaAPI { // Get the details of the block World world = this.computer.getWorld(); BlockPos position = new BlockPos(x, y, z); - if (World.method_24794(position)) { + if (World.isValid(position)) { return getBlockInfo(world, position); } else { throw new LuaException("Co-ordinates out of range"); diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java index 770e78af8..cc4b1a7fa 100644 --- a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java +++ b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java @@ -316,7 +316,7 @@ public class ServerComputer extends ServerTerminal implements IComputer, IComput @Nonnull @Override public String getHostString() { - return String.format("ComputerCraft %s (Minecraft %s)", ComputerCraftAPI.getInstalledVersion(), "1.16.2"); + return String.format("ComputerCraft %s (Minecraft %s)", ComputerCraftAPI.getInstalledVersion(), "1.16.4"); } @Nonnull diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleMoveCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleMoveCommand.java index 780e117b1..1e0bfde8c 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleMoveCommand.java +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleMoveCommand.java @@ -119,10 +119,10 @@ public class TurtleMoveCommand implements ITurtleCommand { } private static TurtleCommandResult canEnter(TurtlePlayer turtlePlayer, World world, BlockPos position) { - if (World.isHeightInvalid(position)) { + if (World.isOutOfBuildLimitVertically(position)) { return TurtleCommandResult.failure(position.getY() < 0 ? "Too low to move" : "Too high to move"); } - if (!World.method_24794(position)) { + if (!World.isValid(position)) { return TurtleCommandResult.failure("Cannot leave the world"); } diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java index e02ec415e..a21479d1c 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java @@ -364,7 +364,7 @@ public class TurtlePlaceCommand implements ITurtleCommand { private static boolean canDeployOnBlock(@Nonnull ItemPlacementContext context, ITurtleAccess turtle, TurtlePlayer player, BlockPos position, Direction side, boolean allowReplaceable, String[] outErrorMessage) { World world = turtle.getWorld(); - if (!World.method_24794(position) || world.isAir(position) || (context.getStack() + if (!World.isValid(position) || world.isAir(position) || (context.getStack() .getItem() instanceof BlockItem && WorldUtil.isLiquidBlock(world, position))) { return false; diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlayer.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlayer.java index 35150d795..724898819 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlayer.java +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlayer.java @@ -77,7 +77,7 @@ public final class TurtlePlayer extends FakePlayer { private void setState(ITurtleAccess turtle) { if (this.currentScreenHandler != playerScreenHandler) { ComputerCraft.log.warn("Turtle has open container ({})", this.currentScreenHandler); - closeCurrentScreen(); + closeHandledScreen(); } BlockPos position = turtle.getPosition(); @@ -91,13 +91,8 @@ public final class TurtlePlayer extends FakePlayer { } public static TurtlePlayer get(ITurtleAccess access) { - ServerWorld world = (ServerWorld) access.getWorld(); if( !(access instanceof TurtleBrain) ) return create( access ); - /*if (!(access instanceof TurtleBrain)) { - return new TurtlePlayer(world, access.getOwningPlayer()); - }*/ - TurtleBrain brain = (TurtleBrain) access; TurtlePlayer player = brain.m_cachedPlayer; if (player == null || player.getGameProfile() != getProfile(access.getOwningPlayer()) || player.getEntityWorld() != access.getWorld()) { diff --git a/src/main/java/dan200/computercraft/shared/util/FakeNetHandler.java b/src/main/java/dan200/computercraft/shared/util/FakeNetHandler.java index cd5d9d8b6..3b0b93d99 100644 --- a/src/main/java/dan200/computercraft/shared/util/FakeNetHandler.java +++ b/src/main/java/dan200/computercraft/shared/util/FakeNetHandler.java @@ -14,7 +14,6 @@ import dan200.computercraft.api.turtle.FakePlayer; import io.netty.channel.ChannelHandlerContext; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; - import net.minecraft.network.ClientConnection; import net.minecraft.network.NetworkSide; import net.minecraft.network.NetworkState; @@ -25,15 +24,12 @@ import net.minecraft.network.packet.c2s.play.BoatPaddleStateC2SPacket; import net.minecraft.network.packet.c2s.play.BookUpdateC2SPacket; import net.minecraft.network.packet.c2s.play.ButtonClickC2SPacket; import net.minecraft.network.packet.c2s.play.ChatMessageC2SPacket; -import net.minecraft.network.packet.c2s.play.ClickWindowC2SPacket; import net.minecraft.network.packet.c2s.play.ClientCommandC2SPacket; import net.minecraft.network.packet.c2s.play.ClientSettingsC2SPacket; import net.minecraft.network.packet.c2s.play.ClientStatusC2SPacket; -import net.minecraft.network.packet.c2s.play.ConfirmGuiActionC2SPacket; import net.minecraft.network.packet.c2s.play.CraftRequestC2SPacket; import net.minecraft.network.packet.c2s.play.CreativeInventoryActionC2SPacket; import net.minecraft.network.packet.c2s.play.CustomPayloadC2SPacket; -import net.minecraft.network.packet.c2s.play.GuiCloseC2SPacket; import net.minecraft.network.packet.c2s.play.HandSwingC2SPacket; import net.minecraft.network.packet.c2s.play.KeepAliveC2SPacket; import net.minecraft.network.packet.c2s.play.PickFromInventoryC2SPacket; @@ -48,7 +44,6 @@ import net.minecraft.network.packet.c2s.play.QueryEntityNbtC2SPacket; import net.minecraft.network.packet.c2s.play.RenameItemC2SPacket; import net.minecraft.network.packet.c2s.play.RequestCommandCompletionsC2SPacket; import net.minecraft.network.packet.c2s.play.ResourcePackStatusC2SPacket; -import net.minecraft.network.packet.c2s.play.SelectVillagerTradeC2SPacket; import net.minecraft.network.packet.c2s.play.SpectatorTeleportC2SPacket; import net.minecraft.network.packet.c2s.play.TeleportConfirmC2SPacket; import net.minecraft.network.packet.c2s.play.UpdateBeaconC2SPacket; @@ -127,10 +122,6 @@ public class FakeNetHandler extends ServerPlayNetworkHandler { public void onJigsawUpdate(@Nonnull UpdateJigsawC2SPacket packet) { } - @Override - public void onVillagerTradeSelect(@Nonnull SelectVillagerTradeC2SPacket packet) { - } - @Override public void onBookUpdate(@Nonnull BookUpdateC2SPacket packet) { } @@ -207,14 +198,6 @@ public class FakeNetHandler extends ServerPlayNetworkHandler { public void onClientStatus(@Nonnull ClientStatusC2SPacket packet) { } - @Override - public void onGuiClose(@Nonnull GuiCloseC2SPacket packet) { - } - - @Override - public void onClickWindow(@Nonnull ClickWindowC2SPacket packet) { - } - @Override public void onCraftRequest(@Nonnull CraftRequestC2SPacket packet) { } @@ -227,10 +210,6 @@ public class FakeNetHandler extends ServerPlayNetworkHandler { public void onCreativeInventoryAction(@Nonnull CreativeInventoryActionC2SPacket packet) { } - @Override - public void onConfirmTransaction(@Nonnull ConfirmGuiActionC2SPacket packet) { - } - @Override public void onSignUpdate(@Nonnull UpdateSignC2SPacket packet) { } @@ -309,10 +288,6 @@ public class FakeNetHandler extends ServerPlayNetworkHandler { this.closeReason = message; } - @Override - public void setupEncryption(@Nonnull SecretKey key) { - } - @Nonnull @Override public PacketListener getPacketListener() { diff --git a/src/main/java/dan200/computercraft/shared/util/WorldUtil.java b/src/main/java/dan200/computercraft/shared/util/WorldUtil.java index 284af67d3..df99b66d1 100644 --- a/src/main/java/dan200/computercraft/shared/util/WorldUtil.java +++ b/src/main/java/dan200/computercraft/shared/util/WorldUtil.java @@ -40,7 +40,7 @@ public final class WorldUtil { .makeMap(); public static boolean isLiquidBlock(World world, BlockPos pos) { - if (!World.method_24794(pos)) { + if (!World.isValid(pos)) { return false; } return world.getBlockState(pos) From df40adce201f1c3dbb424153fd5036c34e365a5d Mon Sep 17 00:00:00 2001 From: Wojbie Date: Mon, 11 Jan 2021 22:59:29 +0100 Subject: [PATCH 10/34] Make rightAlt only close menu, never open it. (#672) Fixes #669 --- patchwork.md | 9 ++++++++- .../data/computercraft/lua/rom/programs/edit.lua | 9 +++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/patchwork.md b/patchwork.md index c413af097..4a624e367 100644 --- a/patchwork.md +++ b/patchwork.md @@ -597,4 +597,11 @@ c5694ea9661c7a40021ebd280c378bd7bdc56988 Merge branch 'mc-1.15.x' into mc-1.16.x ``` -Update to 1.16.4. \ No newline at end of file +Update to 1.16.4. + +``` +1f84480a80677cfaaf19d319290f5b44635eba47 + +Make rightAlt only close menu, never open it. (#672) +``` +Lua changes. \ No newline at end of file diff --git a/src/main/resources/data/computercraft/lua/rom/programs/edit.lua b/src/main/resources/data/computercraft/lua/rom/programs/edit.lua index 8656798df..78c6f5ca9 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/edit.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/edit.lua @@ -687,7 +687,7 @@ while bRunning do end - elseif param == keys.leftCtrl or param == keys.rightCtrl or param == keys.rightAlt then + elseif param == keys.leftCtrl or param == keys.rightCtrl then -- Menu toggle bMenu = not bMenu if bMenu then @@ -696,7 +696,12 @@ while bRunning do term.setCursorBlink(true) end redrawMenu() - + elseif param == keys.rightAlt then + if bMenu then + bMenu = false + term.setCursorBlink(true) + redrawMenu() + end end elseif sEvent == "char" then From 094e0d4f33ff28462d9e4721edcb8c8d1581b410 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 13 Jan 2021 22:10:44 +0000 Subject: [PATCH 11/34] Fix mounts being usable after a disk is ejected This probably fails "responsible disclosure", but it's not an RCE and frankly the whole bug is utterly hilarious so here we are... It's possible to open a file on a disk drive and continue to read/write to them after the disk has been removed: local disk = peripheral.find("drive") local input = fs.open(fs.combine(disk.getMountPath(), "stream"), "rb") local output = fs.open(fs.combine(disk.getMountPath(), "stream"), "wb") disk.ejectDisk() -- input/output can still be interacted with. This is pretty amusing, as now it allows us to move the disk somewhere else and repeat - we've now got a private tunnel which two computers can use to communicate. Fixing this is intuitively quite simple - just close any open files belonging to this mount. However, this is where things get messy thanks to the wonderful joy of how CC's streams are handled. As things stand, the filesystem effectively does the following flow:: - There is a function `open : String -> Channel' (file modes are irrelevant here). - Once a file is opened, we transform it into some . This is, for instance, a BufferedReader. - We generate a "token" (i.e. FileSystemWrapper), which we generate a week reference to and map it to a tuple of our Channel and T. If this token is ever garbage collected (someone forgot to call close() on a file), then we close our T and Channel. - This token and T are returned to the calling function, which then constructs a Lua object. The problem here is that if we close the underlying Channel+T before the Lua object calls .close(), then it won't know the underlying channel is closed, and you get some pretty ugly errors (e.g. "Stream Closed"). So we've moved the "is open" state into the FileSystemWrapper. The whole system is incredibly complex at this point, and I'd really like to clean it up. Ideally we could treat the HandleGeneric as the token instead - this way we could potentially also clean up FileSystemWrapperMount. BBut something to play with in the future, and not when it's 10:30pm. --- All this wall of text, and this isn't the only bug I've found with disks today :/. --- patchwork.md | 9 +- .../core/apis/handles/ArrayByteChannel.java | 85 +- .../apis/handles/BinaryReadableHandle.java | 244 +++-- .../apis/handles/BinaryWritableHandle.java | 117 ++- .../apis/handles/EncodedReadableHandle.java | 167 +-- .../apis/handles/EncodedWritableHandle.java | 104 +- .../core/apis/handles/HandleGeneric.java | 150 +-- .../core/filesystem/ChannelWrapper.java | 31 +- .../core/filesystem/FileSystem.java | 967 ++++++++++-------- .../core/filesystem/FileSystemWrapper.java | 52 +- .../core/filesystem/TrackingCloseable.java | 44 + .../core/filesystem/FileSystemTest.java | 28 +- 12 files changed, 1137 insertions(+), 861 deletions(-) create mode 100644 src/main/java/dan200/computercraft/core/filesystem/TrackingCloseable.java diff --git a/patchwork.md b/patchwork.md index 4a624e367..f7be787fe 100644 --- a/patchwork.md +++ b/patchwork.md @@ -604,4 +604,11 @@ Update to 1.16.4. Make rightAlt only close menu, never open it. (#672) ``` -Lua changes. \ No newline at end of file +Lua changes. + +``` +1255bd00fd21247a50046020d7d9a396f66bc6bd + +Fix mounts being usable after a disk is ejected +``` +Reverted a lot of code style changes made by Zundrel, so the diffs are huge. \ No newline at end of file diff --git a/src/main/java/dan200/computercraft/core/apis/handles/ArrayByteChannel.java b/src/main/java/dan200/computercraft/core/apis/handles/ArrayByteChannel.java index 345e4b949..464e23a4e 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/ArrayByteChannel.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/ArrayByteChannel.java @@ -3,7 +3,6 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.apis.handles; import java.nio.ByteBuffer; @@ -15,83 +14,81 @@ import java.util.Objects; /** * A seekable, readable byte channel which is backed by a simple byte array. */ -public class ArrayByteChannel implements SeekableByteChannel { - private final byte[] backing; +public class ArrayByteChannel implements SeekableByteChannel +{ private boolean closed = false; private int position = 0; - public ArrayByteChannel(byte[] backing) { + private final byte[] backing; + + public ArrayByteChannel( byte[] backing ) + { this.backing = backing; } @Override - public int read(ByteBuffer destination) throws ClosedChannelException { - if (this.closed) { - throw new ClosedChannelException(); - } - Objects.requireNonNull(destination, "destination"); + public int read( ByteBuffer destination ) throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + Objects.requireNonNull( destination, "destination" ); - if (this.position >= this.backing.length) { - return -1; - } + if( position >= backing.length ) return -1; - int remaining = Math.min(this.backing.length - this.position, destination.remaining()); - destination.put(this.backing, this.position, remaining); - this.position += remaining; + int remaining = Math.min( backing.length - position, destination.remaining() ); + destination.put( backing, position, remaining ); + position += remaining; return remaining; } @Override - public int write(ByteBuffer src) throws ClosedChannelException { - if (this.closed) { - throw new ClosedChannelException(); - } + public int write( ByteBuffer src ) throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); throw new NonWritableChannelException(); } @Override - public long position() throws ClosedChannelException { - if (this.closed) { - throw new ClosedChannelException(); - } - return this.position; + public long position() throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + return position; } @Override - public SeekableByteChannel position(long newPosition) throws ClosedChannelException { - if (this.closed) { - throw new ClosedChannelException(); + public SeekableByteChannel position( long newPosition ) throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + if( newPosition < 0 || newPosition > Integer.MAX_VALUE ) + { + throw new IllegalArgumentException( "Position out of bounds" ); } - if (newPosition < 0 || newPosition > Integer.MAX_VALUE) { - throw new IllegalArgumentException("Position out of bounds"); - } - this.position = (int) newPosition; + position = (int) newPosition; return this; } @Override - public long size() throws ClosedChannelException { - if (this.closed) { - throw new ClosedChannelException(); - } - return this.backing.length; + public long size() throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + return backing.length; } @Override - public SeekableByteChannel truncate(long size) throws ClosedChannelException { - if (this.closed) { - throw new ClosedChannelException(); - } + public SeekableByteChannel truncate( long size ) throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); throw new NonWritableChannelException(); } @Override - public boolean isOpen() { - return !this.closed; + public boolean isOpen() + { + return !closed; } @Override - public void close() { - this.closed = true; + public void close() + { + closed = true; } } diff --git a/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java index e6dcc17ad..4477dde92 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java @@ -3,11 +3,13 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.apis.handles; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.filesystem.TrackingCloseable; + import java.io.ByteArrayOutputStream; -import java.io.Closeable; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ReadableByteChannel; @@ -16,40 +18,43 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.lua.LuaFunction; - /** - * A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "rb"} mode. + * A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "rb"} + * mode. * * @cc.module fs.BinaryReadHandle */ -public class BinaryReadableHandle extends HandleGeneric { +public class BinaryReadableHandle extends HandleGeneric +{ private static final int BUFFER_SIZE = 8192; - final SeekableByteChannel seekable; - private final ReadableByteChannel reader; - private final ByteBuffer single = ByteBuffer.allocate(1); - BinaryReadableHandle(ReadableByteChannel reader, SeekableByteChannel seekable, Closeable closeable) { - super(closeable); + private final ReadableByteChannel reader; + final SeekableByteChannel seekable; + private final ByteBuffer single = ByteBuffer.allocate( 1 ); + + BinaryReadableHandle( ReadableByteChannel reader, SeekableByteChannel seekable, TrackingCloseable closeable ) + { + super( closeable ); this.reader = reader; this.seekable = seekable; } - public static BinaryReadableHandle of(ReadableByteChannel channel) { - return of(channel, channel); + public static BinaryReadableHandle of( ReadableByteChannel channel, TrackingCloseable closeable ) + { + SeekableByteChannel seekable = asSeekable( channel ); + return seekable == null ? new BinaryReadableHandle( channel, null, closeable ) : new Seekable( seekable, closeable ); } - public static BinaryReadableHandle of(ReadableByteChannel channel, Closeable closeable) { - SeekableByteChannel seekable = asSeekable(channel); - return seekable == null ? new BinaryReadableHandle(channel, null, closeable) : new Seekable(seekable, closeable); + public static BinaryReadableHandle of( ReadableByteChannel channel ) + { + return of( channel, new TrackingCloseable.Impl( channel ) ); } /** * Read a number of bytes from this file. * - * @param countArg The number of bytes to read. When absent, a single byte will be read as a number. This may be 0 to determine we are at - * the end of the file. + * @param countArg The number of bytes to read. When absent, a single byte will be read as a number. This + * may be 0 to determine we are at the end of the file. * @return The read bytes. * @throws LuaException When trying to read a negative number of bytes. * @throws LuaException If the file has been closed. @@ -58,72 +63,78 @@ public class BinaryReadableHandle extends HandleGeneric { * @cc.treturn [3] string The bytes read as a string. This is returned when the {@code count} is given. */ @LuaFunction - public final Object[] read(Optional countArg) throws LuaException { - this.checkOpen(); - try { - if (countArg.isPresent()) { + public final Object[] read( Optional countArg ) throws LuaException + { + checkOpen(); + try + { + if( countArg.isPresent() ) + { int count = countArg.get(); - if (count < 0) { - throw new LuaException("Cannot read a negative number of bytes"); - } - if (count == 0 && this.seekable != null) { - return this.seekable.position() >= this.seekable.size() ? null : new Object[] {""}; + if( count < 0 ) throw new LuaException( "Cannot read a negative number of bytes" ); + if( count == 0 && seekable != null ) + { + return seekable.position() >= seekable.size() ? null : new Object[] { "" }; } - if (count <= BUFFER_SIZE) { - ByteBuffer buffer = ByteBuffer.allocate(count); + if( count <= BUFFER_SIZE ) + { + ByteBuffer buffer = ByteBuffer.allocate( count ); - int read = this.reader.read(buffer); - if (read < 0) { - return null; - } + int read = reader.read( buffer ); + if( read < 0 ) return null; buffer.flip(); - return new Object[] {buffer}; - } else { + return new Object[] { buffer }; + } + else + { // Read the initial set of characters, failing if none are read. - ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); - int read = this.reader.read(buffer); - if (read < 0) { - return null; - } + ByteBuffer buffer = ByteBuffer.allocate( BUFFER_SIZE ); + int read = reader.read( buffer ); + if( read < 0 ) return null; // If we failed to read "enough" here, let's just abort - if (read >= count || read < BUFFER_SIZE) { + if( read >= count || read < BUFFER_SIZE ) + { buffer.flip(); - return new Object[] {buffer}; + return new Object[] { buffer }; } // Build up an array of ByteBuffers. Hopefully this means we can perform less allocation // than doubling up the buffer each time. int totalRead = read; - List parts = new ArrayList<>(4); - parts.add(buffer); - while (read >= BUFFER_SIZE && totalRead < count) { - buffer = ByteBuffer.allocate(Math.min(BUFFER_SIZE, count - totalRead)); - read = this.reader.read(buffer); - if (read < 0) { - break; - } + List parts = new ArrayList<>( 4 ); + parts.add( buffer ); + while( read >= BUFFER_SIZE && totalRead < count ) + { + buffer = ByteBuffer.allocate( Math.min( BUFFER_SIZE, count - totalRead ) ); + read = reader.read( buffer ); + if( read < 0 ) break; totalRead += read; - parts.add(buffer); + parts.add( buffer ); } // Now just copy all the bytes across! byte[] bytes = new byte[totalRead]; int pos = 0; - for (ByteBuffer part : parts) { - System.arraycopy(part.array(), 0, bytes, pos, part.position()); + for( ByteBuffer part : parts ) + { + System.arraycopy( part.array(), 0, bytes, pos, part.position() ); pos += part.position(); } - return new Object[] {bytes}; + return new Object[] { bytes }; } - } else { - this.single.clear(); - int b = this.reader.read(this.single); - return b == -1 ? null : new Object[] {this.single.get(0) & 0xFF}; } - } catch (IOException e) { + else + { + single.clear(); + int b = reader.read( single ); + return b == -1 ? null : new Object[] { single.get( 0 ) & 0xFF }; + } + } + catch( IOException e ) + { return null; } } @@ -136,29 +147,30 @@ public class BinaryReadableHandle extends HandleGeneric { * @cc.treturn string|nil The remaining contents of the file, or {@code nil} if we are at the end. */ @LuaFunction - public final Object[] readAll() throws LuaException { - this.checkOpen(); - try { + public final Object[] readAll() throws LuaException + { + checkOpen(); + try + { int expected = 32; - if (this.seekable != null) { - expected = Math.max(expected, (int) (this.seekable.size() - this.seekable.position())); - } - ByteArrayOutputStream stream = new ByteArrayOutputStream(expected); + if( seekable != null ) expected = Math.max( expected, (int) (seekable.size() - seekable.position()) ); + ByteArrayOutputStream stream = new ByteArrayOutputStream( expected ); - ByteBuffer buf = ByteBuffer.allocate(8192); + ByteBuffer buf = ByteBuffer.allocate( 8192 ); boolean readAnything = false; - while (true) { + while( true ) + { buf.clear(); - int r = this.reader.read(buf); - if (r == -1) { - break; - } + int r = reader.read( buf ); + if( r == -1 ) break; readAnything = true; - stream.write(buf.array(), 0, r); + stream.write( buf.array(), 0, r ); } - return readAnything ? new Object[] {stream.toByteArray()} : null; - } catch (IOException e) { + return readAnything ? new Object[] { stream.toByteArray() } : null; + } + catch( IOException e ) + { return null; } } @@ -172,65 +184,70 @@ public class BinaryReadableHandle extends HandleGeneric { * @cc.treturn string|nil The read line or {@code nil} if at the end of the file. */ @LuaFunction - public final Object[] readLine(Optional withTrailingArg) throws LuaException { - this.checkOpen(); - boolean withTrailing = withTrailingArg.orElse(false); - try { + public final Object[] readLine( Optional withTrailingArg ) throws LuaException + { + checkOpen(); + boolean withTrailing = withTrailingArg.orElse( false ); + try + { ByteArrayOutputStream stream = new ByteArrayOutputStream(); boolean readAnything = false, readRc = false; - while (true) { - this.single.clear(); - int read = this.reader.read(this.single); - if (read <= 0) { + while( true ) + { + single.clear(); + int read = reader.read( single ); + if( read <= 0 ) + { // Nothing else to read, and we saw no \n. Return the array. If we saw a \r, then add it // back. - if (readRc) { - stream.write('\r'); - } - return readAnything ? new Object[] {stream.toByteArray()} : null; + if( readRc ) stream.write( '\r' ); + return readAnything ? new Object[] { stream.toByteArray() } : null; } readAnything = true; - byte chr = this.single.get(0); - if (chr == '\n') { - if (withTrailing) { - if (readRc) { - stream.write('\r'); - } - stream.write(chr); + byte chr = single.get( 0 ); + if( chr == '\n' ) + { + if( withTrailing ) + { + if( readRc ) stream.write( '\r' ); + stream.write( chr ); } - return new Object[] {stream.toByteArray()}; - } else { + return new Object[] { stream.toByteArray() }; + } + else + { // We want to skip \r\n, but obviously need to include cases where \r is not followed by \n. // Note, this behaviour is non-standard compliant (strictly speaking we should have no // special logic for \r), but we preserve compatibility with EncodedReadableHandle and // previous behaviour of the io library. - if (readRc) { - stream.write('\r'); - } + if( readRc ) stream.write( '\r' ); readRc = chr == '\r'; - if (!readRc) { - stream.write(chr); - } + if( !readRc ) stream.write( chr ); } } - } catch (IOException e) { + } + catch( IOException e ) + { return null; } } - public static class Seekable extends BinaryReadableHandle { - Seekable(SeekableByteChannel seekable, Closeable closeable) { - super(seekable, seekable, closeable); + public static class Seekable extends BinaryReadableHandle + { + Seekable( SeekableByteChannel seekable, TrackingCloseable closeable ) + { + super( seekable, seekable, closeable ); } /** - * Seek to a new position within the file, changing where bytes are written to. The new position is an offset given by {@code offset}, relative to a - * start position determined by {@code whence}: + * Seek to a new position within the file, changing where bytes are written to. The new position is an offset + * given by {@code offset}, relative to a start position determined by {@code whence}: * - * - {@code "set"}: {@code offset} is relative to the beginning of the file. - {@code "cur"}: Relative to the current position. This is the default. + * - {@code "set"}: {@code offset} is relative to the beginning of the file. + * - {@code "cur"}: Relative to the current position. This is the default. * - {@code "end"}: Relative to the end of the file. * * In case of success, {@code seek} returns the new file position from the beginning of the file. @@ -244,9 +261,10 @@ public class BinaryReadableHandle extends HandleGeneric { * @cc.treturn string The reason seeking failed. */ @LuaFunction - public final Object[] seek(Optional whence, Optional offset) throws LuaException { - this.checkOpen(); - return handleSeek(this.seekable, whence, offset); + public final Object[] seek( Optional whence, Optional offset ) throws LuaException + { + checkOpen(); + return handleSeek( seekable, whence, offset ); } } } diff --git a/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java index da466dca1..796582855 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java @@ -3,10 +3,14 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.apis.handles; -import java.io.Closeable; +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.lua.LuaValues; +import dan200.computercraft.core.filesystem.TrackingCloseable; + import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; @@ -14,34 +18,34 @@ import java.nio.channels.SeekableByteChannel; import java.nio.channels.WritableByteChannel; import java.util.Optional; -import dan200.computercraft.api.lua.IArguments; -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.lua.LuaFunction; -import dan200.computercraft.api.lua.LuaValues; - /** - * A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "wb"} or {@code "ab"} modes. + * A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "wb"} or {@code "ab"} + * modes. * * @cc.module fs.BinaryWriteHandle */ -public class BinaryWritableHandle extends HandleGeneric { - final SeekableByteChannel seekable; +public class BinaryWritableHandle extends HandleGeneric +{ private final WritableByteChannel writer; - private final ByteBuffer single = ByteBuffer.allocate(1); + final SeekableByteChannel seekable; + private final ByteBuffer single = ByteBuffer.allocate( 1 ); - protected BinaryWritableHandle(WritableByteChannel writer, SeekableByteChannel seekable, Closeable closeable) { - super(closeable); + protected BinaryWritableHandle( WritableByteChannel writer, SeekableByteChannel seekable, TrackingCloseable closeable ) + { + super( closeable ); this.writer = writer; this.seekable = seekable; } - public static BinaryWritableHandle of(WritableByteChannel channel) { - return of(channel, channel); + public static BinaryWritableHandle of( WritableByteChannel channel, TrackingCloseable closeable ) + { + SeekableByteChannel seekable = asSeekable( channel ); + return seekable == null ? new BinaryWritableHandle( channel, null, closeable ) : new Seekable( seekable, closeable ); } - public static BinaryWritableHandle of(WritableByteChannel channel, Closeable closeable) { - SeekableByteChannel seekable = asSeekable(channel); - return seekable == null ? new BinaryWritableHandle(channel, null, closeable) : new Seekable(seekable, closeable); + public static BinaryWritableHandle of( WritableByteChannel channel ) + { + return of( channel, new TrackingCloseable.Impl( channel ) ); } /** @@ -53,24 +57,33 @@ public class BinaryWritableHandle extends HandleGeneric { * @cc.tparam [2] string The string to write. */ @LuaFunction - public final void write(IArguments arguments) throws LuaException { - this.checkOpen(); - try { - Object arg = arguments.get(0); - if (arg instanceof Number) { + public final void write( IArguments arguments ) throws LuaException + { + checkOpen(); + try + { + Object arg = arguments.get( 0 ); + if( arg instanceof Number ) + { int number = ((Number) arg).intValue(); - this.single.clear(); - this.single.put((byte) number); - this.single.flip(); + single.clear(); + single.put( (byte) number ); + single.flip(); - this.writer.write(this.single); - } else if (arg instanceof String) { - this.writer.write(arguments.getBytes(0)); - } else { - throw LuaValues.badArgumentOf(0, "string or number", arg); + writer.write( single ); } - } catch (IOException e) { - throw new LuaException(e.getMessage()); + else if( arg instanceof String ) + { + writer.write( arguments.getBytes( 0 ) ); + } + else + { + throw LuaValues.badArgumentOf( 0, "string or number", arg ); + } + } + catch( IOException e ) + { + throw new LuaException( e.getMessage() ); } } @@ -80,27 +93,32 @@ public class BinaryWritableHandle extends HandleGeneric { * @throws LuaException If the file has been closed. */ @LuaFunction - public final void flush() throws LuaException { - this.checkOpen(); - try { + public final void flush() throws LuaException + { + checkOpen(); + try + { // Technically this is not needed - if (this.writer instanceof FileChannel) { - ((FileChannel) this.writer).force(false); - } - } catch (IOException ignored) { + if( writer instanceof FileChannel ) ((FileChannel) writer).force( false ); + } + catch( IOException ignored ) + { } } - public static class Seekable extends BinaryWritableHandle { - public Seekable(SeekableByteChannel seekable, Closeable closeable) { - super(seekable, seekable, closeable); + public static class Seekable extends BinaryWritableHandle + { + public Seekable( SeekableByteChannel seekable, TrackingCloseable closeable ) + { + super( seekable, seekable, closeable ); } /** - * Seek to a new position within the file, changing where bytes are written to. The new position is an offset given by {@code offset}, relative to a - * start position determined by {@code whence}: + * Seek to a new position within the file, changing where bytes are written to. The new position is an offset + * given by {@code offset}, relative to a start position determined by {@code whence}: * - * - {@code "set"}: {@code offset} is relative to the beginning of the file. - {@code "cur"}: Relative to the current position. This is the default. + * - {@code "set"}: {@code offset} is relative to the beginning of the file. + * - {@code "cur"}: Relative to the current position. This is the default. * - {@code "end"}: Relative to the end of the file. * * In case of success, {@code seek} returns the new file position from the beginning of the file. @@ -114,9 +132,10 @@ public class BinaryWritableHandle extends HandleGeneric { * @cc.treturn string The reason seeking failed. */ @LuaFunction - public final Object[] seek(Optional whence, Optional offset) throws LuaException { - this.checkOpen(); - return handleSeek(this.seekable, whence, offset); + public final Object[] seek( Optional whence, Optional offset ) throws LuaException + { + checkOpen(); + return handleSeek( seekable, whence, offset ); } } } diff --git a/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java index c173a26ef..28576f70d 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java @@ -3,11 +3,14 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.apis.handles; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.filesystem.TrackingCloseable; + +import javax.annotation.Nonnull; import java.io.BufferedReader; -import java.io.Closeable; import java.io.IOException; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; @@ -17,41 +20,27 @@ import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.util.Optional; -import javax.annotation.Nonnull; - -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.lua.LuaFunction; - /** - * A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "r"} mode. + * A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "r"} + * mode. * * @cc.module fs.ReadHandle */ -public class EncodedReadableHandle extends HandleGeneric { +public class EncodedReadableHandle extends HandleGeneric +{ private static final int BUFFER_SIZE = 8192; private final BufferedReader reader; - public EncodedReadableHandle(@Nonnull BufferedReader reader) { - this(reader, reader); - } - - public EncodedReadableHandle(@Nonnull BufferedReader reader, @Nonnull Closeable closable) { - super(closable); + public EncodedReadableHandle( @Nonnull BufferedReader reader, @Nonnull TrackingCloseable closable ) + { + super( closable ); this.reader = reader; } - public static BufferedReader openUtf8(ReadableByteChannel channel) { - return open(channel, StandardCharsets.UTF_8); - } - - public static BufferedReader open(ReadableByteChannel channel, Charset charset) { - // Create a charset decoder with the same properties as StreamDecoder does for - // InputStreams: namely, replace everything instead of erroring. - CharsetDecoder decoder = charset.newDecoder() - .onMalformedInput(CodingErrorAction.REPLACE) - .onUnmappableCharacter(CodingErrorAction.REPLACE); - return new BufferedReader(Channels.newReader(channel, decoder, -1)); + public EncodedReadableHandle( @Nonnull BufferedReader reader ) + { + this( reader, new TrackingCloseable.Impl( reader ) ); } /** @@ -63,21 +52,26 @@ public class EncodedReadableHandle extends HandleGeneric { * @cc.treturn string|nil The read line or {@code nil} if at the end of the file. */ @LuaFunction - public final Object[] readLine(Optional withTrailingArg) throws LuaException { - this.checkOpen(); - boolean withTrailing = withTrailingArg.orElse(false); - try { - String line = this.reader.readLine(); - if (line != null) { + public final Object[] readLine( Optional withTrailingArg ) throws LuaException + { + checkOpen(); + boolean withTrailing = withTrailingArg.orElse( false ); + try + { + String line = reader.readLine(); + if( line != null ) + { // While this is technically inaccurate, it's better than nothing - if (withTrailing) { - line += "\n"; - } - return new Object[] {line}; - } else { + if( withTrailing ) line += "\n"; + return new Object[] { line }; + } + else + { return null; } - } catch (IOException e) { + } + catch( IOException e ) + { return null; } } @@ -90,20 +84,26 @@ public class EncodedReadableHandle extends HandleGeneric { * @cc.treturn nil|string The remaining contents of the file, or {@code nil} if we are at the end. */ @LuaFunction - public final Object[] readAll() throws LuaException { - this.checkOpen(); - try { + public final Object[] readAll() throws LuaException + { + checkOpen(); + try + { StringBuilder result = new StringBuilder(); - String line = this.reader.readLine(); - while (line != null) { - result.append(line); - line = this.reader.readLine(); - if (line != null) { - result.append("\n"); + String line = reader.readLine(); + while( line != null ) + { + result.append( line ); + line = reader.readLine(); + if( line != null ) + { + result.append( "\n" ); } } - return new Object[] {result.toString()}; - } catch (IOException e) { + return new Object[] { result.toString() }; + } + catch( IOException e ) + { return null; } } @@ -118,50 +118,71 @@ public class EncodedReadableHandle extends HandleGeneric { * @cc.treturn string|nil The read characters, or {@code nil} if at the of the file. */ @LuaFunction - public final Object[] read(Optional countA) throws LuaException { - this.checkOpen(); - try { - int count = countA.orElse(1); - if (count < 0) { + public final Object[] read( Optional countA ) throws LuaException + { + checkOpen(); + try + { + int count = countA.orElse( 1 ); + if( count < 0 ) + { // Whilst this may seem absurd to allow reading 0 characters, PUC Lua it so // it seems best to remain somewhat consistent. - throw new LuaException("Cannot read a negative number of characters"); - } else if (count <= BUFFER_SIZE) { + throw new LuaException( "Cannot read a negative number of characters" ); + } + else if( count <= BUFFER_SIZE ) + { // If we've got a small count, then allocate that and read it. char[] chars = new char[count]; - int read = this.reader.read(chars); + int read = reader.read( chars ); - return read < 0 ? null : new Object[] {new String(chars, 0, read)}; - } else { + return read < 0 ? null : new Object[] { new String( chars, 0, read ) }; + } + else + { // If we've got a large count, read in bunches of 8192. char[] buffer = new char[BUFFER_SIZE]; // Read the initial set of characters, failing if none are read. - int read = this.reader.read(buffer, 0, Math.min(buffer.length, count)); - if (read < 0) { - return null; - } + int read = reader.read( buffer, 0, Math.min( buffer.length, count ) ); + if( read < 0 ) return null; - StringBuilder out = new StringBuilder(read); + StringBuilder out = new StringBuilder( read ); int totalRead = read; - out.append(buffer, 0, read); + out.append( buffer, 0, read ); // Otherwise read until we either reach the limit or we no longer consume // the full buffer. - while (read >= BUFFER_SIZE && totalRead < count) { - read = this.reader.read(buffer, 0, Math.min(BUFFER_SIZE, count - totalRead)); - if (read < 0) { - break; - } + while( read >= BUFFER_SIZE && totalRead < count ) + { + read = reader.read( buffer, 0, Math.min( BUFFER_SIZE, count - totalRead ) ); + if( read < 0 ) break; totalRead += read; - out.append(buffer, 0, read); + out.append( buffer, 0, read ); } - return new Object[] {out.toString()}; + return new Object[] { out.toString() }; } - } catch (IOException e) { + } + catch( IOException e ) + { return null; } } + + public static BufferedReader openUtf8( ReadableByteChannel channel ) + { + return open( channel, StandardCharsets.UTF_8 ); + } + + public static BufferedReader open( ReadableByteChannel channel, Charset charset ) + { + // Create a charset decoder with the same properties as StreamDecoder does for + // InputStreams: namely, replace everything instead of erroring. + CharsetDecoder decoder = charset.newDecoder() + .onMalformedInput( CodingErrorAction.REPLACE ) + .onUnmappableCharacter( CodingErrorAction.REPLACE ); + return new BufferedReader( Channels.newReader( channel, decoder, -1 ) ); + } } diff --git a/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java index 7b02d91bd..b012b6d0d 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java @@ -3,11 +3,16 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.apis.handles; +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.filesystem.TrackingCloseable; +import dan200.computercraft.shared.util.StringUtil; + +import javax.annotation.Nonnull; import java.io.BufferedWriter; -import java.io.Closeable; import java.io.IOException; import java.nio.channels.Channels; import java.nio.channels.WritableByteChannel; @@ -16,39 +21,21 @@ import java.nio.charset.CharsetEncoder; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; -import javax.annotation.Nonnull; - -import dan200.computercraft.api.lua.IArguments; -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.lua.LuaFunction; -import dan200.computercraft.shared.util.StringUtil; - /** * A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "w"} or {@code "a"} modes. * * @cc.module fs.WriteHandle */ -public class EncodedWritableHandle extends HandleGeneric { +public class EncodedWritableHandle extends HandleGeneric +{ private final BufferedWriter writer; - public EncodedWritableHandle(@Nonnull BufferedWriter writer, @Nonnull Closeable closable) { - super(closable); + public EncodedWritableHandle( @Nonnull BufferedWriter writer, @Nonnull TrackingCloseable closable ) + { + super( closable ); this.writer = writer; } - public static BufferedWriter openUtf8(WritableByteChannel channel) { - return open(channel, StandardCharsets.UTF_8); - } - - public static BufferedWriter open(WritableByteChannel channel, Charset charset) { - // Create a charset encoder with the same properties as StreamEncoder does for - // OutputStreams: namely, replace everything instead of erroring. - CharsetEncoder encoder = charset.newEncoder() - .onMalformedInput(CodingErrorAction.REPLACE) - .onUnmappableCharacter(CodingErrorAction.REPLACE); - return new BufferedWriter(Channels.newWriter(channel, encoder, -1)); - } - /** * Write a string of characters to the file. * @@ -57,13 +44,17 @@ public class EncodedWritableHandle extends HandleGeneric { * @cc.param value The value to write to the file. */ @LuaFunction - public final void write(IArguments args) throws LuaException { - this.checkOpen(); - String text = StringUtil.toString(args.get(0)); - try { - this.writer.write(text, 0, text.length()); - } catch (IOException e) { - throw new LuaException(e.getMessage()); + public final void write( IArguments args ) throws LuaException + { + checkOpen(); + String text = StringUtil.toString( args.get( 0 ) ); + try + { + writer.write( text, 0, text.length() ); + } + catch( IOException e ) + { + throw new LuaException( e.getMessage() ); } } @@ -75,14 +66,18 @@ public class EncodedWritableHandle extends HandleGeneric { * @cc.param value The value to write to the file. */ @LuaFunction - public final void writeLine(IArguments args) throws LuaException { - this.checkOpen(); - String text = StringUtil.toString(args.get(0)); - try { - this.writer.write(text, 0, text.length()); - this.writer.newLine(); - } catch (IOException e) { - throw new LuaException(e.getMessage()); + public final void writeLine( IArguments args ) throws LuaException + { + checkOpen(); + String text = StringUtil.toString( args.get( 0 ) ); + try + { + writer.write( text, 0, text.length() ); + writer.newLine(); + } + catch( IOException e ) + { + throw new LuaException( e.getMessage() ); } } @@ -92,11 +87,30 @@ public class EncodedWritableHandle extends HandleGeneric { * @throws LuaException If the file has been closed. */ @LuaFunction - public final void flush() throws LuaException { - this.checkOpen(); - try { - this.writer.flush(); - } catch (IOException ignored) { + public final void flush() throws LuaException + { + checkOpen(); + try + { + writer.flush(); + } + catch( IOException ignored ) + { } } + + public static BufferedWriter openUtf8( WritableByteChannel channel ) + { + return open( channel, StandardCharsets.UTF_8 ); + } + + public static BufferedWriter open( WritableByteChannel channel, Charset charset ) + { + // Create a charset encoder with the same properties as StreamEncoder does for + // OutputStreams: namely, replace everything instead of erroring. + CharsetEncoder encoder = charset.newEncoder() + .onMalformedInput( CodingErrorAction.REPLACE ) + .onUnmappableCharacter( CodingErrorAction.REPLACE ); + return new BufferedWriter( Channels.newWriter( channel, encoder, -1 ) ); + } } diff --git a/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java b/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java index f7b25dffa..fc1954354 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java @@ -3,79 +3,38 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.apis.handles; -import java.io.Closeable; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.filesystem.TrackingCloseable; +import dan200.computercraft.shared.util.IoUtil; + +import javax.annotation.Nonnull; import java.io.IOException; import java.nio.channels.Channel; import java.nio.channels.SeekableByteChannel; import java.util.Optional; -import javax.annotation.Nonnull; +public abstract class HandleGeneric +{ + private TrackingCloseable closeable; -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.lua.LuaFunction; -import dan200.computercraft.shared.util.IoUtil; - -public abstract class HandleGeneric { - private Closeable closable; - private boolean open = true; - - protected HandleGeneric(@Nonnull Closeable closable) { - this.closable = closable; + protected HandleGeneric( @Nonnull TrackingCloseable closeable ) + { + this.closeable = closeable; } - /** - * Shared implementation for various file handle types. - * - * @param channel The channel to seek in - * @param whence The seeking mode. - * @param offset The offset to seek to. - * @return The new position of the file, or null if some error occurred. - * @throws LuaException If the arguments were invalid - * @see {@code file:seek} in the Lua manual. - */ - protected static Object[] handleSeek(SeekableByteChannel channel, Optional whence, Optional offset) throws LuaException { - long actualOffset = offset.orElse(0L); - try { - switch (whence.orElse("cur")) { - case "set": - channel.position(actualOffset); - break; - case "cur": - channel.position(channel.position() + actualOffset); - break; - case "end": - channel.position(channel.size() + actualOffset); - break; - default: - throw new LuaException("bad argument #1 to 'seek' (invalid option '" + whence + "'"); - } - - return new Object[] {channel.position()}; - } catch (IllegalArgumentException e) { - return new Object[] { - null, - "Position is negative" - }; - } catch (IOException e) { - return null; - } + protected void checkOpen() throws LuaException + { + TrackingCloseable closeable = this.closeable; + if( closeable == null || !closeable.isOpen() ) throw new LuaException( "attempt to use a closed file" ); } - protected static SeekableByteChannel asSeekable(Channel channel) { - if (!(channel instanceof SeekableByteChannel)) { - return null; - } - - SeekableByteChannel seekable = (SeekableByteChannel) channel; - try { - seekable.position(seekable.position()); - return seekable; - } catch (IOException | UnsupportedOperationException e) { - return null; - } + protected final void close() + { + IoUtil.closeQuietly( closeable ); + closeable = null; } /** @@ -85,22 +44,69 @@ public abstract class HandleGeneric { * * @throws LuaException If the file has already been closed. */ - @LuaFunction ("close") - public final void doClose() throws LuaException { - this.checkOpen(); - this.close(); + @LuaFunction( "close" ) + public final void doClose() throws LuaException + { + checkOpen(); + close(); } - protected void checkOpen() throws LuaException { - if (!this.open) { - throw new LuaException("attempt to use a closed file"); + + /** + * Shared implementation for various file handle types. + * + * @param channel The channel to seek in + * @param whence The seeking mode. + * @param offset The offset to seek to. + * @return The new position of the file, or null if some error occurred. + * @throws LuaException If the arguments were invalid + * @see {@code file:seek} in the Lua manual. + */ + protected static Object[] handleSeek( SeekableByteChannel channel, Optional whence, Optional offset ) throws LuaException + { + long actualOffset = offset.orElse( 0L ); + try + { + switch( whence.orElse( "cur" ) ) + { + case "set": + channel.position( actualOffset ); + break; + case "cur": + channel.position( channel.position() + actualOffset ); + break; + case "end": + channel.position( channel.size() + actualOffset ); + break; + default: + throw new LuaException( "bad argument #1 to 'seek' (invalid option '" + whence + "'" ); + } + + return new Object[] { channel.position() }; + } + catch( IllegalArgumentException e ) + { + return new Object[] { null, "Position is negative" }; + } + catch( IOException e ) + { + return null; } } - protected final void close() { - this.open = false; + protected static SeekableByteChannel asSeekable( Channel channel ) + { + if( !(channel instanceof SeekableByteChannel) ) return null; - IoUtil.closeQuietly(this.closable); - this.closable = null; + SeekableByteChannel seekable = (SeekableByteChannel) channel; + try + { + seekable.position( seekable.position() ); + return seekable; + } + catch( IOException | UnsupportedOperationException e ) + { + return null; + } } } diff --git a/src/main/java/dan200/computercraft/core/filesystem/ChannelWrapper.java b/src/main/java/dan200/computercraft/core/filesystem/ChannelWrapper.java index 0a2ebab45..f5de836bc 100644 --- a/src/main/java/dan200/computercraft/core/filesystem/ChannelWrapper.java +++ b/src/main/java/dan200/computercraft/core/filesystem/ChannelWrapper.java @@ -3,7 +3,6 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.filesystem; import java.io.Closeable; @@ -13,30 +12,38 @@ import java.nio.channels.Channel; /** * Wraps some closeable object such as a buffered writer, and the underlying stream. * - * When flushing a buffer before closing, some implementations will not close the buffer if an exception is thrown this causes us to release the channel, - * but not actually close it. This wrapper will attempt to close the wrapper (and so hopefully flush the channel), and then close the underlying channel. + * When flushing a buffer before closing, some implementations will not close the buffer if an exception is thrown + * this causes us to release the channel, but not actually close it. This wrapper will attempt to close the wrapper (and + * so hopefully flush the channel), and then close the underlying channel. * * @param The type of the closeable object to write. */ -class ChannelWrapper implements Closeable { +class ChannelWrapper implements Closeable +{ private final T wrapper; private final Channel channel; - ChannelWrapper(T wrapper, Channel channel) { + ChannelWrapper( T wrapper, Channel channel ) + { this.wrapper = wrapper; this.channel = channel; } @Override - public void close() throws IOException { - try { - this.wrapper.close(); - } finally { - this.channel.close(); + public void close() throws IOException + { + try + { + wrapper.close(); + } + finally + { + channel.close(); } } - public T get() { - return this.wrapper; + T get() + { + return wrapper; } } diff --git a/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java b/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java index fd66c09ae..cfbc9d92e 100644 --- a/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java +++ b/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java @@ -3,9 +3,16 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.filesystem; +import com.google.common.io.ByteStreams; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.filesystem.IFileSystem; +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.filesystem.IWritableMount; +import dan200.computercraft.shared.util.IoUtil; + +import javax.annotation.Nonnull; import java.io.Closeable; import java.io.IOException; import java.lang.ref.Reference; @@ -16,506 +23,598 @@ import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.file.AccessDeniedException; import java.nio.file.attribute.BasicFileAttributes; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.OptionalLong; -import java.util.Stack; +import java.util.*; import java.util.function.Function; import java.util.regex.Pattern; -import javax.annotation.Nonnull; - -import com.google.common.io.ByteStreams; -import dan200.computercraft.ComputerCraft; -import dan200.computercraft.api.filesystem.IFileSystem; -import dan200.computercraft.api.filesystem.IMount; -import dan200.computercraft.api.filesystem.IWritableMount; -import dan200.computercraft.shared.util.IoUtil; - -public class FileSystem { +public class FileSystem +{ /** * Maximum depth that {@link #copyRecursive(String, MountWrapper, String, MountWrapper, int)} will descend into. * - * This is a pretty arbitrary value, though hopefully it is large enough that it'll never be normally hit. This exists to prevent it overflowing if it - * ever gets into an infinite loop. + * This is a pretty arbitrary value, though hopefully it is large enough that it'll never be normally hit. This + * exists to prevent it overflowing if it ever gets into an infinite loop. */ private static final int MAX_COPY_DEPTH = 128; - private static final Pattern threeDotsPattern = Pattern.compile("^\\.{3,}$"); - private final FileSystemWrapperMount m_wrapper = new FileSystemWrapperMount(this); - private final Map mounts = new HashMap<>(); - private final HashMap>, ChannelWrapper> m_openFiles = new HashMap<>(); - private final ReferenceQueue> m_openFileQueue = new ReferenceQueue<>(); - public FileSystem(String rootLabel, IMount rootMount) throws FileSystemException { - this.mount(rootLabel, "", rootMount); + private final FileSystemWrapperMount wrapper = new FileSystemWrapperMount( this ); + private final Map mounts = new HashMap<>(); + + private final HashMap>, ChannelWrapper> openFiles = new HashMap<>(); + private final ReferenceQueue> openFileQueue = new ReferenceQueue<>(); + + public FileSystem( String rootLabel, IMount rootMount ) throws FileSystemException + { + mount( rootLabel, "", rootMount ); } - public synchronized void mount(String label, String location, IMount mount) throws FileSystemException { - if (mount == null) { + public FileSystem( String rootLabel, IWritableMount rootMount ) throws FileSystemException + { + mountWritable( rootLabel, "", rootMount ); + } + + public void close() + { + // Close all dangling open files + synchronized( openFiles ) + { + for( Closeable file : openFiles.values() ) IoUtil.closeQuietly( file ); + openFiles.clear(); + while( openFileQueue.poll() != null ) ; + } + } + + public synchronized void mount( String label, String location, IMount mount ) throws FileSystemException + { + if( mount == null ) throw new NullPointerException(); + location = sanitizePath( location ); + if( location.contains( ".." ) ) throw new FileSystemException( "Cannot mount below the root" ); + mount( new MountWrapper( label, location, mount ) ); + } + + public synchronized void mountWritable( String label, String location, IWritableMount mount ) throws FileSystemException + { + if( mount == null ) + { throw new NullPointerException(); } - location = sanitizePath(location); - if (location.contains("..")) { - throw new FileSystemException("Cannot mount below the root"); + location = sanitizePath( location ); + if( location.contains( ".." ) ) + { + throw new FileSystemException( "Cannot mount below the root" ); } - this.mount(new MountWrapper(label, location, mount)); + mount( new MountWrapper( label, location, mount ) ); } - private static String sanitizePath(String path) { - return sanitizePath(path, false); - } - - private synchronized void mount(MountWrapper wrapper) { + private synchronized void mount( MountWrapper wrapper ) + { String location = wrapper.getLocation(); - this.mounts.remove(location); - this.mounts.put(location, wrapper); + mounts.remove( location ); + mounts.put( location, wrapper ); } - public static String sanitizePath(String path, boolean allowWildcards) { + public synchronized void unmount( String path ) + { + MountWrapper mount = mounts.remove( sanitizePath( path ) ); + if( mount == null ) return; + + cleanup(); + + // Close any files which belong to this mount - don't want people writing to a disk after it's been ejected! + // There's no point storing a Mount -> Wrapper[] map, as openFiles is small and unmount isn't called very + // often. + synchronized( openFiles ) + { + for( Iterator>> iterator = openFiles.keySet().iterator(); iterator.hasNext(); ) + { + WeakReference> reference = iterator.next(); + FileSystemWrapper wrapper = reference.get(); + if( wrapper == null ) continue; + + if( wrapper.mount == mount ) + { + wrapper.closeExternally(); + iterator.remove(); + } + } + } + } + + public String combine( String path, String childPath ) + { + path = sanitizePath( path, true ); + childPath = sanitizePath( childPath, true ); + + if( path.isEmpty() ) + { + return childPath; + } + else if( childPath.isEmpty() ) + { + return path; + } + else + { + return sanitizePath( path + '/' + childPath, true ); + } + } + + public static String getDirectory( String path ) + { + path = sanitizePath( path, true ); + if( path.isEmpty() ) + { + return ".."; + } + + int lastSlash = path.lastIndexOf( '/' ); + if( lastSlash >= 0 ) + { + return path.substring( 0, lastSlash ); + } + else + { + return ""; + } + } + + public static String getName( String path ) + { + path = sanitizePath( path, true ); + if( path.isEmpty() ) return "root"; + + int lastSlash = path.lastIndexOf( '/' ); + return lastSlash >= 0 ? path.substring( lastSlash + 1 ) : path; + } + + public synchronized long getSize( String path ) throws FileSystemException + { + return getMount( sanitizePath( path ) ).getSize( sanitizePath( path ) ); + } + + public synchronized BasicFileAttributes getAttributes( String path ) throws FileSystemException + { + return getMount( sanitizePath( path ) ).getAttributes( sanitizePath( path ) ); + } + + public synchronized String[] list( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + + // Gets a list of the files in the mount + List list = new ArrayList<>(); + mount.list( path, list ); + + // Add any mounts that are mounted at this location + for( MountWrapper otherMount : mounts.values() ) + { + if( getDirectory( otherMount.getLocation() ).equals( path ) ) + { + list.add( getName( otherMount.getLocation() ) ); + } + } + + // Return list + String[] array = new String[list.size()]; + list.toArray( array ); + Arrays.sort( array ); + return array; + } + + private void findIn( String dir, List matches, Pattern wildPattern ) throws FileSystemException + { + String[] list = list( dir ); + for( String entry : list ) + { + String entryPath = dir.isEmpty() ? entry : dir + "/" + entry; + if( wildPattern.matcher( entryPath ).matches() ) + { + matches.add( entryPath ); + } + if( isDir( entryPath ) ) + { + findIn( entryPath, matches, wildPattern ); + } + } + } + + public synchronized String[] find( String wildPath ) throws FileSystemException + { + // Match all the files on the system + wildPath = sanitizePath( wildPath, true ); + + // If we don't have a wildcard at all just check the file exists + int starIndex = wildPath.indexOf( '*' ); + if( starIndex == -1 ) + { + return exists( wildPath ) ? new String[] { wildPath } : new String[0]; + } + + // Find the all non-wildcarded directories. For instance foo/bar/baz* -> foo/bar + int prevDir = wildPath.substring( 0, starIndex ).lastIndexOf( '/' ); + String startDir = prevDir == -1 ? "" : wildPath.substring( 0, prevDir ); + + // If this isn't a directory then just abort + if( !isDir( startDir ) ) return new String[0]; + + // Scan as normal, starting from this directory + Pattern wildPattern = Pattern.compile( "^\\Q" + wildPath.replaceAll( "\\*", "\\\\E[^\\\\/]*\\\\Q" ) + "\\E$" ); + List matches = new ArrayList<>(); + findIn( startDir, matches, wildPattern ); + + // Return matches + String[] array = new String[matches.size()]; + matches.toArray( array ); + return array; + } + + public synchronized boolean exists( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + return mount.exists( path ); + } + + public synchronized boolean isDir( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + return mount.isDirectory( path ); + } + + public synchronized boolean isReadOnly( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + return mount.isReadOnly( path ); + } + + public synchronized String getMountLabel( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + return mount.getLabel(); + } + + public synchronized void makeDir( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + mount.makeDirectory( path ); + } + + public synchronized void delete( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + mount.delete( path ); + } + + public synchronized void move( String sourcePath, String destPath ) throws FileSystemException + { + sourcePath = sanitizePath( sourcePath ); + destPath = sanitizePath( destPath ); + if( isReadOnly( sourcePath ) || isReadOnly( destPath ) ) + { + throw new FileSystemException( "Access denied" ); + } + if( !exists( sourcePath ) ) + { + throw new FileSystemException( "No such file" ); + } + if( exists( destPath ) ) + { + throw new FileSystemException( "File exists" ); + } + if( contains( sourcePath, destPath ) ) + { + throw new FileSystemException( "Can't move a directory inside itself" ); + } + copy( sourcePath, destPath ); + delete( sourcePath ); + } + + public synchronized void copy( String sourcePath, String destPath ) throws FileSystemException + { + sourcePath = sanitizePath( sourcePath ); + destPath = sanitizePath( destPath ); + if( isReadOnly( destPath ) ) + { + throw new FileSystemException( "/" + destPath + ": Access denied" ); + } + if( !exists( sourcePath ) ) + { + throw new FileSystemException( "/" + sourcePath + ": No such file" ); + } + if( exists( destPath ) ) + { + throw new FileSystemException( "/" + destPath + ": File exists" ); + } + if( contains( sourcePath, destPath ) ) + { + throw new FileSystemException( "/" + sourcePath + ": Can't copy a directory inside itself" ); + } + copyRecursive( sourcePath, getMount( sourcePath ), destPath, getMount( destPath ), 0 ); + } + + private synchronized void copyRecursive( String sourcePath, MountWrapper sourceMount, String destinationPath, MountWrapper destinationMount, int depth ) throws FileSystemException + { + if( !sourceMount.exists( sourcePath ) ) return; + if( depth >= MAX_COPY_DEPTH ) throw new FileSystemException( "Too many directories to copy" ); + + if( sourceMount.isDirectory( sourcePath ) ) + { + // Copy a directory: + // Make the new directory + destinationMount.makeDirectory( destinationPath ); + + // Copy the source contents into it + List sourceChildren = new ArrayList<>(); + sourceMount.list( sourcePath, sourceChildren ); + for( String child : sourceChildren ) + { + copyRecursive( + combine( sourcePath, child ), sourceMount, + combine( destinationPath, child ), destinationMount, + depth + 1 + ); + } + } + else + { + // Copy a file: + try( ReadableByteChannel source = sourceMount.openForRead( sourcePath ); + WritableByteChannel destination = destinationMount.openForWrite( destinationPath ) ) + { + // Copy bytes as fast as we can + ByteStreams.copy( source, destination ); + } + catch( AccessDeniedException e ) + { + throw new FileSystemException( "Access denied" ); + } + catch( IOException e ) + { + throw new FileSystemException( e.getMessage() ); + } + } + } + + private void cleanup() + { + synchronized( openFiles ) + { + Reference ref; + while( (ref = openFileQueue.poll()) != null ) + { + IoUtil.closeQuietly( openFiles.remove( ref ) ); + } + } + } + + private synchronized FileSystemWrapper openFile( @Nonnull MountWrapper mount, @Nonnull Channel channel, @Nonnull T file ) throws FileSystemException + { + synchronized( openFiles ) + { + if( ComputerCraft.maximumFilesOpen > 0 && + openFiles.size() >= ComputerCraft.maximumFilesOpen ) + { + IoUtil.closeQuietly( file ); + IoUtil.closeQuietly( channel ); + throw new FileSystemException( "Too many files already open" ); + } + + ChannelWrapper channelWrapper = new ChannelWrapper<>( file, channel ); + FileSystemWrapper fsWrapper = new FileSystemWrapper<>( this, mount, channelWrapper, openFileQueue ); + openFiles.put( fsWrapper.self, channelWrapper ); + return fsWrapper; + } + } + + void removeFile( FileSystemWrapper handle ) + { + synchronized( openFiles ) + { + openFiles.remove( handle.self ); + } + } + + public synchronized FileSystemWrapper openForRead( String path, Function open ) throws FileSystemException + { + cleanup(); + + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + ReadableByteChannel channel = mount.openForRead( path ); + return channel != null ? openFile( mount, channel, open.apply( channel ) ) : null; + } + + public synchronized FileSystemWrapper openForWrite( String path, boolean append, Function open ) throws FileSystemException + { + cleanup(); + + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + WritableByteChannel channel = append ? mount.openForAppend( path ) : mount.openForWrite( path ); + return channel != null ? openFile( mount, channel, open.apply( channel ) ) : null; + } + + public synchronized long getFreeSpace( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + return mount.getFreeSpace(); + } + + @Nonnull + public synchronized OptionalLong getCapacity( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + return mount.getCapacity(); + } + + private synchronized MountWrapper getMount( String path ) throws FileSystemException + { + // Return the deepest mount that contains a given path + Iterator it = mounts.values().iterator(); + MountWrapper match = null; + int matchLength = 999; + while( it.hasNext() ) + { + MountWrapper mount = it.next(); + if( contains( mount.getLocation(), path ) ) + { + int len = toLocal( path, mount.getLocation() ).length(); + if( match == null || len < matchLength ) + { + match = mount; + matchLength = len; + } + } + } + if( match == null ) + { + throw new FileSystemException( "/" + path + ": Invalid Path" ); + } + return match; + } + + public IFileSystem getMountWrapper() + { + return wrapper; + } + + private static String sanitizePath( String path ) + { + return sanitizePath( path, false ); + } + + private static final Pattern threeDotsPattern = Pattern.compile( "^\\.{3,}$" ); + + public static String sanitizePath( String path, boolean allowWildcards ) + { // Allow windowsy slashes - path = path.replace('\\', '/'); + path = path.replace( '\\', '/' ); // Clean the path or illegal characters. final char[] specialChars = new char[] { - '"', - ':', - '<', - '>', - '?', - '|', - // Sorted by ascii value (important) + '"', ':', '<', '>', '?', '|', // Sorted by ascii value (important) }; StringBuilder cleanName = new StringBuilder(); - for (int i = 0; i < path.length(); i++) { - char c = path.charAt(i); - if (c >= 32 && Arrays.binarySearch(specialChars, c) < 0 && (allowWildcards || c != '*')) { - cleanName.append(c); + for( int i = 0; i < path.length(); i++ ) + { + char c = path.charAt( i ); + if( c >= 32 && Arrays.binarySearch( specialChars, c ) < 0 && (allowWildcards || c != '*') ) + { + cleanName.append( c ); } } path = cleanName.toString(); // Collapse the string into its component parts, removing ..'s - String[] parts = path.split("/"); + String[] parts = path.split( "/" ); Stack outputParts = new Stack<>(); - for (String part : parts) { - if (part.isEmpty() || part.equals(".") || threeDotsPattern.matcher(part) - .matches()) { + for( String part : parts ) + { + if( part.isEmpty() || part.equals( "." ) || threeDotsPattern.matcher( part ).matches() ) + { // . is redundant // ... and more are treated as . continue; } - if (part.equals("..")) { + if( part.equals( ".." ) ) + { // .. can cancel out the last folder entered - if (!outputParts.empty()) { + if( !outputParts.empty() ) + { String top = outputParts.peek(); - if (!top.equals("..")) { + if( !top.equals( ".." ) ) + { outputParts.pop(); - } else { - outputParts.push(".."); } - } else { - outputParts.push(".."); + else + { + outputParts.push( ".." ); + } } - } else if (part.length() >= 255) { + else + { + outputParts.push( ".." ); + } + } + else if( part.length() >= 255 ) + { // If part length > 255 and it is the last part - outputParts.push(part.substring(0, 255)); - } else { + outputParts.push( part.substring( 0, 255 ) ); + } + else + { // Anything else we add to the stack - outputParts.push(part); + outputParts.push( part ); } } // Recombine the output parts into a new string StringBuilder result = new StringBuilder(); Iterator it = outputParts.iterator(); - while (it.hasNext()) { + while( it.hasNext() ) + { String part = it.next(); - result.append(part); - if (it.hasNext()) { - result.append('/'); + result.append( part ); + if( it.hasNext() ) + { + result.append( '/' ); } } return result.toString(); } - public FileSystem(String rootLabel, IWritableMount rootMount) throws FileSystemException { - this.mountWritable(rootLabel, "", rootMount); - } + public static boolean contains( String pathA, String pathB ) + { + pathA = sanitizePath( pathA ).toLowerCase( Locale.ROOT ); + pathB = sanitizePath( pathB ).toLowerCase( Locale.ROOT ); - public synchronized void mountWritable(String label, String location, IWritableMount mount) throws FileSystemException { - if (mount == null) { - throw new NullPointerException(); - } - location = sanitizePath(location); - if (location.contains("..")) { - throw new FileSystemException("Cannot mount below the root"); - } - this.mount(new MountWrapper(label, location, mount)); - } - - public static String getDirectory(String path) { - path = sanitizePath(path, true); - if (path.isEmpty()) { - return ".."; - } - - int lastSlash = path.lastIndexOf('/'); - if (lastSlash >= 0) { - return path.substring(0, lastSlash); - } else { - return ""; - } - } - - public static String getName(String path) { - path = sanitizePath(path, true); - if (path.isEmpty()) { - return "root"; - } - - int lastSlash = path.lastIndexOf('/'); - return lastSlash >= 0 ? path.substring(lastSlash + 1) : path; - } - - public static boolean contains(String pathA, String pathB) { - pathA = sanitizePath(pathA).toLowerCase(Locale.ROOT); - pathB = sanitizePath(pathB).toLowerCase(Locale.ROOT); - - if (pathB.equals("..")) { + if( pathB.equals( ".." ) ) + { return false; - } else if (pathB.startsWith("../")) { + } + else if( pathB.startsWith( "../" ) ) + { return false; - } else if (pathB.equals(pathA)) { + } + else if( pathB.equals( pathA ) ) + { return true; - } else if (pathA.isEmpty()) { + } + else if( pathA.isEmpty() ) + { return true; - } else { - return pathB.startsWith(pathA + "/"); + } + else + { + return pathB.startsWith( pathA + "/" ); } } - public static String toLocal(String path, String location) { - path = sanitizePath(path); - location = sanitizePath(location); + public static String toLocal( String path, String location ) + { + path = sanitizePath( path ); + location = sanitizePath( location ); - assert contains(location, path); - String local = path.substring(location.length()); - if (local.startsWith("/")) { - return local.substring(1); - } else { + assert contains( location, path ); + String local = path.substring( location.length() ); + if( local.startsWith( "/" ) ) + { + return local.substring( 1 ); + } + else + { return local; } } - - public void close() { - // Close all dangling open files - synchronized (this.m_openFiles) { - for (Closeable file : this.m_openFiles.values()) { - IoUtil.closeQuietly(file); - } - this.m_openFiles.clear(); - while (this.m_openFileQueue.poll() != null) { - } - } - } - - public synchronized void unmount(String path) { - this.mounts.remove(sanitizePath(path)); - } - - public String combine( String path, String childPath ) { - path = sanitizePath(path, true); - childPath = sanitizePath(childPath, true); - - if (path.isEmpty()) { - return childPath; - } else if (childPath.isEmpty()) { - return path; - } else { - return sanitizePath(path + '/' + childPath, true); - } - } - - public synchronized long getSize(String path) throws FileSystemException { - return this.getMount(sanitizePath(path)).getSize(sanitizePath(path)); - } - - public synchronized BasicFileAttributes getAttributes(String path) throws FileSystemException { - return this.getMount(sanitizePath(path)).getAttributes(sanitizePath(path)); - } - - public synchronized String[] list(String path) throws FileSystemException { - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - - // Gets a list of the files in the mount - List list = new ArrayList<>(); - mount.list(path, list); - - // Add any mounts that are mounted at this location - for (MountWrapper otherMount : this.mounts.values()) { - if (getDirectory(otherMount.getLocation()).equals(path)) { - list.add(getName(otherMount.getLocation())); - } - } - - // Return list - String[] array = new String[list.size()]; - list.toArray(array); - Arrays.sort(array); - return array; - } - - private void findIn(String dir, List matches, Pattern wildPattern) throws FileSystemException { - String[] list = this.list(dir); - for (String entry : list) { - String entryPath = dir.isEmpty() ? entry : dir + "/" + entry; - if (wildPattern.matcher(entryPath) - .matches()) { - matches.add(entryPath); - } - if (this.isDir(entryPath)) { - this.findIn(entryPath, matches, wildPattern); - } - } - } - - public synchronized String[] find(String wildPath) throws FileSystemException { - // Match all the files on the system - wildPath = sanitizePath(wildPath, true); - - // If we don't have a wildcard at all just check the file exists - int starIndex = wildPath.indexOf('*'); - if (starIndex == -1) { - return this.exists(wildPath) ? new String[] {wildPath} : new String[0]; - } - - // Find the all non-wildcarded directories. For instance foo/bar/baz* -> foo/bar - int prevDir = wildPath.substring(0, starIndex) - .lastIndexOf('/'); - String startDir = prevDir == -1 ? "" : wildPath.substring(0, prevDir); - - // If this isn't a directory then just abort - if (!this.isDir(startDir)) { - return new String[0]; - } - - // Scan as normal, starting from this directory - Pattern wildPattern = Pattern.compile("^\\Q" + wildPath.replaceAll("\\*", "\\\\E[^\\\\/]*\\\\Q") + "\\E$"); - List matches = new ArrayList<>(); - this.findIn(startDir, matches, wildPattern); - - // Return matches - String[] array = new String[matches.size()]; - matches.toArray(array); - return array; - } - - public synchronized boolean exists(String path) throws FileSystemException { - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - return mount.exists(path); - } - - public synchronized boolean isDir(String path) throws FileSystemException { - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - return mount.isDirectory(path); - } - - public synchronized boolean isReadOnly(String path) throws FileSystemException { - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - return mount.isReadOnly(path); - } - - public synchronized String getMountLabel(String path) throws FileSystemException { - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - return mount.getLabel(); - } - - public synchronized void makeDir(String path) throws FileSystemException { - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - mount.makeDirectory(path); - } - - public synchronized void delete(String path) throws FileSystemException { - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - mount.delete(path); - } - - public synchronized void move(String sourcePath, String destPath) throws FileSystemException { - sourcePath = sanitizePath(sourcePath); - destPath = sanitizePath(destPath); - if (this.isReadOnly(sourcePath) || this.isReadOnly(destPath)) { - throw new FileSystemException("Access denied"); - } - if (!this.exists(sourcePath)) { - throw new FileSystemException("No such file"); - } - if (this.exists(destPath)) { - throw new FileSystemException("File exists"); - } - if (contains(sourcePath, destPath)) { - throw new FileSystemException("Can't move a directory inside itself"); - } - this.copy(sourcePath, destPath); - this.delete(sourcePath); - } - - public synchronized void copy(String sourcePath, String destPath) throws FileSystemException { - sourcePath = sanitizePath(sourcePath); - destPath = sanitizePath(destPath); - if (this.isReadOnly(destPath)) { - throw new FileSystemException("/" + destPath + ": Access denied"); - } - if (!this.exists(sourcePath)) { - throw new FileSystemException("/" + sourcePath + ": No such file"); - } - if (this.exists(destPath)) { - throw new FileSystemException("/" + destPath + ": File exists"); - } - if (contains(sourcePath, destPath)) { - throw new FileSystemException("/" + sourcePath + ": Can't copy a directory inside itself"); - } - this.copyRecursive(sourcePath, this.getMount(sourcePath), destPath, this.getMount(destPath), 0); - } - - private synchronized void copyRecursive(String sourcePath, MountWrapper sourceMount, String destinationPath, MountWrapper destinationMount, - int depth) throws FileSystemException { - if (!sourceMount.exists(sourcePath)) { - return; - } - if (depth >= MAX_COPY_DEPTH) { - throw new FileSystemException("Too many directories to copy"); - } - - if (sourceMount.isDirectory(sourcePath)) { - // Copy a directory: - // Make the new directory - destinationMount.makeDirectory(destinationPath); - - // Copy the source contents into it - List sourceChildren = new ArrayList<>(); - sourceMount.list(sourcePath, sourceChildren); - for (String child : sourceChildren) { - this.copyRecursive(this.combine(sourcePath, child), sourceMount, this.combine(destinationPath, child), destinationMount, depth + 1); - } - } else { - // Copy a file: - try (ReadableByteChannel source = sourceMount.openForRead(sourcePath); WritableByteChannel destination = destinationMount.openForWrite( - destinationPath)) { - // Copy bytes as fast as we can - ByteStreams.copy(source, destination); - } catch (AccessDeniedException e) { - throw new FileSystemException("Access denied"); - } catch (IOException e) { - throw new FileSystemException(e.getMessage()); - } - } - } - - private void cleanup() { - synchronized (this.m_openFiles) { - Reference ref; - while ((ref = this.m_openFileQueue.poll()) != null) { - IoUtil.closeQuietly(this.m_openFiles.remove(ref)); - } - } - } - - private synchronized FileSystemWrapper openFile(@Nonnull Channel channel, @Nonnull T file) throws FileSystemException { - synchronized (this.m_openFiles) { - if (ComputerCraft.maximumFilesOpen > 0 && this.m_openFiles.size() >= ComputerCraft.maximumFilesOpen) { - IoUtil.closeQuietly(file); - IoUtil.closeQuietly(channel); - throw new FileSystemException("Too many files already open"); - } - - ChannelWrapper channelWrapper = new ChannelWrapper<>(file, channel); - FileSystemWrapper fsWrapper = new FileSystemWrapper<>(this, channelWrapper, this.m_openFileQueue); - this.m_openFiles.put(fsWrapper.self, channelWrapper); - return fsWrapper; - } - } - - synchronized void removeFile(FileSystemWrapper handle) { - synchronized (this.m_openFiles) { - this.m_openFiles.remove(handle.self); - } - } - - public synchronized FileSystemWrapper openForRead(String path, Function open) throws FileSystemException { - this.cleanup(); - - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - ReadableByteChannel channel = mount.openForRead(path); - if (channel != null) { - return this.openFile(channel, open.apply(channel)); - } - return null; - } - - public synchronized FileSystemWrapper openForWrite(String path, boolean append, Function open) throws FileSystemException { - this.cleanup(); - - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - WritableByteChannel channel = append ? mount.openForAppend(path) : mount.openForWrite(path); - if (channel != null) { - return this.openFile(channel, open.apply(channel)); - } - return null; - } - - public synchronized long getFreeSpace(String path) throws FileSystemException { - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - return mount.getFreeSpace(); - } - - @Nonnull - public synchronized OptionalLong getCapacity(String path) throws FileSystemException { - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - return mount.getCapacity(); - } - - private synchronized MountWrapper getMount(String path) throws FileSystemException { - // Return the deepest mount that contains a given path - Iterator it = this.mounts.values() - .iterator(); - MountWrapper match = null; - int matchLength = 999; - while (it.hasNext()) { - MountWrapper mount = it.next(); - if (contains(mount.getLocation(), path)) { - int len = toLocal(path, mount.getLocation()).length(); - if (match == null || len < matchLength) { - match = mount; - matchLength = len; - } - } - } - if (match == null) { - throw new FileSystemException("/" + path + ": Invalid Path"); - } - return match; - } - - public IFileSystem getMountWrapper() { - return this.m_wrapper; - } } diff --git a/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java b/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java index e3a359478..a65e04326 100644 --- a/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java +++ b/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java @@ -3,48 +3,68 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.filesystem; +import dan200.computercraft.shared.util.IoUtil; + +import javax.annotation.Nonnull; import java.io.Closeable; import java.io.IOException; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; -import javax.annotation.Nonnull; - /** * An alternative closeable implementation that will free up resources in the filesystem. * - * The {@link FileSystem} maps weak references of this to its underlying object. If the wrapper has been disposed of (say, the Lua object referencing it has - * gone), then the wrapped object will be closed by the filesystem. + * The {@link FileSystem} maps weak references of this to its underlying object. If the wrapper has been disposed of + * (say, the Lua object referencing it has gone), then the wrapped object will be closed by the filesystem. * * Closing this will stop the filesystem tracking it, reducing the current descriptor count. * - * In an ideal world, we'd just wrap the closeable. However, as we do some {@code instanceof} checks on the stream, it's not really possible as it'd require - * numerous instances. + * In an ideal world, we'd just wrap the closeable. However, as we do some {@code instanceof} checks + * on the stream, it's not really possible as it'd require numerous instances. * * @param The type of writer or channel to wrap. */ -public class FileSystemWrapper implements Closeable { - final WeakReference> self; +public class FileSystemWrapper implements TrackingCloseable +{ private final FileSystem fileSystem; + final MountWrapper mount; private final ChannelWrapper closeable; + final WeakReference> self; + private boolean isOpen = true; - FileSystemWrapper(FileSystem fileSystem, ChannelWrapper closeable, ReferenceQueue> queue) { + FileSystemWrapper( FileSystem fileSystem, MountWrapper mount, ChannelWrapper closeable, ReferenceQueue> queue ) + { this.fileSystem = fileSystem; + this.mount = mount; this.closeable = closeable; - this.self = new WeakReference<>(this, queue); + self = new WeakReference<>( this, queue ); } @Override - public void close() throws IOException { - this.fileSystem.removeFile(this); - this.closeable.close(); + public void close() throws IOException + { + isOpen = false; + fileSystem.removeFile( this ); + closeable.close(); + } + + void closeExternally() + { + isOpen = false; + IoUtil.closeQuietly( closeable ); + } + + @Override + public boolean isOpen() + { + return isOpen; } @Nonnull - public T get() { - return this.closeable.get(); + public T get() + { + return closeable.get(); } } diff --git a/src/main/java/dan200/computercraft/core/filesystem/TrackingCloseable.java b/src/main/java/dan200/computercraft/core/filesystem/TrackingCloseable.java new file mode 100644 index 000000000..19ffc978f --- /dev/null +++ b/src/main/java/dan200/computercraft/core/filesystem/TrackingCloseable.java @@ -0,0 +1,44 @@ +/* + * 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.filesystem; + +import java.io.Closeable; +import java.io.IOException; + +/** + * A {@link Closeable} which knows when it has been closed. + * + * This is a quick (though racey) way of providing more friendly (and more similar to Lua) + * error messages to the user. + */ +public interface TrackingCloseable extends Closeable +{ + boolean isOpen(); + + class Impl implements TrackingCloseable + { + private final Closeable object; + private boolean isOpen = true; + + public Impl( Closeable object ) + { + this.object = object; + } + + @Override + public boolean isOpen() + { + return isOpen; + } + + @Override + public void close() throws IOException + { + isOpen = false; + object.close(); + } + } +} diff --git a/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java b/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java index 17855cf4a..6d4e52278 100644 --- a/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java +++ b/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java @@ -18,10 +18,19 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; public class FileSystemTest { private static final File ROOT = new File( "test-files/filesystem" ); + private static final long CAPACITY = 1000000; + + private static FileSystem mkFs() throws FileSystemException + { + IWritableMount writableMount = new FileMount( ROOT, CAPACITY ); + return new FileSystem( "hdd", writableMount ); + + } /** * Ensures writing a file truncates it. @@ -33,8 +42,7 @@ public class FileSystemTest @Test public void testWriteTruncates() throws FileSystemException, LuaException, IOException { - IWritableMount writableMount = new FileMount( ROOT, 1000000 ); - FileSystem fs = new FileSystem( "hdd", writableMount ); + FileSystem fs = mkFs(); { FileSystemWrapper writer = fs.openForWrite( "out.txt", false, EncodedWritableHandle::openUtf8 ); @@ -54,4 +62,20 @@ public class FileSystemTest assertEquals( "Tiny line", Files.asCharSource( new File( ROOT, "out.txt" ), StandardCharsets.UTF_8 ).read() ); } + + @Test + public void testUnmountCloses() throws FileSystemException + { + FileSystem fs = mkFs(); + IWritableMount mount = new FileMount( new File( ROOT, "child" ), CAPACITY ); + fs.mountWritable( "disk", "disk", mount ); + + FileSystemWrapper writer = fs.openForWrite( "disk/out.txt", false, EncodedWritableHandle::openUtf8 ); + ObjectWrapper wrapper = new ObjectWrapper( new EncodedWritableHandle( writer.get(), writer ) ); + + fs.unmount( "disk" ); + + LuaException err = assertThrows( LuaException.class, () -> wrapper.call( "write", "Tiny line" ) ); + assertEquals( "attempt to use a closed file", err.getMessage() ); + } } From 891dde43a9448f2cbe892cc9a4996b03234a9295 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Thu, 14 Jan 2021 09:11:08 +0000 Subject: [PATCH 12/34] Clarify the cc.strings.wrap docs a little Also make the example a bit more "useful". Hopefully this should clarify that the function returns a table rather than a single string. Closes #678. --- .../lua/rom/modules/main/cc/strings.lua | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/strings.lua b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/strings.lua index 49f5cd0d4..89d6e475c 100644 --- a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/strings.lua +++ b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/strings.lua @@ -5,17 +5,24 @@ local expect = require "cc.expect".expect ---- Wraps a block of text, so that each line fits within the given width. --- --- This may be useful if you want to wrap text before displaying it to a --- @{monitor} or @{printer} without using @{_G.print|print}. --- --- @tparam string text The string to wrap. --- @tparam[opt] number width The width to constrain to, defaults to the width of --- the terminal. --- --- @treturn { string... } The wrapped input string. --- @usage require "cc.strings".wrap("This is a long piece of text", 10) +--[[- Wraps a block of text, so that each line fits within the given width. + +This may be useful if you want to wrap text before displaying it to a +@{monitor} or @{printer} without using @{_G.print|print}. + +@tparam string text The string to wrap. +@tparam[opt] number width The width to constrain to, defaults to the width of +the terminal. +@treturn { string... } The wrapped input string as a list of lines. +@usage Wrap a string and write it to the terminal. + + term.clear() + local lines = require "cc.strings".wrap("This is a long piece of text", 10) + for i = 1, #lines do + term.setCursorPos(1, i) + term.write(lines[i]) + end +]] local function wrap(text, width) expect(1, text, "string") expect(2, width, "number", "nil") From 2f35bbb538dbec8ff51765a27c2af23ef869337b Mon Sep 17 00:00:00 2001 From: SquidDev Date: Thu, 14 Jan 2021 18:19:22 +0000 Subject: [PATCH 13/34] Add some initial documentation for events Credit to @BradyFromDiscord for writing these. See #640 and #565. Co-authored-by: Brady Ctrl) do not have any +corresponding character. The @{key} should be used if you want to listen to key presses themselves. + +## Return values +1. @{string}: The event name. +2. @{string}: The string representing the character that was pressed. + + +## Example +Prints each character the user presses: +```lua +while true do + local event, character = os.pullEvent("char") + print(character .. " was pressed.") +end +``` diff --git a/doc/events/key.md b/doc/events/key.md new file mode 100644 index 000000000..839dc553f --- /dev/null +++ b/doc/events/key.md @@ -0,0 +1,26 @@ +--- +module: [kind=event] key +--- + +This event is fired when any key is pressed while the terminal is focused. + +This event returns a numerical "key code" (for instance, F1 is 290). This value may vary between versions and +so it is recommended to use the constants in the @{keys} API rather than hard coding numeric values. + +If the button pressed represented a printable character, then the @{key} event will be followed immediately by a @{char} +event. If you are consuming text input, use a @{char} event instead! + +## Return values +1. [`string`]: The event name. +2. [`number`]: The numerical key value of the key pressed. +3. [`boolean`]: Whether the key event was generated while holding the key (@{true}), rather than pressing it the first time (@{false}). + +## Example +Prints each key when the user presses it, and if the key is being held. + +```lua +while true do + local event, key, is_held = os.pullEvent("key") + print(("%s held=%b"):format(keys.getName(key), is_held)) +end +``` diff --git a/doc/events/key_up.md b/doc/events/key_up.md new file mode 100644 index 000000000..e957cae6b --- /dev/null +++ b/doc/events/key_up.md @@ -0,0 +1,24 @@ +--- +module: [kind=event] key_up +see: keys For a lookup table of the given keys. +--- + +Fired whenever a key is released (or the terminal is closed while a key was being pressed). + +This event returns a numerical "key code" (for instance, F1 is 290). This value may vary between versions and +so it is recommended to use the constants in the @{keys} API rather than hard coding numeric values. + +## Return values +1. @{string}: The event name. +2. @{number}: The numerical key value of the key pressed. + +## Example +Prints each key released on the keyboard whenever a @{key_up} event is fired. + +```lua +while true do + local event, key = os.pullEvent("key_up") + local name = keys.getName(key) or "unknown key" + print(name .. " was released.") +end +``` diff --git a/doc/events/mouse_click.md b/doc/events/mouse_click.md new file mode 100644 index 000000000..83d371260 --- /dev/null +++ b/doc/events/mouse_click.md @@ -0,0 +1,34 @@ +--- +module: [kind=event] mouse_click +--- + +This event is fired when the terminal is clicked with a mouse. This event is only fired on advanced computers (including +advanced turtles and pocket computers). + +## Return values +1. @{string}: The event name. +2. @{number}: The mouse button that was clicked. +3. @{number}: The X-coordinate of the click. +4. @{number}: The Y-coordinate of the click. + +## Mouse buttons +Several mouse events (@{mouse_click}, @{mouse_up}, @{mouse_scroll}) contain a "mouse button" code. This takes a +numerical value depending on which button on your mouse was last pressed when this event occurred. + + + + + + + +
Button codeMouse button
1Left button
2Middle button
3Right button
+ +## Example +Print the button and the coordinates whenever the mouse is clicked. + +```lua +while true do + local event, button, x, y = os.pullEvent("mouse_click") + print(("The mouse button %s was pressed at %d, %d"):format(button, x, y)) +end +``` diff --git a/doc/events/mouse_drag.md b/doc/events/mouse_drag.md new file mode 100644 index 000000000..6ccb3ee6d --- /dev/null +++ b/doc/events/mouse_drag.md @@ -0,0 +1,24 @@ +--- +module: [kind=event] mouse_drag +see: mouse_click For when a mouse button is initially pressed. +--- + +This event is fired every time the mouse is moved while a mouse button is being held. + +## Return values +1. @{string}: The event name. +2. @{number}: The [mouse button](mouse_click.html#Mouse_buttons) that is being pressed. +3. @{number}: The X-coordinate of the mouse. +4. @{number}: The Y-coordinate of the mouse. + +## Example +Print the button and the coordinates whenever the mouse is dragged. + +```lua +while true do + local event, button, x, y = os.pullEvent("mouse_drag") + print(("The mouse button %s was dragged at %d, %d"):format(button, x, y)) +end +``` + + diff --git a/doc/events/mouse_scroll.md b/doc/events/mouse_scroll.md new file mode 100644 index 000000000..6248220a5 --- /dev/null +++ b/doc/events/mouse_scroll.md @@ -0,0 +1,21 @@ +--- +module: [kind=event] mouse_scroll +--- + +This event is fired when a mouse wheel is scrolled in the terminal. + +## Return values +1. @{string}: The event name. +2. @{number}: The direction of the scroll. (-1 = up, 1 = down) +3. @{number}: The X-coordinate of the mouse when scrolling. +4. @{number}: The Y-coordinate of the mouse when scrolling. + +## Example +Prints the direction of each scroll, and the position of the mouse at the time. + +```lua +while true do + local event, dir, x, y = os.pullEvent("mouse_scroll") + print(("The mouse was scrolled in direction %s at %d, %d"):format(dir, x, y)) +end +``` diff --git a/doc/events/mouse_up.md b/doc/events/mouse_up.md new file mode 100644 index 000000000..f3b382387 --- /dev/null +++ b/doc/events/mouse_up.md @@ -0,0 +1,24 @@ +--- +module: [kind=event] mouse_up +--- + +This event is fired when a mouse button is released or a held mouse leaves the computer's terminal. + +## Return values +1. @{string}: The event name. +2. @{number}: The [mouse button](mouse_click.html#Mouse_buttons) that was released. +3. @{number}: The X-coordinate of the mouse. +4. @{number}: The Y-coordinate of the mouse. + +## Example +Prints the coordinates and button number whenever the mouse is released. + +```lua +while true do + local event, button, x, y = os.pullEvent("mouse_up") + print(("The mouse button %s was released at %d, %d"):format(button, x, y)) +end +``` + +[`string`]: string +[`number`]: number From b546a10bd62523f85f8399a226c1fdaaeaa0d03b Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Fri, 15 Jan 2021 15:32:11 +0000 Subject: [PATCH 14/34] Preserve registration order of upgrades Makes them display in a more reasonable order within JEI. Closes #647 (note, the title is an entirley separate issue)! --- patchwork.md | 9 +- .../computercraft/shared/PocketUpgrades.java | 89 ++++---- .../computercraft/shared/TurtleUpgrades.java | 195 +++++++++--------- 3 files changed, 150 insertions(+), 143 deletions(-) diff --git a/patchwork.md b/patchwork.md index f7be787fe..aead31369 100644 --- a/patchwork.md +++ b/patchwork.md @@ -611,4 +611,11 @@ Lua changes. Fix mounts being usable after a disk is ejected ``` -Reverted a lot of code style changes made by Zundrel, so the diffs are huge. \ No newline at end of file +Reverted a lot of code style changes made by Zundrel, so the diffs are huge. + +``` +b90611b4b4c176ec1c80df002cc4ac36aa0c4dc8 + +Preserve registration order of upgrades +``` +Again, a huge diff because of code style changes. diff --git a/src/main/java/dan200/computercraft/shared/PocketUpgrades.java b/src/main/java/dan200/computercraft/shared/PocketUpgrades.java index 072bc47f6..6704ac02b 100644 --- a/src/main/java/dan200/computercraft/shared/PocketUpgrades.java +++ b/src/main/java/dan200/computercraft/shared/PocketUpgrades.java @@ -3,61 +3,59 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.shared; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import dan200.computercraft.api.pocket.IPocketUpgrade; +import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenCustomHashMap; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Util; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.*; -import dan200.computercraft.ComputerCraft; -import dan200.computercraft.api.pocket.IPocketUpgrade; -import dan200.computercraft.shared.util.InventoryUtil; - -import net.minecraft.item.ItemStack; - -public final class PocketUpgrades { +public final class PocketUpgrades +{ private static final Map upgrades = new HashMap<>(); - private static final IdentityHashMap upgradeOwners = new IdentityHashMap<>(); + private static final Map upgradeOwners = new Object2ObjectLinkedOpenCustomHashMap<>( Util.identityHashStrategy() ); private PocketUpgrades() {} - public static synchronized void register(@Nonnull IPocketUpgrade upgrade) { - Objects.requireNonNull(upgrade, "upgrade cannot be null"); + public static synchronized void register( @Nonnull IPocketUpgrade upgrade ) + { + Objects.requireNonNull( upgrade, "upgrade cannot be null" ); - String id = upgrade.getUpgradeID() - .toString(); - IPocketUpgrade existing = upgrades.get(id); - if (existing != null) { - throw new IllegalStateException("Error registering '" + upgrade.getUnlocalisedAdjective() + " pocket computer'. UpgradeID '" + id + "' is " + - "already registered by '" + existing.getUnlocalisedAdjective() + " pocket computer'"); + String id = upgrade.getUpgradeID().toString(); + IPocketUpgrade existing = upgrades.get( id ); + if( existing != null ) + { + throw new IllegalStateException( "Error registering '" + upgrade.getUnlocalisedAdjective() + " pocket computer'. UpgradeID '" + id + "' is already registered by '" + existing.getUnlocalisedAdjective() + " pocket computer'" ); } - upgrades.put(id, upgrade); + upgrades.put( id, upgrade ); + + // Infer the mod id by the identifier of the upgrade. This is not how the forge api works, so it may break peripheral mods using the api. + // TODO: get the mod id of the mod that is currently being loaded. + ModContainer mc = FabricLoader.getInstance().getModContainer(upgrade.getUpgradeID().getNamespace()).orElseGet(null); + if( mc != null && mc.getMetadata().getId() != null ) upgradeOwners.put( upgrade, mc.getMetadata().getId() ); } - public static IPocketUpgrade get(String id) { + public static IPocketUpgrade get( String id ) + { // Fix a typo in the advanced modem upgrade's name. I'm sorry, I realise this is horrible. - if (id.equals("computercraft:advanved_modem")) { - id = "computercraft:advanced_modem"; - } + if( id.equals( "computercraft:advanved_modem" ) ) id = "computercraft:advanced_modem"; - return upgrades.get(id); + return upgrades.get( id ); } - public static IPocketUpgrade get(@Nonnull ItemStack stack) { - if (stack.isEmpty()) { - return null; - } + public static IPocketUpgrade get( @Nonnull ItemStack stack ) + { + if( stack.isEmpty() ) return null; - for (IPocketUpgrade upgrade : upgrades.values()) { + for( IPocketUpgrade upgrade : upgrades.values() ) + { ItemStack craftingStack = upgrade.getCraftingItem(); if( !craftingStack.isEmpty() && craftingStack.getItem() == stack.getItem() && upgrade.isItemSuitable( stack ) ) { @@ -69,19 +67,22 @@ public final class PocketUpgrades { } @Nullable - public static String getOwner(IPocketUpgrade upgrade) { - return upgradeOwners.get(upgrade); + public static String getOwner( IPocketUpgrade upgrade ) + { + return upgradeOwners.get( upgrade ); } - public static Iterable getVanillaUpgrades() { + public static Iterable getVanillaUpgrades() + { List vanilla = new ArrayList<>(); - vanilla.add(ComputerCraftRegistry.PocketUpgrades.wirelessModemNormal); - vanilla.add(ComputerCraftRegistry.PocketUpgrades.wirelessModemAdvanced); - vanilla.add(ComputerCraftRegistry.PocketUpgrades.speaker); + vanilla.add( ComputerCraftRegistry.PocketUpgrades.wirelessModemNormal ); + vanilla.add( ComputerCraftRegistry.PocketUpgrades.wirelessModemAdvanced ); + vanilla.add( ComputerCraftRegistry.PocketUpgrades.speaker ); return vanilla; } - public static Iterable getUpgrades() { - return Collections.unmodifiableCollection(upgrades.values()); + public static Iterable getUpgrades() + { + return Collections.unmodifiableCollection( upgrades.values() ); } -} +} \ No newline at end of file diff --git a/src/main/java/dan200/computercraft/shared/TurtleUpgrades.java b/src/main/java/dan200/computercraft/shared/TurtleUpgrades.java index 31d3686f7..5153367ca 100644 --- a/src/main/java/dan200/computercraft/shared/TurtleUpgrades.java +++ b/src/main/java/dan200/computercraft/shared/TurtleUpgrades.java @@ -3,9 +3,15 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.shared; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import net.minecraft.item.ItemStack; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Arrays; import java.util.HashMap; import java.util.IdentityHashMap; @@ -13,86 +19,72 @@ import java.util.Map; import java.util.Objects; import java.util.stream.Stream; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; +public final class TurtleUpgrades +{ + private static class Wrapper + { + final ITurtleUpgrade upgrade; + final String id; + final String modId; + boolean enabled; -import dan200.computercraft.ComputerCraft; -import dan200.computercraft.api.turtle.ITurtleUpgrade; -import dan200.computercraft.shared.computer.core.ComputerFamily; + Wrapper( ITurtleUpgrade upgrade ) + { + this.upgrade = upgrade; + this.id = upgrade.getUpgradeID() + .toString(); + // TODO This should be the mod id of the mod the peripheral comes from + this.modId = ComputerCraft.MOD_ID; + this.enabled = true; + } + } -import net.minecraft.item.ItemStack; + private static ITurtleUpgrade[] vanilla; -public final class TurtleUpgrades { private static final Map upgrades = new HashMap<>(); private static final IdentityHashMap wrappers = new IdentityHashMap<>(); - private static ITurtleUpgrade[] vanilla; private static boolean needsRebuild; + private TurtleUpgrades() {} - public static void register(@Nonnull ITurtleUpgrade upgrade) { - Objects.requireNonNull(upgrade, "upgrade cannot be null"); + public static void register( @Nonnull ITurtleUpgrade upgrade ) + { + Objects.requireNonNull( upgrade, "upgrade cannot be null" ); rebuild(); - Wrapper wrapper = new Wrapper(upgrade); + Wrapper wrapper = new Wrapper( upgrade ); String id = wrapper.id; - ITurtleUpgrade existing = upgrades.get(id); - if (existing != null) { - throw new IllegalStateException("Error registering '" + upgrade.getUnlocalisedAdjective() + " Turtle'. Upgrade ID '" + id + "' is already " + - "registered by '" + existing.getUnlocalisedAdjective() + " Turtle'"); + ITurtleUpgrade existing = upgrades.get( id ); + if( existing != null ) + { + throw new IllegalStateException( "Error registering '" + upgrade.getUnlocalisedAdjective() + " Turtle'. Upgrade ID '" + id + "' is already registered by '" + existing.getUnlocalisedAdjective() + " Turtle'" ); } - upgrades.put(id, upgrade); - wrappers.put(upgrade, wrapper); - } - - /** - * Rebuild the cache of turtle upgrades. This is done before querying the cache or registering new upgrades. - */ - private static void rebuild() { - if (!needsRebuild) { - return; - } - - upgrades.clear(); - for (Wrapper wrapper : wrappers.values()) { - if (!wrapper.enabled) { - continue; - } - - ITurtleUpgrade existing = upgrades.get(wrapper.id); - if (existing != null) { - ComputerCraft.log.error("Error registering '" + wrapper.upgrade.getUnlocalisedAdjective() + " Turtle'." + " Upgrade ID '" + wrapper.id + - "' is already registered by '" + existing.getUnlocalisedAdjective() + " Turtle'"); - continue; - } - - upgrades.put(wrapper.id, wrapper.upgrade); - } - - needsRebuild = false; + upgrades.put( id, upgrade ); + wrappers.put( upgrade, wrapper ); } @Nullable - public static ITurtleUpgrade get(String id) { + public static ITurtleUpgrade get( String id ) + { rebuild(); - return upgrades.get(id); + return upgrades.get( id ); } @Nullable - public static String getOwner(@Nonnull ITurtleUpgrade upgrade) { - Wrapper wrapper = wrappers.get(upgrade); + public static String getOwner( @Nonnull ITurtleUpgrade upgrade ) + { + Wrapper wrapper = wrappers.get( upgrade ); return wrapper != null ? wrapper.modId : null; } - public static ITurtleUpgrade get(@Nonnull ItemStack stack) { - if (stack.isEmpty()) { - return null; - } + public static ITurtleUpgrade get( @Nonnull ItemStack stack ) + { + if( stack.isEmpty() ) return null; - for (Wrapper wrapper : wrappers.values()) { - if (!wrapper.enabled) { - continue; - } + for( Wrapper wrapper : wrappers.values() ) + { + if( !wrapper.enabled ) continue; ItemStack craftingStack = wrapper.upgrade.getCraftingItem(); if( !craftingStack.isEmpty() && craftingStack.getItem() == stack.getItem() && wrapper.upgrade.isItemSuitable( stack ) ) @@ -104,8 +96,10 @@ public final class TurtleUpgrades { return null; } - public static Stream getVanillaUpgrades() { - if (vanilla == null) { + public static Stream getVanillaUpgrades() + { + if( vanilla == null ) + { vanilla = new ITurtleUpgrade[] { // ComputerCraft upgrades ComputerCraftRegistry.TurtleUpgrades.wirelessModemNormal, @@ -119,64 +113,69 @@ public final class TurtleUpgrades { ComputerCraftRegistry.TurtleUpgrades.diamondShovel, ComputerCraftRegistry.TurtleUpgrades.diamondHoe, ComputerCraftRegistry.TurtleUpgrades.craftingTable, - - ComputerCraftRegistry.TurtleUpgrades.netheritePickaxe, }; } - return Arrays.stream(vanilla) - .filter(x -> x != null && wrappers.get(x).enabled); + return Arrays.stream( vanilla ).filter( x -> x != null && wrappers.get( x ).enabled ); } - public static Stream getUpgrades() { - return wrappers.values() - .stream() - .filter(x -> x.enabled) - .map(x -> x.upgrade); + public static Stream getUpgrades() + { + return wrappers.values().stream().filter( x -> x.enabled ).map( x -> x.upgrade ); } - public static boolean suitableForFamily(ComputerFamily family, ITurtleUpgrade upgrade) { + public static boolean suitableForFamily( ComputerFamily family, ITurtleUpgrade upgrade ) + { return true; } - public static void enable(ITurtleUpgrade upgrade) { - Wrapper wrapper = wrappers.get(upgrade); - if (wrapper.enabled) { - return; + /** + * Rebuild the cache of turtle upgrades. This is done before querying the cache or registering new upgrades. + */ + private static void rebuild() + { + if( !needsRebuild ) return; + + upgrades.clear(); + for( Wrapper wrapper : wrappers.values() ) + { + if( !wrapper.enabled ) continue; + + ITurtleUpgrade existing = upgrades.get( wrapper.id ); + if( existing != null ) + { + ComputerCraft.log.error( "Error registering '" + wrapper.upgrade.getUnlocalisedAdjective() + " Turtle'." + + " Upgrade ID '" + wrapper.id + "' is already registered by '" + existing.getUnlocalisedAdjective() + " Turtle'" ); + continue; + } + + upgrades.put( wrapper.id, wrapper.upgrade ); } + needsRebuild = false; + } + + public static void enable( ITurtleUpgrade upgrade ) + { + Wrapper wrapper = wrappers.get( upgrade ); + if( wrapper.enabled ) return; + wrapper.enabled = true; needsRebuild = true; } - public static void disable(ITurtleUpgrade upgrade) { - Wrapper wrapper = wrappers.get(upgrade); - if (!wrapper.enabled) { - return; - } + public static void disable( ITurtleUpgrade upgrade ) + { + Wrapper wrapper = wrappers.get( upgrade ); + if( !wrapper.enabled ) return; wrapper.enabled = false; - upgrades.remove(wrapper.id); + upgrades.remove( wrapper.id ); } - public static void remove(ITurtleUpgrade upgrade) { - wrappers.remove(upgrade); + public static void remove( ITurtleUpgrade upgrade ) + { + wrappers.remove( upgrade ); needsRebuild = true; } - - private static class Wrapper { - final ITurtleUpgrade upgrade; - final String id; - final String modId; - boolean enabled; - - Wrapper(ITurtleUpgrade upgrade) { - this.upgrade = upgrade; - this.id = upgrade.getUpgradeID() - .toString(); - // TODO This should be the mod id of the mod the peripheral comes from - this.modId = ComputerCraft.MOD_ID; - this.enabled = true; - } - } -} +} \ No newline at end of file From 70a1cf5c5acb413b099670b630af6995e07286bf Mon Sep 17 00:00:00 2001 From: Wojbie Date: Fri, 15 Jan 2021 20:30:21 +0100 Subject: [PATCH 15/34] id.lua now handles more disk types (#677) Co-authored-by: Lupus590 --- .../computercraft/lua/rom/programs/id.lua | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/resources/data/computercraft/lua/rom/programs/id.lua b/src/main/resources/data/computercraft/lua/rom/programs/id.lua index 964503e24..79aecf9e6 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/id.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/id.lua @@ -13,16 +13,30 @@ if sDrive == nil then end else - local bData = disk.hasData(sDrive) - if not bData then + if disk.hasAudio(sDrive) then + local title = disk.getAudioTitle(sDrive) + if title then + print("Has audio track \"" .. title .. "\"") + else + print("Has untitled audio") + end + return + end + + if not disk.hasData(sDrive) then print("No disk in drive " .. sDrive) return end - print("The disk is #" .. disk.getID(sDrive)) + local id = disk.getID(sDrive) + if id then + print("The disk is #" .. id) + else + print("Non-disk data source") + end local label = disk.getLabel(sDrive) if label then - print("The disk is labelled \"" .. label .. "\"") + print("Labelled \"" .. label .. "\"") end end From d10d1b45fee16d2f441702bf691151bc7228be73 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Fri, 15 Jan 2021 19:39:12 +0000 Subject: [PATCH 16/34] Bump several action versions --- .github/workflows/main-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index 38142dbea..0dbdaf69c 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -16,7 +16,7 @@ jobs: java-version: 8 - name: Cache gradle dependencies - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ~/.gradle/caches key: ${{ runner.os }}-gradle-${{ hashFiles('gradle.properties') }} From 9c48c99be71e5ebf61eb846cefec9542fb53a8c4 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 16 Jan 2021 11:18:59 +0000 Subject: [PATCH 17/34] Bump version to 1.95.2 --- gradle.properties | 2 +- .../data/computercraft/lua/rom/help/changelog.txt | 13 +++++++++++-- .../data/computercraft/lua/rom/help/whatsnew.txt | 13 ++++++++----- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/gradle.properties b/gradle.properties index 430289d0e..3030d466c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ org.gradle.jvmargs=-Xmx1G # Mod properties -mod_version=1.95.1-beta +mod_version=1.95.2-beta # Minecraft properties mc_version=1.16.4 diff --git a/src/main/resources/data/computercraft/lua/rom/help/changelog.txt b/src/main/resources/data/computercraft/lua/rom/help/changelog.txt index e08f993ff..d39833f3f 100644 --- a/src/main/resources/data/computercraft/lua/rom/help/changelog.txt +++ b/src/main/resources/data/computercraft/lua/rom/help/changelog.txt @@ -1,10 +1,19 @@ +# New features in CC: Restitched 1.95.2 + +* Add `isReadOnly` to `fs.attributes` (Lupus590) +* Many more programs now support numpad enter (Wojbie) + +Several bug fixes: +* Fix some commands failing to parse on dedicated servers. +* Hopefully improve edit's behaviour with AltGr on some European keyboards. +* Prevent files being usable after their mount was removed. +* Fix the `id` program crashing on non-disk items (Wojbie). + # New features in CC: Restitched 1.95.1 Several bug fixes: * Command computers now drop items again. * Restore crafting of disks with dyes. -* Fix CraftTweaker integrations for damageable items. -* Catch reflection errors in the generic peripheral system, resolving crashes with Botania. # New features in CC: Restitched 1.95.0 diff --git a/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt b/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt index 1fdb9aa79..68d964b44 100644 --- a/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt +++ b/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt @@ -1,9 +1,12 @@ -New features in CC: Restitched 1.95.1 +New features in CC: Restitched 1.95.2 + +* Add `isReadOnly` to `fs.attributes` (Lupus590) +* Many more programs now support numpad enter (Wojbie) Several bug fixes: -* Command computers now drop items again. -* Restore crafting of disks with dyes. -* Fix CraftTweaker integrations for damageable items. -* Catch reflection errors in the generic peripheral system, resolving crashes with Botania. +* Fix some commands failing to parse on dedicated servers. +* Hopefully improve edit's behaviour with AltGr on some European keyboards. +* Prevent files being usable after their mount was removed. +* Fix the `id` program crashing on non-disk items (Wojbie). Type "help changelog" to see the full version history. From 1710ad9861612e8bcffa92af5564454d2f1fe704 Mon Sep 17 00:00:00 2001 From: FensieRenaud <65811543+FensieRenaud@users.noreply.github.com> Date: Mon, 18 Jan 2021 17:44:39 +0100 Subject: [PATCH 18/34] Serialise sparse arrays into JSON (#685) --- .../computercraft/lua/rom/apis/textutils.lua | 12 ++- .../test-rom/spec/apis/textutils_spec.lua | 99 +++++++++++++++++++ 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua b/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua index c80f8c3c9..d6b4c5705 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua @@ -381,6 +381,7 @@ local function serializeJSONImpl(t, tTracking, bNBTStyle) local sArrayResult = "[" local nObjectSize = 0 local nArraySize = 0 + local largestArrayIndex = 0 for k, v in pairs(t) do if type(k) == "string" then local sEntry @@ -395,10 +396,17 @@ local function serializeJSONImpl(t, tTracking, bNBTStyle) sObjectResult = sObjectResult .. "," .. sEntry end nObjectSize = nObjectSize + 1 + elseif type(k) == "number" and k > largestArrayIndex then --the largest index is kept to avoid losing half the array if there is any single nil in that array + largestArrayIndex = k end end - for _, v in ipairs(t) do - local sEntry = serializeJSONImpl(v, tTracking, bNBTStyle) + for k = 1, largestArrayIndex, 1 do --the array is read up to the very last valid array index, ipairs() would stop at the first nil value and we would lose any data after. + local sEntry + if t[k] == nil then --if the array is nil at index k the value is "null" as to keep the unused indexes in between used ones. + sEntry = "null" + else -- if the array index does not point to a nil we serialise it's content. + sEntry = serializeJSONImpl(t[k], tTracking, bNBTStyle) + end if nArraySize == 0 then sArrayResult = sArrayResult .. sEntry else diff --git a/src/test/resources/test-rom/spec/apis/textutils_spec.lua b/src/test/resources/test-rom/spec/apis/textutils_spec.lua index ab881d60b..6e1ba2bb7 100644 --- a/src/test/resources/test-rom/spec/apis/textutils_spec.lua +++ b/src/test/resources/test-rom/spec/apis/textutils_spec.lua @@ -70,6 +70,105 @@ describe("The textutils library", function() expect.error(textutils.serialiseJSON, nil):eq("bad argument #1 (expected table, string, number or boolean, got nil)") expect.error(textutils.serialiseJSON, "", 1):eq("bad argument #2 (expected boolean, got number)") end) + + it("serializes empty arrays", function() + expect(textutils.serializeJSON(textutils.empty_json_array)):eq("[]") + end) + + it("serializes null", function() + expect(textutils.serializeJSON(textutils.json_null)):eq("null") + end) + + it("serializes strings", function() + expect(textutils.serializeJSON('a')):eq('"a"') + expect(textutils.serializeJSON('"')):eq('"\\""') + expect(textutils.serializeJSON('\\')):eq('"\\\\"') + expect(textutils.serializeJSON('/')):eq('"/"') + expect(textutils.serializeJSON('\b')):eq('"\\b"') + expect(textutils.serializeJSON('\n')):eq('"\\n"') + expect(textutils.serializeJSON(string.char(0))):eq('"\\u0000"') + expect(textutils.serializeJSON(string.char(0x0A))):eq('"\\n"') + expect(textutils.serializeJSON(string.char(0x1D))):eq('"\\u001D"') + expect(textutils.serializeJSON(string.char(0x81))):eq('"\\u0081"') + expect(textutils.serializeJSON(string.char(0xFF))):eq('"\\u00FF"') + end) + + it("serializes arrays until the last index with content", function() + expect(textutils.serializeJSON({ 5, "test", nil, nil, 7 })):eq('[5,"test",null,null,7]') + expect(textutils.serializeJSON({ 5, "test", nil, nil, textutils.json_null })):eq('[5,"test",null,null,null]') + expect(textutils.serializeJSON({ nil, nil, nil, nil, "text" })):eq('[null,null,null,null,"text"]') + end) + end) + + describe("textutils.unserializeJSON", function() + describe("parses", function() + it("a list of primitives", function() + expect(textutils.unserializeJSON('[1, true, false, "hello"]')):same { 1, true, false, "hello" } + end) + + it("null when parse_null is true", function() + expect(textutils.unserializeJSON("null", { parse_null = true })):eq(textutils.json_null) + end) + + it("null when parse_null is false", function() + expect(textutils.unserializeJSON("null", { parse_null = false })):eq(nil) + end) + + it("an empty array", function() + expect(textutils.unserializeJSON("[]", { parse_null = false })):eq(textutils.empty_json_array) + end) + + it("basic objects", function() + expect(textutils.unserializeJSON([[{ "a": 1, "b":2 }]])):same { a = 1, b = 2 } + end) + end) + + describe("parses using NBT-style syntax", function() + local function exp(x) + local res, err = textutils.unserializeJSON(x, { nbt_style = true }) + if not res then error(err, 2) end + return expect(res) + end + it("basic objects", function() + exp([[{ a: 1, b:2 }]]):same { a = 1, b = 2 } + end) + + it("suffixed numbers", function() + exp("1b"):eq(1) + exp("1.1d"):eq(1.1) + end) + + it("strings", function() + exp("'123'"):eq("123") + exp("\"123\""):eq("123") + end) + + it("typed arrays", function() + exp("[B; 1, 2, 3]"):same { 1, 2, 3 } + exp("[B;]"):same {} + end) + end) + + describe("passes nst/JSONTestSuite", function() + local search_path = "test-rom/data/json-parsing" + local skip = dofile(search_path .. "/skip.lua") + for _, file in pairs(fs.find(search_path .. "/*.json")) do + local name = fs.getName(file):sub(1, -6); + (skip[name] and pending or it)(name, function() + local h = io.open(file, "r") + local contents = h:read("*a") + h:close() + + local res, err = textutils.unserializeJSON(contents) + local kind = fs.getName(file):sub(1, 1) + if kind == "n" then + expect(res):eq(nil) + elseif kind == "y" then + if err ~= nil then fail("Expected test to pass, but failed with " .. err) end + end + end) + end + end) end) describe("textutils.urlEncode", function() From 3e14b84c23a3fb9be85bfb7e5253490ef682b6ca Mon Sep 17 00:00:00 2001 From: JackMacWindows Date: Tue, 19 Jan 2021 04:20:52 -0500 Subject: [PATCH 19/34] Finish the rest of the event documentation (#683) --- doc/events/alarm.md | 21 ++++++++++++++++++ doc/events/computer_command.md | 18 +++++++++++++++ doc/events/disk.md | 19 ++++++++++++++++ doc/events/disk_eject.md | 19 ++++++++++++++++ doc/events/http_check.md | 14 ++++++++++++ doc/events/http_failure.md | 39 +++++++++++++++++++++++++++++++++ doc/events/http_success.md | 27 +++++++++++++++++++++++ doc/events/key.md | 6 ++--- doc/events/modem_message.md | 22 +++++++++++++++++++ doc/events/monitor_resize.md | 18 +++++++++++++++ doc/events/monitor_touch.md | 20 +++++++++++++++++ doc/events/mouse_drag.md | 2 -- doc/events/mouse_up.md | 3 --- doc/events/paste.md | 18 +++++++++++++++ doc/events/peripheral.md | 19 ++++++++++++++++ doc/events/peripheral_detach.md | 19 ++++++++++++++++ doc/events/rednet_message.md | 30 +++++++++++++++++++++++++ doc/events/redstone.md | 14 ++++++++++++ doc/events/task_complete.md | 28 +++++++++++++++++++++++ doc/events/term_resize.md | 15 +++++++++++++ doc/events/terminate.md | 25 +++++++++++++++++++++ doc/events/timer.md | 21 ++++++++++++++++++ doc/events/turtle_inventory.md | 14 ++++++++++++ doc/events/websocket_closed.md | 21 ++++++++++++++++++ doc/events/websocket_failure.md | 25 +++++++++++++++++++++ doc/events/websocket_message.md | 26 ++++++++++++++++++++++ doc/events/websocket_success.md | 28 +++++++++++++++++++++++ 27 files changed, 523 insertions(+), 8 deletions(-) create mode 100644 doc/events/alarm.md create mode 100644 doc/events/computer_command.md create mode 100644 doc/events/disk.md create mode 100644 doc/events/disk_eject.md create mode 100644 doc/events/http_check.md create mode 100644 doc/events/http_failure.md create mode 100644 doc/events/http_success.md create mode 100644 doc/events/modem_message.md create mode 100644 doc/events/monitor_resize.md create mode 100644 doc/events/monitor_touch.md create mode 100644 doc/events/paste.md create mode 100644 doc/events/peripheral.md create mode 100644 doc/events/peripheral_detach.md create mode 100644 doc/events/rednet_message.md create mode 100644 doc/events/redstone.md create mode 100644 doc/events/task_complete.md create mode 100644 doc/events/term_resize.md create mode 100644 doc/events/terminate.md create mode 100644 doc/events/timer.md create mode 100644 doc/events/turtle_inventory.md create mode 100644 doc/events/websocket_closed.md create mode 100644 doc/events/websocket_failure.md create mode 100644 doc/events/websocket_message.md create mode 100644 doc/events/websocket_success.md diff --git a/doc/events/alarm.md b/doc/events/alarm.md new file mode 100644 index 000000000..db7f04845 --- /dev/null +++ b/doc/events/alarm.md @@ -0,0 +1,21 @@ +--- +module: [kind=event] alarm +see: os.setAlarm To start an alarm. +--- + +The @{timer} event is fired when an alarm started with @{os.setAlarm} completes. + +## Return Values +1. @{string}: The event name. +2. @{number}: The ID of the alarm that finished. + +## Example +Starts a timer and then prints its ID: +```lua +local alarmID = os.setAlarm(os.time() + 0.05) +local event, id +repeat + event, id = os.pullEvent("alarm") +until id == alarmID +print("Alarm with ID " .. id .. " was fired") +``` diff --git a/doc/events/computer_command.md b/doc/events/computer_command.md new file mode 100644 index 000000000..245252399 --- /dev/null +++ b/doc/events/computer_command.md @@ -0,0 +1,18 @@ +--- +module: [kind=event] computer_command +--- + +The @{computer_command} event is fired when the `/computercraft queue` command is run for the current computer. + +## Return Values +1. @{string}: The event name. +... @{string}: The arguments passed to the command. + +## Example +Prints the contents of messages sent: +```lua +while true do + local event = {os.pullEvent("computer_command")} + print("Received message:", table.unpack(event, 2)) +end +``` diff --git a/doc/events/disk.md b/doc/events/disk.md new file mode 100644 index 000000000..2946d70c4 --- /dev/null +++ b/doc/events/disk.md @@ -0,0 +1,19 @@ +--- +module: [kind=event] disk +see: disk_eject For the event sent when a disk is removed. +--- + +The @{disk} event is fired when a disk is inserted into an adjacent or networked disk drive. + +## Return Values +1. @{string}: The event name. +2. @{string}: The side of the disk drive that had a disk inserted. + +## Example +Prints a message when a disk is inserted: +```lua +while true do + local event, side = os.pullEvent("disk") + print("Inserted a disk on side " .. side) +end +``` diff --git a/doc/events/disk_eject.md b/doc/events/disk_eject.md new file mode 100644 index 000000000..71c3ede0a --- /dev/null +++ b/doc/events/disk_eject.md @@ -0,0 +1,19 @@ +--- +module: [kind=event] disk_eject +see: disk For the event sent when a disk is inserted. +--- + +The @{disk_eject} event is fired when a disk is removed from an adjacent or networked disk drive. + +## Return Values +1. @{string}: The event name. +2. @{string}: The side of the disk drive that had a disk removed. + +## Example +Prints a message when a disk is removed: +```lua +while true do + local event, side = os.pullEvent("disk_eject") + print("Removed a disk on side " .. side) +end +``` diff --git a/doc/events/http_check.md b/doc/events/http_check.md new file mode 100644 index 000000000..9af5ea7ca --- /dev/null +++ b/doc/events/http_check.md @@ -0,0 +1,14 @@ +--- +module: [kind=event] http_check +see: http.checkURLAsync To check a URL asynchronously. +--- + +The @{http_check} event is fired when a URL check finishes. + +This event is normally handled inside @{http.checkURL}, but it can still be seen when using @{http.checkURLAsync}. + +## Return Values +1. @{string}: The event name. +2. @{string}: The URL requested to be checked. +3. @{boolean}: Whether the check succeeded. +4. @{string|nil}: If the check failed, a reason explaining why the check failed. diff --git a/doc/events/http_failure.md b/doc/events/http_failure.md new file mode 100644 index 000000000..d7572e601 --- /dev/null +++ b/doc/events/http_failure.md @@ -0,0 +1,39 @@ +--- +module: [kind=event] http_failure +see: http.request To send an HTTP request. +--- + +The @{http_failure} event is fired when an HTTP request fails. + +This event is normally handled inside @{http.get} and @{http.post}, but it can still be seen when using @{http.request}. + +## Return Values +1. @{string}: The event name. +2. @{string}: The URL of the site requested. +3. @{string}: An error describing the failure. +4. @{http.Response|nil}: A response handle if the connection succeeded, but the server's response indicated failure. + +## Example +Prints an error why the website cannot be contacted: +```lua +local myURL = "http://this.website.does.not.exist" +http.request(myURL) +local event, url, err +repeat + event, url, err = os.pullEvent("http_failure") +until url == myURL +print("The URL " .. url .. " could not be reached: " .. err) +``` + +Prints the contents of a webpage that does not exist: +```lua +local myURL = "https://tweaked.cc/this/does/not/exist" +http.request(myURL) +local event, url, err, handle +repeat + event, url, err, handle = os.pullEvent("http_failure") +until url == myURL +print("The URL " .. url .. " could not be reached: " .. err) +print(handle.getResponseCode()) +handle.close() +``` diff --git a/doc/events/http_success.md b/doc/events/http_success.md new file mode 100644 index 000000000..3700b9211 --- /dev/null +++ b/doc/events/http_success.md @@ -0,0 +1,27 @@ +--- +module: [kind=event] http_success +see: http.request To make an HTTP request. +--- + +The @{http_success} event is fired when an HTTP request returns successfully. + +This event is normally handled inside @{http.get} and @{http.post}, but it can still be seen when using @{http.request}. + +## Return Values +1. @{string}: The event name. +2. @{string}: The URL of the site requested. +3. @{http.Response}: The handle for the response text. + +## Example +Prints the content of a website (this may fail if the request fails): +```lua +local myURL = "https://tweaked.cc/" +http.request(myURL) +local event, url, handle +repeat + event, url, handle = os.pullEvent("http_success") +until url == myURL +print("Contents of " .. url .. ":") +print(handle.readAll()) +handle.close() +``` diff --git a/doc/events/key.md b/doc/events/key.md index 839dc553f..59598d8dd 100644 --- a/doc/events/key.md +++ b/doc/events/key.md @@ -11,9 +11,9 @@ If the button pressed represented a printable character, then the @{key} event w event. If you are consuming text input, use a @{char} event instead! ## Return values -1. [`string`]: The event name. -2. [`number`]: The numerical key value of the key pressed. -3. [`boolean`]: Whether the key event was generated while holding the key (@{true}), rather than pressing it the first time (@{false}). +1. @{string}: The event name. +2. @{number}: The numerical key value of the key pressed. +3. @{boolean}: Whether the key event was generated while holding the key (@{true}), rather than pressing it the first time (@{false}). ## Example Prints each key when the user presses it, and if the key is being held. diff --git a/doc/events/modem_message.md b/doc/events/modem_message.md new file mode 100644 index 000000000..ec619f3d4 --- /dev/null +++ b/doc/events/modem_message.md @@ -0,0 +1,22 @@ +--- +module: [kind=event] modem_message +--- + +The @{modem_message} event is fired when a message is received on an open channel on any modem. + +## Return Values +1. @{string}: The event name. +2. @{string}: The side of the modem that received the message. +3. @{number}: The channel that the message was sent on. +4. @{number}: The reply channel set by the sender. +5. @{any}: The message as sent by the sender. +6. @{number}: The distance between the sender and the receiver, in blocks (decimal). + +## Example +Prints a message when one is sent: +```lua +while true do + local event, side, channel, replyChannel, message, distance = os.pullEvent("modem_message") + print(("Message received on side %s on channel %d (reply to %d) from %f blocks away with message %s"):format(side, channel, replyChannel, distance, tostring(message))) +end +``` diff --git a/doc/events/monitor_resize.md b/doc/events/monitor_resize.md new file mode 100644 index 000000000..03de804e7 --- /dev/null +++ b/doc/events/monitor_resize.md @@ -0,0 +1,18 @@ +--- +module: [kind=event] monitor_resize +--- + +The @{monitor_resize} event is fired when an adjacent or networked monitor's size is changed. + +## Return Values +1. @{string}: The event name. +2. @{string}: The side or network ID of the monitor that resized. + +## Example +Prints a message when a monitor is resized: +```lua +while true do + local event, side = os.pullEvent("monitor_resize") + print("The monitor on side " .. side .. " was resized.") +end +``` diff --git a/doc/events/monitor_touch.md b/doc/events/monitor_touch.md new file mode 100644 index 000000000..0f27a9cc1 --- /dev/null +++ b/doc/events/monitor_touch.md @@ -0,0 +1,20 @@ +--- +module: [kind=event] monitor_touch +--- + +The @{monitor_touch} event is fired when an adjacent or networked Advanced Monitor is right-clicked. + +## Return Values +1. @{string}: The event name. +2. @{string}: The side or network ID of the monitor that was touched. +3. @{number}: The X coordinate of the touch, in characters. +4. @{number}: The Y coordinate of the touch, in characters. + +## Example +Prints a message when a monitor is touched: +```lua +while true do + local event, side, x, y = os.pullEvent("monitor_touch") + print("The monitor on side " .. side .. " was touched at (" .. x .. ", " .. y .. ")") +end +``` diff --git a/doc/events/mouse_drag.md b/doc/events/mouse_drag.md index 6ccb3ee6d..15451c9f8 100644 --- a/doc/events/mouse_drag.md +++ b/doc/events/mouse_drag.md @@ -20,5 +20,3 @@ while true do print(("The mouse button %s was dragged at %d, %d"):format(button, x, y)) end ``` - - diff --git a/doc/events/mouse_up.md b/doc/events/mouse_up.md index f3b382387..886330a6d 100644 --- a/doc/events/mouse_up.md +++ b/doc/events/mouse_up.md @@ -19,6 +19,3 @@ while true do print(("The mouse button %s was released at %d, %d"):format(button, x, y)) end ``` - -[`string`]: string -[`number`]: number diff --git a/doc/events/paste.md b/doc/events/paste.md new file mode 100644 index 000000000..b4f8713c5 --- /dev/null +++ b/doc/events/paste.md @@ -0,0 +1,18 @@ +--- +module: [kind=event] paste +--- + +The @{paste} event is fired when text is pasted into the computer through Ctrl-V (or ⌘V on Mac). + +## Return values +1. @{string}: The event name. +2. @{string} The text that was pasted. + +## Example +Prints pasted text: +```lua +while true do + local event, text = os.pullEvent("paste") + print('"' .. text .. '" was pasted') +end +``` diff --git a/doc/events/peripheral.md b/doc/events/peripheral.md new file mode 100644 index 000000000..5769f3942 --- /dev/null +++ b/doc/events/peripheral.md @@ -0,0 +1,19 @@ +--- +module: [kind=event] peripheral +see: peripheral_detach For the event fired when a peripheral is detached. +--- + +The @{peripheral} event is fired when a peripheral is attached on a side or to a modem. + +## Return Values +1. @{string}: The event name. +2. @{string}: The side the peripheral was attached to. + +## Example +Prints a message when a peripheral is attached: +```lua +while true do + local event, side = os.pullEvent("peripheral") + print("A peripheral was attached on side " .. side) +end +``` diff --git a/doc/events/peripheral_detach.md b/doc/events/peripheral_detach.md new file mode 100644 index 000000000..c8a462cf0 --- /dev/null +++ b/doc/events/peripheral_detach.md @@ -0,0 +1,19 @@ +--- +module: [kind=event] peripheral_detach +see: peripheral For the event fired when a peripheral is attached. +--- + +The @{peripheral_detach} event is fired when a peripheral is detached from a side or from a modem. + +## Return Values +1. @{string}: The event name. +2. @{string}: The side the peripheral was detached from. + +## Example +Prints a message when a peripheral is detached: +```lua +while true do + local event, side = os.pullEvent("peripheral_detach") + print("A peripheral was detached on side " .. side) +end +``` diff --git a/doc/events/rednet_message.md b/doc/events/rednet_message.md new file mode 100644 index 000000000..8d0bdf697 --- /dev/null +++ b/doc/events/rednet_message.md @@ -0,0 +1,30 @@ +--- +module: [kind=event] rednet_message +see: modem_message For raw modem messages sent outside of Rednet. +see: rednet.receive To wait for a Rednet message with an optional timeout and protocol filter. +--- + +The @{rednet_message} event is fired when a message is sent over Rednet. + +This event is usually handled by @{rednet.receive}, but it can also be pulled manually. + +@{rednet_message} events are sent by @{rednet.run} in the top-level coroutine in response to @{modem_message} events. A @{rednet_message} event is always preceded by a @{modem_message} event. They are generated inside CraftOS rather than being sent by the ComputerCraft machine. + +## Return Values +1. @{string}: The event name. +2. @{number}: The ID of the sending computer. +3. @{any}: The message sent. +4. @{string|nil}: The protocol of the message, if provided. + +## Example +Prints a message when one is sent: +```lua +while true do + local event, sender, message, protocol = os.pullEvent("rednet_message") + if protocol ~= nil then + print("Received message from " .. sender .. " with protocol " .. protocol .. " and message " .. tostring(message)) + else + print("Received message from " .. sender .. " with message " .. tostring(message)) + end +end +``` diff --git a/doc/events/redstone.md b/doc/events/redstone.md new file mode 100644 index 000000000..44eda304a --- /dev/null +++ b/doc/events/redstone.md @@ -0,0 +1,14 @@ +--- +module: [kind=event] redstone +--- + +The @{redstone} event is fired whenever any redstone inputs on the computer change. + +## Example +Prints a message when a redstone input changes: +```lua +while true do + os.pullEvent("redstone") + print("A redstone input has changed!") +end +``` diff --git a/doc/events/task_complete.md b/doc/events/task_complete.md new file mode 100644 index 000000000..eddec51d2 --- /dev/null +++ b/doc/events/task_complete.md @@ -0,0 +1,28 @@ +--- +module: [kind=event] task_complete +see: commands.execAsync To run a command which fires a task_complete event. +--- + +The @{task_complete} event is fired when an asynchronous task completes. This is usually handled inside the function call that queued the task; however, functions such as @{commands.execAsync} return immediately so the user can wait for completion. + +## Return Values +1. @{string}: The event name. +2. @{number}: The ID of the task that completed. +3. @{boolean}: Whether the command succeeded. +4. @{string}: If the command failed, an error message explaining the failure. (This is not present if the command succeeded.) +...: Any parameters returned from the command. + +## Example +Prints the results of an asynchronous command: +```lua +local taskID = commands.execAsync("say Hello") +local event +repeat + event = {os.pullEvent("task_complete")} +until event[2] == taskID +if event[3] == true then + print("Task " .. event[2] .. " succeeded:", table.unpack(event, 4)) +else + print("Task " .. event[2] .. " failed: " .. event[4]) +end +``` diff --git a/doc/events/term_resize.md b/doc/events/term_resize.md new file mode 100644 index 000000000..0eb503bad --- /dev/null +++ b/doc/events/term_resize.md @@ -0,0 +1,15 @@ +--- +module: [kind=event] term_resize +--- + +The @{term_resize} event is fired when the main terminal is resized, mainly when a new tab is opened or closed in @{multishell}. + +## Example +Prints : +```lua +while true do + os.pullEvent("term_resize") + local w, h = term.getSize() + print("The term was resized to (" .. w .. ", " .. h .. ")") +end +``` diff --git a/doc/events/terminate.md b/doc/events/terminate.md new file mode 100644 index 000000000..0760b8c3b --- /dev/null +++ b/doc/events/terminate.md @@ -0,0 +1,25 @@ +--- +module: [kind=event] terminate +--- + +The @{terminate} event is fired when Ctrl-T is held down. + +This event is normally handled by @{os.pullEvent}, and will not be returned. However, @{os.pullEventRaw} will return this event when fired. + +@{terminate} will be sent even when a filter is provided to @{os.pullEventRaw}. When using @{os.pullEventRaw} with a filter, make sure to check that the event is not @{terminate}. + +## Example +Prints a message when Ctrl-T is held: +```lua +while true do + local event = os.pullEventRaw("terminate") + if event == "terminate" then print("Terminate requested!") end +end +``` + +Exits when Ctrl-T is held: +```lua +while true do + os.pullEvent() +end +``` diff --git a/doc/events/timer.md b/doc/events/timer.md new file mode 100644 index 000000000..c359c37b4 --- /dev/null +++ b/doc/events/timer.md @@ -0,0 +1,21 @@ +--- +module: [kind=event] timer +see: os.startTimer To start a timer. +--- + +The @{timer} event is fired when a timer started with @{os.startTimer} completes. + +## Return Values +1. @{string}: The event name. +2. @{number}: The ID of the timer that finished. + +## Example +Starts a timer and then prints its ID: +```lua +local timerID = os.startTimer(2) +local event, id +repeat + event, id = os.pullEvent("timer") +until id == timerID +print("Timer with ID " .. id .. " was fired") +``` diff --git a/doc/events/turtle_inventory.md b/doc/events/turtle_inventory.md new file mode 100644 index 000000000..bc9392b6b --- /dev/null +++ b/doc/events/turtle_inventory.md @@ -0,0 +1,14 @@ +--- +module: [kind=event] turtle_inventory +--- + +The @{turtle_inventory} event is fired when a turtle's inventory is changed. + +## Example +Prints a message when the inventory is changed: +```lua +while true do + os.pullEvent("turtle_inventory") + print("The inventory was changed.") +end +``` diff --git a/doc/events/websocket_closed.md b/doc/events/websocket_closed.md new file mode 100644 index 000000000..60a8f59c2 --- /dev/null +++ b/doc/events/websocket_closed.md @@ -0,0 +1,21 @@ +--- +module: [kind=event] websocket_closed +--- + +The @{websocket_closed} event is fired when an open WebSocket connection is closed. + +## Return Values +1. @{string}: The event name. +2. @{string}: The URL of the WebSocket that was closed. + +## Example +Prints a message when a WebSocket is closed (this may take a minute): +```lua +local myURL = "ws://echo.websocket.org" +local ws = http.websocket(myURL) +local event, url +repeat + event, url = os.pullEvent("websocket_closed") +until url == myURL +print("The WebSocket at " .. url .. " was closed.") +``` diff --git a/doc/events/websocket_failure.md b/doc/events/websocket_failure.md new file mode 100644 index 000000000..f53bf10af --- /dev/null +++ b/doc/events/websocket_failure.md @@ -0,0 +1,25 @@ +--- +module: [kind=event] websocket_failure +see: http.websocketAsync To send an HTTP request. +--- + +The @{websocket_failure} event is fired when a WebSocket connection request fails. + +This event is normally handled inside @{http.websocket}, but it can still be seen when using @{http.websocketAsync}. + +## Return Values +1. @{string}: The event name. +2. @{string}: The URL of the site requested. +3. @{string}: An error describing the failure. + +## Example +Prints an error why the website cannot be contacted: +```lua +local myURL = "ws://this.website.does.not.exist" +http.websocketAsync(myURL) +local event, url, err +repeat + event, url, err = os.pullEvent("websocket_failure") +until url == myURL +print("The URL " .. url .. " could not be reached: " .. err) +``` diff --git a/doc/events/websocket_message.md b/doc/events/websocket_message.md new file mode 100644 index 000000000..f42dcaefe --- /dev/null +++ b/doc/events/websocket_message.md @@ -0,0 +1,26 @@ +--- +module: [kind=event] websocket_message +--- + +The @{websocket_message} event is fired when a message is received on an open WebSocket connection. + +This event is normally handled by @{http.Websocket.receive}, but it can also be pulled manually. + +## Return Values +1. @{string}: The event name. +2. @{string}: The URL of the WebSocket. +3. @{string}: The contents of the message. + +## Example +Prints a message sent by a WebSocket: +```lua +local myURL = "ws://echo.websocket.org" +local ws = http.websocket(myURL) +ws.send("Hello!") +local event, url, message +repeat + event, url, message = os.pullEvent("websocket_message") +until url == myURL +print("Received message from " .. url .. " with contents " .. message) +ws.close() +``` diff --git a/doc/events/websocket_success.md b/doc/events/websocket_success.md new file mode 100644 index 000000000..dc8d95dd2 --- /dev/null +++ b/doc/events/websocket_success.md @@ -0,0 +1,28 @@ +--- +module: [kind=event] websocket_success +see: http.websocketAsync To open a WebSocket asynchronously. +--- + +The @{websocket_success} event is fired when a WebSocket connection request returns successfully. + +This event is normally handled inside @{http.websocket}, but it can still be seen when using @{http.websocketAsync}. + +## Return Values +1. @{string}: The event name. +2. @{string}: The URL of the site. +3. @{http.Websocket}: The handle for the WebSocket. + +## Example +Prints the content of a website (this may fail if the request fails): +```lua +local myURL = "ws://echo.websocket.org" +http.websocketAsync(myURL) +local event, url, handle +repeat + event, url, handle = os.pullEvent("websocket_success") +until url == myURL +print("Connected to " .. url) +handle.send("Hello!") +print(handle.receive()) +handle.close() +``` From a65b8ed04cba68caa17f9f12e25b5daf777db05f Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 23 Jan 2021 14:58:08 +0000 Subject: [PATCH 20/34] Migrate all examples to use tweaked.cc Might as well, I've got the server capacity to spare. Hopefully. --- doc/events/http_failure.md | 2 +- doc/events/websocket_closed.md | 2 +- doc/events/websocket_failure.md | 2 +- doc/events/websocket_message.md | 2 +- doc/events/websocket_success.md | 2 +- src/main/resources/data/computercraft/lua/rom/motd.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/events/http_failure.md b/doc/events/http_failure.md index d7572e601..dc10b40d7 100644 --- a/doc/events/http_failure.md +++ b/doc/events/http_failure.md @@ -16,7 +16,7 @@ This event is normally handled inside @{http.get} and @{http.post}, but it can s ## Example Prints an error why the website cannot be contacted: ```lua -local myURL = "http://this.website.does.not.exist" +local myURL = "https://does.not.exist.tweaked.cc" http.request(myURL) local event, url, err repeat diff --git a/doc/events/websocket_closed.md b/doc/events/websocket_closed.md index 60a8f59c2..9e3783d19 100644 --- a/doc/events/websocket_closed.md +++ b/doc/events/websocket_closed.md @@ -11,7 +11,7 @@ The @{websocket_closed} event is fired when an open WebSocket connection is clos ## Example Prints a message when a WebSocket is closed (this may take a minute): ```lua -local myURL = "ws://echo.websocket.org" +local myURL = "wss://example.tweaked.cc/echo" local ws = http.websocket(myURL) local event, url repeat diff --git a/doc/events/websocket_failure.md b/doc/events/websocket_failure.md index f53bf10af..eef34e777 100644 --- a/doc/events/websocket_failure.md +++ b/doc/events/websocket_failure.md @@ -15,7 +15,7 @@ This event is normally handled inside @{http.websocket}, but it can still be see ## Example Prints an error why the website cannot be contacted: ```lua -local myURL = "ws://this.website.does.not.exist" +local myURL = "wss://example.tweaked.cc/not-a-websocket" http.websocketAsync(myURL) local event, url, err repeat diff --git a/doc/events/websocket_message.md b/doc/events/websocket_message.md index f42dcaefe..53b9d4bd2 100644 --- a/doc/events/websocket_message.md +++ b/doc/events/websocket_message.md @@ -14,7 +14,7 @@ This event is normally handled by @{http.Websocket.receive}, but it can also be ## Example Prints a message sent by a WebSocket: ```lua -local myURL = "ws://echo.websocket.org" +local myURL = "wss://example.tweaked.cc/echo" local ws = http.websocket(myURL) ws.send("Hello!") local event, url, message diff --git a/doc/events/websocket_success.md b/doc/events/websocket_success.md index dc8d95dd2..dcde934b3 100644 --- a/doc/events/websocket_success.md +++ b/doc/events/websocket_success.md @@ -15,7 +15,7 @@ This event is normally handled inside @{http.websocket}, but it can still be see ## Example Prints the content of a website (this may fail if the request fails): ```lua -local myURL = "ws://echo.websocket.org" +local myURL = "wss://example.tweaked.cc/echo" http.websocketAsync(myURL) local event, url, handle repeat diff --git a/src/main/resources/data/computercraft/lua/rom/motd.txt b/src/main/resources/data/computercraft/lua/rom/motd.txt index d94437ab9..0c5db751b 100644 --- a/src/main/resources/data/computercraft/lua/rom/motd.txt +++ b/src/main/resources/data/computercraft/lua/rom/motd.txt @@ -1,4 +1,4 @@ -Please report bugs at https://github.com/Merith-TK/cc-restiched. Thanks! +Please report bugs at https://github.com/Merith-TK/cc-restitched/issues. Thanks! View the documentation at https://tweaked.cc Show off your programs or ask for help at our forum: https://forums.computercraft.cc You can disable these messages by running "set motd.enable false". From 7fe3ac9222aa4ab3bf06c147cee754fedae5d86c Mon Sep 17 00:00:00 2001 From: SkyTheCodeMaster <34724753+SkyTheCodeMaster@users.noreply.github.com> Date: Fri, 5 Feb 2021 14:10:11 -0500 Subject: [PATCH 21/34] Fix `redstone.getBundledInput(side)` returning the output of said side. --- .../java/dan200/computercraft/core/apis/RedstoneAPI.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/dan200/computercraft/core/apis/RedstoneAPI.java b/src/main/java/dan200/computercraft/core/apis/RedstoneAPI.java index 4ce5ac261..9f026729a 100644 --- a/src/main/java/dan200/computercraft/core/apis/RedstoneAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/RedstoneAPI.java @@ -185,8 +185,9 @@ public class RedstoneAPI implements ILuaAPI { * @see #testBundledInput To determine if a specific colour is set. */ @LuaFunction - public final int getBundledInput(ComputerSide side) { - return this.environment.getBundledOutput(side); + public final int getBundledInput( ComputerSide side ) + { + return environment.getBundledInput( side ); } /** From 5e0ceda7ce2113725c244c22ddb619a25b95d4d7 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 20 Feb 2021 20:19:22 +0000 Subject: [PATCH 22/34] Clarify the turtle.place docs a little Closes #714 --- .../dan200/computercraft/shared/turtle/apis/TurtleAPI.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java b/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java index 85d602672..44625bdc3 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java +++ b/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java @@ -191,6 +191,10 @@ public class TurtleAPI implements ILuaAPI { /** * Place a block or item into the world in front of the turtle. * + * "Placing" an item allows it to interact with blocks and entities in front of the turtle. For instance, buckets + * can pick up and place down fluids, and wheat can be used to breed cows. However, you cannot use {@link #place} to + * perform arbitrary block interactions, such as clicking buttons or flipping levers. + * * @param args Arguments to place. * @return The turtle command result. * @cc.tparam [opt] string text When placing a sign, set its contents to this text. @@ -210,6 +214,7 @@ public class TurtleAPI implements ILuaAPI { * @cc.tparam [opt] string text When placing a sign, set its contents to this text. * @cc.treturn boolean Whether the block could be placed. * @cc.treturn string|nil The reason the block was not placed. + * @see #place For more information about placing items. */ @LuaFunction public final MethodResult placeUp(IArguments args) { @@ -224,6 +229,7 @@ public class TurtleAPI implements ILuaAPI { * @cc.tparam [opt] string text When placing a sign, set its contents to this text. * @cc.treturn boolean Whether the block could be placed. * @cc.treturn string|nil The reason the block was not placed. + * @see #place For more information about placing items. */ @LuaFunction public final MethodResult placeDown(IArguments args) { From 98dc7a6e583d4baebac6d6c2530a79384ca6b8da Mon Sep 17 00:00:00 2001 From: Weblate Date: Sun, 21 Feb 2021 13:42:57 +0000 Subject: [PATCH 23/34] Translations for Portuguese (Brazil) Co-authored-by: Matheus Medeiros Souza --- .../assets/computercraft/lang/pt_br.json | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/resources/assets/computercraft/lang/pt_br.json b/src/main/resources/assets/computercraft/lang/pt_br.json index 0f7df3362..f5ec8017a 100644 --- a/src/main/resources/assets/computercraft/lang/pt_br.json +++ b/src/main/resources/assets/computercraft/lang/pt_br.json @@ -38,5 +38,21 @@ "upgrade.computercraft.speaker.adjective": "(Alto-Falante)", "chat.computercraft.wired_modem.peripheral_connected": "Periférico \"%s\" conectado à rede", "chat.computercraft.wired_modem.peripheral_disconnected": "Periférico \"%s\" desconectado da rede", - "gui.computercraft.tooltip.copy": "Copiar para a área de transferência" + "gui.computercraft.tooltip.copy": "Copiar para a área de transferência", + "commands.computercraft.tp.synopsis": "Teleprota para um computador específico.", + "commands.computercraft.turn_on.done": "Ligou %s/%s computadores", + "commands.computercraft.turn_on.desc": "Liga os computadores em escuta. Você pode especificar o id de instância do computador (ex.: 123), id do computador (ex.: #123) ou o rótulo (ex.: \"@MeuComputador\").", + "commands.computercraft.turn_on.synopsis": "Liga computadores remotamente.", + "commands.computercraft.shutdown.done": "Desliga %s/%s computadores", + "commands.computercraft.shutdown.desc": "Desliga os computadores em escuta ou todos caso não tenha sido especificado. Você pode especificar o id de instância do computador (ex.: 123), id do computador (ex.: #123) ou o rótulo (ex.: \"@MeuComputador\").", + "commands.computercraft.shutdown.synopsis": "Desliga computadores remotamente.", + "commands.computercraft.dump.action": "Ver mais informação sobre este computador", + "commands.computercraft.dump.desc": "Mostra o status de todos os computadores ou uma informação específica sobre um computador. Você pode especificar o id de instância do computador (ex.: 123), id do computador (ex.: #123) ou o rótulo (ex.: \"@MeuComputador\").", + "commands.computercraft.dump.synopsis": "Mostra status de computadores.", + "commands.computercraft.help.no_command": "Comando '%s' não existe", + "commands.computercraft.help.no_children": "%s não tem sub-comandos", + "commands.computercraft.help.desc": "Mostra essa mensagem de ajuda", + "commands.computercraft.help.synopsis": "Providencia ajuda para um comando específico", + "commands.computercraft.desc": "O comando /computercraft providencia várias ferramentas de depuração e administração para controle e interação com computadores.", + "commands.computercraft.synopsis": "Vários comandos para controlar computadores." } From 5b31c2536a592f497abbae87ceab2f16ab42fc32 Mon Sep 17 00:00:00 2001 From: Wojbie Date: Tue, 23 Feb 2021 21:50:19 +0100 Subject: [PATCH 24/34] Make edit display errors/results of execution and handle require. (#723) --- .../computercraft/lua/rom/programs/edit.lua | 50 ++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/src/main/resources/data/computercraft/lua/rom/programs/edit.lua b/src/main/resources/data/computercraft/lua/rom/programs/edit.lua index 78c6f5ca9..f67bd0ede 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/edit.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/edit.lua @@ -47,6 +47,30 @@ else stringColour = colours.white end +local runHandler = [[multishell.setTitle(multishell.getCurrent(), %q) +local current = term.current() +local ok, err = load(%q, %q, nil, _ENV) +if ok then ok, err = pcall(ok, ...) end +term.redirect(current) +term.setTextColor(term.isColour() and colours.yellow or colours.white) +term.setBackgroundColor(colours.black) +term.setCursorBlink(false) +local _, y = term.getCursorPos() +local _, h = term.getSize() +if not ok then + printError(err) +end +if ok and y >= h then + term.scroll(1) +end +term.setCursorPos(1, h) +if ok then + write("Program finished. ") +end +write("Press any key to continue") +os.pullEvent('key') +]] + -- Menus local bMenu = false local nMenuItem = 1 @@ -89,7 +113,7 @@ local function load(_sPath) end end -local function save(_sPath) +local function save(_sPath, fWrite) -- Create intervening folder local sDir = _sPath:sub(1, _sPath:len() - fs.getName(_sPath):len()) if not fs.exists(sDir) then @@ -101,8 +125,8 @@ local function save(_sPath) local function innerSave() file, fileerr = fs.open(_sPath, "w") if file then - for _, sLine in ipairs(tLines) do - file.write(sLine .. "\n") + if file then + fWrite(file) end else error("Failed to open " .. _sPath) @@ -293,7 +317,11 @@ local tMenuFuncs = { if bReadOnly then sStatus = "Access denied" else - local ok, _, fileerr = save(sPath) + local ok, _, fileerr = save(sPath, function(file) + for _, sLine in ipairs(tLines) do + file.write(sLine .. "\n") + end + end) if ok then sStatus = "Saved to " .. sPath else @@ -390,8 +418,18 @@ local tMenuFuncs = { bRunning = false end, Run = function() - local sTempPath = "/.temp" - local ok = save(sTempPath) + local sTitle = fs.getName(sPath) + if sTitle:sub(-4) == ".lua" then + sTitle = sTitle:sub(1, -5) + end + local sTempPath = bReadOnly and ".temp." .. sTitle or fs.combine(fs.getDir(sPath), ".temp." .. sTitle) + if fs.exists(sTempPath) then + sStatus = "Error saving to " .. sTempPath + return + end + local ok = save(sTempPath, function(file) + file.write(runHandler:format(sTitle, table.concat(tLines, "\n"), "@" .. fs.getName(sPath))) + end) if ok then local nTask = shell.openTab(sTempPath) if nTask then From 3860e2466cb9083efe77504724a7b4955c1d75cf Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Fri, 12 Mar 2021 09:14:52 +0000 Subject: [PATCH 25/34] Add User-Agent to Websockets I think, haven't actually tested this :D:. Closes #730. --- .../computercraft/core/apis/HTTPAPI.java | 251 ++++++++------- .../apis/http/request/HttpRequestHandler.java | 304 +++++++++--------- 2 files changed, 281 insertions(+), 274 deletions(-) diff --git a/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java b/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java index 9f1c5f951..0996e5300 100644 --- a/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java @@ -3,185 +3,206 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.apis; -import static dan200.computercraft.core.apis.TableHelper.getStringField; -import static dan200.computercraft.core.apis.TableHelper.optBooleanField; -import static dan200.computercraft.core.apis.TableHelper.optStringField; -import static dan200.computercraft.core.apis.TableHelper.optTableField; - -import java.net.URI; -import java.util.Collections; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; - -import javax.annotation.Nonnull; - import dan200.computercraft.ComputerCraft; import dan200.computercraft.api.lua.IArguments; import dan200.computercraft.api.lua.ILuaAPI; import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaFunction; -import dan200.computercraft.core.apis.http.CheckUrl; -import dan200.computercraft.core.apis.http.HTTPRequestException; -import dan200.computercraft.core.apis.http.Resource; -import dan200.computercraft.core.apis.http.ResourceGroup; -import dan200.computercraft.core.apis.http.ResourceQueue; +import dan200.computercraft.core.apis.http.*; import dan200.computercraft.core.apis.http.request.HttpRequest; import dan200.computercraft.core.apis.http.websocket.Websocket; import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; +import javax.annotation.Nonnull; +import java.net.URI; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +import static dan200.computercraft.core.apis.TableHelper.*; + /** * The http library allows communicating with web servers, sending and receiving data from them. * * @cc.module http * @hidden */ -public class HTTPAPI implements ILuaAPI { - private final IAPIEnvironment m_apiEnvironment; +public class HTTPAPI implements ILuaAPI +{ + private final IAPIEnvironment apiEnvironment; private final ResourceGroup checkUrls = new ResourceGroup<>(); - private final ResourceGroup requests = new ResourceQueue<>(() -> ComputerCraft.httpMaxRequests); - private final ResourceGroup websockets = new ResourceGroup<>(() -> ComputerCraft.httpMaxWebsockets); + private final ResourceGroup requests = new ResourceQueue<>( () -> ComputerCraft.httpMaxRequests ); + private final ResourceGroup websockets = new ResourceGroup<>( () -> ComputerCraft.httpMaxWebsockets ); - public HTTPAPI(IAPIEnvironment environment) { - this.m_apiEnvironment = environment; + public HTTPAPI( IAPIEnvironment environment ) + { + apiEnvironment = environment; } @Override - public String[] getNames() { - return new String[] {"http"}; + public String[] getNames() + { + return new String[] { "http" }; } @Override - public void startup() { - this.checkUrls.startup(); - this.requests.startup(); - this.websockets.startup(); + public void startup() + { + checkUrls.startup(); + requests.startup(); + websockets.startup(); } @Override - public void update() { + public void shutdown() + { + checkUrls.shutdown(); + requests.shutdown(); + websockets.shutdown(); + } + + @Override + public void update() + { // It's rather ugly to run this here, but we need to clean up // resources as often as possible to reduce blocking. Resource.cleanup(); } - @Override - public void shutdown() { - this.checkUrls.shutdown(); - this.requests.shutdown(); - this.websockets.shutdown(); - } - @LuaFunction - public final Object[] request(IArguments args) throws LuaException { + public final Object[] request( IArguments args ) throws LuaException + { String address, postString, requestMethod; Map headerTable; boolean binary, redirect; - if (args.get(0) instanceof Map) { - Map options = args.getTable(0); - address = getStringField(options, "url"); - postString = optStringField(options, "body", null); - headerTable = optTableField(options, "headers", Collections.emptyMap()); - binary = optBooleanField(options, "binary", false); - requestMethod = optStringField(options, "method", null); - redirect = optBooleanField(options, "redirect", true); + if( args.get( 0 ) instanceof Map ) + { + Map options = args.getTable( 0 ); + address = getStringField( options, "url" ); + postString = optStringField( options, "body", null ); + headerTable = optTableField( options, "headers", Collections.emptyMap() ); + binary = optBooleanField( options, "binary", false ); + requestMethod = optStringField( options, "method", null ); + redirect = optBooleanField( options, "redirect", true ); - } else { + } + else + { // Get URL and post information - address = args.getString(0); - postString = args.optString(1, null); - headerTable = args.optTable(2, Collections.emptyMap()); - binary = args.optBoolean(3, false); + address = args.getString( 0 ); + postString = args.optString( 1, null ); + headerTable = args.optTable( 2, Collections.emptyMap() ); + binary = args.optBoolean( 3, false ); requestMethod = null; redirect = true; } - HttpHeaders headers = getHeaders(headerTable); + HttpHeaders headers = getHeaders( headerTable ); HttpMethod httpMethod; - if (requestMethod == null) { + if( requestMethod == null ) + { httpMethod = postString == null ? HttpMethod.GET : HttpMethod.POST; - } else { - httpMethod = HttpMethod.valueOf(requestMethod.toUpperCase(Locale.ROOT)); - if (httpMethod == null || requestMethod.equalsIgnoreCase("CONNECT")) { - throw new LuaException("Unsupported HTTP method"); + } + else + { + httpMethod = HttpMethod.valueOf( requestMethod.toUpperCase( Locale.ROOT ) ); + if( httpMethod == null || requestMethod.equalsIgnoreCase( "CONNECT" ) ) + { + throw new LuaException( "Unsupported HTTP method" ); } } - try { - URI uri = HttpRequest.checkUri(address); - HttpRequest request = new HttpRequest(this.requests, this.m_apiEnvironment, address, postString, headers, binary, redirect); + try + { + URI uri = HttpRequest.checkUri( address ); + HttpRequest request = new HttpRequest( requests, apiEnvironment, address, postString, headers, binary, redirect ); // Make the request - request.queue(r -> r.request(uri, httpMethod)); + request.queue( r -> r.request( uri, httpMethod ) ); - return new Object[] {true}; - } catch (HTTPRequestException e) { - return new Object[] { - false, - e.getMessage() - }; + return new Object[] { true }; + } + catch( HTTPRequestException e ) + { + return new Object[] { false, e.getMessage() }; + } + } + + @LuaFunction + public final Object[] checkURL( String address ) + { + try + { + URI uri = HttpRequest.checkUri( address ); + new CheckUrl( checkUrls, apiEnvironment, address, uri ).queue( CheckUrl::run ); + + return new Object[] { true }; + } + catch( HTTPRequestException e ) + { + return new Object[] { false, e.getMessage() }; + } + } + + @LuaFunction + public final Object[] websocket( String address, Optional> headerTbl ) throws LuaException + { + if( !ComputerCraft.http_websocket_enable ) + { + throw new LuaException( "Websocket connections are disabled" ); + } + + HttpHeaders headers = getHeaders( headerTbl.orElse( Collections.emptyMap() ) ); + + try + { + URI uri = Websocket.checkUri( address ); + if( !new Websocket( websockets, apiEnvironment, uri, address, headers ).queue( Websocket::connect ) ) + { + throw new LuaException( "Too many websockets already open" ); + } + + return new Object[] { true }; + } + catch( HTTPRequestException e ) + { + return new Object[] { false, e.getMessage() }; } } @Nonnull - private static HttpHeaders getHeaders(@Nonnull Map headerTable) throws LuaException { + private HttpHeaders getHeaders( @Nonnull Map headerTable ) throws LuaException + { HttpHeaders headers = new DefaultHttpHeaders(); - for (Map.Entry entry : headerTable.entrySet()) { + for( Map.Entry entry : headerTable.entrySet() ) + { Object value = entry.getValue(); - if (entry.getKey() instanceof String && value instanceof String) { - try { - headers.add((String) entry.getKey(), value); - } catch (IllegalArgumentException e) { - throw new LuaException(e.getMessage()); + if( entry.getKey() instanceof String && value instanceof String ) + { + try + { + headers.add( (String) entry.getKey(), value ); + } + catch( IllegalArgumentException e ) + { + throw new LuaException( e.getMessage() ); } } } + + if( !headers.contains( HttpHeaderNames.USER_AGENT ) ) + { + headers.set( HttpHeaderNames.USER_AGENT, apiEnvironment.getComputerEnvironment().getUserAgent() ); + } return headers; } - - @LuaFunction - public final Object[] checkURL(String address) { - try { - URI uri = HttpRequest.checkUri(address); - new CheckUrl(this.checkUrls, this.m_apiEnvironment, address, uri).queue(CheckUrl::run); - - return new Object[] {true}; - } catch (HTTPRequestException e) { - return new Object[] { - false, - e.getMessage() - }; - } - } - - @LuaFunction - public final Object[] websocket(String address, Optional> headerTbl) throws LuaException { - if (!ComputerCraft.http_websocket_enable) { - throw new LuaException("Websocket connections are disabled"); - } - - HttpHeaders headers = getHeaders(headerTbl.orElse(Collections.emptyMap())); - - try { - URI uri = Websocket.checkUri(address); - if (!new Websocket(this.websockets, this.m_apiEnvironment, uri, address, headers).queue(Websocket::connect)) { - throw new LuaException("Too many websockets already open"); - } - - return new Object[] {true}; - } catch (HTTPRequestException e) { - return new Object[] { - false, - e.getMessage() - }; - } - } -} +} \ No newline at end of file diff --git a/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java b/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java index 23931eb18..845a185ee 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java +++ b/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java @@ -3,19 +3,8 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.apis.http.request; -import static dan200.computercraft.core.apis.http.request.HttpRequest.getHeaderSize; - -import java.io.Closeable; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; - import dan200.computercraft.ComputerCraft; import dan200.computercraft.core.apis.handles.ArrayByteChannel; import dan200.computercraft.core.apis.handles.BinaryReadableHandle; @@ -29,22 +18,20 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.CompositeByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.handler.codec.http.DefaultFullHttpRequest; -import io.netty.handler.codec.http.DefaultHttpHeaders; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.HttpContent; -import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpHeaderValues; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpMethod; -import io.netty.handler.codec.http.HttpObject; -import io.netty.handler.codec.http.HttpResponse; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http.HttpUtil; -import io.netty.handler.codec.http.HttpVersion; -import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http.*; -public final class HttpRequestHandler extends SimpleChannelInboundHandler implements Closeable { +import java.io.Closeable; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import static dan200.computercraft.core.apis.http.request.HttpRequest.getHeaderSize; + +public final class HttpRequestHandler extends SimpleChannelInboundHandler implements Closeable +{ /** * Same as {@link io.netty.handler.codec.MessageAggregator}. */ @@ -53,16 +40,19 @@ public final class HttpRequestHandler extends SimpleChannelInboundHandler 0) { - URI redirect = this.getRedirect(response.status(), response.headers()); - if (redirect != null && !this.uri.equals(redirect) && this.request.redirects.getAndDecrement() > 0) { + if( request.redirects.get() > 0 ) + { + URI redirect = getRedirect( response.status(), response.headers() ); + if( redirect != null && !uri.equals( redirect ) && request.redirects.getAndDecrement() > 0 ) + { // If we have a redirect, and don't end up at the same place, then follow it. // We mark ourselves as disposed first though, to avoid firing events when the channel // becomes inactive or disposed. - this.closed = true; + closed = true; ctx.close(); - try { - HttpRequest.checkUri(redirect); - } catch (HTTPRequestException e) { + try + { + HttpRequest.checkUri( redirect ); + } + catch( HTTPRequestException e ) + { // If we cannot visit this uri, then fail. - this.request.failure(e.getMessage()); + request.failure( e.getMessage() ); return; } - this.request.request(redirect, - response.status() - .code() == 303 ? HttpMethod.GET : this.method); + request.request( redirect, response.status().code() == 303 ? HttpMethod.GET : method ); return; } } - this.responseCharset = HttpUtil.getCharset(response, StandardCharsets.UTF_8); - this.responseStatus = response.status(); - this.responseHeaders.add(response.headers()); + responseCharset = HttpUtil.getCharset( response, StandardCharsets.UTF_8 ); + responseStatus = response.status(); + responseHeaders.add( response.headers() ); } - if (message instanceof HttpContent) { + if( message instanceof HttpContent ) + { HttpContent content = (HttpContent) message; - if (this.responseBody == null) { - this.responseBody = ctx.alloc() - .compositeBuffer(DEFAULT_MAX_COMPOSITE_BUFFER_COMPONENTS); + if( responseBody == null ) + { + responseBody = ctx.alloc().compositeBuffer( DEFAULT_MAX_COMPOSITE_BUFFER_COMPONENTS ); } ByteBuf partial = content.content(); - if (partial.isReadable()) { + if( partial.isReadable() ) + { // If we've read more than we're allowed to handle, abort as soon as possible. - if (this.options.maxDownload != 0 && this.responseBody.readableBytes() + partial.readableBytes() > this.options.maxDownload) { - this.closed = true; + if( options.maxDownload != 0 && responseBody.readableBytes() + partial.readableBytes() > options.maxDownload ) + { + closed = true; ctx.close(); - this.request.failure("Response is too large"); + request.failure( "Response is too large" ); return; } - this.responseBody.addComponent(true, partial.retain()); + responseBody.addComponent( true, partial.retain() ); } - if (message instanceof LastHttpContent) { + if( message instanceof LastHttpContent ) + { LastHttpContent last = (LastHttpContent) message; - this.responseHeaders.add(last.trailingHeaders()); + responseHeaders.add( last.trailingHeaders() ); // Set the content length, if not already given. - if (this.responseHeaders.contains(HttpHeaderNames.CONTENT_LENGTH)) { - this.responseHeaders.set(HttpHeaderNames.CONTENT_LENGTH, this.responseBody.readableBytes()); + if( responseHeaders.contains( HttpHeaderNames.CONTENT_LENGTH ) ) + { + responseHeaders.set( HttpHeaderNames.CONTENT_LENGTH, responseBody.readableBytes() ); } ctx.close(); - this.sendResponse(); + sendResponse(); } } } + @Override + public void exceptionCaught( ChannelHandlerContext ctx, Throwable cause ) + { + if( ComputerCraft.logPeripheralErrors ) ComputerCraft.log.error( "Error handling HTTP response", cause ); + request.failure( cause ); + } + + private void sendResponse() + { + // Read the ByteBuf into a channel. + CompositeByteBuf body = responseBody; + byte[] bytes = body == null ? EMPTY_BYTES : NetworkUtils.toBytes( body ); + + // Decode the headers + HttpResponseStatus status = responseStatus; + Map headers = new HashMap<>(); + for( Map.Entry header : responseHeaders ) + { + String existing = headers.get( header.getKey() ); + headers.put( header.getKey(), existing == null ? header.getValue() : existing + "," + header.getValue() ); + } + + // Fire off a stats event + request.environment().addTrackingChange( TrackingField.HTTP_DOWNLOAD, getHeaderSize( responseHeaders ) + bytes.length ); + + // Prepare to queue an event + ArrayByteChannel contents = new ArrayByteChannel( bytes ); + HandleGeneric reader = request.isBinary() + ? BinaryReadableHandle.of( contents ) + : new EncodedReadableHandle( EncodedReadableHandle.open( contents, responseCharset ) ); + HttpResponseHandle stream = new HttpResponseHandle( reader, status.code(), status.reasonPhrase(), headers ); + + if( status.code() >= 200 && status.code() < 400 ) + { + request.success( stream ); + } + else + { + request.failure( status.reasonPhrase(), stream ); + } + } + /** * Determine the redirect from this response. * - * @param status The status of the HTTP response. + * @param status The status of the HTTP response. * @param headers The headers of the HTTP response. * @return The URI to redirect to, or {@code null} if no redirect should occur. */ - private URI getRedirect(HttpResponseStatus status, HttpHeaders headers) { + private URI getRedirect( HttpResponseStatus status, HttpHeaders headers ) + { int code = status.code(); - if (code < 300 || code > 307 || code == 304 || code == 306) { + if( code < 300 || code > 307 || code == 304 || code == 306 ) return null; + + String location = headers.get( HttpHeaderNames.LOCATION ); + if( location == null ) return null; + + try + { + return uri.resolve( new URI( location ) ); + } + catch( IllegalArgumentException | URISyntaxException e ) + { return null; } - - String location = headers.get(HttpHeaderNames.LOCATION); - if (location == null) { - return null; - } - - try { - return this.uri.resolve(new URI( location )); - } catch( IllegalArgumentException | URISyntaxException e ) { - return null; - } - } - - private void sendResponse() { - // Read the ByteBuf into a channel. - CompositeByteBuf body = this.responseBody; - byte[] bytes = body == null ? EMPTY_BYTES : NetworkUtils.toBytes(body); - - // Decode the headers - HttpResponseStatus status = this.responseStatus; - Map headers = new HashMap<>(); - for (Map.Entry header : this.responseHeaders) { - String existing = headers.get(header.getKey()); - headers.put(header.getKey(), existing == null ? header.getValue() : existing + "," + header.getValue()); - } - - // Fire off a stats event - this.request.environment() - .addTrackingChange(TrackingField.HTTP_DOWNLOAD, getHeaderSize(this.responseHeaders) + bytes.length); - - // Prepare to queue an event - ArrayByteChannel contents = new ArrayByteChannel(bytes); - HandleGeneric reader = this.request.isBinary() ? BinaryReadableHandle.of(contents) : new EncodedReadableHandle(EncodedReadableHandle.open(contents, - this.responseCharset)); - HttpResponseHandle stream = new HttpResponseHandle(reader, status.code(), status.reasonPhrase(), headers); - - if (status.code() >= 200 && status.code() < 400) { - this.request.success(stream); - } else { - this.request.failure(status.reasonPhrase(), stream); - } } @Override - public void close() { - this.closed = true; - if (this.responseBody != null) { - this.responseBody.release(); - this.responseBody = null; + public void close() + { + closed = true; + if( responseBody != null ) + { + responseBody.release(); + responseBody = null; } } -} +} \ No newline at end of file From a28e7e2db3666d87281df5762c99dae087e1c867 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Fri, 12 Mar 2021 09:19:16 +0000 Subject: [PATCH 26/34] Bump version to 1.95.3 --- gradle.properties | 2 +- .../data/computercraft/lua/rom/help/changelog.txt | 8 ++++++++ .../data/computercraft/lua/rom/help/whatsnew.txt | 13 +++++-------- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/gradle.properties b/gradle.properties index 3030d466c..a645c8f2d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ org.gradle.jvmargs=-Xmx1G # Mod properties -mod_version=1.95.2-beta +mod_version=1.95.3-beta # Minecraft properties mc_version=1.16.4 diff --git a/src/main/resources/data/computercraft/lua/rom/help/changelog.txt b/src/main/resources/data/computercraft/lua/rom/help/changelog.txt index d39833f3f..bcad65f44 100644 --- a/src/main/resources/data/computercraft/lua/rom/help/changelog.txt +++ b/src/main/resources/data/computercraft/lua/rom/help/changelog.txt @@ -1,3 +1,11 @@ +New features in CC: Restitched 1.95.3 + +Several bug fixes: +* Correctly serialise sparse arrays into JSON (livegamer999) +* Fix rs.getBundledInput returning the output instead (SkyTheCodeMaster) +* Programs run via edit are now a little better behaved (Wojbie) +* Add User-Agent to a websocket's headers. + # New features in CC: Restitched 1.95.2 * Add `isReadOnly` to `fs.attributes` (Lupus590) diff --git a/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt b/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt index 68d964b44..982911f36 100644 --- a/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt +++ b/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt @@ -1,12 +1,9 @@ -New features in CC: Restitched 1.95.2 - -* Add `isReadOnly` to `fs.attributes` (Lupus590) -* Many more programs now support numpad enter (Wojbie) +New features in CC: Restitched 1.95.3 Several bug fixes: -* Fix some commands failing to parse on dedicated servers. -* Hopefully improve edit's behaviour with AltGr on some European keyboards. -* Prevent files being usable after their mount was removed. -* Fix the `id` program crashing on non-disk items (Wojbie). +* Correctly serialise sparse arrays into JSON (livegamer999) +* Fix rs.getBundledInput returning the output instead (SkyTheCodeMaster) +* Programs run via edit are now a little better behaved (Wojbie) +* Add User-Agent to a websocket's headers. Type "help changelog" to see the full version history. From 8984ebcf80921ba3d4fcce0ff9a2cc736b27ffc4 Mon Sep 17 00:00:00 2001 From: Ronan Hanley Date: Tue, 16 Mar 2021 21:19:54 +0000 Subject: [PATCH 27/34] Refactor and add tests for TextBuffer (#738) --- .../core/terminal/TextBuffer.java | 188 +++++------------- .../core/terminal/TextBufferTest.java | 150 ++++++++++++++ 2 files changed, 202 insertions(+), 136 deletions(-) create mode 100644 src/test/java/dan200/computercraft/core/terminal/TextBufferTest.java diff --git a/src/main/java/dan200/computercraft/core/terminal/TextBuffer.java b/src/main/java/dan200/computercraft/core/terminal/TextBuffer.java index 8047cc8b7..44d5bcd2c 100644 --- a/src/main/java/dan200/computercraft/core/terminal/TextBuffer.java +++ b/src/main/java/dan200/computercraft/core/terminal/TextBuffer.java @@ -3,168 +3,84 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.terminal; -public class TextBuffer { - private final char[] m_text; +public class TextBuffer +{ + private final char[] text; - public TextBuffer(char c, int length) { - this.m_text = new char[length]; - for (int i = 0; i < length; i++) { - this.m_text[i] = c; - } + public TextBuffer( char c, int length ) + { + text = new char[length]; + this.fill( c ); } - public TextBuffer(String text) { - this(text, 1); + public TextBuffer( String text ) + { + this.text = text.toCharArray(); } - public TextBuffer(String text, int repetitions) { - int textLength = text.length(); - this.m_text = new char[textLength * repetitions]; - for (int i = 0; i < repetitions; i++) { - for (int j = 0; j < textLength; j++) { - this.m_text[j + i * textLength] = text.charAt(j); - } - } + public int length() + { + return text.length; } - public TextBuffer(TextBuffer text) { - this(text, 1); + public void write( String text ) + { + write( text, 0 ); } - public TextBuffer(TextBuffer text, int repetitions) { - int textLength = text.length(); - this.m_text = new char[textLength * repetitions]; - for (int i = 0; i < repetitions; i++) { - for (int j = 0; j < textLength; j++) { - this.m_text[j + i * textLength] = text.charAt(j); - } - } - } - - public int length() { - return this.m_text.length; - } - - public char charAt(int i) { - return this.m_text[i]; - } - - public String read() { - return this.read(0, this.m_text.length); - } - - public String read(int start, int end) { - start = Math.max(start, 0); - end = Math.min(end, this.m_text.length); - int textLength = Math.max(end - start, 0); - return new String(this.m_text, start, textLength); - } - - public String read(int start) { - return this.read(start, this.m_text.length); - } - - public void write(String text) { - this.write(text, 0, text.length()); - } - - public void write(String text, int start, int end) { + public void write( String text, int start ) + { int pos = start; - start = Math.max(start, 0); - end = Math.min(end, pos + text.length()); - end = Math.min(end, this.m_text.length); - for (int i = start; i < end; i++) { - this.m_text[i] = text.charAt(i - pos); + start = Math.max( start, 0 ); + int end = Math.min( start + text.length(), pos + text.length() ); + end = Math.min( end, this.text.length ); + for( int i = start; i < end; i++ ) + { + this.text[i] = text.charAt( i - pos ); } } - public void write(String text, int start) { - this.write(text, start, start + text.length()); - } - - public void write(TextBuffer text) { - this.write(text, 0, text.length()); - } - - public void write(TextBuffer text, int start, int end) { - int pos = start; - start = Math.max(start, 0); - end = Math.min(end, pos + text.length()); - end = Math.min(end, this.m_text.length); - for (int i = start; i < end; i++) { - this.m_text[i] = text.charAt(i - pos); + public void write( TextBuffer text ) + { + int end = Math.min( text.length(), this.text.length ); + for( int i = 0; i < end; i++ ) + { + this.text[i] = text.charAt( i ); } } - public void write(TextBuffer text, int start) { - this.write(text, start, start + text.length()); + public void fill( char c ) + { + fill( c, 0, text.length ); } - public void fill(char c) { - this.fill(c, 0, this.m_text.length); - } - - public void fill(char c, int start, int end) { - start = Math.max(start, 0); - end = Math.min(end, this.m_text.length); - for (int i = start; i < end; i++) { - this.m_text[i] = c; + public void fill( char c, int start, int end ) + { + start = Math.max( start, 0 ); + end = Math.min( end, text.length ); + for( int i = start; i < end; i++ ) + { + text[i] = c; } } - public void fill(char c, int start) { - this.fill(c, start, this.m_text.length); + public char charAt( int i ) + { + return text[i]; } - public void fill(String text) { - this.fill(text, 0, this.m_text.length); - } - - public void fill(String text, int start, int end) { - int pos = start; - start = Math.max(start, 0); - end = Math.min(end, this.m_text.length); - - int textLength = text.length(); - for (int i = start; i < end; i++) { - this.m_text[i] = text.charAt((i - pos) % textLength); + public void setChar( int i, char c ) + { + if( i >= 0 && i < text.length ) + { + text[i] = c; } } - public void fill(String text, int start) { - this.fill(text, start, this.m_text.length); + public String toString() + { + return new String( text ); } - - public void fill(TextBuffer text) { - this.fill(text, 0, this.m_text.length); - } - - public void fill(TextBuffer text, int start, int end) { - int pos = start; - start = Math.max(start, 0); - end = Math.min(end, this.m_text.length); - - int textLength = text.length(); - for (int i = start; i < end; i++) { - this.m_text[i] = text.charAt((i - pos) % textLength); - } - } - - public void fill(TextBuffer text, int start) { - this.fill(text, start, this.m_text.length); - } - - public void setChar(int i, char c) { - if (i >= 0 && i < this.m_text.length) { - this.m_text[i] = c; - } - } - - @Override - public String toString() { - return new String(this.m_text); - } -} +} \ No newline at end of file diff --git a/src/test/java/dan200/computercraft/core/terminal/TextBufferTest.java b/src/test/java/dan200/computercraft/core/terminal/TextBufferTest.java new file mode 100644 index 000000000..286e62ad6 --- /dev/null +++ b/src/test/java/dan200/computercraft/core/terminal/TextBufferTest.java @@ -0,0 +1,150 @@ +/* + * 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.terminal; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class TextBufferTest +{ + @Test + void testStringConstructor() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + assertEquals( "test", textBuffer.toString() ); + } + + @Test + void testCharRepetitionConstructor() + { + TextBuffer textBuffer = new TextBuffer( 'a', 5 ); + assertEquals( "aaaaa", textBuffer.toString() ); + } + + @Test + void testLength() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + assertEquals( 4, textBuffer.length() ); + } + + @Test + void testWrite() + { + TextBuffer textBuffer = new TextBuffer( ' ', 4 ); + textBuffer.write( "test" ); + assertEquals( "test", textBuffer.toString() ); + } + + @Test + void testWriteTextBuffer() + { + TextBuffer source = new TextBuffer( "test" ); + TextBuffer target = new TextBuffer( " " ); + target.write( source ); + assertEquals( "test", target.toString() ); + } + + @Test + void testWriteFromPos() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + textBuffer.write( "il", 1 ); + assertEquals( "tilt", textBuffer.toString() ); + } + + @Test + void testWriteOutOfBounds() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + textBuffer.write( "abcdefghijklmnop", -5 ); + assertEquals( "fghi", textBuffer.toString() ); + } + + @Test + void testWriteOutOfBounds2() + { + TextBuffer textBuffer = new TextBuffer( " " ); + textBuffer.write( "Hello, world!", -3 ); + assertEquals( "lo, world! ", textBuffer.toString() ); + } + + @Test + void testFill() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + textBuffer.fill( 'c' ); + assertEquals( "cccc", textBuffer.toString() ); + } + + @Test + void testFillSubstring() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + textBuffer.fill( 'c', 1, 3 ); + assertEquals( "tcct", textBuffer.toString() ); + } + + @Test + void testFillOutOfBounds() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + textBuffer.fill( 'c', -5, 5 ); + assertEquals( "cccc", textBuffer.toString() ); + } + + @Test + void testCharAt() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + assertEquals( 'e', textBuffer.charAt( 1 ) ); + } + + @Test + void testSetChar() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + textBuffer.setChar( 2, 'n' ); + assertEquals( "tent", textBuffer.toString() ); + } + + @Test + void testSetCharWithNegativeIndex() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + textBuffer.setChar( -5, 'n' ); + assertEquals( "test", textBuffer.toString(), "Buffer should not change after setting char with negative index." ); + } + + @Test + void testSetCharWithIndexBeyondBufferEnd() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + textBuffer.setChar( 10, 'n' ); + assertEquals( "test", textBuffer.toString(), "Buffer should not change after setting char beyond buffer end." ); + } + + @Test + void testMultipleOperations() + { + TextBuffer textBuffer = new TextBuffer( ' ', 5 ); + textBuffer.setChar( 0, 'H' ); + textBuffer.setChar( 1, 'e' ); + textBuffer.setChar( 2, 'l' ); + textBuffer.write( "lo", 3 ); + assertEquals( "Hello", textBuffer.toString(), "TextBuffer failed to persist over multiple operations." ); + } + + @Test + void testEmptyBuffer() + { + TextBuffer textBuffer = new TextBuffer( "" ); + // exception on writing to empty buffer would fail the test + textBuffer.write( "test" ); + assertEquals( "", textBuffer.toString() ); + } +} From cf2b332c3c3e0694bf3dcf399a9c7adecb5d8e4d Mon Sep 17 00:00:00 2001 From: Wojbie Date: Fri, 19 Mar 2021 16:07:20 +0100 Subject: [PATCH 28/34] Fix missing `term.setCursorBlink(true)` in edit.lua --- src/main/resources/data/computercraft/lua/rom/programs/edit.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/data/computercraft/lua/rom/programs/edit.lua b/src/main/resources/data/computercraft/lua/rom/programs/edit.lua index f67bd0ede..c913aa234 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/edit.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/edit.lua @@ -801,6 +801,7 @@ while bRunning do end else bMenu = false + term.setCursorBlink(true) redrawMenu() end end From 3a7470a108225ced250e6f97d53057dd343c5001 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 28 Mar 2021 19:38:25 +0100 Subject: [PATCH 29/34] Add documentation for io.setvbuf Fixes #746. Love how "good first issue" guarantees that nobody will do it. Not actually true, and thank you for those people who have contributed! --- .../resources/data/computercraft/lua/rom/apis/io.lua | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/resources/data/computercraft/lua/rom/apis/io.lua b/src/main/resources/data/computercraft/lua/rom/apis/io.lua index 5ecbdf652..4898df308 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/io.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/io.lua @@ -137,6 +137,15 @@ handleMetatable = { return handle.seek(whence, offset) end, + --[[- Sets the buffering mode for an output file. + + This has no effect under ComputerCraft, and exists with compatility + with base Lua. + @tparam string mode The buffering mode. + @tparam[opt] number size The size of the buffer. + @see file:setvbuf Lua's documentation for `setvbuf`. + @deprecated This has no effect in CC. + ]] setvbuf = function(self, mode, size) end, --- Write one or more values to the file From 558976e4ca2bdb7f0e0404ff41fa3280849caf21 Mon Sep 17 00:00:00 2001 From: lily <56274881+lilyzeiset@users.noreply.github.com> Date: Fri, 2 Apr 2021 10:30:28 -0400 Subject: [PATCH 30/34] Fixed sortCoords for draw functions (#749) --- .../data/computercraft/lua/rom/apis/paintutils.lua | 12 +++++++++++- .../test-rom/spec/apis/paintutils_spec.lua | 13 +++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua b/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua index b920341c3..f6f1efaac 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua @@ -132,7 +132,17 @@ function drawLine(startX, startY, endX, endY, colour) return end - local minX, maxX, minY, maxY = sortCoords(startX, startY, endX, endY) + local minX = math.min(startX, endX) + local maxX, minY, maxY + if minX == startX then + minY = startY + maxX = endX + maxY = endY + else + minY = endY + maxX = startX + maxY = startY + end -- TODO: clip to screen rectangle? diff --git a/src/test/resources/test-rom/spec/apis/paintutils_spec.lua b/src/test/resources/test-rom/spec/apis/paintutils_spec.lua index fc2b6008c..f13bbcfd2 100644 --- a/src/test/resources/test-rom/spec/apis/paintutils_spec.lua +++ b/src/test/resources/test-rom/spec/apis/paintutils_spec.lua @@ -67,6 +67,19 @@ describe("The paintutils library", function() { " ", "000", "ffe" }, }) end) + + it("draws a line going diagonally from bottom left", function() + local w = with_window(3, 3, function() + term.setBackgroundColour(colours.red) + paintutils.drawLine(1, 3, 3, 1) + end) + + window_eq(w, { + { " ", "000", "ffe" }, + { " ", "000", "fef" }, + { " ", "000", "eff" }, + }) + end) end) describe("paintutils.drawBox", function() From 66dbab7a6bd1c4bf6adf34cc7e3b9b1c135bd113 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 3 Apr 2021 12:44:22 +0100 Subject: [PATCH 31/34] "Finish" documentation for several modules - Add remaining docs for the turtle API - Add documentation for the fluid storage peripheral. - Enforce undocumented warning for most modules (only io and window remaining). "Finish" in quotes, because these are clearly a long way from perfect. I'm bad at writing docs, OK! --- .../generic/methods/InventoryMethods.java | 4 +- .../shared/turtle/apis/TurtleAPI.java | 118 ++++++++++++++++++ .../lua/rom/apis/turtle/turtle.lua | 1 + 3 files changed, 121 insertions(+), 2 deletions(-) diff --git a/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java index 76186fc24..48ea969fb 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java @@ -85,8 +85,8 @@ public class InventoryMethods implements GenericSource * @link dan200.computercraft.shared.turtle.apis.TurtleAPI#getItemDetail includes. More information can be fetched * with {@link #getItemDetail}. * - * The table is sparse, and so empty slots will be `nil` - it is recommended to loop over using `pairs` rather than - * `ipairs`. + * The returned table is sparse, and so empty slots will be `nil` - it is recommended to loop over using `pairs` + * rather than `ipairs`. * * @param inventory The current inventory. * @return All items in this inventory. diff --git a/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java b/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java index 44625bdc3..00a4f3638 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java +++ b/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java @@ -386,16 +386,34 @@ public class TurtleAPI implements ILuaAPI { return this.trackCommand(new TurtleDetectCommand(InteractDirection.DOWN)); } + /** + * Check if the block in front of the turtle is equal to the item in the currently selected slot. + * + * @return If the block and item are equal. + * @cc.treturn boolean If the block and item are equal. + */ @LuaFunction public final MethodResult compare() { return this.trackCommand(new TurtleCompareCommand(InteractDirection.FORWARD)); } + /** + * Check if the block above the turtle is equal to the item in the currently selected slot. + * + * @return If the block and item are equal. + * @cc.treturn boolean If the block and item are equal. + */ @LuaFunction public final MethodResult compareUp() { return this.trackCommand(new TurtleCompareCommand(InteractDirection.UP)); } + /** + * Check if the block below the turtle is equal to the item in the currently selected slot. + * + * @return If the block and item are equal. + * @cc.treturn boolean If the block and item are equal. + */ @LuaFunction public final MethodResult compareDown() { return this.trackCommand(new TurtleCompareCommand(InteractDirection.DOWN)); @@ -484,11 +502,56 @@ public class TurtleAPI implements ILuaAPI { return this.trackCommand(new TurtleSuckCommand(InteractDirection.DOWN, checkCount(count))); } + /** + * Get the maximum amount of fuel this turtle currently holds. + * + * @return The fuel level, or "unlimited". + * @cc.treturn[1] number The current amount of fuel a turtle this turtle has. + * @cc.treturn[2] "unlimited" If turtles do not consume fuel when moving. + * @see #getFuelLimit() + * @see #refuel(Optional) + */ @LuaFunction public final Object getFuelLevel() { return this.turtle.isFuelNeeded() ? this.turtle.getFuelLevel() : "unlimited"; } + /** + * Refuel this turtle. + * + * While most actions a turtle can perform (such as digging or placing blocks), moving consumes fuel from the + * turtle's internal buffer. If a turtle has no fuel, it will not move. + * + * {@link #refuel} refuels the turtle, consuming fuel items (such as coal or lava buckets) from the currently + * selected slot and converting them into energy. This finishes once the turtle is fully refuelled or all items have + * been consumed. + * + * @param countA The maximum number of items to consume. One can pass `0` to check if an item is combustable or not. + * @return If this turtle could be refuelled. + * @throws LuaException If the refuel count is out of range. + * @cc.treturn[1] true If the turtle was refuelled. + * @cc.treturn[2] false If the turtle was not refuelled. + * @cc.treturn[2] string The reason the turtle was not refuelled ( + * @cc.usage Refuel a turtle from the currently selected slot. + *
{@code
+     * local level = turtle.getFuelLevel()
+     * if new_level == "unlimited" then error("Turtle does not need fuel", 0) end
+     *
+     * local ok, err = turtle.refuel()
+     * if ok then
+     *   local new_level = turtle.getFuelLevel()
+     *   print(("Refuelled %d, current level is %d"):format(new_level - level, new_level))
+     * else
+     *   printError(err)
+     * end}
+ * @cc.usage Check if the current item is a valid fuel source. + *
{@code
+     * local is_fuel, reason = turtle.refuel(0)
+     * if not is_fuel then printError(reason) end
+     * }
+ * @see #getFuelLevel() + * @see #getFuelLimit() + */ @LuaFunction public final MethodResult refuel(Optional countA) throws LuaException { int count = countA.orElse(Integer.MAX_VALUE); @@ -498,11 +561,29 @@ public class TurtleAPI implements ILuaAPI { return this.trackCommand(new TurtleRefuelCommand(count)); } + /** + * Compare the item in the currently selected slot to the item in another slot. + * + * @param slot The slot to compare to. + * @return If the items are the same. + * @throws LuaException If the slot is out of range. + * @cc.treturn boolean If the two items are equal. + */ @LuaFunction public final MethodResult compareTo(int slot) throws LuaException { return this.trackCommand(new TurtleCompareToCommand(checkSlot(slot))); } + /** + * Move an item from the selected slot to another one. + * + * @param slotArg The slot to move this item to. + * @param countArg The maximum number of items to move. + * @return If the item was moved or not. + * @throws LuaException If the slot is out of range. + * @throws LuaException If the number of items is out of range. + * @cc.treturn boolean If some items were successfully moved. + */ @LuaFunction public final MethodResult transferTo(int slotArg, Optional countArg) throws LuaException { int slot = checkSlot(slotArg); @@ -521,16 +602,53 @@ public class TurtleAPI implements ILuaAPI { return this.turtle.getSelectedSlot() + 1; } + /** + * Get the maximum amount of fuel this turtle can hold. + * + * By default, normal turtles have a limit of 20,000 and advanced turtles of 100,000. + * + * @return The limit, or "unlimited". + * @cc.treturn[1] number The maximum amount of fuel a turtle can hold. + * @cc.treturn[2] "unlimited" If turtles do not consume fuel when moving. + * @see #getFuelLevel() + * @see #refuel(Optional) + */ @LuaFunction public final Object getFuelLimit() { return this.turtle.isFuelNeeded() ? this.turtle.getFuelLimit() : "unlimited"; } + /** + * Equip (or unequip) an item on the left side of this turtle. + * + * This finds the item in the currently selected slot and attempts to equip it to the left side of the turtle. The + * previous upgrade is removed and placed into the turtle's inventory. If there is no item in the slot, the previous + * upgrade is removed, but no new one is equipped. + * + * @return Whether an item was equiped or not. + * @cc.treturn[1] true If the item was equipped. + * @cc.treturn[2] false If we could not equip the item. + * @cc.treturn[2] string The reason equipping this item failed. + * @see #equipRight() + */ @LuaFunction public final MethodResult equipLeft() { return this.trackCommand(new TurtleEquipCommand(TurtleSide.LEFT)); } + /** + * Equip (or unequip) an item on the right side of this turtle. + * + * This finds the item in the currently selected slot and attempts to equip it to the right side of the turtle. The + * previous upgrade is removed and placed into the turtle's inventory. If there is no item in the slot, the previous + * upgrade is removed, but no new one is equipped. + * + * @return Whether an item was equiped or not. + * @cc.treturn[1] true If the item was equipped. + * @cc.treturn[2] false If we could not equip the item. + * @cc.treturn[2] string The reason equipping this item failed. + * @see #equipRight() + */ @LuaFunction public final MethodResult equipRight() { return this.trackCommand(new TurtleEquipCommand(TurtleSide.RIGHT)); diff --git a/src/main/resources/data/computercraft/lua/rom/apis/turtle/turtle.lua b/src/main/resources/data/computercraft/lua/rom/apis/turtle/turtle.lua index c9d57bf12..0a66add16 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/turtle/turtle.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/turtle/turtle.lua @@ -10,6 +10,7 @@ end -- -- Generally you should not need to use this table - it only exists for -- backwards compatibility reasons. +-- @deprecated native = turtle.native or turtle local function addCraftMethod(object) From 57e6c49844eaf7b3ca0ac5930673bbc2e0bad668 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 7 Apr 2021 18:34:55 +0100 Subject: [PATCH 32/34] Make the peripheral API examples a little clearer --- .../computercraft/lua/rom/apis/peripheral.lua | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/main/resources/data/computercraft/lua/rom/apis/peripheral.lua b/src/main/resources/data/computercraft/lua/rom/apis/peripheral.lua index c284d0691..0f3478046 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/peripheral.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/peripheral.lua @@ -161,7 +161,9 @@ end -- @tparam string name The name of the peripheral to wrap. -- @treturn table|nil The table containing the peripheral's methods, or `nil` if -- there is no peripheral present with the given name. --- @usage peripheral.wrap("top").open(1) +-- @usage Open the modem on the top of this computer. +-- +-- peripheral.wrap("top").open(1) function wrap(name) expect(1, name, "string") @@ -183,16 +185,25 @@ function wrap(name) return result end ---- Find all peripherals of a specific type, and return the --- @{peripheral.wrap|wrapped} peripherals. --- --- @tparam string ty The type of peripheral to look for. --- @tparam[opt] function(name:string, wrapped:table):boolean filter A --- filter function, which takes the peripheral's name and wrapped table --- and returns if it should be included in the result. --- @treturn table... 0 or more wrapped peripherals matching the given filters. --- @usage { peripheral.find("monitor") } --- @usage peripheral.find("modem", rednet.open) +--[[- Find all peripherals of a specific type, and return the +@{peripheral.wrap|wrapped} peripherals. + +@tparam string ty The type of peripheral to look for. +@tparam[opt] function(name:string, wrapped:table):boolean filter A +filter function, which takes the peripheral's name and wrapped table +and returns if it should be included in the result. +@treturn table... 0 or more wrapped peripherals matching the given filters. +@usage Find all monitors and store them in a table, writing "Hello" on each one. + + local monitors = { peripheral.find("monitor") } + for _, monitor in pairs(monitors) do + monitor.write("Hello") + end + +@usage This abuses the `filter` argument to call @{rednet.open} on every modem. + + peripheral.find("modem", rednet.open) +]] function find(ty, filter) expect(1, ty, "string") expect(2, filter, "function", "nil") From d955443b21f834cf77f40ce5da0641323543d866 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 11 Apr 2021 18:43:24 +0100 Subject: [PATCH 33/34] Add citation to cc.pretty ust to look extra pretentious. --- .../lua/rom/modules/main/cc/pretty.lua | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/pretty.lua b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/pretty.lua index 9a3aa7b0a..f202c204e 100644 --- a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/pretty.lua +++ b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/pretty.lua @@ -1,23 +1,28 @@ ---- Provides a "pretty printer", for rendering data structures in an --- aesthetically pleasing manner. --- --- In order to display something using @{cc.pretty}, you build up a series of --- @{Doc|documents}. These behave a little bit like strings; you can concatenate --- them together and then print them to the screen. --- --- However, documents also allow you to control how they should be printed. There --- are several functions (such as @{nest} and @{group}) which allow you to control --- the "layout" of the document. When you come to display the document, the 'best' --- (most compact) layout is used. --- --- @module cc.pretty --- @usage Print a table to the terminal --- local pretty = require "cc.pretty" --- pretty.print(pretty.pretty({ 1, 2, 3 })) --- --- @usage Build a custom document and display it --- local pretty = require "cc.pretty" --- pretty.print(pretty.group(pretty.text("hello") .. pretty.space_line .. pretty.text("world"))) +--[[- Provides a "pretty printer", for rendering data structures in an +aesthetically pleasing manner. + +In order to display something using @{cc.pretty}, you build up a series of +@{Doc|documents}. These behave a little bit like strings; you can concatenate +them together and then print them to the screen. + +However, documents also allow you to control how they should be printed. There +are several functions (such as @{nest} and @{group}) which allow you to control +the "layout" of the document. When you come to display the document, the 'best' +(most compact) layout is used. + +The structure of this module is based on [A Prettier Printer][prettier]. + +[prettier]: https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf "A Prettier Printer" + +@module cc.pretty +@usage Print a table to the terminal + local pretty = require "cc.pretty" + pretty.print(pretty.pretty({ 1, 2, 3 })) + +@usage Build a custom document and display it + local pretty = require "cc.pretty" + pretty.print(pretty.group(pretty.text("hello") .. pretty.space_line .. pretty.text("world"))) +]] local expect = require "cc.expect" local expect, field = expect.expect, expect.field From 46846a4fdee6003a20c5d80d8688d1f1046d86e6 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Tue, 13 Apr 2021 13:01:28 +0100 Subject: [PATCH 34/34] Improve UX when a resource mount cannot be found - Add a full example of the docs. Hopefully is a little more explicit. - Print a warning when the mount is empty. Closes #762 --- patchwork.md | 6 +++++ .../computercraft/api/ComputerCraftAPI.java | 5 +++- .../core/filesystem/ResourceMount.java | 25 +++++++++++++------ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/patchwork.md b/patchwork.md index aead31369..40372bb64 100644 --- a/patchwork.md +++ b/patchwork.md @@ -619,3 +619,9 @@ b90611b4b4c176ec1c80df002cc4ac36aa0c4dc8 Preserve registration order of upgrades ``` Again, a huge diff because of code style changes. + +``` +8494ba8ce29cd8d7b9105eef497fe3fe3f89d350 + +Improve UX when a resource mount cannot be found +``` diff --git a/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java b/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java index 7a7de9cb7..4868de353 100644 --- a/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java +++ b/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java @@ -108,7 +108,10 @@ public final class ComputerCraftAPI { * Use in conjunction with {@link IComputerAccess#mount} or {@link IComputerAccess#mountWritable} to mount a resource folder onto a computer's file * system. * - * The files in this mount will be a combination of files in all mod jar, and data packs that contain resources with the same domain and path. + * The files in this mount will be a combination of files in all mod jar, and data packs that contain + * resources with the same domain and path. For instance, ComputerCraft's resources are stored in + * "/data/computercraft/lua/rom". We construct a mount for that with + * {@code createResourceMount("computercraft", "lua/rom")}. * * @param domain The domain under which to look for resources. eg: "mymod". * @param subPath The subPath under which to look for resources. eg: "lua/myfiles". diff --git a/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java b/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java index a9f2b243c..e767525b6 100644 --- a/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java +++ b/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java @@ -90,19 +90,30 @@ public final class ResourceMount implements IMount { private void load() { boolean hasAny = false; - FileEntry newRoot = new FileEntry(new Identifier(this.namespace, this.subPath)); - for (Identifier file : this.manager.findResources(this.subPath, s -> true)) { - if (!file.getNamespace() - .equals(this.namespace)) { - continue; - } + String existingNamespace = null; + + FileEntry newRoot = new FileEntry( new Identifier( namespace, subPath ) ); + for( Identifier file : manager.findResources( subPath, s -> true ) ) + { + existingNamespace = file.getNamespace(); + + if( !file.getNamespace().equals( namespace ) ) continue; String localPath = FileSystem.toLocal(file.getPath(), this.subPath); this.create(newRoot, localPath); hasAny = true; } - this.root = hasAny ? newRoot : null; + root = hasAny ? newRoot : null; + + if( !hasAny ) + { + ComputerCraft.log.warn("Cannot find any files under /data/{}/{} for resource mount.", namespace, subPath); + if( newRoot != null ) + { + ComputerCraft.log.warn("There are files under /data/{}/{} though. Did you get the wrong namespace?", existingNamespace, subPath); + } + } } private void create(FileEntry lastEntry, String path) {