From 88f0c441525d11b194fd432df910e163b0887f9d Mon Sep 17 00:00:00 2001 From: Wojbie Date: Tue, 20 Jun 2023 23:33:53 +0200 Subject: [PATCH 01/32] Update lua.lua require logic. This makes it more consistent in situations when someone requires with path starting with /. --- .../computercraft/lua/rom/programs/lua.lua | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/lua.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/lua.lua index 7e00ead50..3d2faebe6 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/lua.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/lua.lua @@ -25,21 +25,13 @@ local tEnv = { } setmetatable(tEnv, { __index = _ENV }) --- Replace our package.path, so that it loads from the current directory, rather --- than from /rom/programs. This makes it a little more friendly to use and --- closer to what you'd expect. +-- Replace our require with new instance that loads from the current directory +-- rather than from /rom/programs. This makes it more friendly to use and closer +-- to what you'd expect. do + local make_package = require "cc.require".make local dir = shell.dir() - if dir:sub(1, 1) ~= "/" then dir = "/" .. dir end - if dir:sub(-1) ~= "/" then dir = dir .. "/" end - - local strip_path = "?;?.lua;?/init.lua;" - local path = package.path - if path:sub(1, #strip_path) == strip_path then - path = path:sub(#strip_path + 1) - end - - package.path = dir .. "?;" .. dir .. "?.lua;" .. dir .. "?/init.lua;" .. path + _ENV.require, _ENV.package = make_package(_ENV, dir) end if term.isColour() then From 672c2cf029f99ea5d5138fab1a63f270743dccf1 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 24 Jun 2023 17:09:34 +0100 Subject: [PATCH 02/32] Limit turtle's reach distance in Item.use When a turtle attempts to place a block, it does so by searching for nearby blocks and attempting to place the item against that block. This has slightly strange behaviour when working with "placable" non-block items though (such as buckets or boats). In this case, we call Item.use, which doesn't take in the position of the block we're placing against. Instead these items do their own ray trace, using the default reach distance. If the block we're trying to place against is non-solid, the ray trace will go straight through it and continue (up to the maximum of 5 blocks), allowing placing the item much further away. Our fix here is to override the default reach distance of our fake players, limiting it to 2. This is easy on Forge (it has built-in support), and requires a mixin on Fabric. Closes #1497. --- .../shared/platform/FakePlayerConstants.java | 31 ++++ .../computercraft/gametest/Turtle_Test.kt | 24 ++- .../turtle_test.place_use_reach_limit.snbt | 139 ++++++++++++++++++ .../dan200/computercraft/mixin/ItemMixin.java | 32 ++++ .../shared/platform/FakePlayer.java | 8 +- .../computercraft.fabric.mixins.json | 1 + .../shared/platform/FakePlayerExt.java | 12 ++ 7 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 projects/common/src/main/java/dan200/computercraft/shared/platform/FakePlayerConstants.java create mode 100644 projects/common/src/testMod/resources/data/cctest/structures/turtle_test.place_use_reach_limit.snbt create mode 100644 projects/fabric/src/main/java/dan200/computercraft/mixin/ItemMixin.java diff --git a/projects/common/src/main/java/dan200/computercraft/shared/platform/FakePlayerConstants.java b/projects/common/src/main/java/dan200/computercraft/shared/platform/FakePlayerConstants.java new file mode 100644 index 000000000..a50c8be85 --- /dev/null +++ b/projects/common/src/main/java/dan200/computercraft/shared/platform/FakePlayerConstants.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.platform; + +import com.mojang.authlib.GameProfile; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.ItemStack; + +/** + * Shared constants for {@linkplain PlatformHelper#createFakePlayer(ServerLevel, GameProfile) fake player} + * implementations. + * + * @see net.minecraft.server.level.ServerPlayer + * @see net.minecraft.world.entity.player.Player + */ +final class FakePlayerConstants { + private FakePlayerConstants() { + } + + /** + * The maximum distance this player can reach. + *

+ * This is used in the override of {@link net.minecraft.world.entity.player.Player#mayUseItemAt(BlockPos, Direction, ItemStack)}, + * to prevent the fake player reaching more than 2 blocks away. + */ + static final double MAX_REACH = 2; +} diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt index 259ff9b8d..454e5273d 100644 --- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt @@ -32,8 +32,10 @@ import net.minecraft.world.item.ItemStack import net.minecraft.world.item.Items import net.minecraft.world.level.block.Blocks import net.minecraft.world.level.block.FenceBlock +import net.minecraft.world.level.block.state.properties.BlockStateProperties import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.* +import org.hamcrest.Matchers.array +import org.hamcrest.Matchers.instanceOf import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotEquals import java.util.* @@ -78,6 +80,26 @@ class Turtle_Test { thenExecute { helper.assertBlockPresent(Blocks.LAVA, BlockPos(2, 2, 2)) } } + /** + * Checks that calling [net.minecraft.world.item.Item.use] will not place blocks too far away. + * + * This is caused by items using [net.minecraft.world.item.Item.getPlayerPOVHitResult] to perform a ray trace, which + * ignores turtle's reduced reach distance. + * + * @see [#1497](https://github.com/cc-tweaked/CC-Tweaked/issues/1497) + */ + @GameTest + fun Place_use_reach_limit(helper: GameTestHelper) = helper.sequence { + thenOnComputer { + turtle.placeDown(ObjectArguments()).await() + .assertArrayEquals(true, message = "Placed water") + } + thenExecute { + helper.assertBlockPresent(Blocks.AIR, BlockPos(2, 2, 2)) + helper.assertBlockHas(BlockPos(2, 5, 2), BlockStateProperties.WATERLOGGED, true) + } + } + /** * Checks turtles can place when waterlogged. * diff --git a/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.place_use_reach_limit.snbt b/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.place_use_reach_limit.snbt new file mode 100644 index 000000000..aaf9f9181 --- /dev/null +++ b/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.place_use_reach_limit.snbt @@ -0,0 +1,139 @@ +{ + DataVersion: 3337, + size: [5, 5, 5], + data: [ + {pos: [0, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [0, 1, 0], state: "minecraft:air"}, + {pos: [0, 1, 1], state: "minecraft:air"}, + {pos: [0, 1, 2], state: "minecraft:air"}, + {pos: [0, 1, 3], state: "minecraft:air"}, + {pos: [0, 1, 4], state: "minecraft:air"}, + {pos: [1, 1, 0], state: "minecraft:air"}, + {pos: [1, 1, 1], state: "minecraft:air"}, + {pos: [1, 1, 2], state: "minecraft:light_gray_stained_glass"}, + {pos: [1, 1, 3], state: "minecraft:air"}, + {pos: [1, 1, 4], state: "minecraft:air"}, + {pos: [2, 1, 0], state: "minecraft:air"}, + {pos: [2, 1, 1], state: "minecraft:light_gray_stained_glass"}, + {pos: [2, 1, 2], state: "minecraft:air"}, + {pos: [2, 1, 3], state: "minecraft:light_gray_stained_glass"}, + {pos: [2, 1, 4], state: "minecraft:air"}, + {pos: [3, 1, 0], state: "minecraft:air"}, + {pos: [3, 1, 1], state: "minecraft:air"}, + {pos: [3, 1, 2], state: "minecraft:light_gray_stained_glass"}, + {pos: [3, 1, 3], state: "minecraft:air"}, + {pos: [3, 1, 4], state: "minecraft:air"}, + {pos: [4, 1, 0], state: "minecraft:air"}, + {pos: [4, 1, 1], state: "minecraft:air"}, + {pos: [4, 1, 2], state: "minecraft:air"}, + {pos: [4, 1, 3], state: "minecraft:air"}, + {pos: [4, 1, 4], state: "minecraft:air"}, + {pos: [0, 2, 0], state: "minecraft:air"}, + {pos: [0, 2, 1], state: "minecraft:air"}, + {pos: [0, 2, 2], state: "minecraft:air"}, + {pos: [0, 2, 3], state: "minecraft:air"}, + {pos: [0, 2, 4], state: "minecraft:air"}, + {pos: [1, 2, 0], state: "minecraft:air"}, + {pos: [1, 2, 1], state: "minecraft:air"}, + {pos: [1, 2, 2], state: "minecraft:air"}, + {pos: [1, 2, 3], state: "minecraft:air"}, + {pos: [1, 2, 4], state: "minecraft:air"}, + {pos: [2, 2, 0], state: "minecraft:air"}, + {pos: [2, 2, 1], state: "minecraft:air"}, + {pos: [2, 2, 2], state: "minecraft:ladder{facing:north,waterlogged:false}"}, + {pos: [2, 2, 3], state: "minecraft:light_gray_stained_glass"}, + {pos: [2, 2, 4], state: "minecraft:air"}, + {pos: [3, 2, 0], state: "minecraft:air"}, + {pos: [3, 2, 1], state: "minecraft:air"}, + {pos: [3, 2, 2], state: "minecraft:air"}, + {pos: [3, 2, 3], state: "minecraft:air"}, + {pos: [3, 2, 4], state: "minecraft:air"}, + {pos: [4, 2, 0], state: "minecraft:air"}, + {pos: [4, 2, 1], state: "minecraft:air"}, + {pos: [4, 2, 2], state: "minecraft:air"}, + {pos: [4, 2, 3], state: "minecraft:air"}, + {pos: [4, 2, 4], state: "minecraft:air"}, + {pos: [0, 3, 0], state: "minecraft:air"}, + {pos: [0, 3, 1], state: "minecraft:air"}, + {pos: [0, 3, 2], state: "minecraft:air"}, + {pos: [0, 3, 3], state: "minecraft:air"}, + {pos: [0, 3, 4], state: "minecraft:air"}, + {pos: [1, 3, 0], state: "minecraft:air"}, + {pos: [1, 3, 1], state: "minecraft:light_gray_stained_glass"}, + {pos: [1, 3, 2], state: "minecraft:light_gray_stained_glass"}, + {pos: [1, 3, 3], state: "minecraft:light_gray_stained_glass"}, + {pos: [1, 3, 4], state: "minecraft:air"}, + {pos: [2, 3, 0], state: "minecraft:air"}, + {pos: [2, 3, 1], state: "minecraft:light_gray_stained_glass"}, + {pos: [2, 3, 2], state: "minecraft:air"}, + {pos: [2, 3, 3], state: "minecraft:light_gray_stained_glass"}, + {pos: [2, 3, 4], state: "minecraft:air"}, + {pos: [3, 3, 0], state: "minecraft:air"}, + {pos: [3, 3, 1], state: "minecraft:light_gray_stained_glass"}, + {pos: [3, 3, 2], state: "minecraft:light_gray_stained_glass"}, + {pos: [3, 3, 3], state: "minecraft:light_gray_stained_glass"}, + {pos: [3, 3, 4], state: "minecraft:air"}, + {pos: [4, 3, 0], state: "minecraft:air"}, + {pos: [4, 3, 1], state: "minecraft:air"}, + {pos: [4, 3, 2], state: "minecraft:air"}, + {pos: [4, 3, 3], state: "minecraft:air"}, + {pos: [4, 3, 4], state: "minecraft:air"}, + {pos: [0, 4, 0], state: "minecraft:air"}, + {pos: [0, 4, 1], state: "minecraft:air"}, + {pos: [0, 4, 2], state: "minecraft:air"}, + {pos: [0, 4, 3], state: "minecraft:air"}, + {pos: [0, 4, 4], state: "minecraft:air"}, + {pos: [1, 4, 0], state: "minecraft:air"}, + {pos: [1, 4, 1], state: "minecraft:light_gray_stained_glass"}, + {pos: [1, 4, 2], state: "minecraft:light_gray_stained_glass"}, + {pos: [1, 4, 3], state: "minecraft:light_gray_stained_glass"}, + {pos: [1, 4, 4], state: "minecraft:air"}, + {pos: [2, 4, 0], state: "minecraft:air"}, + {pos: [2, 4, 1], state: "minecraft:light_gray_stained_glass"}, + {pos: [2, 4, 2], state: "computercraft:turtle_normal{facing:north,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [{Count: 1b, Slot: 0b, id: "minecraft:water_bucket"}], Label: "turtle_test.place_use_reach_limit", On: 1b, Slot: 0, id: "computercraft:turtle_normal"}}, + {pos: [2, 4, 3], state: "minecraft:light_gray_stained_glass"}, + {pos: [2, 4, 4], state: "minecraft:air"}, + {pos: [3, 4, 0], state: "minecraft:air"}, + {pos: [3, 4, 1], state: "minecraft:light_gray_stained_glass"}, + {pos: [3, 4, 2], state: "minecraft:light_gray_stained_glass"}, + {pos: [3, 4, 3], state: "minecraft:light_gray_stained_glass"}, + {pos: [3, 4, 4], state: "minecraft:air"}, + {pos: [4, 4, 0], state: "minecraft:air"}, + {pos: [4, 4, 1], state: "minecraft:air"}, + {pos: [4, 4, 2], state: "minecraft:air"}, + {pos: [4, 4, 3], state: "minecraft:air"}, + {pos: [4, 4, 4], state: "minecraft:air"} + ], + entities: [], + palette: [ + "minecraft:polished_andesite", + "minecraft:light_gray_stained_glass", + "minecraft:air", + "minecraft:ladder{facing:north,waterlogged:false}", + "computercraft:turtle_normal{facing:north,waterlogged:false}" + ] +} diff --git a/projects/fabric/src/main/java/dan200/computercraft/mixin/ItemMixin.java b/projects/fabric/src/main/java/dan200/computercraft/mixin/ItemMixin.java new file mode 100644 index 000000000..b1b18593b --- /dev/null +++ b/projects/fabric/src/main/java/dan200/computercraft/mixin/ItemMixin.java @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.mixin; + +import dan200.computercraft.shared.platform.FakePlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +@Mixin(Item.class) +class ItemMixin { + /** + * Replace the reach distance in {@link Item#getPlayerPOVHitResult(Level, Player, ClipContext.Fluid)}. + * + * @param reach The original reach distance. + * @param level The current level. + * @param player The current player. + * @return The new reach distance. + * @see FakePlayer#getBlockReach() + */ + @ModifyConstant(method = "getPlayerPOVHitResult", constant = @Constant(doubleValue = 5)) + @SuppressWarnings("UnusedMethod") + private static double getReachDistance(double reach, Level level, Player player) { + return player instanceof FakePlayer fp ? fp.getBlockReach() : reach; + } +} diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FakePlayer.java b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FakePlayer.java index 5f2b0c412..c48c1db1f 100644 --- a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FakePlayer.java +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FakePlayer.java @@ -11,7 +11,9 @@ import net.minecraft.world.entity.EntityDimensions; import net.minecraft.world.entity.Pose; import net.minecraft.world.entity.player.Player; -final class FakePlayer extends net.fabricmc.fabric.api.entity.FakePlayer { +import static dan200.computercraft.shared.platform.FakePlayerConstants.MAX_REACH; + +public final class FakePlayer extends net.fabricmc.fabric.api.entity.FakePlayer { private FakePlayer(ServerLevel serverLevel, GameProfile gameProfile) { super(serverLevel, gameProfile); } @@ -33,4 +35,8 @@ final class FakePlayer extends net.fabricmc.fabric.api.entity.FakePlayer { public float getStandingEyeHeight(Pose pose, EntityDimensions dimensions) { return 0; } + + public double getBlockReach() { + return MAX_REACH; + } } diff --git a/projects/fabric/src/main/resources/computercraft.fabric.mixins.json b/projects/fabric/src/main/resources/computercraft.fabric.mixins.json index 186ada5fe..0b6d01a0c 100644 --- a/projects/fabric/src/main/resources/computercraft.fabric.mixins.json +++ b/projects/fabric/src/main/resources/computercraft.fabric.mixins.json @@ -12,6 +12,7 @@ "EntityMixin", "ExplosionDamageCalculatorMixin", "ItemEntityMixin", + "ItemMixin", "ServerLevelMixin", "ShapedRecipeMixin", "TagEntryAccessor", diff --git a/projects/forge/src/main/java/dan200/computercraft/shared/platform/FakePlayerExt.java b/projects/forge/src/main/java/dan200/computercraft/shared/platform/FakePlayerExt.java index 060e080a4..aa8340722 100644 --- a/projects/forge/src/main/java/dan200/computercraft/shared/platform/FakePlayerExt.java +++ b/projects/forge/src/main/java/dan200/computercraft/shared/platform/FakePlayerExt.java @@ -16,6 +16,8 @@ import net.minecraftforge.common.util.FakePlayer; import javax.annotation.Nullable; import java.util.OptionalInt; +import static dan200.computercraft.shared.platform.FakePlayerConstants.MAX_REACH; + class FakePlayerExt extends FakePlayer { FakePlayerExt(ServerLevel serverLevel, GameProfile profile) { super(serverLevel, profile); @@ -45,4 +47,14 @@ class FakePlayerExt extends FakePlayer { public float getStandingEyeHeight(Pose pose, EntityDimensions dimensions) { return 0; } + + @Override + public double getBlockReach() { + return MAX_REACH; + } + + @Override + public double getEntityReach() { + return MAX_REACH; + } } From 7ffdbb231696e6fa78ce4e6f4592c003d7040f54 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 25 Jun 2023 09:47:56 +0100 Subject: [PATCH 03/32] Publish docs for 1.20 as well --- .github/workflows/make-doc.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/make-doc.yml b/.github/workflows/make-doc.yml index 08ca1df77..5b22ee6f2 100644 --- a/.github/workflows/make-doc.yml +++ b/.github/workflows/make-doc.yml @@ -4,6 +4,7 @@ on: push: branches: - mc-1.19.x + - mc-1.20.x jobs: make_doc: From 54ab98473f92f85dda8249f4b74ea6a1d3a839a2 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 25 Jun 2023 15:48:57 +0100 Subject: [PATCH 04/32] Be lazy in reporting errors in the lexer Instead of reporting an error with `.report(f(...))`, we now do `.report(f, ...)`. This allows consumers to ignore error messages when not needed, such as when just doing syntax highlighting. --- .../modules/main/cc/internal/syntax/init.lua | 12 ++++++---- .../modules/main/cc/internal/syntax/lexer.lua | 24 +++++++++---------- .../main/cc/internal/syntax/parser.lua | 2 +- .../cc/internal/syntax/syntax_helpers.lua | 5 ++-- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/init.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/init.lua index 7df278186..59125c945 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/init.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/init.lua @@ -21,6 +21,8 @@ local error_printer = require "cc.internal.error_printer" local error_sentinel = {} local function make_context(input) + expect(1, input, "string") + local context = {} local lines = { 1 } @@ -73,8 +75,9 @@ local function parse(input, start_symbol) expect(2, start_symbol, "number") local context = make_context(input) - function context.report(msg) - expect(1, msg, "table") + function context.report(msg, ...) + expect(1, msg, "table", "function") + if type(msg) == "function" then msg = msg(...) end error_printer(context, msg) error(error_sentinel) end @@ -110,8 +113,9 @@ local function parse_repl(input) local context = make_context(input) local last_error = nil - function context.report(msg) - expect(1, msg, "table") + function context.report(msg, ...) + expect(1, msg, "table", "function") + if type(msg) == "function" then msg = msg(...) end last_error = msg error(error_sentinel) end diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/lexer.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/lexer.lua index 16436b3ea..e775d590b 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/lexer.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/lexer.lua @@ -96,7 +96,7 @@ local function lex_number(context, str, start) local contents = sub(str, start, pos - 1) if not tonumber(contents) then -- TODO: Separate error for "2..3"? - context.report(errors.malformed_number(start, pos - 1)) + context.report(errors.malformed_number, start, pos - 1) end return tokens.NUMBER, pos - 1 @@ -118,14 +118,14 @@ local function lex_string(context, str, start_pos, quote) return tokens.STRING, pos elseif c == "\n" or c == "\r" or c == "" then -- We don't call newline here, as that's done for the next token. - context.report(errors.unfinished_string(start_pos, pos, quote)) + context.report(errors.unfinished_string, start_pos, pos, quote) return tokens.STRING, pos - 1 elseif c == "\\" then c = sub(str, pos + 1, pos + 1) if c == "\n" or c == "\r" then pos = newline(context, str, pos + 1, c) elseif c == "" then - context.report(errors.unfinished_string_escape(start_pos, pos, quote)) + context.report(errors.unfinished_string_escape, start_pos, pos, quote) return tokens.STRING, pos elseif c == "z" then pos = pos + 2 @@ -133,7 +133,7 @@ local function lex_string(context, str, start_pos, quote) local next_pos, _, c = find(str, "([%S\r\n])", pos) if not next_pos then - context.report(errors.unfinished_string(start_pos, #str, quote)) + context.report(errors.unfinished_string, start_pos, #str, quote) return tokens.STRING, #str end @@ -196,7 +196,7 @@ local function lex_long_str(context, str, start, len) elseif c == "[" then local ok, boundary_pos = lex_long_str_boundary(str, pos + 1, "[") if ok and boundary_pos - pos == len and len == 1 then - context.report(errors.nested_long_str(pos, boundary_pos)) + context.report(errors.nested_long_str, pos, boundary_pos) end pos = boundary_pos @@ -238,12 +238,12 @@ local function lex_token(context, str, pos) local end_pos = lex_long_str(context, str, boundary_pos + 1, boundary_pos - pos) if end_pos then return tokens.STRING, end_pos end - context.report(errors.unfinished_long_string(pos, boundary_pos, boundary_pos - pos)) + context.report(errors.unfinished_long_string, pos, boundary_pos, boundary_pos - pos) return tokens.ERROR, #str elseif pos + 1 == boundary_pos then -- Just a "[" return tokens.OSQUARE, pos else -- Malformed long string, for instance "[=" - context.report(errors.malformed_long_string(pos, boundary_pos, boundary_pos - pos)) + context.report(errors.malformed_long_string, pos, boundary_pos, boundary_pos - pos) return tokens.ERROR, boundary_pos end @@ -260,7 +260,7 @@ local function lex_token(context, str, pos) local end_pos = lex_long_str(context, str, boundary_pos + 1, boundary_pos - comment_pos) if end_pos then return tokens.COMMENT, end_pos end - context.report(errors.unfinished_long_comment(pos, boundary_pos, boundary_pos - comment_pos)) + context.report(errors.unfinished_long_comment, pos, boundary_pos, boundary_pos - comment_pos) return tokens.ERROR, #str end end @@ -317,18 +317,18 @@ local function lex_token(context, str, pos) if end_pos - pos <= 3 then local contents = sub(str, pos, end_pos) if contents == "&&" then - context.report(errors.wrong_and(pos, end_pos)) + context.report(errors.wrong_and, pos, end_pos) return tokens.AND, end_pos elseif contents == "||" then - context.report(errors.wrong_or(pos, end_pos)) + context.report(errors.wrong_or, pos, end_pos) return tokens.OR, end_pos elseif contents == "!=" or contents == "<>" then - context.report(errors.wrong_ne(pos, end_pos)) + context.report(errors.wrong_ne, pos, end_pos) return tokens.NE, end_pos end end - context.report(errors.unexpected_character(pos)) + context.report(errors.unexpected_character, pos) return tokens.ERROR, end_pos end end diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/parser.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/parser.lua index 2ee238c68..507f03928 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/parser.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/parser.lua @@ -244,7 +244,7 @@ local function handle_error(context, stack, stack_n, token, token_start, token_e end end - context.report(errors.unexpected_token(token, token_start, token_end)) + context.report(errors.unexpected_token, token, token_start, token_end) return false end diff --git a/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/syntax_helpers.lua b/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/syntax_helpers.lua index 18b310d68..87022065d 100644 --- a/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/syntax_helpers.lua +++ b/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/syntax_helpers.lua @@ -49,8 +49,9 @@ local function capture_parser(input, print_tokens, start) end local context = make_context(input) - function context.report(message) - expect(3, message, "table") + function context.report(message, ...) + expect(3, message, "table", "function") + if type(message) == "function" then message = message(...) end for _, msg in ipairs(message) do if type(msg) == "table" and msg.tag == "annotate" then From 4accda6b8ef75d0c2e9ff0d1cc2c23bb3a21465e Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 25 Jun 2023 20:56:27 +0100 Subject: [PATCH 05/32] Some tiny optimisations to the window API - Use integer indexes instead of strings (i.e. text, textColour). This is a tiny bit faster. - Avoid re-creating tables when clearing. We're still mostly limited by the VM (slow) and string concatenation (slow!). Short of having some low-level mutable buffer type, I don't think we can improve this much :(. --- .../computercraft/lua/rom/apis/window.lua | 74 ++++++++----------- 1 file changed, 29 insertions(+), 45 deletions(-) diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua index 302372a39..62c86d361 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua @@ -131,11 +131,7 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible) local sEmptyTextColor = tEmptyColorLines[nTextColor] local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor] for y = 1, nHeight do - tLines[y] = { - text = sEmptyText, - textColor = sEmptyTextColor, - backgroundColor = sEmptyBackgroundColor, - } + tLines[y] = { sEmptyText, sEmptyTextColor, sEmptyBackgroundColor } end for i = 0, 15 do @@ -165,7 +161,7 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible) local function redrawLine(n) local tLine = tLines[n] parent.setCursorPos(nX, nY + n - 1) - parent.blit(tLine.text, tLine.textColor, tLine.backgroundColor) + parent.blit(tLine[1], tLine[2], tLine[3]) end local function redraw() @@ -188,9 +184,9 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible) -- Modify line local tLine = tLines[nCursorY] if nStart == 1 and nEnd == nWidth then - tLine.text = sText - tLine.textColor = sTextColor - tLine.backgroundColor = sBackgroundColor + tLine[1] = sText + tLine[2] = sTextColor + tLine[3] = sBackgroundColor else local sClippedText, sClippedTextColor, sClippedBackgroundColor if nStart < 1 then @@ -210,9 +206,9 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible) sClippedBackgroundColor = sBackgroundColor end - local sOldText = tLine.text - local sOldTextColor = tLine.textColor - local sOldBackgroundColor = tLine.backgroundColor + local sOldText = tLine[1] + local sOldTextColor = tLine[2] + local sOldBackgroundColor = tLine[3] local sNewText, sNewTextColor, sNewBackgroundColor if nStart > 1 then local nOldEnd = nStart - 1 @@ -231,9 +227,9 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible) sNewBackgroundColor = sNewBackgroundColor .. string_sub(sOldBackgroundColor, nOldStart, nWidth) end - tLine.text = sNewText - tLine.textColor = sNewTextColor - tLine.backgroundColor = sNewBackgroundColor + tLine[1] = sNewText + tLine[2] = sNewTextColor + tLine[3] = sNewBackgroundColor end -- Redraw line @@ -280,11 +276,10 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible) local sEmptyTextColor = tEmptyColorLines[nTextColor] local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor] for y = 1, nHeight do - tLines[y] = { - text = sEmptyText, - textColor = sEmptyTextColor, - backgroundColor = sEmptyBackgroundColor, - } + local line = tLines[y] + line[1] = sEmptyText + line[2] = sEmptyTextColor + line[3] = sEmptyBackgroundColor end if bVisible then redraw() @@ -295,14 +290,10 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible) function window.clearLine() if nCursorY >= 1 and nCursorY <= nHeight then - local sEmptyText = sEmptySpaceLine - local sEmptyTextColor = tEmptyColorLines[nTextColor] - local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor] - tLines[nCursorY] = { - text = sEmptyText, - textColor = sEmptyTextColor, - backgroundColor = sEmptyBackgroundColor, - } + local line = tLines[nCursorY] + line[1] = sEmptySpaceLine + line[2] = tEmptyColorLines[nTextColor] + line[3] = tEmptyColorLines[nBackgroundColor] if bVisible then redrawLine(nCursorY) updateCursorColor() @@ -431,11 +422,7 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible) if y >= 1 and y <= nHeight then tNewLines[newY] = tLines[y] else - tNewLines[newY] = { - text = sEmptyText, - textColor = sEmptyTextColor, - backgroundColor = sEmptyBackgroundColor, - } + tNewLines[newY] = { sEmptyText, sEmptyTextColor, sEmptyBackgroundColor } end end tLines = tNewLines @@ -478,7 +465,8 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible) error("Line is out of range.", 2) end - return tLines[y].text, tLines[y].textColor, tLines[y].backgroundColor + local line = tLines[y] + return line[1], line[2], line[3] end -- Other functions @@ -574,26 +562,22 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible) local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor] for y = 1, new_height do if y > nHeight then - tNewLines[y] = { - text = sEmptyText, - textColor = sEmptyTextColor, - backgroundColor = sEmptyBackgroundColor, - } + tNewLines[y] = { sEmptyText, sEmptyTextColor, sEmptyBackgroundColor } else local tOldLine = tLines[y] if new_width == nWidth then tNewLines[y] = tOldLine elseif new_width < nWidth then tNewLines[y] = { - text = string_sub(tOldLine.text, 1, new_width), - textColor = string_sub(tOldLine.textColor, 1, new_width), - backgroundColor = string_sub(tOldLine.backgroundColor, 1, new_width), + string_sub(tOldLine[1], 1, new_width), + string_sub(tOldLine[2], 1, new_width), + string_sub(tOldLine[3], 1, new_width), } else tNewLines[y] = { - text = tOldLine.text .. string_sub(sEmptyText, nWidth + 1, new_width), - textColor = tOldLine.textColor .. string_sub(sEmptyTextColor, nWidth + 1, new_width), - backgroundColor = tOldLine.backgroundColor .. string_sub(sEmptyBackgroundColor, nWidth + 1, new_width), + tOldLine[1] .. string_sub(sEmptyText, nWidth + 1, new_width), + tOldLine[2] .. string_sub(sEmptyTextColor, nWidth + 1, new_width), + tOldLine[3] .. string_sub(sEmptyBackgroundColor, nWidth + 1, new_width), } end end From bc500df921db1ffcd3786799d6c60b977bb68cbf Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Mon, 26 Jun 2023 18:42:19 +0100 Subject: [PATCH 06/32] Use a builder for constructing ComputerContexts We've got a few optional fields here, and more on their way, so this ends up being a little nicer. --- .../shared/computer/core/ServerContext.java | 11 +- .../computercraft/core/ComputerContext.java | 105 +++++++++++++++--- .../core/ComputerTestDelegate.java | 4 +- .../core/computer/ComputerBootstrap.java | 2 +- .../core/computer/KotlinComputerManager.kt | 10 +- 5 files changed, 99 insertions(+), 33 deletions(-) diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java index b2d266133..4172ad1d9 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java @@ -9,7 +9,6 @@ import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.filesystem.Mount; import dan200.computercraft.api.network.PacketNetwork; import dan200.computercraft.core.ComputerContext; -import dan200.computercraft.core.computer.ComputerThread; import dan200.computercraft.core.computer.GlobalEnvironment; import dan200.computercraft.core.computer.mainthread.MainThread; import dan200.computercraft.core.computer.mainthread.MainThreadConfig; @@ -67,11 +66,11 @@ public final class ServerContext { this.server = server; storageDir = server.getWorldPath(FOLDER); mainThread = new MainThread(mainThreadConfig); - context = new ComputerContext( - new Environment(server), - new ComputerThread(ConfigSpec.computerThreads.get()), - mainThread, luaMachine - ); + context = ComputerContext.builder(new Environment(server)) + .computerThreads(ConfigSpec.computerThreads.get()) + .mainThreadScheduler(mainThread) + .luaFactory(luaMachine) + .build(); idAssigner = new IDAssigner(storageDir.resolve("ids.json")); } diff --git a/projects/core/src/main/java/dan200/computercraft/core/ComputerContext.java b/projects/core/src/main/java/dan200/computercraft/core/ComputerContext.java index 305b598a9..94a01997a 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/ComputerContext.java +++ b/projects/core/src/main/java/dan200/computercraft/core/ComputerContext.java @@ -7,10 +7,13 @@ package dan200.computercraft.core; import dan200.computercraft.core.computer.ComputerThread; import dan200.computercraft.core.computer.GlobalEnvironment; import dan200.computercraft.core.computer.mainthread.MainThreadScheduler; +import dan200.computercraft.core.computer.mainthread.NoWorkMainThreadScheduler; import dan200.computercraft.core.lua.CobaltLuaMachine; import dan200.computercraft.core.lua.ILuaMachine; import javax.annotation.CheckReturnValue; +import javax.annotation.Nullable; +import java.util.Objects; import java.util.concurrent.TimeUnit; /** @@ -20,27 +23,16 @@ public final class ComputerContext { private final GlobalEnvironment globalEnvironment; private final ComputerThread computerScheduler; private final MainThreadScheduler mainThreadScheduler; - private final ILuaMachine.Factory factory; + private final ILuaMachine.Factory luaFactory; - public ComputerContext( + ComputerContext( GlobalEnvironment globalEnvironment, ComputerThread computerScheduler, - MainThreadScheduler mainThreadScheduler, ILuaMachine.Factory factory + MainThreadScheduler mainThreadScheduler, ILuaMachine.Factory luaFactory ) { this.globalEnvironment = globalEnvironment; this.computerScheduler = computerScheduler; this.mainThreadScheduler = mainThreadScheduler; - this.factory = factory; - } - - /** - * Create a default {@link ComputerContext} with the given global environment. - * - * @param environment The current global environment. - * @param threads The number of threads to use for the {@link #computerScheduler()} - * @param mainThreadScheduler The main thread scheduler to use. - */ - public ComputerContext(GlobalEnvironment environment, int threads, MainThreadScheduler mainThreadScheduler) { - this(environment, new ComputerThread(threads), mainThreadScheduler, CobaltLuaMachine::new); + this.luaFactory = luaFactory; } /** @@ -77,7 +69,7 @@ public final class ComputerContext { * @return The current Lua machine factory. */ public ILuaMachine.Factory luaFactory() { - return factory; + return luaFactory; } /** @@ -106,4 +98,85 @@ public final class ComputerContext { throw new IllegalStateException("Failed to shutdown ComputerContext in time."); } } + + /** + * Create a new {@linkplain Builder builder} for a computer context. + * + * @param environment The {@linkplain ComputerContext#globalEnvironment() global environment} for this context. + * @return The builder for a new context. + */ + public static Builder builder(GlobalEnvironment environment) { + return new Builder(environment); + } + + /** + * A builder for a {@link ComputerContext}. + * + * @see ComputerContext#builder(GlobalEnvironment) + */ + public static class Builder { + private final GlobalEnvironment environment; + private int threads = 1; + private @Nullable MainThreadScheduler mainThreadScheduler; + private @Nullable ILuaMachine.Factory luaFactory; + + Builder(GlobalEnvironment environment) { + this.environment = environment; + } + + /** + * Set the number of threads the {@link ComputerThread} will use. + * + * @param threads The number of threads to use. + * @return {@code this}, for chaining + * @see ComputerContext#computerScheduler() + */ + public Builder computerThreads(int threads) { + if (threads < 1) throw new IllegalArgumentException("Threads must be >= 1"); + this.threads = threads; + return this; + } + + /** + * Set the {@link MainThreadScheduler} for this context. + * + * @param scheduler The main thread scheduler. + * @return {@code this}, for chaining + * @see ComputerContext#mainThreadScheduler() + */ + public Builder mainThreadScheduler(MainThreadScheduler scheduler) { + Objects.requireNonNull(scheduler); + if (mainThreadScheduler != null) throw new IllegalStateException("Main-thread scheduler already specified"); + mainThreadScheduler = scheduler; + return this; + } + + /** + * Set the {@link ILuaMachine.Factory} for this context. + * + * @param factory The Lua machine factory. + * @return {@code this}, for chaining + * @see ComputerContext#luaFactory() + */ + public Builder luaFactory(ILuaMachine.Factory factory) { + Objects.requireNonNull(factory); + if (luaFactory != null) throw new IllegalStateException("Main-thread scheduler already specified"); + luaFactory = factory; + return this; + } + + /** + * Create a new {@link ComputerContext}. + * + * @return The newly created context. + */ + public ComputerContext build() { + return new ComputerContext( + environment, + new ComputerThread(threads), + mainThreadScheduler == null ? new NoWorkMainThreadScheduler() : mainThreadScheduler, + luaFactory == null ? CobaltLuaMachine::new : luaFactory + ); + } + } } diff --git a/projects/core/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java b/projects/core/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java index ce832e54b..962b683c2 100644 --- a/projects/core/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java +++ b/projects/core/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java @@ -11,8 +11,6 @@ import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.core.computer.Computer; import dan200.computercraft.core.computer.ComputerSide; -import dan200.computercraft.core.computer.ComputerThread; -import dan200.computercraft.core.computer.mainthread.NoWorkMainThreadScheduler; import dan200.computercraft.core.filesystem.FileSystemException; import dan200.computercraft.core.filesystem.WritableFileMount; import dan200.computercraft.core.lua.CobaltLuaMachine; @@ -112,7 +110,7 @@ public class ComputerTestDelegate { } var environment = new BasicEnvironment(mount); - context = new ComputerContext(environment, new ComputerThread(1), new NoWorkMainThreadScheduler(), CoverageLuaMachine::new); + context = ComputerContext.builder(environment).luaFactory(CoverageLuaMachine::new).build(); computer = new Computer(context, environment, term, 0); computer.getEnvironment().setPeripheral(ComputerSide.TOP, new FakeModem()); computer.getEnvironment().setPeripheral(ComputerSide.BOTTOM, new FakePeripheralHub()); diff --git a/projects/core/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java b/projects/core/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java index e9975d0ca..42f0947af 100644 --- a/projects/core/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java +++ b/projects/core/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java @@ -49,7 +49,7 @@ public class ComputerBootstrap { var term = new Terminal(51, 19, true); var mainThread = new MainThread(new MainThreadConfig.Basic(Integer.MAX_VALUE, Integer.MAX_VALUE)); var environment = new BasicEnvironment(mount); - var context = new ComputerContext(environment, 1, mainThread); + var context = ComputerContext.builder(environment).mainThreadScheduler(mainThread).build(); final var computer = new Computer(context, environment, term, 0); var api = new AssertApi(); diff --git a/projects/core/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/KotlinComputerManager.kt b/projects/core/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/KotlinComputerManager.kt index a6fef3a26..8a93930e1 100644 --- a/projects/core/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/KotlinComputerManager.kt +++ b/projects/core/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/KotlinComputerManager.kt @@ -7,9 +7,7 @@ package dan200.computercraft.test.core.computer import dan200.computercraft.api.lua.ILuaAPI import dan200.computercraft.core.ComputerContext import dan200.computercraft.core.computer.Computer -import dan200.computercraft.core.computer.ComputerThread import dan200.computercraft.core.computer.TimeoutState -import dan200.computercraft.core.computer.mainthread.NoWorkMainThreadScheduler import dan200.computercraft.core.lua.MachineEnvironment import dan200.computercraft.core.lua.MachineResult import dan200.computercraft.core.terminal.Terminal @@ -27,11 +25,9 @@ typealias FakeComputerTask = (state: TimeoutState) -> MachineResult class KotlinComputerManager : AutoCloseable { private val machines: MutableMap> = HashMap() - private val context = ComputerContext( - BasicEnvironment(), - ComputerThread(1), - NoWorkMainThreadScheduler(), - ) { env, _ -> DummyLuaMachine(env) } + private val context = ComputerContext.builder(BasicEnvironment()) + .luaFactory { env, _ -> DummyLuaMachine(env) } + .build() private val errorLock: Lock = ReentrantLock() private val hasError = errorLock.newCondition() From 50d460624ffbb9c780b0d499943375bb3fdd1a17 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Mon, 26 Jun 2023 18:48:26 +0100 Subject: [PATCH 07/32] Make the list of API factories per-ComputerContext The registry is still a singleton inside the Minecraft code, but this makes the core a little cleaner. --- .../impl/AbstractComputerCraftAPI.java | 1 - .../computercraft/impl}/ApiFactories.java | 4 +-- .../shared/computer/core/ServerContext.java | 2 ++ .../computercraft/core/ComputerContext.java | 35 +++++++++++++++++-- .../core/computer/ComputerExecutor.java | 2 +- 5 files changed, 38 insertions(+), 6 deletions(-) rename projects/{core/src/main/java/dan200/computercraft/core/apis => common/src/main/java/dan200/computercraft/impl}/ApiFactories.java (88%) diff --git a/projects/common/src/main/java/dan200/computercraft/impl/AbstractComputerCraftAPI.java b/projects/common/src/main/java/dan200/computercraft/impl/AbstractComputerCraftAPI.java index 45e4ed707..b6ba303f8 100644 --- a/projects/common/src/main/java/dan200/computercraft/impl/AbstractComputerCraftAPI.java +++ b/projects/common/src/main/java/dan200/computercraft/impl/AbstractComputerCraftAPI.java @@ -19,7 +19,6 @@ import dan200.computercraft.api.pocket.PocketUpgradeSerialiser; import dan200.computercraft.api.redstone.BundledRedstoneProvider; import dan200.computercraft.api.turtle.TurtleRefuelHandler; import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser; -import dan200.computercraft.core.apis.ApiFactories; import dan200.computercraft.core.asm.GenericMethod; import dan200.computercraft.core.filesystem.WritableFileMount; import dan200.computercraft.impl.detail.DetailRegistryImpl; diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/ApiFactories.java b/projects/common/src/main/java/dan200/computercraft/impl/ApiFactories.java similarity index 88% rename from projects/core/src/main/java/dan200/computercraft/core/apis/ApiFactories.java rename to projects/common/src/main/java/dan200/computercraft/impl/ApiFactories.java index 232419aa1..ef96738ef 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/ApiFactories.java +++ b/projects/common/src/main/java/dan200/computercraft/impl/ApiFactories.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MPL-2.0 -package dan200.computercraft.core.apis; +package dan200.computercraft.impl; import dan200.computercraft.api.lua.ILuaAPIFactory; @@ -23,7 +23,7 @@ public final class ApiFactories { factories.add(factory); } - public static Iterable getAll() { + public static Collection getAll() { return factoriesView; } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java index 4172ad1d9..1ddff50d2 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java @@ -15,6 +15,7 @@ import dan200.computercraft.core.computer.mainthread.MainThreadConfig; import dan200.computercraft.core.lua.CobaltLuaMachine; import dan200.computercraft.core.lua.ILuaMachine; import dan200.computercraft.impl.AbstractComputerCraftAPI; +import dan200.computercraft.impl.ApiFactories; import dan200.computercraft.shared.CommonHooks; import dan200.computercraft.shared.computer.metrics.GlobalMetrics; import dan200.computercraft.shared.config.ConfigSpec; @@ -70,6 +71,7 @@ public final class ServerContext { .computerThreads(ConfigSpec.computerThreads.get()) .mainThreadScheduler(mainThread) .luaFactory(luaMachine) + .apiFactories(ApiFactories.getAll()) .build(); idAssigner = new IDAssigner(storageDir.resolve("ids.json")); } diff --git a/projects/core/src/main/java/dan200/computercraft/core/ComputerContext.java b/projects/core/src/main/java/dan200/computercraft/core/ComputerContext.java index 94a01997a..ce0270687 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/ComputerContext.java +++ b/projects/core/src/main/java/dan200/computercraft/core/ComputerContext.java @@ -4,6 +4,7 @@ package dan200.computercraft.core; +import dan200.computercraft.api.lua.ILuaAPIFactory; import dan200.computercraft.core.computer.ComputerThread; import dan200.computercraft.core.computer.GlobalEnvironment; import dan200.computercraft.core.computer.mainthread.MainThreadScheduler; @@ -13,6 +14,8 @@ import dan200.computercraft.core.lua.ILuaMachine; import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; +import java.util.Collection; +import java.util.List; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -24,15 +27,18 @@ public final class ComputerContext { private final ComputerThread computerScheduler; private final MainThreadScheduler mainThreadScheduler; private final ILuaMachine.Factory luaFactory; + private final List apiFactories; ComputerContext( GlobalEnvironment globalEnvironment, ComputerThread computerScheduler, - MainThreadScheduler mainThreadScheduler, ILuaMachine.Factory luaFactory + MainThreadScheduler mainThreadScheduler, ILuaMachine.Factory luaFactory, + List apiFactories ) { this.globalEnvironment = globalEnvironment; this.computerScheduler = computerScheduler; this.mainThreadScheduler = mainThreadScheduler; this.luaFactory = luaFactory; + this.apiFactories = apiFactories; } /** @@ -72,6 +78,15 @@ public final class ComputerContext { return luaFactory; } + /** + * Additional APIs to inject into each computer. + * + * @return All available API factories. + */ + public List apiFactories() { + return apiFactories; + } + /** * Close the current {@link ComputerContext}, disposing of any resources inside. * @@ -119,6 +134,7 @@ public final class ComputerContext { private int threads = 1; private @Nullable MainThreadScheduler mainThreadScheduler; private @Nullable ILuaMachine.Factory luaFactory; + private @Nullable List apiFactories; Builder(GlobalEnvironment environment) { this.environment = environment; @@ -165,6 +181,20 @@ public final class ComputerContext { return this; } + /** + * Set the additional {@linkplain ILuaAPIFactory APIs} to add to each computer. + * + * @param apis A list of API factories. + * @return {@code this}, for chaining + * @see ComputerContext#apiFactories() + */ + public Builder apiFactories(Collection apis) { + Objects.requireNonNull(apis); + if (apiFactories != null) throw new IllegalStateException("Main-thread scheduler already specified"); + apiFactories = List.copyOf(apis); + return this; + } + /** * Create a new {@link ComputerContext}. * @@ -175,7 +205,8 @@ public final class ComputerContext { environment, new ComputerThread(threads), mainThreadScheduler == null ? new NoWorkMainThreadScheduler() : mainThreadScheduler, - luaFactory == null ? CobaltLuaMachine::new : luaFactory + luaFactory == null ? CobaltLuaMachine::new : luaFactory, + apiFactories == null ? List.of() : apiFactories ); } } diff --git a/projects/core/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java b/projects/core/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java index 36220801e..4c7015502 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java +++ b/projects/core/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java @@ -181,7 +181,7 @@ final class ComputerExecutor { if (CoreConfig.httpEnabled) apis.add(new HTTPAPI(environment)); // Load in the externally registered APIs. - for (var factory : ApiFactories.getAll()) { + for (var factory : context.apiFactories()) { var system = new ComputerSystem(environment); var api = factory.create(system); if (api != null) apis.add(new ApiWrapper(api, system)); From 4a5e03c11a4698b841ced723c38260a4df5be63f Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Mon, 26 Jun 2023 18:51:14 +0100 Subject: [PATCH 08/32] Convert NamedMethod into a record --- .../peripheral/generic/SaturatedMethod.java | 4 +- .../core/apis/PeripheralAPI.java | 2 +- .../computercraft/core/asm/NamedMethod.java | 45 +++++++------------ .../core/lua/CobaltLuaMachine.java | 6 +-- .../core/apis/ObjectWrapper.java | 2 +- .../computercraft/core/asm/GeneratorTest.java | 8 ++-- .../generic/GenericPeripheralProvider.java | 2 +- .../generic/GenericPeripheralProvider.java | 2 +- 8 files changed, 28 insertions(+), 43 deletions(-) diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/SaturatedMethod.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/SaturatedMethod.java index 78049da8c..d43a30de0 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/SaturatedMethod.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/SaturatedMethod.java @@ -19,8 +19,8 @@ final class SaturatedMethod { SaturatedMethod(Object target, NamedMethod method) { this.target = target; - name = method.getName(); - this.method = method.getMethod(); + name = method.name(); + this.method = method.method(); } MethodResult apply(ILuaContext context, IComputerAccess computer, IArguments args) throws LuaException { diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java b/projects/core/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java index 5c8ab85d4..cfdaea0e3 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java @@ -328,7 +328,7 @@ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange methodMap.put(dynamicMethods[i], PeripheralMethod.DYNAMIC.get(i)); } for (var method : methods) { - methodMap.put(method.getName(), method.getMethod()); + methodMap.put(method.name(), method.method()); } return methodMap; } diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/NamedMethod.java b/projects/core/src/main/java/dan200/computercraft/core/asm/NamedMethod.java index 35f5c51c7..4c4cfbf8d 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/NamedMethod.java +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/NamedMethod.java @@ -4,38 +4,23 @@ package dan200.computercraft.core.asm; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.lua.MethodResult; +import dan200.computercraft.api.peripheral.GenericPeripheral; import dan200.computercraft.api.peripheral.PeripheralType; import javax.annotation.Nullable; -public final class NamedMethod { - private final String name; - private final T method; - private final boolean nonYielding; - - private final @Nullable PeripheralType genericType; - - NamedMethod(String name, T method, boolean nonYielding, @Nullable PeripheralType genericType) { - this.name = name; - this.method = method; - this.nonYielding = nonYielding; - this.genericType = genericType; - } - - public String getName() { - return name; - } - - public T getMethod() { - return method; - } - - public boolean nonYielding() { - return nonYielding; - } - - @Nullable - public PeripheralType getGenericType() { - return genericType; - } +/** + * A method generated from a {@link LuaFunction}. + * + * @param name The name of this method. + * @param method The underlying method implementation. + * @param nonYielding If this method is guaranteed to never yield, and will always return a + * {@linkplain MethodResult#of(Object...) basic result}. + * @param genericType The peripheral type of this method. This is only set if this is a method on a + * {@link GenericPeripheral}. + * @param The type of method, either a {@link LuaMethod} or {@link PeripheralMethod}. + */ +public record NamedMethod(String name, T method, boolean nonYielding, @Nullable PeripheralType genericType) { } diff --git a/projects/core/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java b/projects/core/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java index 40174a438..7ce55baa1 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java +++ b/projects/core/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java @@ -172,9 +172,9 @@ public class CobaltLuaMachine implements ILuaMachine { } ObjectSource.allMethods(LuaMethod.GENERATOR, object, (instance, method) -> - table.rawset(method.getName(), method.nonYielding() - ? new BasicFunction(this, method.getMethod(), instance, context, method.getName()) - : new ResultInterpreterFunction(this, method.getMethod(), instance, context, method.getName()))); + table.rawset(method.name(), method.nonYielding() + ? new BasicFunction(this, method.method(), instance, context, method.name()) + : new ResultInterpreterFunction(this, method.method(), instance, context, method.name()))); try { if (table.next(Constants.NIL).first().isNil()) return null; diff --git a/projects/core/src/test/java/dan200/computercraft/core/apis/ObjectWrapper.java b/projects/core/src/test/java/dan200/computercraft/core/apis/ObjectWrapper.java index e8370ed84..513848e7b 100644 --- a/projects/core/src/test/java/dan200/computercraft/core/apis/ObjectWrapper.java +++ b/projects/core/src/test/java/dan200/computercraft/core/apis/ObjectWrapper.java @@ -28,7 +28,7 @@ public class ObjectWrapper implements ILuaContext { methodMap.put(dynamicMethods[i], LuaMethod.DYNAMIC.get(i)); } for (var method : methods) { - methodMap.put(method.getName(), method.getMethod()); + methodMap.put(method.name(), method.method()); } } diff --git a/projects/core/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java b/projects/core/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java index 94b33f0d3..85de1ba28 100644 --- a/projects/core/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java +++ b/projects/core/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java @@ -44,7 +44,7 @@ public class GeneratorTest { var methods = LuaMethod.GENERATOR.getMethods(Basic.class); var methods2 = LuaMethod.GENERATOR.getMethods(Basic2.class); assertThat(methods, contains(named("go"))); - assertThat(methods.get(0).getMethod(), sameInstance(methods2.get(0).getMethod())); + assertThat(methods.get(0).method(), sameInstance(methods2.get(0).method())); } @Test @@ -217,8 +217,8 @@ public class GeneratorTest { private static T find(Collection> methods, String name) { return methods.stream() - .filter(x -> x.getName().equals(name)) - .map(NamedMethod::getMethod) + .filter(x -> x.name().equals(name)) + .map(NamedMethod::method) .findAny() .orElseThrow(NullPointerException::new); } @@ -235,7 +235,7 @@ public class GeneratorTest { } public static Matcher> named(String method) { - return contramap(is(method), "name", NamedMethod::getName); + return contramap(is(method), "name", NamedMethod::name); } private static final ILuaContext CONTEXT = new ILuaContext() { diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java b/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java index 4b36a5d84..de61e0d69 100644 --- a/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java @@ -71,7 +71,7 @@ public class GenericPeripheralProvider { // If we have a peripheral type, use it. Always pick the smallest one, so it's consistent (assuming mods // don't change). - var type = method.getGenericType(); + var type = method.genericType(); if (type != null && type.getPrimaryType() != null) { var name = type.getPrimaryType(); if (this.name == null || this.name.compareTo(name) > 0) this.name = name; diff --git a/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java b/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java index a2a26e65a..c9a59e366 100644 --- a/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java +++ b/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java @@ -71,7 +71,7 @@ public class GenericPeripheralProvider { // If we have a peripheral type, use it. Always pick the smallest one, so it's consistent (assuming mods // don't change). - var type = method.getGenericType(); + var type = method.genericType(); if (type != null && type.getPrimaryType() != null) { var name = type.getPrimaryType(); if (this.name == null || this.name.compareTo(name) > 0) this.name = name; From a29a516a3f64fd1fdac33deecd24796c6d6b3577 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Mon, 26 Jun 2023 19:11:59 +0100 Subject: [PATCH 09/32] Small refactoring to generic peripherals - Remove SidedGenericPeripheral (we never used this!), adding the functionality to GenericPeripheral directly. This is just used on the Fabric side for now, but might make sense with Forge too. - Move GenericPeripheralBuilder into the common project - this is identical between the two projects! - GenericPeripheralBuilder now generates a list of methods internally, rather than being passed the methods. - Add a tiny bit of documentation. --- .../peripheral/generic/GenericPeripheral.java | 15 +++-- .../generic/GenericPeripheralBuilder.java | 61 +++++++++++++++++++ .../peripheral/generic/SaturatedMethod.java | 10 +-- .../generic/GenericPeripheralProvider.java | 53 ++-------------- .../generic/SidedGenericPeripheral.java | 25 -------- .../generic/methods/InventoryMethods.java | 3 +- .../generic/GenericPeripheralProvider.java | 59 +++--------------- 7 files changed, 95 insertions(+), 131 deletions(-) create mode 100644 projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralBuilder.java delete mode 100644 projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/SidedGenericPeripheral.java diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheral.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheral.java index f181ae7c4..8874a12b9 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheral.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheral.java @@ -12,19 +12,23 @@ import dan200.computercraft.api.peripheral.IComputerAccess; import dan200.computercraft.api.peripheral.IDynamicPeripheral; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.shared.platform.RegistryWrappers; +import net.minecraft.core.Direction; import net.minecraft.world.level.block.entity.BlockEntity; import javax.annotation.Nullable; import java.util.List; import java.util.Set; -class GenericPeripheral implements IDynamicPeripheral { +public final class GenericPeripheral implements IDynamicPeripheral { + private final BlockEntity tile; + private final Direction side; + private final String type; private final Set additionalTypes; - private final BlockEntity tile; private final List methods; - GenericPeripheral(BlockEntity tile, @Nullable String name, Set additionalTypes, List methods) { + GenericPeripheral(BlockEntity tile, Direction side, @Nullable String name, Set additionalTypes, List methods) { + this.side = side; var type = RegistryWrappers.BLOCK_ENTITY_TYPES.getKey(tile.getType()); this.tile = tile; this.type = name != null ? name : type.toString(); @@ -32,6 +36,10 @@ class GenericPeripheral implements IDynamicPeripheral { this.methods = methods; } + public Direction side() { + return side; + } + @Override public String[] getMethodNames() { var names = new String[methods.size()]; @@ -54,7 +62,6 @@ class GenericPeripheral implements IDynamicPeripheral { return additionalTypes; } - @Nullable @Override public Object getTarget() { return tile; diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralBuilder.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralBuilder.java new file mode 100644 index 000000000..1d2c61134 --- /dev/null +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralBuilder.java @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.peripheral.generic; + +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.peripheral.PeripheralType; +import dan200.computercraft.core.asm.NamedMethod; +import dan200.computercraft.core.asm.PeripheralMethod; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.entity.BlockEntity; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +/** + * A builder for a {@link GenericPeripheral}. + *

+ * This handles building a list of {@linkplain SaturatedMethod methods} and computing the appropriate + * {@link PeripheralType} from the {@linkplain NamedMethod#genericType() methods' peripheral types}. + *

+ * See the platform-specific peripheral providers for the usage of this. + */ +final class GenericPeripheralBuilder { + private @Nullable String name; + private final Set additionalTypes = new HashSet<>(0); + private final ArrayList methods = new ArrayList<>(0); + + @Nullable + IPeripheral toPeripheral(BlockEntity tile, Direction side) { + if (methods.isEmpty()) return null; + + methods.trimToSize(); + return new GenericPeripheral(tile, side, name, additionalTypes, methods); + } + + boolean addMethods(Object target) { + var methods = PeripheralMethod.GENERATOR.getMethods(target.getClass()); + if (methods.isEmpty()) return false; + + var saturatedMethods = this.methods; + saturatedMethods.ensureCapacity(saturatedMethods.size() + methods.size()); + for (var method : methods) { + saturatedMethods.add(new SaturatedMethod(target, method.name(), method.method())); + + // If we have a peripheral type, use it. Always pick the smallest one, so it's consistent (assuming mods + // don't change). + var type = method.genericType(); + if (type != null && type.getPrimaryType() != null) { + var name = type.getPrimaryType(); + if (this.name == null || this.name.compareTo(name) > 0) this.name = name; + } + if (type != null) additionalTypes.addAll(type.getAdditionalTypes()); + } + + return true; + } +} diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/SaturatedMethod.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/SaturatedMethod.java index d43a30de0..8d0c92920 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/SaturatedMethod.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/SaturatedMethod.java @@ -9,18 +9,20 @@ import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.MethodResult; import dan200.computercraft.api.peripheral.IComputerAccess; -import dan200.computercraft.core.asm.NamedMethod; import dan200.computercraft.core.asm.PeripheralMethod; +/** + * A {@link PeripheralMethod} along with the method's target. + */ final class SaturatedMethod { private final Object target; private final String name; private final PeripheralMethod method; - SaturatedMethod(Object target, NamedMethod method) { + SaturatedMethod(Object target, String name, PeripheralMethod method) { this.target = target; - name = method.name(); - this.method = method.method(); + this.name = name; + this.method = method; } MethodResult apply(ILuaContext context, IComputerAccess computer, IArguments args) throws LuaException { diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java b/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java index de61e0d69..a9eefd5bb 100644 --- a/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java @@ -5,8 +5,6 @@ package dan200.computercraft.shared.peripheral.generic; import dan200.computercraft.api.peripheral.IPeripheral; -import dan200.computercraft.core.asm.NamedMethod; -import dan200.computercraft.core.asm.PeripheralMethod; import dan200.computercraft.shared.peripheral.generic.methods.InventoryMethods; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; @@ -15,10 +13,7 @@ import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; public class GenericPeripheralProvider { interface Lookup { @@ -31,53 +26,17 @@ public class GenericPeripheralProvider { ); @Nullable - public static IPeripheral getPeripheral(Level world, BlockPos pos, Direction side, @Nullable BlockEntity blockEntity) { + public static IPeripheral getPeripheral(Level level, BlockPos pos, Direction side, @Nullable BlockEntity blockEntity) { if (blockEntity == null) return null; - var saturated = new GenericPeripheralBuilder(); - - var tileMethods = PeripheralMethod.GENERATOR.getMethods(blockEntity.getClass()); - if (!tileMethods.isEmpty()) saturated.addMethods(blockEntity, tileMethods); + var builder = new GenericPeripheralBuilder(); + builder.addMethods(blockEntity); for (var lookup : lookups) { - var contents = lookup.find(world, pos, blockEntity.getBlockState(), blockEntity, side); - if (contents == null) continue; - - var methods = PeripheralMethod.GENERATOR.getMethods(contents.getClass()); - if (!methods.isEmpty()) saturated.addMethods(contents, methods); + var contents = lookup.find(level, pos, blockEntity.getBlockState(), blockEntity, side); + if (contents != null) builder.addMethods(contents); } - return saturated.toPeripheral(blockEntity); - } - - private static class GenericPeripheralBuilder { - private @Nullable String name; - private final Set additionalTypes = new HashSet<>(0); - private final ArrayList methods = new ArrayList<>(0); - - @Nullable - IPeripheral toPeripheral(BlockEntity tile) { - if (methods.isEmpty()) return null; - - methods.trimToSize(); - return new GenericPeripheral(tile, name, additionalTypes, methods); - } - - void addMethods(Object target, List> methods) { - var saturatedMethods = this.methods; - saturatedMethods.ensureCapacity(saturatedMethods.size() + methods.size()); - for (var method : methods) { - saturatedMethods.add(new SaturatedMethod(target, method)); - - // If we have a peripheral type, use it. Always pick the smallest one, so it's consistent (assuming mods - // don't change). - var type = method.genericType(); - if (type != null && type.getPrimaryType() != null) { - var name = type.getPrimaryType(); - if (this.name == null || this.name.compareTo(name) > 0) this.name = name; - } - if (type != null) additionalTypes.addAll(type.getAdditionalTypes()); - } - } + return builder.toPeripheral(blockEntity, side); } } diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/SidedGenericPeripheral.java b/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/SidedGenericPeripheral.java deleted file mode 100644 index b0aa1a351..000000000 --- a/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/SidedGenericPeripheral.java +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.shared.peripheral.generic; - -import net.minecraft.core.Direction; -import net.minecraft.world.level.block.entity.BlockEntity; - -import javax.annotation.Nullable; -import java.util.List; -import java.util.Set; - -public class SidedGenericPeripheral extends GenericPeripheral { - private final Direction direction; - - SidedGenericPeripheral(BlockEntity tile, Direction direction, @Nullable String name, Set additionalTypes, List methods) { - super(tile, name, additionalTypes, methods); - this.direction = direction; - } - - public Direction direction() { - return direction; - } -} diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java b/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java index 527e62dfe..dfbbdf5b7 100644 --- a/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java @@ -12,7 +12,6 @@ import dan200.computercraft.api.peripheral.GenericPeripheral; import dan200.computercraft.api.peripheral.IComputerAccess; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.PeripheralType; -import dan200.computercraft.shared.peripheral.generic.SidedGenericPeripheral; import dan200.computercraft.shared.platform.FabricContainerTransfer; import net.fabricmc.fabric.api.transfer.v1.item.ItemStorage; import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant; @@ -157,7 +156,7 @@ public class InventoryMethods implements GenericPeripheral { @Nullable private static SlottedStorage extractHandler(IPeripheral peripheral) { var object = peripheral.getTarget(); - var direction = peripheral instanceof SidedGenericPeripheral sided ? sided.direction() : null; + var direction = peripheral instanceof dan200.computercraft.shared.peripheral.generic.GenericPeripheral sided ? sided.side() : null; if (object instanceof BlockEntity blockEntity) { if (blockEntity.isRemoved()) return null; diff --git a/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java b/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java index c9a59e366..d8c275101 100644 --- a/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java +++ b/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java @@ -5,18 +5,16 @@ package dan200.computercraft.shared.peripheral.generic; import dan200.computercraft.api.peripheral.IPeripheral; -import dan200.computercraft.core.asm.NamedMethod; -import dan200.computercraft.core.asm.PeripheralMethod; import dan200.computercraft.shared.util.CapabilityUtil; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraftforge.common.capabilities.Capability; import net.minecraftforge.common.util.NonNullConsumer; import javax.annotation.Nullable; -import java.util.*; +import java.util.ArrayList; +import java.util.Objects; public class GenericPeripheralProvider { private static final ArrayList> capabilities = new ArrayList<>(); @@ -27,57 +25,20 @@ public class GenericPeripheralProvider { } @Nullable - public static IPeripheral getPeripheral(Level world, BlockPos pos, Direction side, NonNullConsumer invalidate) { - var tile = world.getBlockEntity(pos); - if (tile == null) return null; + public static IPeripheral getPeripheral(Level level, BlockPos pos, Direction side, NonNullConsumer invalidate) { + var blockEntity = level.getBlockEntity(pos); + if (blockEntity == null) return null; - var saturated = new GenericPeripheralBuilder(); - - var tileMethods = PeripheralMethod.GENERATOR.getMethods(tile.getClass()); - if (!tileMethods.isEmpty()) saturated.addMethods(tile, tileMethods); + var builder = new GenericPeripheralBuilder(); + builder.addMethods(blockEntity); for (var capability : capabilities) { - var wrapper = CapabilityUtil.getCapability(tile, capability, side); + var wrapper = CapabilityUtil.getCapability(blockEntity, capability, side); wrapper.ifPresent(contents -> { - var capabilityMethods = PeripheralMethod.GENERATOR.getMethods(contents.getClass()); - if (capabilityMethods.isEmpty()) return; - - saturated.addMethods(contents, capabilityMethods); - CapabilityUtil.addListener(wrapper, invalidate); + if (builder.addMethods(contents)) CapabilityUtil.addListener(wrapper, invalidate); }); } - return saturated.toPeripheral(tile); - } - - private static class GenericPeripheralBuilder { - private @Nullable String name; - private final Set additionalTypes = new HashSet<>(0); - private final ArrayList methods = new ArrayList<>(0); - - @Nullable - IPeripheral toPeripheral(BlockEntity tile) { - if (methods.isEmpty()) return null; - - methods.trimToSize(); - return new GenericPeripheral(tile, name, additionalTypes, methods); - } - - void addMethods(Object target, List> methods) { - var saturatedMethods = this.methods; - saturatedMethods.ensureCapacity(saturatedMethods.size() + methods.size()); - for (var method : methods) { - saturatedMethods.add(new SaturatedMethod(target, method)); - - // If we have a peripheral type, use it. Always pick the smallest one, so it's consistent (assuming mods - // don't change). - var type = method.genericType(); - if (type != null && type.getPrimaryType() != null) { - var name = type.getPrimaryType(); - if (this.name == null || this.name.compareTo(name) > 0) this.name = name; - } - if (type != null) additionalTypes.addAll(type.getAdditionalTypes()); - } - } + return builder.toPeripheral(blockEntity, side); } } From 591a7eca231bd31ed39f8302c194fd2d7c09efbb Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Mon, 26 Jun 2023 19:32:09 +0100 Subject: [PATCH 10/32] Clean up how we enumerate Lua/peripheral methods - Move several interfaces out of `d00.computercraft.core.asm` into a new `aethods` package. It may make sense to expose this to the public API in a future commit (possibly part of #1462). - Add a new MethodSupplier interface, which provides methods to iterate over all methods exported by an object (either directly, or including those from ObjectSources). This interface's concrete implementation (asm.MethodSupplierImpl), uses Generators and IntCaches as before - we can now make that all package-private though, which is nice! - Make the LuaMethod and PeripheralMethod MethodSupplier local to the ComputerContext. This currently has no effect (the underlying Generator is still global), but eventually we'll make GenericMethods non-global, which unlocks the door for #1382. - Update everything to use this new interface. This is mostly pretty sensible, but is a little uglier on the MC side (especially in generic peripherals), as we need to access the global ServerContext. --- .../shared/computer/core/ServerContext.java | 12 +++ .../computer/upload/TransferredFile.java | 2 +- .../generic/GenericPeripheralBuilder.java | 38 +++++---- .../peripheral/generic/SaturatedMethod.java | 2 +- .../modem/wired/WiredModemPeripheral.java | 11 ++- .../computercraft/core/ComputerContext.java | 36 +++++++- .../core/apis/IAPIEnvironment.java | 2 +- .../core/apis/PeripheralAPI.java | 28 ++----- .../computercraft/core/apis/TableHelper.java | 2 +- .../apis/http/request/HttpResponseHandle.java | 2 +- .../computercraft/core/asm/Generator.java | 3 +- .../computercraft/core/asm/IntCache.java | 2 +- .../computercraft/core/asm/LuaMethod.java | 23 ----- .../core/asm/LuaMethodSupplier.java | 34 ++++++++ .../core/asm/MethodSupplierImpl.java | 61 ++++++++++++++ .../computercraft/core/asm/ObjectSource.java | 27 ------ .../core/asm/PeripheralMethod.java | 26 ------ .../core/asm/PeripheralMethodSupplier.java | 30 +++++++ .../core/computer/ComputerExecutor.java | 7 +- .../computercraft/core/lua/BasicFunction.java | 2 +- .../core/lua/CobaltLuaMachine.java | 31 ++----- .../core/lua/MachineEnvironment.java | 4 + .../core/lua/ResultInterpreterFunction.java | 2 +- .../computercraft/core/methods/LuaMethod.java | 31 +++++++ .../core/methods/MethodSupplier.java | 83 +++++++++++++++++++ .../core/{asm => methods}/NamedMethod.java | 2 +- .../core/methods/ObjectSource.java | 15 ++++ .../core/methods/PeripheralMethod.java | 35 ++++++++ .../core/apis/ObjectWrapper.java | 27 +++--- .../computercraft/core/asm/GeneratorTest.java | 32 +++---- .../computercraft/core/asm/MethodTest.java | 1 + .../generic/GenericPeripheralProvider.java | 12 ++- .../generic/GenericPeripheralProvider.java | 12 ++- 33 files changed, 449 insertions(+), 188 deletions(-) delete mode 100644 projects/core/src/main/java/dan200/computercraft/core/asm/LuaMethod.java create mode 100644 projects/core/src/main/java/dan200/computercraft/core/asm/LuaMethodSupplier.java create mode 100644 projects/core/src/main/java/dan200/computercraft/core/asm/MethodSupplierImpl.java delete mode 100644 projects/core/src/main/java/dan200/computercraft/core/asm/ObjectSource.java delete mode 100644 projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethod.java create mode 100644 projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethodSupplier.java create mode 100644 projects/core/src/main/java/dan200/computercraft/core/methods/LuaMethod.java create mode 100644 projects/core/src/main/java/dan200/computercraft/core/methods/MethodSupplier.java rename projects/core/src/main/java/dan200/computercraft/core/{asm => methods}/NamedMethod.java (96%) create mode 100644 projects/core/src/main/java/dan200/computercraft/core/methods/ObjectSource.java create mode 100644 projects/core/src/main/java/dan200/computercraft/core/methods/PeripheralMethod.java diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java index 1ddff50d2..108c95483 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java @@ -14,6 +14,8 @@ import dan200.computercraft.core.computer.mainthread.MainThread; import dan200.computercraft.core.computer.mainthread.MainThreadConfig; import dan200.computercraft.core.lua.CobaltLuaMachine; import dan200.computercraft.core.lua.ILuaMachine; +import dan200.computercraft.core.methods.MethodSupplier; +import dan200.computercraft.core.methods.PeripheralMethod; import dan200.computercraft.impl.AbstractComputerCraftAPI; import dan200.computercraft.impl.ApiFactories; import dan200.computercraft.shared.CommonHooks; @@ -134,6 +136,16 @@ public final class ServerContext { return context; } + /** + * Get the {@link MethodSupplier} used to find methods on peripherals. + * + * @return The {@link PeripheralMethod} method supplier. + * @see ComputerContext#peripheralMethods() + */ + public MethodSupplier peripheralMethods() { + return context.peripheralMethods(); + } + /** * Tick all components of this server context. This should NOT be called outside of {@link CommonHooks}. */ diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/upload/TransferredFile.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/upload/TransferredFile.java index 687ea6989..c059496f2 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/upload/TransferredFile.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/upload/TransferredFile.java @@ -7,7 +7,7 @@ package dan200.computercraft.shared.computer.upload; import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.core.apis.handles.BinaryReadableHandle; import dan200.computercraft.core.apis.handles.ByteBufferChannel; -import dan200.computercraft.core.asm.ObjectSource; +import dan200.computercraft.core.methods.ObjectSource; import java.nio.ByteBuffer; import java.util.Collections; diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralBuilder.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralBuilder.java index 1d2c61134..e6b69f0cb 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralBuilder.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralBuilder.java @@ -6,9 +6,12 @@ package dan200.computercraft.shared.peripheral.generic; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.PeripheralType; -import dan200.computercraft.core.asm.NamedMethod; -import dan200.computercraft.core.asm.PeripheralMethod; +import dan200.computercraft.core.methods.MethodSupplier; +import dan200.computercraft.core.methods.NamedMethod; +import dan200.computercraft.core.methods.PeripheralMethod; +import dan200.computercraft.shared.computer.core.ServerContext; import net.minecraft.core.Direction; +import net.minecraft.server.MinecraftServer; import net.minecraft.world.level.block.entity.BlockEntity; import javax.annotation.Nullable; @@ -25,37 +28,36 @@ import java.util.Set; * See the platform-specific peripheral providers for the usage of this. */ final class GenericPeripheralBuilder { + private final MethodSupplier peripheralMethods; + private @Nullable String name; private final Set additionalTypes = new HashSet<>(0); - private final ArrayList methods = new ArrayList<>(0); + private final ArrayList methods = new ArrayList<>(); + + GenericPeripheralBuilder(MinecraftServer server) { + peripheralMethods = ServerContext.get(server).peripheralMethods(); + } @Nullable - IPeripheral toPeripheral(BlockEntity tile, Direction side) { + IPeripheral toPeripheral(BlockEntity blockEntity, Direction side) { if (methods.isEmpty()) return null; methods.trimToSize(); - return new GenericPeripheral(tile, side, name, additionalTypes, methods); + return new GenericPeripheral(blockEntity, side, name, additionalTypes, methods); } boolean addMethods(Object target) { - var methods = PeripheralMethod.GENERATOR.getMethods(target.getClass()); - if (methods.isEmpty()) return false; - - var saturatedMethods = this.methods; - saturatedMethods.ensureCapacity(saturatedMethods.size() + methods.size()); - for (var method : methods) { - saturatedMethods.add(new SaturatedMethod(target, method.name(), method.method())); + return peripheralMethods.forEachSelfMethod(target, (name, method, info) -> { + methods.add(new SaturatedMethod(target, name, method)); // If we have a peripheral type, use it. Always pick the smallest one, so it's consistent (assuming mods // don't change). - var type = method.genericType(); + var type = info == null ? null : info.genericType(); if (type != null && type.getPrimaryType() != null) { - var name = type.getPrimaryType(); - if (this.name == null || this.name.compareTo(name) > 0) this.name = name; + var primaryType = type.getPrimaryType(); + if (this.name == null || this.name.compareTo(primaryType) > 0) this.name = primaryType; } if (type != null) additionalTypes.addAll(type.getAdditionalTypes()); - } - - return true; + }); } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/SaturatedMethod.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/SaturatedMethod.java index 8d0c92920..ce978718c 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/SaturatedMethod.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/SaturatedMethod.java @@ -9,7 +9,7 @@ import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.MethodResult; import dan200.computercraft.api.peripheral.IComputerAccess; -import dan200.computercraft.core.asm.PeripheralMethod; +import dan200.computercraft.core.methods.PeripheralMethod; /** * A {@link PeripheralMethod} along with the method's target. diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java index 49eccfea8..a4956ac69 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java @@ -16,10 +16,12 @@ import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.NotAttachedException; import dan200.computercraft.api.peripheral.WorkMonitor; import dan200.computercraft.core.apis.PeripheralAPI; -import dan200.computercraft.core.asm.PeripheralMethod; +import dan200.computercraft.core.methods.PeripheralMethod; import dan200.computercraft.core.util.LuaUtil; +import dan200.computercraft.shared.computer.core.ServerContext; import dan200.computercraft.shared.peripheral.modem.ModemPeripheral; import dan200.computercraft.shared.peripheral.modem.ModemState; +import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.Level; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -284,7 +286,8 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi private void attachPeripheralImpl(IComputerAccess computer, ConcurrentMap peripherals, String periphName, IPeripheral peripheral) { if (!peripherals.containsKey(periphName) && !periphName.equals(getLocalPeripheral().getConnectedName())) { - var wrapper = new RemotePeripheralWrapper(modem, peripheral, computer, periphName); + var methods = ServerContext.get(((ServerLevel) getLevel()).getServer()).peripheralMethods().getSelfMethods(peripheral); + var wrapper = new RemotePeripheralWrapper(modem, peripheral, computer, periphName, methods); peripherals.put(periphName, wrapper); wrapper.attach(); } @@ -314,7 +317,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi private volatile boolean attached; private final Set mounts = new HashSet<>(); - RemotePeripheralWrapper(WiredModemElement element, IPeripheral peripheral, IComputerAccess computer, String name) { + RemotePeripheralWrapper(WiredModemElement element, IPeripheral peripheral, IComputerAccess computer, String name, Map methods) { this.element = element; this.peripheral = peripheral; this.computer = computer; @@ -322,7 +325,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi type = Objects.requireNonNull(peripheral.getType(), "Peripheral type cannot be null"); additionalTypes = peripheral.getAdditionalTypes(); - methodMap = PeripheralAPI.getMethods(peripheral); + methodMap = methods; } public void attach() { diff --git a/projects/core/src/main/java/dan200/computercraft/core/ComputerContext.java b/projects/core/src/main/java/dan200/computercraft/core/ComputerContext.java index ce0270687..75a6cac6d 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/ComputerContext.java +++ b/projects/core/src/main/java/dan200/computercraft/core/ComputerContext.java @@ -5,12 +5,18 @@ package dan200.computercraft.core; import dan200.computercraft.api.lua.ILuaAPIFactory; +import dan200.computercraft.core.asm.LuaMethodSupplier; +import dan200.computercraft.core.asm.PeripheralMethodSupplier; import dan200.computercraft.core.computer.ComputerThread; import dan200.computercraft.core.computer.GlobalEnvironment; import dan200.computercraft.core.computer.mainthread.MainThreadScheduler; import dan200.computercraft.core.computer.mainthread.NoWorkMainThreadScheduler; import dan200.computercraft.core.lua.CobaltLuaMachine; import dan200.computercraft.core.lua.ILuaMachine; +import dan200.computercraft.core.lua.MachineEnvironment; +import dan200.computercraft.core.methods.LuaMethod; +import dan200.computercraft.core.methods.MethodSupplier; +import dan200.computercraft.core.methods.PeripheralMethod; import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; @@ -28,17 +34,22 @@ public final class ComputerContext { private final MainThreadScheduler mainThreadScheduler; private final ILuaMachine.Factory luaFactory; private final List apiFactories; + private final MethodSupplier luaMethods; + private final MethodSupplier peripheralMethods; ComputerContext( GlobalEnvironment globalEnvironment, ComputerThread computerScheduler, MainThreadScheduler mainThreadScheduler, ILuaMachine.Factory luaFactory, - List apiFactories + List apiFactories, MethodSupplier luaMethods, + MethodSupplier peripheralMethods ) { this.globalEnvironment = globalEnvironment; this.computerScheduler = computerScheduler; this.mainThreadScheduler = mainThreadScheduler; this.luaFactory = luaFactory; this.apiFactories = apiFactories; + this.luaMethods = luaMethods; + this.peripheralMethods = peripheralMethods; } /** @@ -87,6 +98,25 @@ public final class ComputerContext { return apiFactories; } + /** + * Get the {@link MethodSupplier} used to find methods on Lua values. + * + * @return The {@link LuaMethod} method supplier. + * @see MachineEnvironment#luaMethods() + */ + public MethodSupplier luaMethods() { + return luaMethods; + } + + /** + * Get the {@link MethodSupplier} used to find methods on peripherals. + * + * @return The {@link PeripheralMethod} method supplier. + */ + public MethodSupplier peripheralMethods() { + return peripheralMethods; + } + /** * Close the current {@link ComputerContext}, disposing of any resources inside. * @@ -206,7 +236,9 @@ public final class ComputerContext { new ComputerThread(threads), mainThreadScheduler == null ? new NoWorkMainThreadScheduler() : mainThreadScheduler, luaFactory == null ? CobaltLuaMachine::new : luaFactory, - apiFactories == null ? List.of() : apiFactories + apiFactories == null ? List.of() : apiFactories, + LuaMethodSupplier.create(), + PeripheralMethodSupplier.create() ); } } diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java b/projects/core/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java index 9f19cf95f..a8b3e9112 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java @@ -10,9 +10,9 @@ import dan200.computercraft.core.computer.ComputerEnvironment; import dan200.computercraft.core.computer.ComputerSide; import dan200.computercraft.core.computer.GlobalEnvironment; import dan200.computercraft.core.filesystem.FileSystem; -import dan200.computercraft.core.metrics.OperationTimer; import dan200.computercraft.core.metrics.Metric; import dan200.computercraft.core.metrics.MetricsObserver; +import dan200.computercraft.core.metrics.OperationTimer; import dan200.computercraft.core.terminal.Terminal; import javax.annotation.Nullable; diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java b/projects/core/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java index cfdaea0e3..70fdb0b1c 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java @@ -7,13 +7,12 @@ package dan200.computercraft.core.apis; import dan200.computercraft.api.filesystem.Mount; import dan200.computercraft.api.filesystem.WritableMount; import dan200.computercraft.api.lua.*; -import dan200.computercraft.api.peripheral.IDynamicPeripheral; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.NotAttachedException; import dan200.computercraft.api.peripheral.WorkMonitor; -import dan200.computercraft.core.asm.LuaMethod; -import dan200.computercraft.core.asm.PeripheralMethod; import dan200.computercraft.core.computer.ComputerSide; +import dan200.computercraft.core.methods.MethodSupplier; +import dan200.computercraft.core.methods.PeripheralMethod; import dan200.computercraft.core.metrics.Metrics; import dan200.computercraft.core.util.LuaUtil; @@ -44,7 +43,7 @@ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange type = Objects.requireNonNull(peripheral.getType(), "Peripheral type cannot be null"); additionalTypes = peripheral.getAdditionalTypes(); - methodMap = PeripheralAPI.getMethods(peripheral); + methodMap = peripheralMethods.getSelfMethods(peripheral); } public IPeripheral getPeripheral() { @@ -172,11 +171,13 @@ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange } private final IAPIEnvironment environment; + private final MethodSupplier peripheralMethods; private final PeripheralWrapper[] peripherals = new PeripheralWrapper[6]; private boolean running; - public PeripheralAPI(IAPIEnvironment environment) { + public PeripheralAPI(IAPIEnvironment environment, MethodSupplier peripheralMethods) { this.environment = environment; + this.peripheralMethods = peripheralMethods; this.environment.setPeripheralChangeListener(this); running = false; } @@ -315,21 +316,4 @@ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange throw e; } } - - public static Map getMethods(IPeripheral peripheral) { - var dynamicMethods = peripheral instanceof IDynamicPeripheral - ? Objects.requireNonNull(((IDynamicPeripheral) peripheral).getMethodNames(), "Peripheral methods cannot be null") - : LuaMethod.EMPTY_METHODS; - - var methods = PeripheralMethod.GENERATOR.getMethods(peripheral.getClass()); - - Map methodMap = new HashMap<>(methods.size() + dynamicMethods.length); - for (var i = 0; i < dynamicMethods.length; i++) { - methodMap.put(dynamicMethods[i], PeripheralMethod.DYNAMIC.get(i)); - } - for (var method : methods) { - methodMap.put(method.name(), method.method()); - } - return methodMap; - } } diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/TableHelper.java b/projects/core/src/main/java/dan200/computercraft/core/apis/TableHelper.java index 9f3f505f0..d4ed5dc9f 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/TableHelper.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/TableHelper.java @@ -103,7 +103,7 @@ public final class TableHelper { public static Optional optRealField(Map table, String key) throws LuaException { var value = table.get(key); - if(value == null) { + if (value == null) { return Optional.empty(); } else { return Optional.of(getRealField(table, key)); diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/request/HttpResponseHandle.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/request/HttpResponseHandle.java index dba9683a6..e82e5c3ad 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/request/HttpResponseHandle.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/request/HttpResponseHandle.java @@ -10,7 +10,7 @@ import dan200.computercraft.core.apis.HTTPAPI; import dan200.computercraft.core.apis.handles.BinaryReadableHandle; import dan200.computercraft.core.apis.handles.EncodedReadableHandle; import dan200.computercraft.core.apis.handles.HandleGeneric; -import dan200.computercraft.core.asm.ObjectSource; +import dan200.computercraft.core.methods.ObjectSource; import java.util.Collections; import java.util.Map; diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/Generator.java b/projects/core/src/main/java/dan200/computercraft/core/asm/Generator.java index ea2870384..4b79e0060 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/Generator.java +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/Generator.java @@ -11,6 +11,7 @@ import com.google.common.primitives.Primitives; import com.google.common.reflect.TypeToken; import dan200.computercraft.api.lua.*; import dan200.computercraft.api.peripheral.PeripheralType; +import dan200.computercraft.core.methods.NamedMethod; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Type; @@ -30,7 +31,7 @@ import java.util.function.Function; import static org.objectweb.asm.Opcodes.*; -public final class Generator { +final class Generator { private static final Logger LOG = LoggerFactory.getLogger(Generator.class); private static final AtomicInteger METHOD_ID = new AtomicInteger(); diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/IntCache.java b/projects/core/src/main/java/dan200/computercraft/core/asm/IntCache.java index 850c6303a..3509dfc4a 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/IntCache.java +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/IntCache.java @@ -7,7 +7,7 @@ package dan200.computercraft.core.asm; import java.util.Arrays; import java.util.function.IntFunction; -public final class IntCache { +final class IntCache { private final IntFunction factory; private volatile Object[] cache = new Object[16]; diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/LuaMethod.java b/projects/core/src/main/java/dan200/computercraft/core/asm/LuaMethod.java deleted file mode 100644 index 7de22ff1b..000000000 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/LuaMethod.java +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.core.asm; - -import dan200.computercraft.api.lua.*; - -import java.util.Collections; - -public interface LuaMethod { - Generator GENERATOR = new Generator<>(LuaMethod.class, Collections.singletonList(ILuaContext.class), - m -> (target, context, args) -> context.executeMainThreadTask(() -> ResultHelpers.checkNormalResult(m.apply(target, context, args.escapes()))) - ); - - IntCache DYNAMIC = new IntCache<>( - method -> (instance, context, args) -> ((IDynamicLuaObject) instance).callMethod(context, method, args) - ); - - String[] EMPTY_METHODS = new String[0]; - - MethodResult apply(Object target, ILuaContext context, IArguments args) throws LuaException; -} diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/LuaMethodSupplier.java b/projects/core/src/main/java/dan200/computercraft/core/asm/LuaMethodSupplier.java new file mode 100644 index 000000000..626e9a329 --- /dev/null +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/LuaMethodSupplier.java @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.asm; + +import dan200.computercraft.api.lua.IDynamicLuaObject; +import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.core.methods.LuaMethod; +import dan200.computercraft.core.methods.MethodSupplier; +import org.jetbrains.annotations.VisibleForTesting; + +import java.util.List; +import java.util.Objects; + +public final class LuaMethodSupplier { + @VisibleForTesting + static final Generator GENERATOR = new Generator<>(LuaMethod.class, List.of(ILuaContext.class), + m -> (target, context, args) -> context.executeMainThreadTask(() -> ResultHelpers.checkNormalResult(m.apply(target, context, args.escapes()))) + ); + private static final IntCache DYNAMIC = new IntCache<>( + method -> (instance, context, args) -> ((IDynamicLuaObject) instance).callMethod(context, method, args) + ); + + private LuaMethodSupplier() { + } + + public static MethodSupplier create() { + return new MethodSupplierImpl<>(GENERATOR, DYNAMIC, x -> x instanceof IDynamicLuaObject dynamic + ? Objects.requireNonNull(dynamic.getMethodNames(), "Dynamic methods cannot be null") + : null + ); + } +} diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/MethodSupplierImpl.java b/projects/core/src/main/java/dan200/computercraft/core/asm/MethodSupplierImpl.java new file mode 100644 index 000000000..1e532d713 --- /dev/null +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/MethodSupplierImpl.java @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.asm; + +import dan200.computercraft.core.methods.MethodSupplier; +import dan200.computercraft.core.methods.ObjectSource; + +import java.util.function.Function; + +final class MethodSupplierImpl implements MethodSupplier { + private final Generator generator; + private final IntCache dynamic; + private final Function dynamicMethods; + + MethodSupplierImpl(Generator generator, IntCache dynamic, Function dynamicMethods) { + this.generator = generator; + this.dynamic = dynamic; + this.dynamicMethods = dynamicMethods; + } + + @Override + public boolean forEachSelfMethod(Object object, UntargetedConsumer consumer) { + var methods = generator.getMethods(object.getClass()); + for (var method : methods) consumer.accept(method.name(), method.method(), method); + + var dynamicMethods = this.dynamicMethods.apply(object); + if (dynamicMethods != null) { + for (var i = 0; i < dynamicMethods.length; i++) consumer.accept(dynamicMethods[i], dynamic.get(i), null); + } + + return !methods.isEmpty() || dynamicMethods != null; + } + + @Override + public boolean forEachMethod(Object object, TargetedConsumer consumer) { + var methods = generator.getMethods(object.getClass()); + for (var method : methods) consumer.accept(object, method.name(), method.method(), method); + + var hasMethods = !methods.isEmpty(); + + if (object instanceof ObjectSource source) { + for (var extra : source.getExtra()) { + var extraMethods = generator.getMethods(extra.getClass()); + if (!extraMethods.isEmpty()) hasMethods = true; + for (var method : extraMethods) consumer.accept(object, method.name(), method.method(), method); + } + } + + var dynamicMethods = this.dynamicMethods.apply(object); + if (dynamicMethods != null) { + hasMethods = true; + for (var i = 0; i < dynamicMethods.length; i++) { + consumer.accept(object, dynamicMethods[i], dynamic.get(i), null); + } + } + + return hasMethods; + } +} diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/ObjectSource.java b/projects/core/src/main/java/dan200/computercraft/core/asm/ObjectSource.java deleted file mode 100644 index 3328c9b89..000000000 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/ObjectSource.java +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.core.asm; - -import java.util.function.BiConsumer; - -/** - * A Lua object which exposes additional methods. - *

- * This can be used to merge multiple objects together into one. Ideally this'd be part of the API, but I'm not entirely - * happy with the interface - something I'd like to think about first. - */ -public interface ObjectSource { - Iterable getExtra(); - - static void allMethods(Generator generator, Object object, BiConsumer> accept) { - for (var method : generator.getMethods(object.getClass())) accept.accept(object, method); - - if (object instanceof ObjectSource source) { - for (var extra : source.getExtra()) { - for (var method : generator.getMethods(extra.getClass())) accept.accept(extra, method); - } - } - } -} diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethod.java b/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethod.java deleted file mode 100644 index f20b85998..000000000 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethod.java +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.core.asm; - -import dan200.computercraft.api.lua.IArguments; -import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.lua.MethodResult; -import dan200.computercraft.api.peripheral.IComputerAccess; -import dan200.computercraft.api.peripheral.IDynamicPeripheral; - -import java.util.Arrays; - -public interface PeripheralMethod { - Generator GENERATOR = new Generator<>(PeripheralMethod.class, Arrays.asList(ILuaContext.class, IComputerAccess.class), - m -> (target, context, computer, args) -> context.executeMainThreadTask(() -> ResultHelpers.checkNormalResult(m.apply(target, context, computer, args.escapes()))) - ); - - IntCache DYNAMIC = new IntCache<>( - method -> (instance, context, computer, args) -> ((IDynamicPeripheral) instance).callMethod(computer, context, method, args) - ); - - MethodResult apply(Object target, ILuaContext context, IComputerAccess computer, IArguments args) throws LuaException; -} diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethodSupplier.java b/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethodSupplier.java new file mode 100644 index 000000000..5010449c9 --- /dev/null +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethodSupplier.java @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.asm; + +import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IDynamicPeripheral; +import dan200.computercraft.core.methods.MethodSupplier; +import dan200.computercraft.core.methods.PeripheralMethod; + +import java.util.List; +import java.util.Objects; + +public class PeripheralMethodSupplier { + private static final Generator GENERATOR = new Generator<>(PeripheralMethod.class, List.of(ILuaContext.class, IComputerAccess.class), + m -> (target, context, computer, args) -> context.executeMainThreadTask(() -> ResultHelpers.checkNormalResult(m.apply(target, context, computer, args.escapes()))) + ); + private static final IntCache DYNAMIC = new IntCache<>( + method -> (instance, context, computer, args) -> ((IDynamicPeripheral) instance).callMethod(computer, context, method, args) + ); + + public static MethodSupplier create() { + return new MethodSupplierImpl<>(GENERATOR, DYNAMIC, x -> x instanceof IDynamicPeripheral dynamic + ? Objects.requireNonNull(dynamic.getMethodNames(), "Dynamic methods cannot be null") + : null + ); + } +} diff --git a/projects/core/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java b/projects/core/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java index 4c7015502..3680ccba1 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java +++ b/projects/core/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java @@ -15,6 +15,8 @@ import dan200.computercraft.core.filesystem.FileSystemException; import dan200.computercraft.core.lua.ILuaMachine; import dan200.computercraft.core.lua.MachineEnvironment; import dan200.computercraft.core.lua.MachineException; +import dan200.computercraft.core.methods.LuaMethod; +import dan200.computercraft.core.methods.MethodSupplier; import dan200.computercraft.core.metrics.Metrics; import dan200.computercraft.core.metrics.MetricsObserver; import dan200.computercraft.core.util.Colour; @@ -61,6 +63,7 @@ final class ComputerExecutor { private final MetricsObserver metrics; private final List apis = new ArrayList<>(); private final ComputerThread scheduler; + private final MethodSupplier luaMethods; final TimeoutState timeout; private @Nullable FileSystem fileSystem; @@ -168,6 +171,7 @@ final class ComputerExecutor { metrics = computerEnvironment.getMetrics(); luaFactory = context.luaFactory(); scheduler = context.computerScheduler(); + luaMethods = context.luaMethods(); timeout = new TimeoutState(scheduler); var environment = computer.getEnvironment(); @@ -176,7 +180,7 @@ final class ComputerExecutor { apis.add(new TermAPI(environment)); apis.add(new RedstoneAPI(environment)); apis.add(new FSAPI(environment)); - apis.add(new PeripheralAPI(environment)); + apis.add(new PeripheralAPI(environment, context.peripheralMethods())); apis.add(new OSAPI(environment)); if (CoreConfig.httpEnabled) apis.add(new HTTPAPI(environment)); @@ -382,6 +386,7 @@ final class ComputerExecutor { return luaFactory.create(new MachineEnvironment( new LuaContext(computer), metrics, timeout, () -> apis.stream().map(api -> api instanceof ApiWrapper wrapper ? wrapper.getDelegate() : api).iterator(), + luaMethods, computer.getGlobalEnvironment().getHostString() ), bios); } catch (IOException e) { diff --git a/projects/core/src/main/java/dan200/computercraft/core/lua/BasicFunction.java b/projects/core/src/main/java/dan200/computercraft/core/lua/BasicFunction.java index dc9593fb7..44924be57 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/lua/BasicFunction.java +++ b/projects/core/src/main/java/dan200/computercraft/core/lua/BasicFunction.java @@ -8,7 +8,7 @@ import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.MethodResult; import dan200.computercraft.core.Logging; -import dan200.computercraft.core.asm.LuaMethod; +import dan200.computercraft.core.methods.LuaMethod; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.squiddev.cobalt.LuaError; diff --git a/projects/core/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java b/projects/core/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java index 7ce55baa1..7d181d5fb 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java +++ b/projects/core/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java @@ -10,9 +10,9 @@ import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.ILuaFunction; import dan200.computercraft.core.CoreConfig; import dan200.computercraft.core.Logging; -import dan200.computercraft.core.asm.LuaMethod; -import dan200.computercraft.core.asm.ObjectSource; import dan200.computercraft.core.computer.TimeoutState; +import dan200.computercraft.core.methods.LuaMethod; +import dan200.computercraft.core.methods.MethodSupplier; import dan200.computercraft.core.util.Nullability; import dan200.computercraft.core.util.SanitisedError; import org.slf4j.Logger; @@ -39,6 +39,7 @@ public class CobaltLuaMachine implements ILuaMachine { private final TimeoutState timeout; private final Runnable timeoutListener = this::updateTimeout; private final ILuaContext context; + private final MethodSupplier luaMethods; private final LuaState state; private final LuaThread mainRoutine; @@ -51,6 +52,7 @@ public class CobaltLuaMachine implements ILuaMachine { public CobaltLuaMachine(MachineEnvironment environment, InputStream bios) throws MachineException, IOException { timeout = environment.timeout(); context = environment.context(); + luaMethods = environment.luaMethods(); // Create an environment to run in var state = this.state = LuaState.builder() @@ -161,28 +163,13 @@ public class CobaltLuaMachine implements ILuaMachine { @Nullable private LuaTable wrapLuaObject(Object object) { - var dynamicMethods = object instanceof IDynamicLuaObject dynamic - ? Objects.requireNonNull(dynamic.getMethodNames(), "Methods cannot be null") - : LuaMethod.EMPTY_METHODS; - var table = new LuaTable(); - for (var i = 0; i < dynamicMethods.length; i++) { - var method = dynamicMethods[i]; - table.rawset(method, new ResultInterpreterFunction(this, LuaMethod.DYNAMIC.get(i), object, context, method)); - } + var found = luaMethods.forEachMethod(object, (target, name, method, info) -> + table.rawset(name, info != null && info.nonYielding() + ? new BasicFunction(this, method, target, context, name) + : new ResultInterpreterFunction(this, method, target, context, name))); - ObjectSource.allMethods(LuaMethod.GENERATOR, object, (instance, method) -> - table.rawset(method.name(), method.nonYielding() - ? new BasicFunction(this, method.method(), instance, context, method.name()) - : new ResultInterpreterFunction(this, method.method(), instance, context, method.name()))); - - try { - if (table.next(Constants.NIL).first().isNil()) return null; - } catch (LuaError ignored) { - // next should never throw on nil. - } - - return table; + return found ? table : null; } private LuaValue toValue(@Nullable Object object, @Nullable IdentityHashMap values) { diff --git a/projects/core/src/main/java/dan200/computercraft/core/lua/MachineEnvironment.java b/projects/core/src/main/java/dan200/computercraft/core/lua/MachineEnvironment.java index f3b7f51d5..2e9d5529c 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/lua/MachineEnvironment.java +++ b/projects/core/src/main/java/dan200/computercraft/core/lua/MachineEnvironment.java @@ -8,6 +8,8 @@ import dan200.computercraft.api.lua.ILuaAPI; import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.core.computer.GlobalEnvironment; import dan200.computercraft.core.computer.TimeoutState; +import dan200.computercraft.core.methods.LuaMethod; +import dan200.computercraft.core.methods.MethodSupplier; import dan200.computercraft.core.metrics.MetricsObserver; /** @@ -18,6 +20,7 @@ import dan200.computercraft.core.metrics.MetricsObserver; * @param timeout The current timeout state. This should be used by the machine to interrupt its execution. * @param apis APIs to inject into the global environment. Each API should be converted into a Lua object * (following the same rules as any other value), and then set to all names in {@link ILuaAPI#getNames()}. + * @param luaMethods A {@link MethodSupplier} to find methods on returned values. * @param hostString A {@linkplain GlobalEnvironment#getHostString() host string} to identify the current environment. * @see ILuaMachine.Factory */ @@ -26,6 +29,7 @@ public record MachineEnvironment( MetricsObserver metrics, TimeoutState timeout, Iterable apis, + MethodSupplier luaMethods, String hostString ) { } diff --git a/projects/core/src/main/java/dan200/computercraft/core/lua/ResultInterpreterFunction.java b/projects/core/src/main/java/dan200/computercraft/core/lua/ResultInterpreterFunction.java index 04ed846f0..57e09f561 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/lua/ResultInterpreterFunction.java +++ b/projects/core/src/main/java/dan200/computercraft/core/lua/ResultInterpreterFunction.java @@ -9,7 +9,7 @@ import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.MethodResult; import dan200.computercraft.core.Logging; -import dan200.computercraft.core.asm.LuaMethod; +import dan200.computercraft.core.methods.LuaMethod; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.squiddev.cobalt.*; diff --git a/projects/core/src/main/java/dan200/computercraft/core/methods/LuaMethod.java b/projects/core/src/main/java/dan200/computercraft/core/methods/LuaMethod.java new file mode 100644 index 000000000..58052f23a --- /dev/null +++ b/projects/core/src/main/java/dan200/computercraft/core/methods/LuaMethod.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.methods; + +import dan200.computercraft.api.lua.*; + +/** + * A basic Lua function (i.e. one not associated with a peripheral) on some object (such as a {@link IDynamicLuaObject} + * or {@link ILuaAPI}. + *

+ * This interface is not typically implemented yourself, but instead generated from a {@link LuaFunction}-annotated + * method. + * + * @see NamedMethod + */ +@FunctionalInterface +public interface LuaMethod { + /** + * Apply this method. + * + * @param target The object instance that this method targets. + * @param context The Lua context for this function call. + * @param args Arguments to this function. + * @return The return call of this function. + * @throws LuaException Thrown by the underlying method call. + * @see IDynamicLuaObject#callMethod(ILuaContext, int, IArguments) + */ + MethodResult apply(Object target, ILuaContext context, IArguments args) throws LuaException; +} diff --git a/projects/core/src/main/java/dan200/computercraft/core/methods/MethodSupplier.java b/projects/core/src/main/java/dan200/computercraft/core/methods/MethodSupplier.java new file mode 100644 index 000000000..13f45b915 --- /dev/null +++ b/projects/core/src/main/java/dan200/computercraft/core/methods/MethodSupplier.java @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.methods; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; + +/** + * Finds methods available on an object and yields them. + * + * @param The type of method, such as {@link LuaMethod} or {@link PeripheralMethod}. + */ +public interface MethodSupplier { + /** + * Iterate over methods available on an object, ignoring {@link ObjectSource}s. + * + * @param object The object to find methods for. + * @param consumer The consumer to call for each method. + * @return Whether any methods were found. + */ + boolean forEachSelfMethod(Object object, UntargetedConsumer consumer); + + /** + * Generate a map of all methods targeting the current object, ignoring {@link ObjectSource}s. + * + * @param object The object to find methods for. + * @return A map of all methods on the object. + */ + default Map getSelfMethods(Object object) { + var map = new HashMap(); + forEachSelfMethod(object, (n, m, i) -> map.put(n, m)); + return map; + } + + /** + * Iterate over all methods on an object, including {@link ObjectSource}s. + * + * @param object The object to find methods for. + * @param consumer The consumer to call for each method. + * @return Whether any methods were found. + */ + boolean forEachMethod(Object object, TargetedConsumer consumer); + + /** + * A function which is called for each method on an object. + * + * @param The type of method, such as {@link LuaMethod} or {@link PeripheralMethod}. + * @see #forEachSelfMethod(Object, UntargetedConsumer) + */ + @FunctionalInterface + interface UntargetedConsumer { + /** + * Consume a method on an object. + * + * @param name The name of this method. + * @param method The actual method definition. + * @param info Additional information about the method, such as whether it will yield. May be {@code null}. + */ + void accept(String name, T method, @Nullable NamedMethod info); + } + + /** + * A function which is called for each method on an object and possibly nested objects. + * + * @param The type of method, such as {@link LuaMethod} or {@link PeripheralMethod}. + * @see #forEachMethod(Object, TargetedConsumer) + */ + @FunctionalInterface + interface TargetedConsumer { + /** + * Consume a method on an object. + * + * @param object The object this method targets, should be passed to the method's {@code apply(...)} function. + * @param name The name of this method. + * @param method The actual method definition. + * @param info Additional information about the method, such as whether it will yield. May be {@code null}. + */ + void accept(Object object, String name, T method, @Nullable NamedMethod info); + } +} diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/NamedMethod.java b/projects/core/src/main/java/dan200/computercraft/core/methods/NamedMethod.java similarity index 96% rename from projects/core/src/main/java/dan200/computercraft/core/asm/NamedMethod.java rename to projects/core/src/main/java/dan200/computercraft/core/methods/NamedMethod.java index 4c4cfbf8d..f4f5de441 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/NamedMethod.java +++ b/projects/core/src/main/java/dan200/computercraft/core/methods/NamedMethod.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MPL-2.0 -package dan200.computercraft.core.asm; +package dan200.computercraft.core.methods; import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.lua.MethodResult; diff --git a/projects/core/src/main/java/dan200/computercraft/core/methods/ObjectSource.java b/projects/core/src/main/java/dan200/computercraft/core/methods/ObjectSource.java new file mode 100644 index 000000000..1ce62fe41 --- /dev/null +++ b/projects/core/src/main/java/dan200/computercraft/core/methods/ObjectSource.java @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.methods; + +/** + * A Lua object which exposes additional methods. + *

+ * This can be used to merge multiple objects together into one. Ideally this'd be part of the API, but I'm not entirely + * happy with the interface - something I'd like to think about first. + */ +public interface ObjectSource { + Iterable getExtra(); +} diff --git a/projects/core/src/main/java/dan200/computercraft/core/methods/PeripheralMethod.java b/projects/core/src/main/java/dan200/computercraft/core/methods/PeripheralMethod.java new file mode 100644 index 000000000..0269146fe --- /dev/null +++ b/projects/core/src/main/java/dan200/computercraft/core/methods/PeripheralMethod.java @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.methods; + +import dan200.computercraft.api.lua.*; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IDynamicPeripheral; +import dan200.computercraft.api.peripheral.IPeripheral; + +/** + * A Lua function associated with some peripheral. + *

+ * This interface is not typically implemented yourself, but instead generated from a {@link LuaFunction}-annotated + * method. + * + * @see NamedMethod + * @see IPeripheral + */ +@FunctionalInterface +public interface PeripheralMethod { + /** + * Apply this method. + * + * @param target The object instance that this method targets. + * @param context The Lua context for this function call. + * @param computer The interface to the computer that is making the call. + * @param args Arguments to this function. + * @return The return call of this function. + * @throws LuaException Thrown by the underlying method call. + * @see IDynamicPeripheral#callMethod(IComputerAccess, ILuaContext, int, IArguments) + */ + MethodResult apply(Object target, ILuaContext context, IComputerAccess computer, IArguments args) throws LuaException; +} diff --git a/projects/core/src/test/java/dan200/computercraft/core/apis/ObjectWrapper.java b/projects/core/src/test/java/dan200/computercraft/core/apis/ObjectWrapper.java index 513848e7b..6b389a1e8 100644 --- a/projects/core/src/test/java/dan200/computercraft/core/apis/ObjectWrapper.java +++ b/projects/core/src/test/java/dan200/computercraft/core/apis/ObjectWrapper.java @@ -4,32 +4,25 @@ package dan200.computercraft.core.apis; -import dan200.computercraft.api.lua.*; -import dan200.computercraft.core.asm.LuaMethod; +import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaTask; +import dan200.computercraft.api.lua.ObjectArguments; +import dan200.computercraft.core.asm.LuaMethodSupplier; +import dan200.computercraft.core.methods.LuaMethod; +import dan200.computercraft.core.methods.MethodSupplier; -import java.util.HashMap; import java.util.Map; -import java.util.Objects; public class ObjectWrapper implements ILuaContext { + private static final MethodSupplier LUA_METHODS = LuaMethodSupplier.create(); + private final Object object; private final Map methodMap; public ObjectWrapper(Object object) { this.object = object; - var dynamicMethods = object instanceof IDynamicLuaObject dynamic - ? Objects.requireNonNull(dynamic.getMethodNames(), "Methods cannot be null") - : LuaMethod.EMPTY_METHODS; - - var methods = LuaMethod.GENERATOR.getMethods(object.getClass()); - - var methodMap = this.methodMap = new HashMap<>(methods.size() + dynamicMethods.length); - for (var i = 0; i < dynamicMethods.length; i++) { - methodMap.put(dynamicMethods[i], LuaMethod.DYNAMIC.get(i)); - } - for (var method : methods) { - methodMap.put(method.name(), method.method()); - } + methodMap = LUA_METHODS.getSelfMethods(object); } public Object[] call(String name, Object... args) throws LuaException { diff --git a/projects/core/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java b/projects/core/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java index 85de1ba28..cc504eb27 100644 --- a/projects/core/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java +++ b/projects/core/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java @@ -7,6 +7,8 @@ package dan200.computercraft.core.asm; import dan200.computercraft.api.lua.*; import dan200.computercraft.api.peripheral.IComputerAccess; import dan200.computercraft.core.computer.ComputerSide; +import dan200.computercraft.core.methods.LuaMethod; +import dan200.computercraft.core.methods.NamedMethod; import org.hamcrest.Matcher; import org.junit.jupiter.api.Test; @@ -21,9 +23,11 @@ import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.assertThrows; public class GeneratorTest { + private static final Generator GENERATOR = LuaMethodSupplier.GENERATOR; + @Test public void testBasic() { - var methods = LuaMethod.GENERATOR.getMethods(Basic.class); + var methods = GENERATOR.getMethods(Basic.class); assertThat(methods, contains( allOf( named("go"), @@ -34,48 +38,48 @@ public class GeneratorTest { @Test public void testIdentical() { - var methods = LuaMethod.GENERATOR.getMethods(Basic.class); - var methods2 = LuaMethod.GENERATOR.getMethods(Basic.class); + var methods = GENERATOR.getMethods(Basic.class); + var methods2 = GENERATOR.getMethods(Basic.class); assertThat(methods, sameInstance(methods2)); } @Test public void testIdenticalMethods() { - var methods = LuaMethod.GENERATOR.getMethods(Basic.class); - var methods2 = LuaMethod.GENERATOR.getMethods(Basic2.class); + var methods = GENERATOR.getMethods(Basic.class); + var methods2 = GENERATOR.getMethods(Basic2.class); assertThat(methods, contains(named("go"))); assertThat(methods.get(0).method(), sameInstance(methods2.get(0).method())); } @Test public void testEmptyClass() { - assertThat(LuaMethod.GENERATOR.getMethods(Empty.class), is(empty())); + assertThat(GENERATOR.getMethods(Empty.class), is(empty())); } @Test public void testNonPublicClass() { - assertThat(LuaMethod.GENERATOR.getMethods(NonPublic.class), is(empty())); + assertThat(GENERATOR.getMethods(NonPublic.class), is(empty())); } @Test public void testNonInstance() { - assertThat(LuaMethod.GENERATOR.getMethods(NonInstance.class), is(empty())); + assertThat(GENERATOR.getMethods(NonInstance.class), is(empty())); } @Test public void testIllegalThrows() { - assertThat(LuaMethod.GENERATOR.getMethods(IllegalThrows.class), is(empty())); + assertThat(GENERATOR.getMethods(IllegalThrows.class), is(empty())); } @Test public void testCustomNames() { - var methods = LuaMethod.GENERATOR.getMethods(CustomNames.class); + var methods = GENERATOR.getMethods(CustomNames.class); assertThat(methods, contains(named("go1"), named("go2"))); } @Test public void testArgKinds() { - var methods = LuaMethod.GENERATOR.getMethods(ArgKinds.class); + var methods = GENERATOR.getMethods(ArgKinds.class); assertThat(methods, containsInAnyOrder( named("objectArg"), named("intArg"), named("optIntArg"), named("context"), named("arguments") @@ -84,7 +88,7 @@ public class GeneratorTest { @Test public void testEnum() throws LuaException { - var methods = LuaMethod.GENERATOR.getMethods(EnumMethods.class); + var methods = GENERATOR.getMethods(EnumMethods.class); assertThat(methods, containsInAnyOrder(named("getEnum"), named("optEnum"))); assertThat(apply(methods, new EnumMethods(), "getEnum", "front"), one(is("FRONT"))); @@ -95,7 +99,7 @@ public class GeneratorTest { @Test public void testMainThread() throws LuaException { - var methods = LuaMethod.GENERATOR.getMethods(MainThread.class); + var methods = GENERATOR.getMethods(MainThread.class); assertThat(methods, contains(allOf( named("go"), contramap(is(false), "non-yielding", NamedMethod::nonYielding) @@ -107,7 +111,7 @@ public class GeneratorTest { @Test public void testUnsafe() { - var methods = LuaMethod.GENERATOR.getMethods(Unsafe.class); + var methods = GENERATOR.getMethods(Unsafe.class); assertThat(methods, contains(named("withUnsafe"))); } diff --git a/projects/core/src/test/java/dan200/computercraft/core/asm/MethodTest.java b/projects/core/src/test/java/dan200/computercraft/core/asm/MethodTest.java index bfff064e1..a61b15ec0 100644 --- a/projects/core/src/test/java/dan200/computercraft/core/asm/MethodTest.java +++ b/projects/core/src/test/java/dan200/computercraft/core/asm/MethodTest.java @@ -10,6 +10,7 @@ import dan200.computercraft.api.peripheral.IDynamicPeripheral; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.core.computer.ComputerBootstrap; import dan200.computercraft.core.computer.ComputerSide; +import dan200.computercraft.core.methods.ObjectSource; import org.junit.jupiter.api.Test; import javax.annotation.Nullable; diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java b/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java index a9eefd5bb..ac0d54e92 100644 --- a/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java @@ -11,11 +11,15 @@ import net.minecraft.core.Direction; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.util.List; public class GenericPeripheralProvider { + private static final Logger LOG = LoggerFactory.getLogger(GenericPeripheralProvider.class); + interface Lookup { @Nullable T find(Level world, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, Direction context); @@ -29,7 +33,13 @@ public class GenericPeripheralProvider { public static IPeripheral getPeripheral(Level level, BlockPos pos, Direction side, @Nullable BlockEntity blockEntity) { if (blockEntity == null) return null; - var builder = new GenericPeripheralBuilder(); + var server = level.getServer(); + if (server == null) { + LOG.warn("Fetching peripherals on a non-server level {}.", level, new IllegalStateException("Fetching peripherals on a non-server level.")); + return null; + } + + var builder = new GenericPeripheralBuilder(server); builder.addMethods(blockEntity); for (var lookup : lookups) { diff --git a/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java b/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java index d8c275101..64c9333e1 100644 --- a/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java +++ b/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java @@ -11,12 +11,16 @@ import net.minecraft.core.Direction; import net.minecraft.world.level.Level; import net.minecraftforge.common.capabilities.Capability; import net.minecraftforge.common.util.NonNullConsumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Objects; public class GenericPeripheralProvider { + private static final Logger LOG = LoggerFactory.getLogger(GenericPeripheralProvider.class); + private static final ArrayList> capabilities = new ArrayList<>(); public static synchronized void addCapability(Capability capability) { @@ -29,7 +33,13 @@ public class GenericPeripheralProvider { var blockEntity = level.getBlockEntity(pos); if (blockEntity == null) return null; - var builder = new GenericPeripheralBuilder(); + var server = level.getServer(); + if (server == null) { + LOG.warn("Fetching peripherals on a non-server level {}.", level, new IllegalStateException("Fetching peripherals on a non-server level.")); + return null; + } + + var builder = new GenericPeripheralBuilder(server); builder.addMethods(blockEntity); for (var capability : capabilities) { From 910a63214e395ecae6993d8e0487384c725b3dd3 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Mon, 26 Jun 2023 21:46:55 +0100 Subject: [PATCH 11/32] Make Generic methods per-ComputerContext - Move the class cache out of Generator into MethodSupplierImpl. This means we cache class generation globally (that's really expensive!), but the class -> method list lookup is local. - Move the global GenericSource/GenericMethod registry out of core, passing in the list of generic methods to the ComputerContext. I'm not entirely thrilled by the slight overlap of MethodSupplierImpl and Generator here, something to clean up in the future. --- .../impl/AbstractComputerCraftAPI.java | 3 +- .../computercraft/impl/ApiFactories.java | 11 ++- .../computercraft/impl/GenericSources.java | 34 +++++++ .../shared/computer/core/ServerContext.java | 2 + .../computercraft/core/ComputerContext.java | 21 +++- .../computercraft/core/asm/Generator.java | 65 +----------- .../computercraft/core/asm/GenericMethod.java | 31 ++---- .../core/asm/LuaMethodSupplier.java | 15 ++- .../core/asm/MethodSupplierImpl.java | 99 ++++++++++++++++++- .../core/asm/PeripheralMethodSupplier.java | 11 ++- .../core/apis/ObjectWrapper.java | 3 +- .../computercraft/core/asm/GeneratorTest.java | 3 +- 12 files changed, 193 insertions(+), 105 deletions(-) create mode 100644 projects/common/src/main/java/dan200/computercraft/impl/GenericSources.java diff --git a/projects/common/src/main/java/dan200/computercraft/impl/AbstractComputerCraftAPI.java b/projects/common/src/main/java/dan200/computercraft/impl/AbstractComputerCraftAPI.java index b6ba303f8..dc4ca4f7a 100644 --- a/projects/common/src/main/java/dan200/computercraft/impl/AbstractComputerCraftAPI.java +++ b/projects/common/src/main/java/dan200/computercraft/impl/AbstractComputerCraftAPI.java @@ -19,7 +19,6 @@ import dan200.computercraft.api.pocket.PocketUpgradeSerialiser; import dan200.computercraft.api.redstone.BundledRedstoneProvider; import dan200.computercraft.api.turtle.TurtleRefuelHandler; import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser; -import dan200.computercraft.core.asm.GenericMethod; import dan200.computercraft.core.filesystem.WritableFileMount; import dan200.computercraft.impl.detail.DetailRegistryImpl; import dan200.computercraft.impl.network.wired.WiredNodeImpl; @@ -78,7 +77,7 @@ public abstract class AbstractComputerCraftAPI implements ComputerCraftAPIServic @Override public final void registerGenericSource(GenericSource source) { - GenericMethod.register(source); + GenericSources.register(source); } @Override diff --git a/projects/common/src/main/java/dan200/computercraft/impl/ApiFactories.java b/projects/common/src/main/java/dan200/computercraft/impl/ApiFactories.java index ef96738ef..388854fce 100644 --- a/projects/common/src/main/java/dan200/computercraft/impl/ApiFactories.java +++ b/projects/common/src/main/java/dan200/computercraft/impl/ApiFactories.java @@ -11,19 +11,24 @@ import java.util.Collections; import java.util.LinkedHashSet; import java.util.Objects; +/** + * The global factory for {@link ILuaAPIFactory}s. + * + * @see dan200.computercraft.core.ComputerContext.Builder#apiFactories(Collection) + * @see dan200.computercraft.api.ComputerCraftAPI#registerAPIFactory(ILuaAPIFactory) + */ public final class ApiFactories { private ApiFactories() { } private static final Collection factories = new LinkedHashSet<>(); - private static final Collection factoriesView = Collections.unmodifiableCollection(factories); - public static synchronized void register(ILuaAPIFactory factory) { + static synchronized void register(ILuaAPIFactory factory) { Objects.requireNonNull(factory, "provider cannot be null"); factories.add(factory); } public static Collection getAll() { - return factoriesView; + return Collections.unmodifiableCollection(factories); } } diff --git a/projects/common/src/main/java/dan200/computercraft/impl/GenericSources.java b/projects/common/src/main/java/dan200/computercraft/impl/GenericSources.java new file mode 100644 index 000000000..0ba250c9c --- /dev/null +++ b/projects/common/src/main/java/dan200/computercraft/impl/GenericSources.java @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.impl; + +import dan200.computercraft.api.lua.GenericSource; +import dan200.computercraft.core.asm.GenericMethod; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Objects; + +/** + * The global registry for {@link GenericSource}s. + * + * @see dan200.computercraft.core.ComputerContext.Builder#genericMethods(Collection) + * @see dan200.computercraft.api.ComputerCraftAPI#registerGenericSource(GenericSource) + */ +public final class GenericSources { + private GenericSources() { + } + + private static final Collection sources = new LinkedHashSet<>(); + + static synchronized void register(GenericSource source) { + Objects.requireNonNull(source, "provider cannot be null"); + sources.add(source); + } + + public static Collection getAllMethods() { + return sources.stream().flatMap(GenericMethod::getMethods).toList(); + } +} diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java index 108c95483..64480353b 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java @@ -18,6 +18,7 @@ import dan200.computercraft.core.methods.MethodSupplier; import dan200.computercraft.core.methods.PeripheralMethod; import dan200.computercraft.impl.AbstractComputerCraftAPI; import dan200.computercraft.impl.ApiFactories; +import dan200.computercraft.impl.GenericSources; import dan200.computercraft.shared.CommonHooks; import dan200.computercraft.shared.computer.metrics.GlobalMetrics; import dan200.computercraft.shared.config.ConfigSpec; @@ -74,6 +75,7 @@ public final class ServerContext { .mainThreadScheduler(mainThread) .luaFactory(luaMachine) .apiFactories(ApiFactories.getAll()) + .genericMethods(GenericSources.getAllMethods()) .build(); idAssigner = new IDAssigner(storageDir.resolve("ids.json")); } diff --git a/projects/core/src/main/java/dan200/computercraft/core/ComputerContext.java b/projects/core/src/main/java/dan200/computercraft/core/ComputerContext.java index 75a6cac6d..b7fefae86 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/ComputerContext.java +++ b/projects/core/src/main/java/dan200/computercraft/core/ComputerContext.java @@ -5,6 +5,7 @@ package dan200.computercraft.core; import dan200.computercraft.api.lua.ILuaAPIFactory; +import dan200.computercraft.core.asm.GenericMethod; import dan200.computercraft.core.asm.LuaMethodSupplier; import dan200.computercraft.core.asm.PeripheralMethodSupplier; import dan200.computercraft.core.computer.ComputerThread; @@ -165,6 +166,7 @@ public final class ComputerContext { private @Nullable MainThreadScheduler mainThreadScheduler; private @Nullable ILuaMachine.Factory luaFactory; private @Nullable List apiFactories; + private @Nullable List genericMethods; Builder(GlobalEnvironment environment) { this.environment = environment; @@ -225,6 +227,21 @@ public final class ComputerContext { return this; } + /** + * Set the set of {@link GenericMethod}s used by the {@linkplain MethodSupplier method suppliers}. + * + * @param genericMethods A list of API factories. + * @return {@code this}, for chaining + * @see ComputerContext#luaMethods() + * @see ComputerContext#peripheralMethods() + */ + public Builder genericMethods(Collection genericMethods) { + Objects.requireNonNull(genericMethods); + if (this.genericMethods != null) throw new IllegalStateException("Main-thread scheduler already specified"); + this.genericMethods = List.copyOf(genericMethods); + return this; + } + /** * Create a new {@link ComputerContext}. * @@ -237,8 +254,8 @@ public final class ComputerContext { mainThreadScheduler == null ? new NoWorkMainThreadScheduler() : mainThreadScheduler, luaFactory == null ? CobaltLuaMachine::new : luaFactory, apiFactories == null ? List.of() : apiFactories, - LuaMethodSupplier.create(), - PeripheralMethodSupplier.create() + LuaMethodSupplier.create(genericMethods == null ? List.of() : genericMethods), + PeripheralMethodSupplier.create(genericMethods == null ? List.of() : genericMethods) ); } } diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/Generator.java b/projects/core/src/main/java/dan200/computercraft/core/asm/Generator.java index 4b79e0060..7a973f5b3 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/Generator.java +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/Generator.java @@ -10,8 +10,6 @@ import com.google.common.cache.LoadingCache; import com.google.common.primitives.Primitives; import com.google.common.reflect.TypeToken; import dan200.computercraft.api.lua.*; -import dan200.computercraft.api.peripheral.PeripheralType; -import dan200.computercraft.core.methods.NamedMethod; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Type; @@ -21,11 +19,8 @@ import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; @@ -55,10 +50,6 @@ final class Generator { private final Function wrap; - private final LoadingCache, List>> classCache = CacheBuilder - .newBuilder() - .build(CacheLoader.from(catching(this::build, Collections.emptyList()))); - private final LoadingCache> methodCache = CacheBuilder .newBuilder() .build(CacheLoader.from(catching(this::build, Optional.empty()))); @@ -75,58 +66,8 @@ final class Generator { this.methodDesc = methodDesc.toString(); } - public List> getMethods(Class klass) { - try { - return classCache.get(klass); - } catch (ExecutionException e) { - LOG.error("Error getting methods for {}.", klass.getName(), e.getCause()); - return Collections.emptyList(); - } - } - - private List> build(Class klass) { - ArrayList> methods = null; - for (var method : klass.getMethods()) { - var annotation = method.getAnnotation(LuaFunction.class); - if (annotation == null) continue; - - if (Modifier.isStatic(method.getModifiers())) { - LOG.warn("LuaFunction method {}.{} should be an instance method.", method.getDeclaringClass(), method.getName()); - continue; - } - - var instance = methodCache.getUnchecked(method).orElse(null); - if (instance == null) continue; - - if (methods == null) methods = new ArrayList<>(); - addMethod(methods, method, annotation, null, instance); - } - - for (var method : GenericMethod.all()) { - if (!method.target.isAssignableFrom(klass)) continue; - - var instance = methodCache.getUnchecked(method.method).orElse(null); - if (instance == null) continue; - - if (methods == null) methods = new ArrayList<>(); - addMethod(methods, method.method, method.annotation, method.peripheralType, instance); - } - - if (methods == null) return Collections.emptyList(); - methods.trimToSize(); - return Collections.unmodifiableList(methods); - } - - private void addMethod(List> methods, Method method, LuaFunction annotation, @Nullable PeripheralType genericType, T instance) { - var names = annotation.value(); - var isSimple = method.getReturnType() != MethodResult.class && !annotation.mainThread(); - if (names.length == 0) { - methods.add(new NamedMethod<>(method.getName(), instance, isSimple, genericType)); - } else { - for (var name : names) { - methods.add(new NamedMethod<>(name, instance, isSimple, genericType)); - } - } + Optional getMethod(Method method) { + return methodCache.getUnchecked(method); } private Optional build(Method method) { @@ -337,7 +278,7 @@ final class Generator { } @SuppressWarnings("Guava") - private static com.google.common.base.Function catching(Function function, U def) { + static com.google.common.base.Function catching(Function function, U def) { return x -> { try { return function.apply(x); diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/GenericMethod.java b/projects/core/src/main/java/dan200/computercraft/core/asm/GenericMethod.java index d7a8468bf..1e18b0a0b 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/GenericMethod.java +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/GenericMethod.java @@ -14,16 +14,14 @@ import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import java.util.Objects; import java.util.stream.Stream; /** * A generic method is a method belonging to a {@link GenericSource} with a known target. */ -public class GenericMethod { +public final class GenericMethod { private static final Logger LOG = LoggerFactory.getLogger(GenericMethod.class); final Method method; @@ -31,37 +29,24 @@ public class GenericMethod { final Class target; final @Nullable PeripheralType peripheralType; - private static final List sources = new ArrayList<>(); - private static @Nullable List cache; - - GenericMethod(Method method, LuaFunction annotation, Class target, @Nullable PeripheralType peripheralType) { + private GenericMethod(Method method, LuaFunction annotation, Class target, @Nullable PeripheralType peripheralType) { this.method = method; this.annotation = annotation; this.target = target; this.peripheralType = peripheralType; } + public String name() { + return method.getName(); + } + /** * Find all public static methods annotated with {@link LuaFunction} which belong to a {@link GenericSource}. * + * @param source The given generic source. * @return All available generic methods. */ - static List all() { - if (cache != null) return cache; - return cache = sources.stream().flatMap(GenericMethod::getMethods).toList(); - } - - public static synchronized void register(GenericSource source) { - Objects.requireNonNull(source, "Source cannot be null"); - - if (cache != null) { - LOG.warn("Registering a generic source {} after cache has been built. This source will be ignored.", cache); - } - - sources.add(source); - } - - private static Stream getMethods(GenericSource source) { + public static Stream getMethods(GenericSource source) { Class klass = source.getClass(); var type = source instanceof GenericPeripheral generic ? generic.getType() : null; diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/LuaMethodSupplier.java b/projects/core/src/main/java/dan200/computercraft/core/asm/LuaMethodSupplier.java index 626e9a329..11f8e4310 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/LuaMethodSupplier.java +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/LuaMethodSupplier.java @@ -6,16 +6,21 @@ package dan200.computercraft.core.asm; import dan200.computercraft.api.lua.IDynamicLuaObject; import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.core.ComputerContext; import dan200.computercraft.core.methods.LuaMethod; import dan200.computercraft.core.methods.MethodSupplier; -import org.jetbrains.annotations.VisibleForTesting; import java.util.List; import java.util.Objects; +/** + * Provides a {@link MethodSupplier} for {@link LuaMethod}s. + *

+ * This is used by {@link ComputerContext} to construct {@linkplain ComputerContext#peripheralMethods() the context-wide + * method supplier}. It should not be used directly. + */ public final class LuaMethodSupplier { - @VisibleForTesting - static final Generator GENERATOR = new Generator<>(LuaMethod.class, List.of(ILuaContext.class), + private static final Generator GENERATOR = new Generator<>(LuaMethod.class, List.of(ILuaContext.class), m -> (target, context, args) -> context.executeMainThreadTask(() -> ResultHelpers.checkNormalResult(m.apply(target, context, args.escapes()))) ); private static final IntCache DYNAMIC = new IntCache<>( @@ -25,8 +30,8 @@ public final class LuaMethodSupplier { private LuaMethodSupplier() { } - public static MethodSupplier create() { - return new MethodSupplierImpl<>(GENERATOR, DYNAMIC, x -> x instanceof IDynamicLuaObject dynamic + public static MethodSupplier create(List genericMethods) { + return new MethodSupplierImpl<>(genericMethods, GENERATOR, DYNAMIC, x -> x instanceof IDynamicLuaObject dynamic ? Objects.requireNonNull(dynamic.getMethodNames(), "Dynamic methods cannot be null") : null ); diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/MethodSupplierImpl.java b/projects/core/src/main/java/dan200/computercraft/core/asm/MethodSupplierImpl.java index 1e532d713..44e5775e4 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/MethodSupplierImpl.java +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/MethodSupplierImpl.java @@ -4,17 +4,49 @@ package dan200.computercraft.core.asm; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.lua.MethodResult; +import dan200.computercraft.api.peripheral.PeripheralType; import dan200.computercraft.core.methods.MethodSupplier; +import dan200.computercraft.core.methods.NamedMethod; import dan200.computercraft.core.methods.ObjectSource; +import org.jetbrains.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import javax.annotation.Nullable; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.function.Function; +import static dan200.computercraft.core.asm.Generator.catching; + final class MethodSupplierImpl implements MethodSupplier { + private static final Logger LOG = LoggerFactory.getLogger(MethodSupplierImpl.class); + + private final List genericMethods; private final Generator generator; private final IntCache dynamic; private final Function dynamicMethods; - MethodSupplierImpl(Generator generator, IntCache dynamic, Function dynamicMethods) { + private final LoadingCache, List>> classCache = CacheBuilder + .newBuilder() + .build(CacheLoader.from(catching(this::getMethodsImpl, List.of()))); + + MethodSupplierImpl( + List genericMethods, + Generator generator, + IntCache dynamic, + Function dynamicMethods + ) { + this.genericMethods = genericMethods; this.generator = generator; this.dynamic = dynamic; this.dynamicMethods = dynamicMethods; @@ -22,7 +54,7 @@ final class MethodSupplierImpl implements MethodSupplier { @Override public boolean forEachSelfMethod(Object object, UntargetedConsumer consumer) { - var methods = generator.getMethods(object.getClass()); + var methods = getMethods(object.getClass()); for (var method : methods) consumer.accept(method.name(), method.method(), method); var dynamicMethods = this.dynamicMethods.apply(object); @@ -35,14 +67,14 @@ final class MethodSupplierImpl implements MethodSupplier { @Override public boolean forEachMethod(Object object, TargetedConsumer consumer) { - var methods = generator.getMethods(object.getClass()); + var methods = getMethods(object.getClass()); for (var method : methods) consumer.accept(object, method.name(), method.method(), method); var hasMethods = !methods.isEmpty(); if (object instanceof ObjectSource source) { for (var extra : source.getExtra()) { - var extraMethods = generator.getMethods(extra.getClass()); + var extraMethods = getMethods(extra.getClass()); if (!extraMethods.isEmpty()) hasMethods = true; for (var method : extraMethods) consumer.accept(object, method.name(), method.method(), method); } @@ -58,4 +90,63 @@ final class MethodSupplierImpl implements MethodSupplier { return hasMethods; } + + @VisibleForTesting + List> getMethods(Class klass) { + try { + return classCache.get(klass); + } catch (ExecutionException e) { + LOG.error("Error getting methods for {}.", klass.getName(), e.getCause()); + return List.of(); + } + } + + private List> getMethodsImpl(Class klass) { + ArrayList> methods = null; + + // Find all methods on the current class + for (var method : klass.getMethods()) { + var annotation = method.getAnnotation(LuaFunction.class); + if (annotation == null) continue; + + if (Modifier.isStatic(method.getModifiers())) { + LOG.warn("LuaFunction method {}.{} should be an instance method.", method.getDeclaringClass(), method.getName()); + continue; + } + + var instance = generator.getMethod(method).orElse(null); + if (instance == null) continue; + + if (methods == null) methods = new ArrayList<>(); + addMethod(methods, method, annotation, null, instance); + } + + // Inject generic methods + for (var method : genericMethods) { + if (!method.target.isAssignableFrom(klass)) continue; + + var instance = generator.getMethod(method.method).orElse(null); + if (instance == null) continue; + + if (methods == null) methods = new ArrayList<>(); + addMethod(methods, method.method, method.annotation, method.peripheralType, instance); + } + + if (methods == null) return List.of(); + methods.trimToSize(); + return Collections.unmodifiableList(methods); + } + + private void addMethod(List> methods, Method method, LuaFunction annotation, @Nullable PeripheralType genericType, T instance) { + var names = annotation.value(); + var isSimple = method.getReturnType() != MethodResult.class && !annotation.mainThread(); + if (names.length == 0) { + methods.add(new NamedMethod<>(method.getName(), instance, isSimple, genericType)); + } else { + for (var name : names) { + methods.add(new NamedMethod<>(name, instance, isSimple, genericType)); + } + } + } + } diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethodSupplier.java b/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethodSupplier.java index 5010449c9..174b4ef19 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethodSupplier.java +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethodSupplier.java @@ -7,12 +7,19 @@ package dan200.computercraft.core.asm; import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.peripheral.IComputerAccess; import dan200.computercraft.api.peripheral.IDynamicPeripheral; +import dan200.computercraft.core.ComputerContext; import dan200.computercraft.core.methods.MethodSupplier; import dan200.computercraft.core.methods.PeripheralMethod; import java.util.List; import java.util.Objects; +/** + * Provides a {@link MethodSupplier} for {@link PeripheralMethod}s. + *

+ * This is used by {@link ComputerContext} to construct {@linkplain ComputerContext#peripheralMethods() the context-wide + * method supplier}. It should not be used directly. + */ public class PeripheralMethodSupplier { private static final Generator GENERATOR = new Generator<>(PeripheralMethod.class, List.of(ILuaContext.class, IComputerAccess.class), m -> (target, context, computer, args) -> context.executeMainThreadTask(() -> ResultHelpers.checkNormalResult(m.apply(target, context, computer, args.escapes()))) @@ -21,8 +28,8 @@ public class PeripheralMethodSupplier { method -> (instance, context, computer, args) -> ((IDynamicPeripheral) instance).callMethod(computer, context, method, args) ); - public static MethodSupplier create() { - return new MethodSupplierImpl<>(GENERATOR, DYNAMIC, x -> x instanceof IDynamicPeripheral dynamic + public static MethodSupplier create(List genericMethods) { + return new MethodSupplierImpl<>(genericMethods, GENERATOR, DYNAMIC, x -> x instanceof IDynamicPeripheral dynamic ? Objects.requireNonNull(dynamic.getMethodNames(), "Dynamic methods cannot be null") : null ); diff --git a/projects/core/src/test/java/dan200/computercraft/core/apis/ObjectWrapper.java b/projects/core/src/test/java/dan200/computercraft/core/apis/ObjectWrapper.java index 6b389a1e8..7c8ebedde 100644 --- a/projects/core/src/test/java/dan200/computercraft/core/apis/ObjectWrapper.java +++ b/projects/core/src/test/java/dan200/computercraft/core/apis/ObjectWrapper.java @@ -12,10 +12,11 @@ import dan200.computercraft.core.asm.LuaMethodSupplier; import dan200.computercraft.core.methods.LuaMethod; import dan200.computercraft.core.methods.MethodSupplier; +import java.util.List; import java.util.Map; public class ObjectWrapper implements ILuaContext { - private static final MethodSupplier LUA_METHODS = LuaMethodSupplier.create(); + private static final MethodSupplier LUA_METHODS = LuaMethodSupplier.create(List.of()); private final Object object; private final Map methodMap; diff --git a/projects/core/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java b/projects/core/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java index cc504eb27..9fc6620e5 100644 --- a/projects/core/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java +++ b/projects/core/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java @@ -14,6 +14,7 @@ import org.junit.jupiter.api.Test; import java.io.IOException; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -23,7 +24,7 @@ import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.assertThrows; public class GeneratorTest { - private static final Generator GENERATOR = LuaMethodSupplier.GENERATOR; + private static final MethodSupplierImpl GENERATOR = (MethodSupplierImpl) LuaMethodSupplier.create(List.of()); @Test public void testBasic() { From 7eb3b691da86bc77079952c127614d038f23ddfd Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Tue, 27 Jun 2023 18:25:34 +0100 Subject: [PATCH 12/32] Fix misplaced calls to IArguments.escapes - Fix mainThread=true methods calling IArguments.escapes too late. This should be done before scheduling on the main thread, not on the main thread itself! - Fix VarargsArguments.escapes not checking that the argument haven't been closed. This is slightly prone to race conditions, but I don't think it's worth the overhead of tracking the owning thread. Maybe when panama and its resource scopes are released. Thanks Sara for pointing this out! Slightly irked that none of our tests caught this. Alas. Also fix a typo in AddressPredicate. Yes, no commit discipline. --- .../java/dan200/computercraft/api/lua/IArguments.java | 4 +++- .../core/apis/http/options/AddressPredicate.java | 2 +- .../dan200/computercraft/core/asm/LuaMethodSupplier.java | 5 ++++- .../computercraft/core/asm/PeripheralMethodSupplier.java | 5 ++++- .../dan200/computercraft/core/lua/VarargArguments.java | 1 + .../computercraft/core/lua/VarargArgumentsTest.java | 8 ++++++++ 6 files changed, 21 insertions(+), 4 deletions(-) diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/lua/IArguments.java b/projects/core-api/src/main/java/dan200/computercraft/api/lua/IArguments.java index 3f3e0f894..336f0b926 100644 --- a/projects/core-api/src/main/java/dan200/computercraft/api/lua/IArguments.java +++ b/projects/core-api/src/main/java/dan200/computercraft/api/lua/IArguments.java @@ -479,9 +479,11 @@ public interface IArguments { * yourself. * * @return An {@link IArguments} instance which can escape the current scope. May be {@code this}. - * @throws LuaException For the same reasons as {@link #get(int)}. + * @throws LuaException For the same reasons as {@link #get(int)}. + * @throws IllegalStateException If marking these arguments as escaping outside the scope of the original function. */ default IArguments escapes() throws LuaException { + // TODO(1.21.0): Make this return void, require that it mutates this. return this; } } diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressPredicate.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressPredicate.java index ce4760452..25252a518 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressPredicate.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressPredicate.java @@ -70,7 +70,7 @@ interface AddressPredicate { } catch (IllegalArgumentException e) { LOG.error( "Malformed http whitelist/blacklist entry '{}': Cannot extract IP address from '{}'.", - addressStr + '/' + prefixSizeStr, prefixSizeStr + addressStr + '/' + prefixSizeStr, addressStr ); return null; } diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/LuaMethodSupplier.java b/projects/core/src/main/java/dan200/computercraft/core/asm/LuaMethodSupplier.java index 11f8e4310..7058ed5b5 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/LuaMethodSupplier.java +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/LuaMethodSupplier.java @@ -21,7 +21,10 @@ import java.util.Objects; */ public final class LuaMethodSupplier { private static final Generator GENERATOR = new Generator<>(LuaMethod.class, List.of(ILuaContext.class), - m -> (target, context, args) -> context.executeMainThreadTask(() -> ResultHelpers.checkNormalResult(m.apply(target, context, args.escapes()))) + m -> (target, context, args) -> { + var escArgs = args.escapes(); + return context.executeMainThreadTask(() -> ResultHelpers.checkNormalResult(m.apply(target, context, escArgs))); + } ); private static final IntCache DYNAMIC = new IntCache<>( method -> (instance, context, args) -> ((IDynamicLuaObject) instance).callMethod(context, method, args) diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethodSupplier.java b/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethodSupplier.java index 174b4ef19..ca7fa8ff3 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethodSupplier.java +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethodSupplier.java @@ -22,7 +22,10 @@ import java.util.Objects; */ public class PeripheralMethodSupplier { private static final Generator GENERATOR = new Generator<>(PeripheralMethod.class, List.of(ILuaContext.class, IComputerAccess.class), - m -> (target, context, computer, args) -> context.executeMainThreadTask(() -> ResultHelpers.checkNormalResult(m.apply(target, context, computer, args.escapes()))) + m -> (target, context, computer, args) -> { + var escArgs = args.escapes(); + return context.executeMainThreadTask(() -> ResultHelpers.checkNormalResult(m.apply(target, context, computer, escArgs))); + } ); private static final IntCache DYNAMIC = new IntCache<>( method -> (instance, context, computer, args) -> ((IDynamicPeripheral) instance).callMethod(computer, context, method, args) diff --git a/projects/core/src/main/java/dan200/computercraft/core/lua/VarargArguments.java b/projects/core/src/main/java/dan200/computercraft/core/lua/VarargArguments.java index 4289d2919..51b71de8c 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/lua/VarargArguments.java +++ b/projects/core/src/main/java/dan200/computercraft/core/lua/VarargArguments.java @@ -191,6 +191,7 @@ final class VarargArguments implements IArguments { @Override public IArguments escapes() { if (escapes) return this; + if (isClosed()) throw new IllegalStateException("Cannot call escapes after IArguments has been closed."); var cache = this.cache; var typeNames = this.typeNames; diff --git a/projects/core/src/test/java/dan200/computercraft/core/lua/VarargArgumentsTest.java b/projects/core/src/test/java/dan200/computercraft/core/lua/VarargArgumentsTest.java index 7a76ea562..97a6aa36c 100644 --- a/projects/core/src/test/java/dan200/computercraft/core/lua/VarargArgumentsTest.java +++ b/projects/core/src/test/java/dan200/computercraft/core/lua/VarargArgumentsTest.java @@ -60,4 +60,12 @@ class VarargArgumentsTest { assertThrows(IllegalStateException.class, () -> args.get(0)); assertThrows(IllegalStateException.class, () -> args.getType(0)); } + + @Test + public void testEscapeAfterClose() { + var args = VarargArguments.of(tableWithCustomType()); + args.close(); + + assertThrows(IllegalStateException.class, args::escapes); + } } From f5b16261cc0980cd73afc3707cfe33310d9af46d Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Thu, 29 Jun 2023 20:10:17 +0100 Subject: [PATCH 13/32] Update to Gradle 8.x - Update to Loom 1.2 and FG 6.0. ForgeGradle has changed how it generates the runXyz tasks, which makes running our tests much harder. I've raised an issue upstream, but for now we do some nasty poking of internals. - Fix Sodium/Iris tests. Loom 1.1 changed how remapped configurations are generated - we create a dummy source set and associate the remapped configuration with that. All nasty stuff. - Publish the common library. I'm not a fan of this, but given how much internals I'm poking elsewhere, should probably get off my high horse. - Add renderdoc support to the client gametests, enabled with -Prenderdoc. --- .../cc/tweaked/gradle/ForgeExtensions.kt | 26 +++++++++++ .../kotlin/cc/tweaked/gradle/Illuaminate.kt | 4 +- .../kotlin/cc/tweaked/gradle/MinecraftExec.kt | 6 ++- .../gradle/common/util/runs/RunConfigSetup.kt | 44 ++++++++++++++++++ gradle/libs.versions.toml | 4 +- gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 62076 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 19 +++++--- gradlew.bat | 1 + projects/common/build.gradle.kts | 1 + projects/fabric/build.gradle.kts | 22 ++++++--- projects/forge/build.gradle.kts | 10 +--- 12 files changed, 113 insertions(+), 27 deletions(-) create mode 100644 buildSrc/src/main/kotlin/cc/tweaked/gradle/ForgeExtensions.kt create mode 100644 buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt diff --git a/buildSrc/src/main/kotlin/cc/tweaked/gradle/ForgeExtensions.kt b/buildSrc/src/main/kotlin/cc/tweaked/gradle/ForgeExtensions.kt new file mode 100644 index 000000000..843ce87ee --- /dev/null +++ b/buildSrc/src/main/kotlin/cc/tweaked/gradle/ForgeExtensions.kt @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package cc.tweaked.gradle + +import net.minecraftforge.gradle.common.util.RunConfig +import net.minecraftforge.gradle.common.util.runs.setRunConfigInternal +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.tasks.JavaExec +import org.gradle.jvm.toolchain.JavaToolchainService +import java.nio.file.Files + +/** + * Set [JavaExec] task to run a given [RunConfig]. + */ +fun JavaExec.setRunConfig(config: RunConfig) { + dependsOn("prepareRuns") + setRunConfigInternal(project, this, config) + doFirst("Create working directory") { Files.createDirectories(workingDir.toPath().parent) } + + javaLauncher.set( + project.extensions.getByType(JavaToolchainService::class.java) + .launcherFor(project.extensions.getByType(JavaPluginExtension::class.java).toolchain), + ) +} diff --git a/buildSrc/src/main/kotlin/cc/tweaked/gradle/Illuaminate.kt b/buildSrc/src/main/kotlin/cc/tweaked/gradle/Illuaminate.kt index d44596fb1..0035ab118 100644 --- a/buildSrc/src/main/kotlin/cc/tweaked/gradle/Illuaminate.kt +++ b/buildSrc/src/main/kotlin/cc/tweaked/gradle/Illuaminate.kt @@ -60,7 +60,7 @@ class IlluaminatePlugin : Plugin { /** Define a dependency for illuaminate from a version number and the current operating system. */ private fun illuaminateArtifact(project: Project, version: String): Dependency { - val osName = System.getProperty("os.name").toLowerCase() + val osName = System.getProperty("os.name").lowercase() val (os, suffix) = when { osName.contains("windows") -> Pair("windows", ".exe") osName.contains("mac os") || osName.contains("darwin") -> Pair("macos", "") @@ -68,7 +68,7 @@ class IlluaminatePlugin : Plugin { else -> error("Unsupported OS $osName for illuaminate") } - val osArch = System.getProperty("os.arch").toLowerCase() + val osArch = System.getProperty("os.arch").lowercase() val arch = when { // On macOS the x86_64 binary will work for both ARM and Intel Macs through Rosetta. os == "macos" -> "x86_64" diff --git a/buildSrc/src/main/kotlin/cc/tweaked/gradle/MinecraftExec.kt b/buildSrc/src/main/kotlin/cc/tweaked/gradle/MinecraftExec.kt index 75cb7b51a..7bc6af2cf 100644 --- a/buildSrc/src/main/kotlin/cc/tweaked/gradle/MinecraftExec.kt +++ b/buildSrc/src/main/kotlin/cc/tweaked/gradle/MinecraftExec.kt @@ -32,11 +32,14 @@ abstract class ClientJavaExec : JavaExec() { usesService(clientRunner) } + @get:Input + val renderdoc get() = project.hasProperty("renderdoc") + /** * When [false], tests will not be run automatically, allowing the user to debug rendering. */ @get:Input - val clientDebug get() = project.hasProperty("clientDebug") + val clientDebug get() = renderdoc || project.hasProperty("clientDebug") /** * When [false], tests will not run under a framebuffer. @@ -63,6 +66,7 @@ abstract class ClientJavaExec : JavaExec() { task.copyToFull(this) if (!clientDebug) systemProperty("cctest.client", "") + if (renderdoc) environment("LD_PRELOAD", "/usr/lib/librenderdoc.so") systemProperty("cctest.gametest-report", testResults.get().asFile.absoluteFile) workingDir(project.buildDir.resolve("gametest").resolve(name)) } diff --git a/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt b/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt new file mode 100644 index 000000000..6ec33f9ea --- /dev/null +++ b/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package net.minecraftforge.gradle.common.util.runs + +import net.minecraftforge.gradle.common.util.RunConfig +import org.gradle.api.Project +import org.gradle.process.JavaExecSpec +import java.io.File + +/** + * Set up a [JavaExecSpec] to execute a [RunConfig]. + * + * [MinecraftRunTask] sets up all its properties when the task is executed, rather than when configured. As such, it's + * not possible to use [cc.tweaked.gradle.copyToFull] like we do for Fabric. Instead, we set up the task manually. + * + * Unfortunately most of the functionality we need is package-private, and so we have to put our code into the package. + */ +internal fun setRunConfigInternal(project: Project, spec: JavaExecSpec, config: RunConfig) { + val originalTask = project.tasks.named(config.taskName, MinecraftRunTask::class.java).get() + + spec.workingDir = File(config.workingDirectory) + + spec.mainClass.set(config.main) + for (source in config.allSources) spec.classpath(source.runtimeClasspath) + + val lazyTokens = RunConfigGenerator.configureTokensLazy( + project, config, + RunConfigGenerator.mapModClassesToGradle(project, config), + originalTask.minecraftArtifacts.files, + originalTask.runtimeClasspathArtifacts.files, + ) + + spec.args(RunConfigGenerator.getArgsStream(config, lazyTokens, false).toList()) + + spec.jvmArgs( + (if (config.isClient) config.jvmArgs + originalTask.additionalClientArgs.get() else config.jvmArgs) + .map { config.replace(lazyTokens, it) }, + ) + + for ((key, value) in config.environment) spec.environment(key, config.replace(lazyTokens, value)) + for ((key, value) in config.properties) spec.systemProperty(key, config.replace(lazyTokens, value)) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9ff0cbb6c..9152cadaa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,8 +53,8 @@ checkstyle = "10.3.4" curseForgeGradle = "1.0.14" errorProne-core = "2.18.0" errorProne-plugin = "3.0.1" -fabric-loom = "1.1.10" -forgeGradle = "5.1.+" +fabric-loom = "1.2.7" +forgeGradle = "6.0.6" githubRelease = "2.2.12" ideaExt = "1.1.6" illuaminate = "0.1.0-28-ga7efd71" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832f090a2944b7473328c07c9755baa3196..c1962a79e29d3e0ab67b14947c167a862655af9b 100644 GIT binary patch delta 39834 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 G$^QWPgI3l6 delta 38507 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 } + } + val capitalName = name.replaceFirstChar { it.titlecase(Locale.ROOT) } + loom.addRemapConfiguration("mod$capitalName") { + onCompileClasspath.set(false) + onRuntimeClasspath.set(true) + sourceSet.set(ourSourceSet) + targetConfigurationName.set(name) + } configurations.create(name) { isCanBeConsumed = false isCanBeResolved = true - } - val capitalName = name.capitalize(Locale.ROOT) - loom.addRemapConfiguration("mod$capitalName") { - onCompileClasspath.set(false) - onRuntimeClasspath.set(false) - targetConfigurationName.set(name) + extendsFrom(configurations["${name}RuntimeClasspath"]) } } diff --git a/projects/forge/build.gradle.kts b/projects/forge/build.gradle.kts index 589e44f0d..0437e531f 100644 --- a/projects/forge/build.gradle.kts +++ b/projects/forge/build.gradle.kts @@ -35,8 +35,6 @@ minecraft { property("forge.logging.markers", "REGISTRIES") property("forge.logging.console.level", "debug") - forceExit = false - mods.register("computercraft") { cct.sourceDirectories.get().forEach { if (it.classes) sources(it.sourceSet) @@ -262,11 +260,7 @@ val runGametest by tasks.registering(JavaExec::class) { dependsOn("cleanRunGametest") usesService(MinecraftRunnerService.get(gradle)) - // Copy from runGameTestServer. We do it in this slightly odd way as runGameTestServer - // isn't created until the task is configured (which is no good for us). - val exec = tasks.getByName("runGameTestServer") - dependsOn(exec.dependsOn) - exec.copyToFull(this) + setRunConfig(minecraft.runs["gameTestServer"]) systemProperty("cctest.gametest-report", project.buildDir.resolve("test-results/$name.xml").absolutePath) } @@ -275,7 +269,7 @@ tasks.check { dependsOn(runGametest) } val runGametestClient by tasks.registering(ClientJavaExec::class) { description = "Runs client-side gametests with no mods" - copyFrom("runTestClient") + setRunConfig(minecraft.runs["testClient"]) tags("client") } cct.jacoco(runGametestClient) From 34f41c4039dab6a4fe05d9b5d9c3f2731e32b0f5 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Thu, 29 Jun 2023 22:31:49 +0100 Subject: [PATCH 14/32] Be lazier in configuring Forge runs --- .github/workflows/main-ci.yml | 11 ++-- .../cc/tweaked/gradle/ForgeExtensions.kt | 2 +- .../gradle/common/util/runs/RunConfigSetup.kt | 53 ++++++++++++++----- 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index ace4c3574..8baff3d9c 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -28,10 +28,13 @@ jobs: echo "org.gradle.daemon=false" >> ~/.gradle/gradle.properties - name: Build with Gradle - run: | - ./gradlew assemble || ./gradlew assemble - ./gradlew downloadAssets || ./gradlew downloadAssets - ./gradlew build + run: ./gradlew assemble || ./gradlew assemble + + - name: Download assets for game tests + run: ./gradlew downloadAssets || ./gradlew downloadAssets + + - name: Run tests and linters + run: ./gradlew build - name: Run client tests run: ./gradlew runGametestClient # Not checkClient, as no point running rendering tests. diff --git a/buildSrc/src/main/kotlin/cc/tweaked/gradle/ForgeExtensions.kt b/buildSrc/src/main/kotlin/cc/tweaked/gradle/ForgeExtensions.kt index 843ce87ee..bbf6e86c5 100644 --- a/buildSrc/src/main/kotlin/cc/tweaked/gradle/ForgeExtensions.kt +++ b/buildSrc/src/main/kotlin/cc/tweaked/gradle/ForgeExtensions.kt @@ -17,7 +17,7 @@ import java.nio.file.Files fun JavaExec.setRunConfig(config: RunConfig) { dependsOn("prepareRuns") setRunConfigInternal(project, this, config) - doFirst("Create working directory") { Files.createDirectories(workingDir.toPath().parent) } + doFirst("Create working directory") { Files.createDirectories(workingDir.toPath()) } javaLauncher.set( project.extensions.getByType(JavaToolchainService::class.java) diff --git a/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt b/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt index 6ec33f9ea..0a0324dcf 100644 --- a/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt +++ b/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt @@ -6,8 +6,12 @@ package net.minecraftforge.gradle.common.util.runs import net.minecraftforge.gradle.common.util.RunConfig import org.gradle.api.Project +import org.gradle.process.CommandLineArgumentProvider import org.gradle.process.JavaExecSpec import java.io.File +import java.util.function.Supplier +import java.util.stream.Collectors +import java.util.stream.Stream /** * Set up a [JavaExecSpec] to execute a [RunConfig]. @@ -18,27 +22,48 @@ import java.io.File * Unfortunately most of the functionality we need is package-private, and so we have to put our code into the package. */ internal fun setRunConfigInternal(project: Project, spec: JavaExecSpec, config: RunConfig) { - val originalTask = project.tasks.named(config.taskName, MinecraftRunTask::class.java).get() - spec.workingDir = File(config.workingDirectory) spec.mainClass.set(config.main) for (source in config.allSources) spec.classpath(source.runtimeClasspath) - val lazyTokens = RunConfigGenerator.configureTokensLazy( - project, config, - RunConfigGenerator.mapModClassesToGradle(project, config), - originalTask.minecraftArtifacts.files, - originalTask.runtimeClasspathArtifacts.files, + val originalTask = project.tasks.named(config.taskName, MinecraftRunTask::class.java) + + // Add argument and JVM argument via providers, to be as lazy as possible with fetching artifacts. + fun lazyTokens(): MutableMap> { + return RunConfigGenerator.configureTokensLazy( + project, config, RunConfigGenerator.mapModClassesToGradle(project, config), + originalTask.get().minecraftArtifacts.files, + originalTask.get().runtimeClasspathArtifacts.files, + ) + } + spec.argumentProviders.add( + CommandLineArgumentProvider { + RunConfigGenerator.getArgsStream(config, lazyTokens(), false).toList() + }, + ) + spec.jvmArgumentProviders.add( + CommandLineArgumentProvider { + val lazyTokens = lazyTokens() + (if (config.isClient) config.jvmArgs + originalTask.get().additionalClientArgs.get() else config.jvmArgs).map { config.replace(lazyTokens, it) } + + config.properties.map { (k, v) -> "-D${k}=${config.replace(lazyTokens, v)}" } + }, ) - spec.args(RunConfigGenerator.getArgsStream(config, lazyTokens, false).toList()) - - spec.jvmArgs( - (if (config.isClient) config.jvmArgs + originalTask.additionalClientArgs.get() else config.jvmArgs) - .map { config.replace(lazyTokens, it) }, + // We can't configure environment variables lazily, so we do these now with a more minimal lazyTokens set. + val lazyTokens = mutableMapOf>() + for ((k, v) in config.tokens) lazyTokens[k] = Supplier { v } + for ((k, v) in config.lazyTokens) lazyTokens[k] = v + lazyTokens.compute( + "source_roots", + { key: String, sourceRoots: Supplier? -> + Supplier { + val modClasses = RunConfigGenerator.mapModClassesToGradle(project, config) + (if (sourceRoots != null) Stream.concat( + sourceRoots.get().split(File.pathSeparator).stream(), modClasses, + ) else modClasses).distinct().collect(Collectors.joining(File.pathSeparator)) + } + }, ) - for ((key, value) in config.environment) spec.environment(key, config.replace(lazyTokens, value)) - for ((key, value) in config.properties) spec.systemProperty(key, config.replace(lazyTokens, value)) } From 655d5aeca80d1b0a91fb346296447c861f01b7b1 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 1 Jul 2023 12:37:48 +0100 Subject: [PATCH 15/32] Improve REPL's handling of expressions - Remove the "force_print" code. This is a relic of before we used table.pack, and so didn't know how many expressions had been returned. - Check the input string is a valid expression separately before wrapping it in an _echo(...). Fixes #1506. --- .../computercraft/lua/rom/programs/lua.lua | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/lua.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/lua.lua index 3d2faebe6..77517bcfb 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/lua.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/lua.lua @@ -74,18 +74,13 @@ while running do local name, offset = "=lua[" .. chunk_idx .. "]", 0 - local force_print = 0 local func, err = load(input, name, "t", tEnv) - - local expr_func = load("return _echo(" .. input .. ");", name, "t", tEnv) - if not func then - if expr_func then - func = expr_func - offset = 13 - force_print = 1 - end - elseif expr_func then - func = expr_func + if load("return " .. input) then + -- We wrap the expression with a call to _echo(...), which prevents tail + -- calls (and thus confusing errors). Note we check this is a valid + -- expression separately, to avoid accepting inputs like `)--` (which are + -- parsed as `_echo()--)`. + func = load("return _echo(" .. input .. "\n)", name, "t", tEnv) offset = 13 end @@ -96,7 +91,7 @@ while running do local results = table.pack(exception.try(func)) if results[1] then local n = 1 - while n < results.n or n <= force_print do + while n < results.n do local value = results[n + 1] local ok, serialised = pcall(pretty.pretty, value, { function_args = settings.get("lua.function_args"), From ecf880ed8253f160698123c737d36322df53b280 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 1 Jul 2023 15:57:30 +0100 Subject: [PATCH 16/32] Document HTTP rules a little better It turns out we don't document the "port" option anywhere, so probably worth doing a bit of an overhaul here. - Expand the top-level HTTP rules comment, clarifying how things are matched and describing each field. - Improve the comments on the default HTTP rule. We now also describe the $private rule and its motivation. - Don't drop/ignore invalid rules. This gets written back to the original config file, so is very annoying! Instead we now log an error and convert the rule into a "deny all" rule, which should make it obvious something is wrong. --- .../shared/config/AddressRuleConfig.java | 134 +++++++++--------- .../shared/config/ConfigSpec.java | 40 +++--- .../apis/http/options/AddressPredicate.java | 21 +-- .../core/apis/http/options/AddressRule.java | 3 +- .../http/options/InvalidRuleException.java | 23 +++ .../computercraft/lua/rom/programs/lua.lua | 6 +- .../assets/computercraft/lang/en_us.json | 4 +- .../assets/computercraft/lang/en_us.json | 4 +- 8 files changed, 124 insertions(+), 111 deletions(-) create mode 100644 projects/core/src/main/java/dan200/computercraft/core/apis/http/options/InvalidRuleException.java diff --git a/projects/common/src/main/java/dan200/computercraft/shared/config/AddressRuleConfig.java b/projects/common/src/main/java/dan200/computercraft/shared/config/AddressRuleConfig.java index 8974c9185..c063569a7 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/config/AddressRuleConfig.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/config/AddressRuleConfig.java @@ -4,21 +4,19 @@ package dan200.computercraft.shared.config; +import com.electronwill.nightconfig.core.CommentedConfig; import com.electronwill.nightconfig.core.Config; import com.electronwill.nightconfig.core.InMemoryCommentedFormat; import com.electronwill.nightconfig.core.UnmodifiableConfig; import dan200.computercraft.core.apis.http.options.Action; import dan200.computercraft.core.apis.http.options.AddressRule; +import dan200.computercraft.core.apis.http.options.InvalidRuleException; import dan200.computercraft.core.apis.http.options.PartialOptions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.Nullable; -import java.util.Locale; -import java.util.Optional; -import java.util.OptionalInt; -import java.util.OptionalLong; -import java.util.concurrent.ConcurrentHashMap; +import java.util.*; +import java.util.function.Consumer; /** * Parses, checks and generates {@link Config}s for {@link AddressRule}. @@ -26,49 +24,65 @@ import java.util.concurrent.ConcurrentHashMap; class AddressRuleConfig { private static final Logger LOG = LoggerFactory.getLogger(AddressRuleConfig.class); - public static UnmodifiableConfig makeRule(String host, Action action) { - var config = InMemoryCommentedFormat.defaultInstance().createConfig(ConcurrentHashMap::new); - config.add("host", host); - config.add("action", action.name().toLowerCase(Locale.ROOT)); + private static final AddressRule REJECT_ALL = AddressRule.parse("*", OptionalInt.empty(), Action.DENY.toPartial()); - if (host.equals("*") && action == Action.ALLOW) { - config.setComment("max_download", """ - The maximum size (in bytes) that a computer can download in a single request. - Note that responses may receive more data than allowed, but this data will not - be returned to the client."""); - config.set("max_download", AddressRule.MAX_DOWNLOAD); + public static List defaultRules() { + return List.of( + makeRule(config -> { + config.setComment("host", """ + The magic "$private" host matches all private address ranges, such as localhost and 192.168.0.0/16. + This rule prevents computers accessing internal services, and is strongly recommended."""); + config.add("host", "$private"); - config.setComment("max_upload", """ - The maximum size (in bytes) that a computer can upload in a single request. This - includes headers and POST text."""); - config.set("max_upload", AddressRule.MAX_UPLOAD); + config.setComment("action", "Deny all requests to private IP addresses."); + config.add("action", Action.DENY.name().toLowerCase(Locale.ROOT)); + }), + makeRule(config -> { + config.setComment("host", """ + The wildcard "*" rule matches all remaining hosts."""); + config.add("host", "*"); - config.setComment("max_websocket_message", "The maximum size (in bytes) that a computer can send or receive in one websocket packet."); - config.set("max_websocket_message", AddressRule.WEBSOCKET_MESSAGE); + config.setComment("action", "Allow all non-denied hosts."); + config.add("action", Action.ALLOW.name().toLowerCase(Locale.ROOT)); - config.setComment("use_proxy", "Enable use of the HTTP/SOCKS proxy if it is configured."); - config.set("use_proxy", false); - } + config.setComment("max_download", """ + The maximum size (in bytes) that a computer can download in a single request. + Note that responses may receive more data than allowed, but this data will not + be returned to the client."""); + config.set("max_download", AddressRule.MAX_DOWNLOAD); + config.setComment("max_upload", """ + The maximum size (in bytes) that a computer can upload in a single request. This + includes headers and POST text."""); + config.set("max_upload", AddressRule.MAX_UPLOAD); + + config.setComment("max_websocket_message", "The maximum size (in bytes) that a computer can send or receive in one websocket packet."); + config.set("max_websocket_message", AddressRule.WEBSOCKET_MESSAGE); + + config.setComment("use_proxy", "Enable use of the HTTP/SOCKS proxy if it is configured."); + config.set("use_proxy", false); + }) + ); + } + + private static UnmodifiableConfig makeRule(Consumer setup) { + var config = InMemoryCommentedFormat.defaultInstance().createConfig(LinkedHashMap::new); + setup.accept(config); return config; } - public static boolean checkRule(UnmodifiableConfig builder) { - var hostObj = get(builder, "host", String.class).orElse(null); - var port = unboxOptInt(get(builder, "port", Number.class)); - return hostObj != null && checkEnum(builder, "action", Action.class) - && check(builder, "port", Number.class) - && check(builder, "max_upload", Number.class) - && check(builder, "max_download", Number.class) - && check(builder, "websocket_message", Number.class) - && check(builder, "use_proxy", Boolean.class) - && AddressRule.parse(hostObj, port, PartialOptions.DEFAULT) != null; + public static AddressRule parseRule(UnmodifiableConfig builder) { + try { + return doParseRule(builder); + } catch (InvalidRuleException e) { + LOG.error("Malformed HTTP rule: {} HTTP will NOT work until this is fixed.", e.getMessage()); + return REJECT_ALL; + } } - @Nullable - public static AddressRule parseRule(UnmodifiableConfig builder) { + public static AddressRule doParseRule(UnmodifiableConfig builder) { var hostObj = get(builder, "host", String.class).orElse(null); - if (hostObj == null) return null; + if (hostObj == null) throw new InvalidRuleException("No 'host' specified"); var action = getEnum(builder, "action", Action.class).orElse(null); var port = unboxOptInt(get(builder, "port", Number.class)); @@ -88,38 +102,19 @@ class AddressRuleConfig { return AddressRule.parse(hostObj, port, options); } - private static boolean check(UnmodifiableConfig config, String field, Class klass) { - var value = config.get(field); - if (value == null || klass.isInstance(value)) return true; - - LOG.warn("HTTP rule's {} is not a {}.", field, klass.getSimpleName()); - return false; - } - - private static > boolean checkEnum(UnmodifiableConfig config, String field, Class klass) { - var value = config.get(field); - if (value == null) return true; - - if (!(value instanceof String)) { - LOG.warn("HTTP rule's {} is not a string", field); - return false; - } - - if (parseEnum(klass, (String) value) == null) { - LOG.warn("HTTP rule's {} is not a known option", field); - return false; - } - - return true; - } - private static Optional get(UnmodifiableConfig config, String field, Class klass) { var value = config.get(field); - return klass.isInstance(value) ? Optional.of(klass.cast(value)) : Optional.empty(); + if (value == null) return Optional.empty(); + if (klass.isInstance(value)) return Optional.of(klass.cast(value)); + + throw new InvalidRuleException(String.format( + "Field '%s' should be a '%s' but is a %s.", + field, klass.getSimpleName(), value.getClass().getSimpleName() + )); } private static > Optional getEnum(UnmodifiableConfig config, String field, Class klass) { - return get(config, field, String.class).map(x -> parseEnum(klass, x)); + return get(config, field, String.class).map(x -> parseEnum(field, klass, x)); } private static OptionalLong unboxOptLong(Optional value) { @@ -130,11 +125,14 @@ class AddressRuleConfig { return value.map(Number::intValue).map(OptionalInt::of).orElse(OptionalInt.empty()); } - @Nullable - private static > T parseEnum(Class klass, String x) { + private static > T parseEnum(String field, Class klass, String x) { for (var value : klass.getEnumConstants()) { if (value.name().equalsIgnoreCase(x)) return value; } - return null; + + throw new InvalidRuleException(String.format( + "Field '%s' should be one of %s, but is '%s'.", + field, Arrays.stream(klass.getEnumConstants()).map(Enum::name).toList(), x + )); } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/config/ConfigSpec.java b/projects/common/src/main/java/dan200/computercraft/shared/config/ConfigSpec.java index fdd603ade..347f7fff0 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/config/ConfigSpec.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/config/ConfigSpec.java @@ -9,7 +9,6 @@ import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.core.CoreConfig; import dan200.computercraft.core.Logging; import dan200.computercraft.core.apis.http.NetworkUtils; -import dan200.computercraft.core.apis.http.options.Action; import dan200.computercraft.core.apis.http.options.ProxyType; import dan200.computercraft.core.computer.mainthread.MainThreadConfig; import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer; @@ -20,9 +19,7 @@ import org.apache.logging.log4j.core.filter.MarkerFilter; import javax.annotation.Nullable; import java.nio.file.Path; -import java.util.Arrays; import java.util.List; -import java.util.Objects; import java.util.concurrent.TimeUnit; public final class ConfigSpec { @@ -182,9 +179,9 @@ public final class ConfigSpec { httpEnabled = builder .comment(""" - Enable the "http" API on Computers. This also disables the "pastebin" and "wget" - programs, that many users rely on. It's recommended to leave this on and use the - "rules" config option to impose more fine-grained control.""") + Enable the "http" API on Computers. Disabling this also disables the "pastebin" and + "wget" programs, that many users rely on. It's recommended to leave this on and use + the "rules" config option to impose more fine-grained control.""") .define("enabled", CoreConfig.httpEnabled); httpWebsocketEnabled = builder @@ -194,16 +191,23 @@ public final class ConfigSpec { httpRules = builder .comment(""" A list of rules which control behaviour of the "http" API for specific domains or - IPs. Each rule is an item with a 'host' to match against, and a series of - properties. Rules are evaluated in order, meaning earlier rules override later - ones. - The host may be a domain name ("pastebin.com"), wildcard ("*.pastebin.com") or - CIDR notation ("127.0.0.0/8"). - If no rules, the domain is blocked.""") - .defineList("rules", Arrays.asList( - AddressRuleConfig.makeRule("$private", Action.DENY), - AddressRuleConfig.makeRule("*", Action.ALLOW) - ), x -> x instanceof UnmodifiableConfig && AddressRuleConfig.checkRule((UnmodifiableConfig) x)); + IPs. Each rule matches against a hostname and an optional port, and then sets several + properties for the request. Rules are evaluated in order, meaning earlier rules override + later ones. + + Valid properties: + - "host" (required): The domain or IP address this rule matches. This may be a domain name + ("pastebin.com"), wildcard ("*.pastebin.com") or CIDR notation ("127.0.0.0/8"). + - "port" (optional): Only match requests for a specific port, such as 80 or 443. + + - "action" (optional): Whether to allow or deny this request. + - "max_download" (optional): The maximum size (in bytes) that a computer can download in this + request. + - "max_upload" (optional): The maximum size (in bytes) that a computer can upload in a this request. + - "max_websocket_message" (optional): The maximum size (in bytes) that a computer can send or + receive in one websocket packet. + - "use_proxy" (optional): Enable use of the HTTP/SOCKS proxy if it is configured.""") + .defineList("rules", AddressRuleConfig.defaultRules(), x -> x instanceof UnmodifiableConfig); httpMaxRequests = builder .comment(""" @@ -395,8 +399,8 @@ public final class ConfigSpec { // HTTP CoreConfig.httpEnabled = httpEnabled.get(); CoreConfig.httpWebsocketEnabled = httpWebsocketEnabled.get(); - CoreConfig.httpRules = httpRules.get().stream() - .map(AddressRuleConfig::parseRule).filter(Objects::nonNull).toList(); + + CoreConfig.httpRules = httpRules.get().stream().map(AddressRuleConfig::parseRule).toList(); CoreConfig.httpMaxRequests = httpMaxRequests.get(); CoreConfig.httpMaxWebsockets = httpMaxWebsockets.get(); diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressPredicate.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressPredicate.java index 25252a518..e9dae1d88 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressPredicate.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressPredicate.java @@ -5,10 +5,7 @@ package dan200.computercraft.core.apis.http.options; import com.google.common.net.InetAddresses; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import javax.annotation.Nullable; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.regex.Pattern; @@ -19,8 +16,6 @@ import java.util.regex.Pattern; * @see AddressRule#apply(Iterable, String, InetSocketAddress) for the actual handling of this rule. */ interface AddressPredicate { - Logger LOG = LoggerFactory.getLogger(AddressPredicate.class); - default boolean matches(String domain) { return false; } @@ -51,28 +46,25 @@ interface AddressPredicate { return true; } - @Nullable public static HostRange parse(String addressStr, String prefixSizeStr) { int prefixSize; try { prefixSize = Integer.parseInt(prefixSizeStr); } catch (NumberFormatException e) { - LOG.error( - "Malformed http whitelist/blacklist entry '{}': Cannot extract size of CIDR mask from '{}'.", + throw new InvalidRuleException(String.format( + "Invalid host host '%s': Cannot extract size of CIDR mask from '%s'.", addressStr + '/' + prefixSizeStr, prefixSizeStr - ); - return null; + )); } InetAddress address; try { address = InetAddresses.forString(addressStr); } catch (IllegalArgumentException e) { - LOG.error( - "Malformed http whitelist/blacklist entry '{}': Cannot extract IP address from '{}'.", + throw new InvalidRuleException(String.format( + "Invalid host '%s': Cannot extract IP address from '%s'.", addressStr + '/' + prefixSizeStr, addressStr - ); - return null; + )); } // Mask the bytes of the IP address. @@ -112,7 +104,6 @@ interface AddressPredicate { } } - final class PrivatePattern implements AddressPredicate { static final PrivatePattern INSTANCE = new PrivatePattern(); diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressRule.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressRule.java index 96857b967..3673e8da0 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressRule.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressRule.java @@ -35,14 +35,13 @@ public final class AddressRule { this.port = port; } - @Nullable public static AddressRule parse(String filter, OptionalInt port, PartialOptions partial) { var cidr = filter.indexOf('/'); if (cidr >= 0) { var addressStr = filter.substring(0, cidr); var prefixSizeStr = filter.substring(cidr + 1); var range = HostRange.parse(addressStr, prefixSizeStr); - return range == null ? null : new AddressRule(range, port, partial); + return new AddressRule(range, port, partial); } else if (filter.equalsIgnoreCase("$private")) { return new AddressRule(PrivatePattern.INSTANCE, port, partial); } else { diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/InvalidRuleException.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/InvalidRuleException.java new file mode 100644 index 000000000..ce9f55c10 --- /dev/null +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/InvalidRuleException.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.apis.http.options; + +import java.io.Serial; +import java.util.OptionalInt; + +/** + * Throw when a {@link AddressRule} cannot be parsed. + * + * @see AddressRule#parse(String, OptionalInt, PartialOptions) + * @see AddressPredicate.HostRange#parse(String, String) + */ +public class InvalidRuleException extends RuntimeException { + @Serial + private static final long serialVersionUID = 1303376302865132758L; + + public InvalidRuleException(String message) { + super(message); + } +} diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/lua.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/lua.lua index 77517bcfb..fd55dfe44 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/lua.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/lua.lua @@ -90,9 +90,8 @@ while running do local results = table.pack(exception.try(func)) if results[1] then - local n = 1 - while n < results.n do - local value = results[n + 1] + for i = 2, results.n do + local value = results[i] local ok, serialised = pcall(pretty.pretty, value, { function_args = settings.get("lua.function_args"), function_source = settings.get("lua.function_source"), @@ -102,7 +101,6 @@ while running do else print(tostring(value)) end - n = n + 1 end else printError(results[2]) diff --git a/projects/fabric/src/generated/resources/assets/computercraft/lang/en_us.json b/projects/fabric/src/generated/resources/assets/computercraft/lang/en_us.json index 845d71ec6..77fe60ee0 100644 --- a/projects/fabric/src/generated/resources/assets/computercraft/lang/en_us.json +++ b/projects/fabric/src/generated/resources/assets/computercraft/lang/en_us.json @@ -99,7 +99,7 @@ "gui.computercraft.config.http.bandwidth.global_upload.tooltip": "The number of bytes which can be uploaded in a second. This is shared across all computers. (bytes/s).\nRange: > 1", "gui.computercraft.config.http.bandwidth.tooltip": "Limits bandwidth used by computers.", "gui.computercraft.config.http.enabled": "Enable the HTTP API", - "gui.computercraft.config.http.enabled.tooltip": "Enable the \"http\" API on Computers. This also disables the \"pastebin\" and \"wget\"\nprograms, that many users rely on. It's recommended to leave this on and use the\n\"rules\" config option to impose more fine-grained control.", + "gui.computercraft.config.http.enabled.tooltip": "Enable the \"http\" API on Computers. Disabling this also disables the \"pastebin\" and\n\"wget\" programs, that many users rely on. It's recommended to leave this on and use\nthe \"rules\" config option to impose more fine-grained control.", "gui.computercraft.config.http.max_requests": "Maximum concurrent requests", "gui.computercraft.config.http.max_requests.tooltip": "The number of http requests a computer can make at one time. Additional requests\nwill be queued, and sent when the running requests have finished. Set to 0 for\nunlimited.\nRange: > 0", "gui.computercraft.config.http.max_websockets": "Maximum concurrent websockets", @@ -113,7 +113,7 @@ "gui.computercraft.config.http.proxy.type": "Proxy type", "gui.computercraft.config.http.proxy.type.tooltip": "The type of proxy to use.\nAllowed Values: HTTP, HTTPS, SOCKS4, SOCKS5", "gui.computercraft.config.http.rules": "Allow/deny rules", - "gui.computercraft.config.http.rules.tooltip": "A list of rules which control behaviour of the \"http\" API for specific domains or\nIPs. Each rule is an item with a 'host' to match against, and a series of\nproperties. Rules are evaluated in order, meaning earlier rules override later\nones.\nThe host may be a domain name (\"pastebin.com\"), wildcard (\"*.pastebin.com\") or\nCIDR notation (\"127.0.0.0/8\").\nIf no rules, the domain is blocked.", + "gui.computercraft.config.http.rules.tooltip": "A list of rules which control behaviour of the \"http\" API for specific domains or\nIPs. Each rule matches against a hostname and an optional port, and then sets several\nproperties for the request. Rules are evaluated in order, meaning earlier rules override\nlater ones.\n\nValid properties:\n - \"host\" (required): The domain or IP address this rule matches. This may be a domain name\n (\"pastebin.com\"), wildcard (\"*.pastebin.com\") or CIDR notation (\"127.0.0.0/8\").\n - \"port\" (optional): Only match requests for a specific port, such as 80 or 443.\n\n - \"action\" (optional): Whether to allow or deny this request.\n - \"max_download\" (optional): The maximum size (in bytes) that a computer can download in this\n request.\n - \"max_upload\" (optional): The maximum size (in bytes) that a computer can upload in a this request.\n - \"max_websocket_message\" (optional): The maximum size (in bytes) that a computer can send or\n receive in one websocket packet.\n - \"use_proxy\" (optional): Enable use of the HTTP/SOCKS proxy if it is configured.", "gui.computercraft.config.http.tooltip": "Controls the HTTP API", "gui.computercraft.config.http.websocket_enabled": "Enable websockets", "gui.computercraft.config.http.websocket_enabled.tooltip": "Enable use of http websockets. This requires the \"http_enable\" option to also be true.", diff --git a/projects/forge/src/generated/resources/assets/computercraft/lang/en_us.json b/projects/forge/src/generated/resources/assets/computercraft/lang/en_us.json index 845d71ec6..77fe60ee0 100644 --- a/projects/forge/src/generated/resources/assets/computercraft/lang/en_us.json +++ b/projects/forge/src/generated/resources/assets/computercraft/lang/en_us.json @@ -99,7 +99,7 @@ "gui.computercraft.config.http.bandwidth.global_upload.tooltip": "The number of bytes which can be uploaded in a second. This is shared across all computers. (bytes/s).\nRange: > 1", "gui.computercraft.config.http.bandwidth.tooltip": "Limits bandwidth used by computers.", "gui.computercraft.config.http.enabled": "Enable the HTTP API", - "gui.computercraft.config.http.enabled.tooltip": "Enable the \"http\" API on Computers. This also disables the \"pastebin\" and \"wget\"\nprograms, that many users rely on. It's recommended to leave this on and use the\n\"rules\" config option to impose more fine-grained control.", + "gui.computercraft.config.http.enabled.tooltip": "Enable the \"http\" API on Computers. Disabling this also disables the \"pastebin\" and\n\"wget\" programs, that many users rely on. It's recommended to leave this on and use\nthe \"rules\" config option to impose more fine-grained control.", "gui.computercraft.config.http.max_requests": "Maximum concurrent requests", "gui.computercraft.config.http.max_requests.tooltip": "The number of http requests a computer can make at one time. Additional requests\nwill be queued, and sent when the running requests have finished. Set to 0 for\nunlimited.\nRange: > 0", "gui.computercraft.config.http.max_websockets": "Maximum concurrent websockets", @@ -113,7 +113,7 @@ "gui.computercraft.config.http.proxy.type": "Proxy type", "gui.computercraft.config.http.proxy.type.tooltip": "The type of proxy to use.\nAllowed Values: HTTP, HTTPS, SOCKS4, SOCKS5", "gui.computercraft.config.http.rules": "Allow/deny rules", - "gui.computercraft.config.http.rules.tooltip": "A list of rules which control behaviour of the \"http\" API for specific domains or\nIPs. Each rule is an item with a 'host' to match against, and a series of\nproperties. Rules are evaluated in order, meaning earlier rules override later\nones.\nThe host may be a domain name (\"pastebin.com\"), wildcard (\"*.pastebin.com\") or\nCIDR notation (\"127.0.0.0/8\").\nIf no rules, the domain is blocked.", + "gui.computercraft.config.http.rules.tooltip": "A list of rules which control behaviour of the \"http\" API for specific domains or\nIPs. Each rule matches against a hostname and an optional port, and then sets several\nproperties for the request. Rules are evaluated in order, meaning earlier rules override\nlater ones.\n\nValid properties:\n - \"host\" (required): The domain or IP address this rule matches. This may be a domain name\n (\"pastebin.com\"), wildcard (\"*.pastebin.com\") or CIDR notation (\"127.0.0.0/8\").\n - \"port\" (optional): Only match requests for a specific port, such as 80 or 443.\n\n - \"action\" (optional): Whether to allow or deny this request.\n - \"max_download\" (optional): The maximum size (in bytes) that a computer can download in this\n request.\n - \"max_upload\" (optional): The maximum size (in bytes) that a computer can upload in a this request.\n - \"max_websocket_message\" (optional): The maximum size (in bytes) that a computer can send or\n receive in one websocket packet.\n - \"use_proxy\" (optional): Enable use of the HTTP/SOCKS proxy if it is configured.", "gui.computercraft.config.http.tooltip": "Controls the HTTP API", "gui.computercraft.config.http.websocket_enabled": "Enable websockets", "gui.computercraft.config.http.websocket_enabled.tooltip": "Enable use of http websockets. This requires the \"http_enable\" option to also be true.", From 9eabb2999999d7f0ef72378b75ec90742578f9e5 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 1 Jul 2023 18:26:38 +0100 Subject: [PATCH 17/32] Move the model cache inside TurtleModelParts This removes a tiny bit of duplication (at the cost of mode code), but makes the interface more intuitive, as there's no bouncing between getCombination -> cache -> buildModel. --- .../client/model/turtle/TurtleModelParts.java | 25 ++++++++++++++----- .../client/model/CompositeBakedModel.java | 4 +++ .../client/model/turtle/TurtleModel.java | 14 +++-------- .../client/model/turtle/TurtleModel.java | 10 +++----- 4 files changed, 30 insertions(+), 23 deletions(-) diff --git a/projects/common/src/client/java/dan200/computercraft/client/model/turtle/TurtleModelParts.java b/projects/common/src/client/java/dan200/computercraft/client/model/turtle/TurtleModelParts.java index e5a2b7365..a473beac3 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/model/turtle/TurtleModelParts.java +++ b/projects/common/src/client/java/dan200/computercraft/client/model/turtle/TurtleModelParts.java @@ -28,8 +28,10 @@ import java.util.function.Function; /** * Combines several individual models together to form a turtle. + * + * @param The type of the resulting "baked model". */ -public final class TurtleModelParts { +public final class TurtleModelParts { private static final Transformation identity, flip; static { @@ -42,7 +44,7 @@ public final class TurtleModelParts { flip = new Transformation(stack.last().pose()); } - public record Combination( + private record Combination( boolean colour, @Nullable ITurtleUpgrade leftUpgrade, @Nullable ITurtleUpgrade rightUpgrade, @@ -55,6 +57,7 @@ public final class TurtleModelParts { private final BakedModel familyModel; private final BakedModel colourModel; private final Function transformer; + private final Function buildModel; /** * A cache of {@link TransformedModel} to the transformed {@link BakedModel}. This helps us pool the transformed @@ -62,13 +65,23 @@ public final class TurtleModelParts { */ private final Map transformCache = new HashMap<>(); - public TurtleModelParts(BakedModel familyModel, BakedModel colourModel, ModelTransformer transformer) { + /** + * A cache of {@link Combination}s to the combined model. + */ + private final Map modelCache = new HashMap<>(); + + public TurtleModelParts(BakedModel familyModel, BakedModel colourModel, ModelTransformer transformer, Function, T> combineModel) { this.familyModel = familyModel; this.colourModel = colourModel; this.transformer = x -> transformer.transform(x.getModel(), x.getMatrix()); + buildModel = x -> combineModel.apply(buildModel(x)); } - public Combination getCombination(ItemStack stack) { + public T getModel(ItemStack stack) { + return modelCache.computeIfAbsent(getCombination(stack), buildModel); + } + + private Combination getCombination(ItemStack stack) { var christmas = Holiday.getCurrent() == Holiday.CHRISTMAS; if (!(stack.getItem() instanceof TurtleItem turtle)) { @@ -85,7 +98,7 @@ public final class TurtleModelParts { return new Combination(colour != -1, leftUpgrade, rightUpgrade, overlay, christmas, flip); } - public List buildModel(Combination combo) { + private List buildModel(Combination combo) { var mc = Minecraft.getInstance(); var modelManager = mc.getItemRenderer().getItemModelShaper().getModelManager(); @@ -109,7 +122,7 @@ public final class TurtleModelParts { return parts; } - public BakedModel transform(BakedModel model, Transformation transformation) { + private BakedModel transform(BakedModel model, Transformation transformation) { if (transformation.equals(Transformation.identity())) return model; return transformCache.computeIfAbsent(new TransformedModel(model, transformation), transformer); } diff --git a/projects/fabric/src/client/java/dan200/computercraft/client/model/CompositeBakedModel.java b/projects/fabric/src/client/java/dan200/computercraft/client/model/CompositeBakedModel.java index aa22ac224..177094cdc 100644 --- a/projects/fabric/src/client/java/dan200/computercraft/client/model/CompositeBakedModel.java +++ b/projects/fabric/src/client/java/dan200/computercraft/client/model/CompositeBakedModel.java @@ -25,6 +25,10 @@ public class CompositeBakedModel extends CustomBakedModel { this.models = models; } + public static BakedModel of(List models) { + return models.size() == 1 ? models.get(0) : new CompositeBakedModel(models); + } + @Override public List getQuads(@Nullable BlockState blockState, @Nullable Direction face, RandomSource rand) { @SuppressWarnings({ "unchecked", "rawtypes" }) diff --git a/projects/fabric/src/client/java/dan200/computercraft/client/model/turtle/TurtleModel.java b/projects/fabric/src/client/java/dan200/computercraft/client/model/turtle/TurtleModel.java index 28ba06bf2..704350b1a 100644 --- a/projects/fabric/src/client/java/dan200/computercraft/client/model/turtle/TurtleModel.java +++ b/projects/fabric/src/client/java/dan200/computercraft/client/model/turtle/TurtleModel.java @@ -14,8 +14,6 @@ import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.item.ItemStack; import javax.annotation.Nullable; -import java.util.HashMap; -import java.util.Map; /** * The custom model for turtle items, which renders tools and overlays as part of the model. @@ -23,28 +21,22 @@ import java.util.Map; * @see TurtleModelParts */ public class TurtleModel extends ForwardingBakedModel { - private final TurtleModelParts parts; + private final TurtleModelParts parts; - private final Map cachedModels = new HashMap<>(); private final ItemOverrides overrides = new ItemOverrides() { @Override public BakedModel resolve(BakedModel model, ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity entity, int seed) { - return cachedModels.computeIfAbsent(parts.getCombination(stack), TurtleModel.this::buildModel); + return parts.getModel(stack); } }; public TurtleModel(BakedModel familyModel, BakedModel colourModel) { wrapped = familyModel; - parts = new TurtleModelParts(familyModel, colourModel, TransformedBakedModel::new); + parts = new TurtleModelParts<>(familyModel, colourModel, TransformedBakedModel::new, CompositeBakedModel::of); } @Override public ItemOverrides getOverrides() { return overrides; } - - private BakedModel buildModel(TurtleModelParts.Combination combo) { - var models = parts.buildModel(combo); - return models.size() == 1 ? models.get(0) : new CompositeBakedModel(models); - } } diff --git a/projects/forge/src/client/java/dan200/computercraft/client/model/turtle/TurtleModel.java b/projects/forge/src/client/java/dan200/computercraft/client/model/turtle/TurtleModel.java index f4ebbe78d..257e48699 100644 --- a/projects/forge/src/client/java/dan200/computercraft/client/model/turtle/TurtleModel.java +++ b/projects/forge/src/client/java/dan200/computercraft/client/model/turtle/TurtleModel.java @@ -11,9 +11,8 @@ import net.minecraft.world.item.ItemDisplayContext; import net.minecraft.world.item.ItemStack; import net.minecraftforge.client.model.BakedModelWrapper; -import java.util.HashMap; import java.util.List; -import java.util.Map; +import java.util.function.Function; /** * The custom model for turtle items, which renders tools and overlays as part of the model. @@ -21,12 +20,11 @@ import java.util.Map; * @see TurtleModelParts */ public class TurtleModel extends BakedModelWrapper { - private final TurtleModelParts parts; - private final Map> cachedModels = new HashMap<>(); + private final TurtleModelParts> parts; public TurtleModel(BakedModel familyModel, BakedModel colourModel) { super(familyModel); - parts = new TurtleModelParts(familyModel, colourModel, TransformedBakedModel::new); + parts = new TurtleModelParts<>(familyModel, colourModel, TransformedBakedModel::new, Function.identity()); } @Override @@ -37,6 +35,6 @@ public class TurtleModel extends BakedModelWrapper { @Override public List getRenderPasses(ItemStack stack, boolean fabulous) { - return cachedModels.computeIfAbsent(parts.getCombination(stack), parts::buildModel); + return parts.getModel(stack); } } From 1977556da474c288aa763e821e57dda9b4e86131 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 1 Jul 2023 18:32:19 +0100 Subject: [PATCH 18/32] Deprecate IPocketAccess.getUpgrades I think this left over from CCTweaks or Peripheral++. It doesn't really make sense as an API - if/when we add multiple upgrades, we'll want a different API for this. --- .../dan200/computercraft/api/pocket/IPocketAccess.java | 7 ++++++- .../shared/pocket/core/PocketServerComputer.java | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/pocket/IPocketAccess.java b/projects/common-api/src/main/java/dan200/computercraft/api/pocket/IPocketAccess.java index 7cadb1b8b..07cb8e768 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/pocket/IPocketAccess.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/pocket/IPocketAccess.java @@ -80,7 +80,10 @@ public interface IPocketAccess { void updateUpgradeNBTData(); /** - * Remove the current peripheral and create a new one. You may wish to do this if the methods available change. + * Remove the current peripheral and create a new one. + *

+ * You may wish to do this if the methods available change, for instance when the {@linkplain #getEntity() owning + * entity} changes. */ void invalidatePeripheral(); @@ -88,6 +91,8 @@ public interface IPocketAccess { * Get a list of all upgrades for the pocket computer. * * @return A collection of all upgrade names. + * @deprecated This is a relic of a previous API, which no longer makes sense with newer versions of ComputerCraft. */ + @Deprecated(forRemoval = true) Map getUpgrades(); } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java b/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java index 6ac22f3dd..937da3341 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java @@ -104,6 +104,7 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces } @Override + @Deprecated(forRemoval = true) public Map getUpgrades() { return upgrade == null ? Collections.emptyMap() : Collections.singletonMap(upgrade.getUpgradeID(), getPeripheral(ComputerSide.BACK)); } From 94f5ede75ab6e8c24997988dbad769ff6342602c Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 1 Jul 2023 19:32:28 +0100 Subject: [PATCH 19/32] Remove superflous Nonnull/Notnull annotations --- .../shared/command/arguments/ComputersArgumentType.java | 5 ++--- .../shared/command/arguments/RepeatArgumentType.java | 3 +-- .../dan200/computercraft/core/apis/http/NetworkUtils.java | 3 +-- .../computercraft/shared/platform/PlatformHelperImpl.java | 8 +++----- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/arguments/ComputersArgumentType.java b/projects/common/src/main/java/dan200/computercraft/shared/command/arguments/ComputersArgumentType.java index f67200b9c..d263cb5ca 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/command/arguments/ComputersArgumentType.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/command/arguments/ComputersArgumentType.java @@ -18,7 +18,6 @@ import net.minecraft.commands.CommandBuildContext; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.synchronization.ArgumentTypeInfo; import net.minecraft.network.FriendlyByteBuf; -import org.jetbrains.annotations.NotNull; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -159,14 +158,14 @@ public final class ComputersArgumentType implements ArgumentType { @Override - public ComputersArgumentType instantiate(@NotNull CommandBuildContext context) { + public ComputersArgumentType instantiate(CommandBuildContext context) { return requireSome ? SOME : MANY; } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java b/projects/common/src/main/java/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java index bc35280b2..51ff6c5fc 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java @@ -17,7 +17,6 @@ import net.minecraft.commands.synchronization.ArgumentTypeInfo; import net.minecraft.commands.synchronization.ArgumentTypeInfos; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.chat.Component; -import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.Collection; @@ -144,7 +143,7 @@ public final class RepeatArgumentType implements ArgumentType> { ) implements ArgumentTypeInfo.Template> { @Override @SuppressWarnings({ "unchecked", "rawtypes" }) - public RepeatArgumentType instantiate(@NotNull CommandBuildContext commandBuildContext) { + public RepeatArgumentType instantiate(CommandBuildContext commandBuildContext) { var child = child().instantiate(commandBuildContext); return flatten ? RepeatArgumentType.someFlat((ArgumentType) child, some()) : RepeatArgumentType.some(child, some()); } diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/NetworkUtils.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/NetworkUtils.java index 517e67790..9a18f4b3d 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/NetworkUtils.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/NetworkUtils.java @@ -27,7 +27,6 @@ import io.netty.handler.ssl.SslHandler; import io.netty.handler.timeout.ReadTimeoutException; import io.netty.handler.traffic.AbstractTrafficShapingHandler; import io.netty.handler.traffic.GlobalTrafficShapingHandler; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -193,7 +192,7 @@ public final class NetworkUtils { * @return The SSL handler. * @see io.netty.handler.ssl.SslHandler */ - private static SslHandler makeSslHandler(SocketChannel ch, @NotNull SslContext sslContext, int timeout, String peerHost, int peerPort) { + private static SslHandler makeSslHandler(SocketChannel ch, SslContext sslContext, int timeout, String peerHost, int peerPort) { var handler = sslContext.newHandler(ch.alloc(), peerHost, peerPort); if (timeout > 0) handler.setHandshakeTimeoutMillis(timeout); return handler; diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java index 0c08744c4..677366b30 100644 --- a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java @@ -72,7 +72,6 @@ import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.EntityHitResult; import net.minecraft.world.phys.Vec3; -import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collection; @@ -284,7 +283,7 @@ public class PlatformHelperImpl implements PlatformHelper { public boolean hasToolUsage(ItemStack stack) { var item = stack.getItem(); return item instanceof ShovelItem || stack.is(ItemTags.SHOVELS) || - item instanceof HoeItem || stack.is(ItemTags.HOES); + item instanceof HoeItem || stack.is(ItemTags.HOES); } @Override @@ -295,8 +294,8 @@ public class PlatformHelperImpl implements PlatformHelper { @Override public boolean interactWithEntity(ServerPlayer player, Entity entity, Vec3 hitPos) { return UseEntityCallback.EVENT.invoker().interact(player, entity.level, InteractionHand.MAIN_HAND, entity, new EntityHitResult(entity, hitPos)).consumesAction() || - entity.interactAt(player, hitPos.subtract(entity.position()), InteractionHand.MAIN_HAND).consumesAction() || - player.interactOn(entity, InteractionHand.MAIN_HAND).consumesAction(); + entity.interactAt(player, hitPos.subtract(entity.position()), InteractionHand.MAIN_HAND).consumesAction() || + player.interactOn(entity, InteractionHand.MAIN_HAND).consumesAction(); } @Override @@ -350,7 +349,6 @@ public class PlatformHelperImpl implements PlatformHelper { return object; } - @Nonnull @Override public Iterator iterator() { return registry.iterator(); From f54cb8a432e5e0a737710b9c0e6ad45e1d68bea7 Mon Sep 17 00:00:00 2001 From: Edvin Date: Sun, 2 Jul 2023 12:55:55 +0300 Subject: [PATCH 20/32] Allow upgrades to read/write upgrade data from ItemStacks (#1465) --- .../client/turtle/TurtleUpgradeModeller.java | 18 +++- .../api/pocket/IPocketAccess.java | 4 + .../api/turtle/ITurtleAccess.java | 39 ++++++++- .../api/turtle/ITurtleUpgrade.java | 14 ++++ .../api/upgrades/UpgradeBase.java | 40 +++++++++ .../api/upgrades/UpgradeData.java | 82 +++++++++++++++++++ .../client/model/turtle/TurtleModelParts.java | 58 +++++++++---- .../client/turtle/TurtleUpgradeModellers.java | 10 ++- .../computercraft/data/RecipeProvider.java | 5 +- .../computercraft/impl/UpgradeManager.java | 5 +- .../computercraft/shared/ModRegistry.java | 5 +- .../shared/integration/RecipeModHelpers.java | 5 +- .../integration/UpgradeRecipeGenerator.java | 34 ++++---- .../shared/platform/PlatformHelper.java | 7 ++ .../shared/pocket/apis/PocketAPI.java | 10 ++- .../pocket/core/PocketServerComputer.java | 11 ++- .../pocket/items/PocketComputerItem.java | 36 +++++--- .../recipes/PocketComputerUpgradeRecipe.java | 3 +- .../shared/turtle/blocks/TurtleBlock.java | 11 ++- .../shared/turtle/core/TurtleBrain.java | 43 +++++----- .../turtle/core/TurtleEquipCommand.java | 9 +- .../turtle/inventory/UpgradeContainer.java | 18 ++-- .../shared/turtle/items/TurtleItem.java | 31 +++++-- .../turtle/recipes/TurtleOverlayRecipe.java | 4 +- .../turtle/recipes/TurtleUpgradeRecipe.java | 8 +- .../shared/turtle/upgrades/TurtleModem.java | 6 ++ .../computercraft/shared/util/NBTUtil.java | 35 ++++++++ .../computercraft/TestPlatformHelper.java | 5 ++ .../shared/platform/PlatformHelperImpl.java | 6 ++ .../shared/platform/PlatformHelperImpl.java | 6 ++ 30 files changed, 455 insertions(+), 113 deletions(-) create mode 100644 projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeData.java diff --git a/projects/common-api/src/client/java/dan200/computercraft/api/client/turtle/TurtleUpgradeModeller.java b/projects/common-api/src/client/java/dan200/computercraft/api/client/turtle/TurtleUpgradeModeller.java index b1a1e72c6..76a57ecf3 100644 --- a/projects/common-api/src/client/java/dan200/computercraft/api/client/turtle/TurtleUpgradeModeller.java +++ b/projects/common-api/src/client/java/dan200/computercraft/api/client/turtle/TurtleUpgradeModeller.java @@ -11,6 +11,7 @@ import dan200.computercraft.api.turtle.ITurtleUpgrade; import dan200.computercraft.api.turtle.TurtleSide; import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser; import net.minecraft.client.resources.model.ModelResourceLocation; +import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceLocation; import javax.annotation.Nullable; @@ -28,12 +29,27 @@ public interface TurtleUpgradeModeller { * When the current turtle is {@literal null}, this function should be constant for a given upgrade and side. * * @param upgrade The upgrade that you're getting the model for. - * @param turtle Access to the turtle that the upgrade resides on. This will be null when getting item models! + * @param turtle Access to the turtle that the upgrade resides on. This will be null when getting item models, unless + * {@link #getModel(ITurtleUpgrade, CompoundTag, TurtleSide)} is overriden. * @param side Which side of the turtle (left or right) the upgrade resides on. * @return The model that you wish to be used to render your upgrade. */ TransformedModel getModel(T upgrade, @Nullable ITurtleAccess turtle, TurtleSide side); + /** + * Obtain the model to be used when rendering a turtle peripheral. + *

+ * This is used when rendering the turtle's item model, and so no {@link ITurtleAccess} is available. + * + * @param upgrade The upgrade that you're getting the model for. + * @param data Upgrade data instance for current turtle side. + * @param side Which side of the turtle (left or right) the upgrade resides on. + * @return The model that you wish to be used to render your upgrade. + */ + default TransformedModel getModel(T upgrade, CompoundTag data, TurtleSide side) { + return getModel(upgrade, (ITurtleAccess) null, side); + } + /** * A basic {@link TurtleUpgradeModeller} which renders using the upgrade's {@linkplain ITurtleUpgrade#getCraftingItem() * crafting item}. diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/pocket/IPocketAccess.java b/projects/common-api/src/main/java/dan200/computercraft/api/pocket/IPocketAccess.java index 07cb8e768..99ac916b9 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/pocket/IPocketAccess.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/pocket/IPocketAccess.java @@ -5,9 +5,11 @@ package dan200.computercraft.api.pocket; import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.upgrades.UpgradeBase; import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; import javax.annotation.Nullable; import java.util.Map; @@ -69,6 +71,8 @@ public interface IPocketAccess { * * @return The upgrade's NBT. * @see #updateUpgradeNBTData() + * @see UpgradeBase#getUpgradeItem(CompoundTag) + * @see UpgradeBase#getUpgradeData(ItemStack) */ CompoundTag getUpgradeNBTData(); diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java index 3ef11c7ab..a057ed46c 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java @@ -8,10 +8,13 @@ import com.mojang.authlib.GameProfile; import dan200.computercraft.api.lua.ILuaCallback; import dan200.computercraft.api.lua.MethodResult; import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.upgrades.UpgradeBase; +import dan200.computercraft.api.upgrades.UpgradeData; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.nbt.CompoundTag; import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.ApiStatus; @@ -245,23 +248,51 @@ public interface ITurtleAccess { void playAnimation(TurtleAnimation animation); /** - * Returns the turtle on the specified side of the turtle, if there is one. + * Returns the upgrade on the specified side of the turtle, if there is one. * * @param side The side to get the upgrade from. * @return The upgrade on the specified side of the turtle, if there is one. - * @see #setUpgrade(TurtleSide, ITurtleUpgrade) + * @see #getUpgradeWithData(TurtleSide) + * @see #setUpgradeWithData(TurtleSide, UpgradeData) */ @Nullable ITurtleUpgrade getUpgrade(TurtleSide side); + /** + * Returns the upgrade on the specified side of the turtle, along with its {@linkplain #getUpgradeNBTData(TurtleSide) + * update data}. + * + * @param side The side to get the upgrade from. + * @return The upgrade on the specified side of the turtle, along with its upgrade data, if there is one. + * @see #getUpgradeWithData(TurtleSide) + * @see #setUpgradeWithData(TurtleSide, UpgradeData) + */ + default @Nullable UpgradeData getUpgradeWithData(TurtleSide side) { + var upgrade = getUpgrade(side); + return upgrade == null ? null : UpgradeData.of(upgrade, getUpgradeNBTData(side)); + } + /** * Set the upgrade for a given side, resetting peripherals and clearing upgrade specific data. * * @param side The side to set the upgrade on. * @param upgrade The upgrade to set, may be {@code null} to clear. * @see #getUpgrade(TurtleSide) + * @deprecated Use {@link #setUpgradeWithData(TurtleSide, UpgradeData)} */ - void setUpgrade(TurtleSide side, @Nullable ITurtleUpgrade upgrade); + @Deprecated + default void setUpgrade(TurtleSide side, @Nullable ITurtleUpgrade upgrade) { + setUpgradeWithData(side, upgrade == null ? null : UpgradeData.ofDefault(upgrade)); + } + + /** + * Set the upgrade for a given side and its upgrade data. + * + * @param side The side to set the upgrade on. + * @param upgrade The upgrade to set, may be {@code null} to clear. + * @see #getUpgradeWithData(TurtleSide) + */ + void setUpgradeWithData(TurtleSide side, @Nullable UpgradeData upgrade); /** * Returns the peripheral created by the upgrade on the specified side of the turtle, if there is one. @@ -281,6 +312,8 @@ public interface ITurtleAccess { * @param side The side to get the upgrade data for. * @return The upgrade-specific data. * @see #updateUpgradeNBTData(TurtleSide) + * @see UpgradeBase#getUpgradeItem(CompoundTag) + * @see UpgradeBase#getUpgradeData(ItemStack) */ CompoundTag getUpgradeNBTData(TurtleSide side); diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java index b39d8429c..2e4d7616b 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java @@ -7,6 +7,7 @@ package dan200.computercraft.api.turtle; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.upgrades.UpgradeBase; import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; import javax.annotation.Nullable; @@ -79,4 +80,17 @@ public interface ITurtleUpgrade extends UpgradeBase { */ default void update(ITurtleAccess turtle, TurtleSide side) { } + + /** + * Get upgrade data that should be persisted when the turtle was broken. + *

+ * This method should be overridden when you don't need to store all upgrade data by default. For instance, if you + * store peripheral state in the upgrade data, which should be lost when the turtle is broken. + * + * @param upgradeData Data that currently stored for this upgrade + * @return Filtered version of this data. + */ + default CompoundTag getPersistedData(CompoundTag upgradeData) { + return upgradeData; + } } diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeBase.java b/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeBase.java index ee83da691..e5e48fd4f 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeBase.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeBase.java @@ -4,10 +4,14 @@ package dan200.computercraft.api.upgrades; +import dan200.computercraft.api.pocket.IPocketAccess; import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.api.turtle.ITurtleAccess; import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.api.turtle.TurtleSide; import dan200.computercraft.impl.PlatformHelper; import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.ItemStack; @@ -50,6 +54,42 @@ public interface UpgradeBase { */ ItemStack getCraftingItem(); + /** + * Returns the item stack representing a currently equipped turtle upgrade. + *

+ * While upgrades can store upgrade data ({@link ITurtleAccess#getUpgradeNBTData(TurtleSide)} and + * {@link IPocketAccess#getUpgradeNBTData()}}, by default this data is discarded when an upgrade is unequipped, + * and the original item stack is returned. + *

+ * By overriding this method, you can create a new {@link ItemStack} which contains enough data to + * {@linkplain #getUpgradeData(ItemStack) re-create the upgrade data} if the item is re-equipped. + *

+ * When overriding this, you should override {@link #getUpgradeData(ItemStack)} and {@link #isItemSuitable(ItemStack)} + * at the same time, + * + * @param upgradeData The current upgrade data. This should NOT be mutated. + * @return The item stack returned when unequipping. + */ + default ItemStack getUpgradeItem(CompoundTag upgradeData) { + return getCraftingItem(); + } + + /** + * Extract upgrade data from an {@link ItemStack}. + *

+ * This upgrade data will be available with {@link ITurtleAccess#getUpgradeNBTData(TurtleSide)} or + * {@link IPocketAccess#getUpgradeNBTData()}. + *

+ * This should be an inverse to {@link #getUpgradeItem(CompoundTag)}. + * + * @param stack The stack that was equipped by the turtle or pocket computer. This will have the same item as + * {@link #getCraftingItem()}. + * @return The upgrade data that should be set on the turtle or pocket computer. + */ + default CompoundTag getUpgradeData(ItemStack stack) { + return new CompoundTag(); + } + /** * Determine if an item is suitable for being used for this upgrade. *

diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeData.java b/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeData.java new file mode 100644 index 000000000..27dd914f2 --- /dev/null +++ b/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeData.java @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.api.upgrades; + +import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Contract; + +import javax.annotation.Nullable; + +/** + * An upgrade (i.e. a {@link ITurtleUpgrade}) and its current upgrade data. + *

+ * IMPORTANT: The {@link #data()} in an upgrade data is often a reference to the original upgrade data. + * Be careful to take a {@linkplain #copy() defensive copy} if you plan to use the data in this upgrade. + * + * @param upgrade The current upgrade. + * @param data The upgrade's data. + * @param The type of upgrade, either {@link ITurtleUpgrade} or {@link IPocketUpgrade}. + */ +public record UpgradeData(T upgrade, CompoundTag data) { + /** + * A utility method to construct a new {@link UpgradeData} instance. + * + * @param upgrade An upgrade. + * @param data The upgrade's data. + * @param The type of upgrade. + * @return The new {@link UpgradeData} instance. + */ + public static UpgradeData of(T upgrade, CompoundTag data) { + return new UpgradeData<>(upgrade, data); + } + + /** + * Create an {@link UpgradeData} containing the default {@linkplain #data() data} for an upgrade. + * + * @param upgrade The upgrade instance. + * @param The type of upgrade. + * @return The default upgrade data. + */ + public static UpgradeData ofDefault(T upgrade) { + return of(upgrade, upgrade.getUpgradeData(upgrade.getCraftingItem())); + } + + /** + * Take a copy of a (possibly {@code null}) {@link UpgradeData} instance. + * + * @param upgrade The copied upgrade data. + * @param The type of upgrade. + * @return The newly created upgrade data. + */ + @Contract("!null -> !null; null -> null") + public static @Nullable UpgradeData copyOf(@Nullable UpgradeData upgrade) { + return upgrade == null ? null : upgrade.copy(); + } + + /** + * Get the {@linkplain UpgradeBase#getUpgradeItem(CompoundTag) upgrade item} for this upgrade. + *

+ * This returns a defensive copy of the item, to prevent accidental mutation of the upgrade data or original + * {@linkplain UpgradeBase#getCraftingItem() upgrade stack}. + * + * @return This upgrade's item. + */ + public ItemStack getUpgradeItem() { + return upgrade.getUpgradeItem(data).copy(); + } + + /** + * Take a copy of this {@link UpgradeData}. This returns a new instance with the same upgrade and a fresh copy of + * the upgrade data. + * + * @return A copy of the current instance. + */ + public UpgradeData copy() { + return new UpgradeData<>(upgrade(), data().copy()); + } +} diff --git a/projects/common/src/client/java/dan200/computercraft/client/model/turtle/TurtleModelParts.java b/projects/common/src/client/java/dan200/computercraft/client/model/turtle/TurtleModelParts.java index a473beac3..fec6d4d7d 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/model/turtle/TurtleModelParts.java +++ b/projects/common/src/client/java/dan200/computercraft/client/model/turtle/TurtleModelParts.java @@ -4,11 +4,13 @@ package dan200.computercraft.client.model.turtle; +import com.google.common.cache.CacheBuilder; import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.math.Transformation; import dan200.computercraft.api.client.TransformedModel; import dan200.computercraft.api.turtle.ITurtleUpgrade; import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.client.platform.ClientPlatformHelper; import dan200.computercraft.client.render.TurtleBlockEntityRenderer; import dan200.computercraft.client.turtle.TurtleUpgradeModellers; @@ -21,9 +23,9 @@ import net.minecraft.world.item.ItemStack; import javax.annotation.Nullable; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.function.Function; /** @@ -46,12 +48,19 @@ public final class TurtleModelParts { private record Combination( boolean colour, - @Nullable ITurtleUpgrade leftUpgrade, - @Nullable ITurtleUpgrade rightUpgrade, + @Nullable UpgradeData leftUpgrade, + @Nullable UpgradeData rightUpgrade, @Nullable ResourceLocation overlay, boolean christmas, boolean flip ) { + Combination copy() { + if (leftUpgrade == null && rightUpgrade == null) return this; + return new Combination( + colour, UpgradeData.copyOf(leftUpgrade), UpgradeData.copyOf(rightUpgrade), + overlay, christmas, flip + ); + } } private final BakedModel familyModel; @@ -63,12 +72,20 @@ public final class TurtleModelParts { * A cache of {@link TransformedModel} to the transformed {@link BakedModel}. This helps us pool the transformed * instances, reducing memory usage and hopefully ensuring their caches are hit more often! */ - private final Map transformCache = new HashMap<>(); + private final Map transformCache = CacheBuilder.newBuilder() + .concurrencyLevel(1) + .expireAfterAccess(30, TimeUnit.SECONDS) + .build() + .asMap(); /** * A cache of {@link Combination}s to the combined model. */ - private final Map modelCache = new HashMap<>(); + private final Map modelCache = CacheBuilder.newBuilder() + .concurrencyLevel(1) + .expireAfterAccess(30, TimeUnit.SECONDS) + .build() + .asMap(); public TurtleModelParts(BakedModel familyModel, BakedModel colourModel, ModelTransformer transformer, Function, T> combineModel) { this.familyModel = familyModel; @@ -78,7 +95,15 @@ public final class TurtleModelParts { } public T getModel(ItemStack stack) { - return modelCache.computeIfAbsent(getCombination(stack), buildModel); + var combination = getCombination(stack); + var existing = modelCache.get(combination); + if (existing != null) return existing; + + // Take a defensive copy of the upgrade data, and add it to the cache. + var newCombination = combination.copy(); + var newModel = buildModel.apply(newCombination); + modelCache.put(newCombination, newModel); + return newModel; } private Combination getCombination(ItemStack stack) { @@ -89,8 +114,8 @@ public final class TurtleModelParts { } var colour = turtle.getColour(stack); - var leftUpgrade = turtle.getUpgrade(stack, TurtleSide.LEFT); - var rightUpgrade = turtle.getUpgrade(stack, TurtleSide.RIGHT); + var leftUpgrade = turtle.getUpgradeWithData(stack, TurtleSide.LEFT); + var rightUpgrade = turtle.getUpgradeWithData(stack, TurtleSide.RIGHT); var overlay = turtle.getOverlay(stack); var label = turtle.getLabel(stack); var flip = label != null && (label.equals("Dinnerbone") || label.equals("Grumm")); @@ -110,18 +135,19 @@ public final class TurtleModelParts { if (overlayModelLocation != null) { parts.add(transform(ClientPlatformHelper.get().getModel(modelManager, overlayModelLocation), transformation)); } - if (combo.leftUpgrade() != null) { - var model = TurtleUpgradeModellers.getModel(combo.leftUpgrade(), null, TurtleSide.LEFT); - parts.add(transform(model.getModel(), transformation.compose(model.getMatrix()))); - } - if (combo.rightUpgrade() != null) { - var model = TurtleUpgradeModellers.getModel(combo.rightUpgrade(), null, TurtleSide.RIGHT); - parts.add(transform(model.getModel(), transformation.compose(model.getMatrix()))); - } + + addUpgrade(parts, transformation, TurtleSide.LEFT, combo.leftUpgrade()); + addUpgrade(parts, transformation, TurtleSide.RIGHT, combo.rightUpgrade()); return parts; } + private void addUpgrade(List parts, Transformation transformation, TurtleSide side, @Nullable UpgradeData upgrade) { + if (upgrade == null) return; + var model = TurtleUpgradeModellers.getModel(upgrade.upgrade(), upgrade.data(), side); + parts.add(transform(model.getModel(), transformation.compose(model.getMatrix()))); + } + private BakedModel transform(BakedModel model, Transformation transformation) { if (transformation.equals(Transformation.identity())) return model; return transformCache.computeIfAbsent(new TransformedModel(model, transformation), transformer); diff --git a/projects/common/src/client/java/dan200/computercraft/client/turtle/TurtleUpgradeModellers.java b/projects/common/src/client/java/dan200/computercraft/client/turtle/TurtleUpgradeModellers.java index 9037748f9..49b08801c 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/turtle/TurtleUpgradeModellers.java +++ b/projects/common/src/client/java/dan200/computercraft/client/turtle/TurtleUpgradeModellers.java @@ -14,8 +14,8 @@ import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser; import dan200.computercraft.impl.TurtleUpgrades; import dan200.computercraft.impl.UpgradeManager; import net.minecraft.client.Minecraft; +import net.minecraft.nbt.CompoundTag; -import javax.annotation.Nullable; import java.util.Map; import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; @@ -52,12 +52,18 @@ public final class TurtleUpgradeModellers { } } - public static TransformedModel getModel(ITurtleUpgrade upgrade, @Nullable ITurtleAccess access, TurtleSide side) { + public static TransformedModel getModel(ITurtleUpgrade upgrade, ITurtleAccess access, TurtleSide side) { @SuppressWarnings("unchecked") var modeller = (TurtleUpgradeModeller) modelCache.computeIfAbsent(upgrade, TurtleUpgradeModellers::getModeller); return modeller.getModel(upgrade, access, side); } + public static TransformedModel getModel(ITurtleUpgrade upgrade, CompoundTag data, TurtleSide side) { + @SuppressWarnings("unchecked") + var modeller = (TurtleUpgradeModeller) modelCache.computeIfAbsent(upgrade, TurtleUpgradeModellers::getModeller); + return modeller.getModel(upgrade, data, side); + } + private static TurtleUpgradeModeller getModeller(ITurtleUpgrade upgradeA) { var wrapper = TurtleUpgrades.instance().getWrapper(upgradeA); if (wrapper == null) return NULL_TURTLE_MODELLER; diff --git a/projects/common/src/main/java/dan200/computercraft/data/RecipeProvider.java b/projects/common/src/main/java/dan200/computercraft/data/RecipeProvider.java index 46854e04d..f23d6711c 100644 --- a/projects/common/src/main/java/dan200/computercraft/data/RecipeProvider.java +++ b/projects/common/src/main/java/dan200/computercraft/data/RecipeProvider.java @@ -8,6 +8,7 @@ import com.google.gson.JsonObject; import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.pocket.PocketUpgradeDataProvider; import dan200.computercraft.api.turtle.TurtleUpgradeDataProvider; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.core.util.Colour; import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.common.IColouredItem; @@ -110,7 +111,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider { var nameId = turtleItem.getFamily().name().toLowerCase(Locale.ROOT); for (var upgrade : turtleUpgrades.getGeneratedUpgrades()) { - var result = turtleItem.create(-1, null, -1, null, upgrade, -1, null); + var result = turtleItem.create(-1, null, -1, null, UpgradeData.ofDefault(upgrade), -1, null); ShapedRecipeBuilder .shaped(RecipeCategory.REDSTONE, result.getItem()) .group(String.format("%s:turtle_%s", ComputerCraftAPI.MOD_ID, nameId)) @@ -146,7 +147,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider { var nameId = pocket.getFamily().name().toLowerCase(Locale.ROOT); for (var upgrade : pocketUpgrades.getGeneratedUpgrades()) { - var result = pocket.create(-1, null, -1, upgrade); + var result = pocket.create(-1, null, -1, UpgradeData.ofDefault(upgrade)); ShapedRecipeBuilder .shaped(RecipeCategory.REDSTONE, result.getItem()) .group(String.format("%s:pocket_%s", ComputerCraftAPI.MOD_ID, nameId)) diff --git a/projects/common/src/main/java/dan200/computercraft/impl/UpgradeManager.java b/projects/common/src/main/java/dan200/computercraft/impl/UpgradeManager.java index 440d4bd2b..a3e25eae5 100644 --- a/projects/common/src/main/java/dan200/computercraft/impl/UpgradeManager.java +++ b/projects/common/src/main/java/dan200/computercraft/impl/UpgradeManager.java @@ -7,6 +7,7 @@ package dan200.computercraft.impl; import com.google.gson.*; import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.upgrades.UpgradeBase; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.api.upgrades.UpgradeSerialiser; import dan200.computercraft.shared.platform.PlatformHelper; import net.minecraft.core.Registry; @@ -74,13 +75,13 @@ public class UpgradeManager, T extends } @Nullable - public T get(ItemStack stack) { + public UpgradeData get(ItemStack stack) { if (stack.isEmpty()) return null; for (var wrapper : current.values()) { var craftingStack = wrapper.upgrade().getCraftingItem(); if (!craftingStack.isEmpty() && craftingStack.getItem() == stack.getItem() && wrapper.upgrade().isItemSuitable(stack)) { - return wrapper.upgrade(); + return UpgradeData.of(wrapper.upgrade, wrapper.upgrade.getUpgradeData(stack)); } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java b/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java index 13ea6d0a6..a903eed6e 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java @@ -11,6 +11,7 @@ import dan200.computercraft.api.detail.VanillaDetailRegistries; import dan200.computercraft.api.media.IMedia; import dan200.computercraft.api.pocket.PocketUpgradeSerialiser; import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.core.util.Colour; import dan200.computercraft.impl.PocketUpgrades; import dan200.computercraft.impl.TurtleUpgrades; @@ -446,12 +447,12 @@ public final class ModRegistry { private static void addTurtle(CreativeModeTab.Output out, TurtleItem turtle) { out.accept(turtle.create(-1, null, -1, null, null, 0, null)); TurtleUpgrades.getVanillaUpgrades() - .map(x -> turtle.create(-1, null, -1, null, x, 0, null)) + .map(x -> turtle.create(-1, null, -1, null, UpgradeData.ofDefault(x), 0, null)) .forEach(out::accept); } private static void addPocket(CreativeModeTab.Output out, PocketComputerItem pocket) { out.accept(pocket.create(-1, null, -1, null)); - PocketUpgrades.getVanillaUpgrades().map(x -> pocket.create(-1, null, -1, x)).forEach(out::accept); + PocketUpgrades.getVanillaUpgrades().map(x -> pocket.create(-1, null, -1, UpgradeData.ofDefault(x))).forEach(out::accept); } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/integration/RecipeModHelpers.java b/projects/common/src/main/java/dan200/computercraft/shared/integration/RecipeModHelpers.java index 1e93c30a9..e848accef 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/integration/RecipeModHelpers.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/integration/RecipeModHelpers.java @@ -5,6 +5,7 @@ package dan200.computercraft.shared.integration; import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.impl.PocketUpgrades; import dan200.computercraft.impl.TurtleUpgrades; import dan200.computercraft.shared.ModRegistry; @@ -56,14 +57,14 @@ public final class RecipeModHelpers { for (var turtleSupplier : TURTLES) { var turtle = turtleSupplier.get(); for (var upgrade : TurtleUpgrades.instance().getUpgrades()) { - upgradeItems.add(turtle.create(-1, null, -1, null, upgrade, 0, null)); + upgradeItems.add(turtle.create(-1, null, -1, null, UpgradeData.ofDefault(upgrade), 0, null)); } } for (var pocketSupplier : POCKET_COMPUTERS) { var pocket = pocketSupplier.get(); for (var upgrade : PocketUpgrades.instance().getUpgrades()) { - upgradeItems.add(pocket.create(-1, null, -1, upgrade)); + upgradeItems.add(pocket.create(-1, null, -1, UpgradeData.ofDefault(upgrade))); } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/integration/UpgradeRecipeGenerator.java b/projects/common/src/main/java/dan200/computercraft/shared/integration/UpgradeRecipeGenerator.java index 0dbe35b5d..253f76a28 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/integration/UpgradeRecipeGenerator.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/integration/UpgradeRecipeGenerator.java @@ -9,6 +9,7 @@ import dan200.computercraft.api.pocket.IPocketUpgrade; import dan200.computercraft.api.turtle.ITurtleUpgrade; import dan200.computercraft.api.turtle.TurtleSide; import dan200.computercraft.api.upgrades.UpgradeBase; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.impl.PocketUpgrades; import dan200.computercraft.impl.TurtleUpgrades; import dan200.computercraft.shared.pocket.items.PocketComputerItem; @@ -111,20 +112,22 @@ public class UpgradeRecipeGenerator { if (stack.getItem() instanceof TurtleItem item) { // Suggest possible upgrades which can be applied to this turtle - var left = item.getUpgrade(stack, TurtleSide.LEFT); - var right = item.getUpgrade(stack, TurtleSide.RIGHT); + var left = item.getUpgradeWithData(stack, TurtleSide.LEFT); + var right = item.getUpgradeWithData(stack, TurtleSide.RIGHT); if (left != null && right != null) return Collections.emptyList(); List recipes = new ArrayList<>(); var ingredient = Ingredient.of(stack); for (var upgrade : turtleUpgrades) { + if (upgrade.turtle == null) throw new NullPointerException(); + // The turtle is facing towards us, so upgrades on the left are actually crafted on the right. if (left == null) { - recipes.add(turtle(ingredient, upgrade.ingredient, turtleWith(stack, upgrade.turtle, right))); + recipes.add(turtle(ingredient, upgrade.ingredient, turtleWith(stack, UpgradeData.ofDefault(upgrade.turtle), right))); } if (right == null) { - recipes.add(turtle(upgrade.ingredient, ingredient, turtleWith(stack, left, upgrade.turtle))); + recipes.add(turtle(upgrade.ingredient, ingredient, turtleWith(stack, left, UpgradeData.ofDefault(upgrade.turtle)))); } } @@ -137,7 +140,8 @@ public class UpgradeRecipeGenerator { List recipes = new ArrayList<>(); var ingredient = Ingredient.of(stack); for (var upgrade : pocketUpgrades) { - recipes.add(pocket(upgrade.ingredient, ingredient, pocketWith(stack, upgrade.pocket))); + if (upgrade.pocket == null) throw new NullPointerException(); + recipes.add(pocket(upgrade.ingredient, ingredient, pocketWith(stack, UpgradeData.ofDefault(upgrade.pocket)))); } return Collections.unmodifiableList(recipes); @@ -180,21 +184,21 @@ public class UpgradeRecipeGenerator { if (stack.getItem() instanceof TurtleItem item) { List recipes = new ArrayList<>(0); - var left = item.getUpgrade(stack, TurtleSide.LEFT); - var right = item.getUpgrade(stack, TurtleSide.RIGHT); + var left = item.getUpgradeWithData(stack, TurtleSide.LEFT); + var right = item.getUpgradeWithData(stack, TurtleSide.RIGHT); // The turtle is facing towards us, so upgrades on the left are actually crafted on the right. if (left != null) { recipes.add(turtle( Ingredient.of(turtleWith(stack, null, right)), - Ingredient.of(left.getCraftingItem()), + Ingredient.of(left.getUpgradeItem()), stack )); } if (right != null) { recipes.add(turtle( - Ingredient.of(right.getCraftingItem()), + Ingredient.of(right.getUpgradeItem()), Ingredient.of(turtleWith(stack, left, null)), stack )); @@ -204,9 +208,9 @@ public class UpgradeRecipeGenerator { } else if (stack.getItem() instanceof PocketComputerItem) { List recipes = new ArrayList<>(0); - var back = PocketComputerItem.getUpgrade(stack); + var back = PocketComputerItem.getUpgradeWithData(stack); if (back != null) { - recipes.add(pocket(Ingredient.of(back.getCraftingItem()), Ingredient.of(pocketWith(stack, null)), stack)); + recipes.add(pocket(Ingredient.of(back.getUpgradeItem()), Ingredient.of(pocketWith(stack, null)), stack)); } return Collections.unmodifiableList(recipes); @@ -215,7 +219,7 @@ public class UpgradeRecipeGenerator { } } - private static ItemStack turtleWith(ItemStack stack, @Nullable ITurtleUpgrade left, @Nullable ITurtleUpgrade right) { + private static ItemStack turtleWith(ItemStack stack, @Nullable UpgradeData left, @Nullable UpgradeData right) { var item = (TurtleItem) stack.getItem(); return item.create( item.getComputerID(stack), item.getLabel(stack), item.getColour(stack), @@ -223,7 +227,7 @@ public class UpgradeRecipeGenerator { ); } - private static ItemStack pocketWith(ItemStack stack, @Nullable IPocketUpgrade back) { + private static ItemStack pocketWith(ItemStack stack, @Nullable UpgradeData back) { var item = (PocketComputerItem) stack.getItem(); return item.create( item.getComputerID(stack), item.getLabel(stack), item.getColour(stack), back @@ -272,7 +276,7 @@ public class UpgradeRecipeGenerator { recipes.add(turtle( ingredient, // Right upgrade, recipe on left Ingredient.of(turtleItem.create(-1, null, -1, null, null, 0, null)), - turtleItem.create(-1, null, -1, null, turtle, 0, null) + turtleItem.create(-1, null, -1, null, UpgradeData.ofDefault(turtle), 0, null) )); } } @@ -283,7 +287,7 @@ public class UpgradeRecipeGenerator { recipes.add(pocket( ingredient, Ingredient.of(pocketItem.create(-1, null, -1, null)), - pocketItem.create(-1, null, -1, pocket) + pocketItem.create(-1, null, -1, UpgradeData.ofDefault(pocket)) )); } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/platform/PlatformHelper.java b/projects/common/src/main/java/dan200/computercraft/shared/platform/PlatformHelper.java index faba05862..614b52446 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/platform/PlatformHelper.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/platform/PlatformHelper.java @@ -68,6 +68,13 @@ public interface PlatformHelper extends dan200.computercraft.impl.PlatformHelper return (PlatformHelper) dan200.computercraft.impl.PlatformHelper.get(); } + /** + * Check if we're running in a development environment. + * + * @return If we're running in a development environment. + */ + boolean isDevelopmentEnvironment(); + /** * Create a new config builder. * diff --git a/projects/common/src/main/java/dan200/computercraft/shared/pocket/apis/PocketAPI.java b/projects/common/src/main/java/dan200/computercraft/shared/pocket/apis/PocketAPI.java index e1b1cb0ad..a0749450d 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/pocket/apis/PocketAPI.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/pocket/apis/PocketAPI.java @@ -7,6 +7,7 @@ package dan200.computercraft.shared.pocket.apis; import dan200.computercraft.api.lua.ILuaAPI; import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.impl.PocketUpgrades; import dan200.computercraft.shared.pocket.core.PocketServerComputer; import net.minecraft.core.NonNullList; @@ -14,6 +15,7 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import javax.annotation.Nullable; +import java.util.Objects; /** * Control the current pocket computer, adding or removing upgrades. @@ -68,7 +70,7 @@ public class PocketAPI implements ILuaAPI { if (newUpgrade == null) return new Object[]{ false, "Cannot find a valid upgrade" }; // Remove the current upgrade - if (previousUpgrade != null) storeItem(player, previousUpgrade.getCraftingItem().copy()); + if (previousUpgrade != null) storeItem(player, previousUpgrade.getUpgradeItem()); // Set the new upgrade computer.setUpgrade(newUpgrade); @@ -93,7 +95,7 @@ public class PocketAPI implements ILuaAPI { computer.setUpgrade(null); - storeItem(player, previousUpgrade.getCraftingItem().copy()); + storeItem(player, previousUpgrade.getUpgradeItem()); return new Object[]{ true }; } @@ -105,13 +107,13 @@ public class PocketAPI implements ILuaAPI { } } - private static @Nullable IPocketUpgrade findUpgrade(NonNullList inv, int start, @Nullable IPocketUpgrade previous) { + private static @Nullable UpgradeData findUpgrade(NonNullList inv, int start, @Nullable UpgradeData previous) { for (var i = 0; i < inv.size(); i++) { var invStack = inv.get((i + start) % inv.size()); if (!invStack.isEmpty()) { var newUpgrade = PocketUpgrades.instance().get(invStack); - if (newUpgrade != null && newUpgrade != previous) { + if (newUpgrade != null && !Objects.equals(newUpgrade, previous)) { // Consume an item from this stack and exit the loop invStack = invStack.copy(); invStack.shrink(1); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java b/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java index 937da3341..64cc6c73f 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java @@ -7,6 +7,7 @@ package dan200.computercraft.shared.pocket.core; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.pocket.IPocketAccess; import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.core.computer.ComputerSide; import dan200.computercraft.shared.common.IColouredItem; import dan200.computercraft.shared.computer.core.ComputerFamily; @@ -109,8 +110,8 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces return upgrade == null ? Collections.emptyMap() : Collections.singletonMap(upgrade.getUpgradeID(), getPeripheral(ComputerSide.BACK)); } - public @Nullable IPocketUpgrade getUpgrade() { - return upgrade; + public @Nullable UpgradeData getUpgrade() { + return upgrade == null ? null : UpgradeData.of(upgrade, getUpgradeNBTData()); } /** @@ -120,13 +121,11 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces * * @param upgrade The new upgrade to set it to, may be {@code null}. */ - public void setUpgrade(@Nullable IPocketUpgrade upgrade) { - if (this.upgrade == upgrade) return; - + public void setUpgrade(@Nullable UpgradeData upgrade) { synchronized (this) { PocketComputerItem.setUpgrade(stack, upgrade); updateUpgradeNBTData(); - this.upgrade = upgrade; + this.upgrade = upgrade == null ? null : upgrade.upgrade(); invalidatePeripheral(); } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItem.java b/projects/common/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItem.java index a3648ba0c..1834288ab 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItem.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItem.java @@ -10,6 +10,7 @@ import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.filesystem.Mount; import dan200.computercraft.api.media.IMedia; import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.core.computer.ComputerSide; import dan200.computercraft.impl.PocketUpgrades; import dan200.computercraft.shared.ModRegistry; @@ -23,6 +24,7 @@ import dan200.computercraft.shared.pocket.apis.PocketAPI; import dan200.computercraft.shared.pocket.core.PocketServerComputer; import dan200.computercraft.shared.pocket.inventory.PocketComputerMenuProvider; import dan200.computercraft.shared.util.IDAssigner; +import dan200.computercraft.shared.util.NBTUtil; import net.minecraft.ChatFormatting; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; @@ -58,7 +60,7 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I this.family = family; } - public static ItemStack create(int id, @Nullable String label, int colour, ComputerFamily family, @Nullable IPocketUpgrade upgrade) { + public static ItemStack create(int id, @Nullable String label, int colour, ComputerFamily family, @Nullable UpgradeData upgrade) { return switch (family) { case NORMAL -> ModRegistry.Items.POCKET_COMPUTER_NORMAL.get().create(id, label, colour, upgrade); case ADVANCED -> ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get().create(id, label, colour, upgrade); @@ -66,11 +68,14 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I }; } - public ItemStack create(int id, @Nullable String label, int colour, @Nullable IPocketUpgrade upgrade) { + public ItemStack create(int id, @Nullable String label, int colour, @Nullable UpgradeData upgrade) { var result = new ItemStack(this); if (id >= 0) result.getOrCreateTag().putInt(NBT_ID, id); if (label != null) result.setHoverName(Component.literal(label)); - if (upgrade != null) result.getOrCreateTag().putString(NBT_UPGRADE, upgrade.getUpgradeID().toString()); + if (upgrade != null) { + result.getOrCreateTag().putString(NBT_UPGRADE, upgrade.upgrade().getUpgradeID().toString()); + if (!upgrade.data().isEmpty()) result.getOrCreateTag().put(NBT_UPGRADE_INFO, upgrade.data().copy()); + } if (colour != -1) result.getOrCreateTag().putInt(NBT_COLOUR, colour); return result; } @@ -207,7 +212,9 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I setInstanceID(stack, computer.register()); setSessionID(stack, registry.getSessionID()); - computer.updateValues(entity, stack, getUpgrade(stack)); + var upgrade = getUpgrade(stack); + + computer.updateValues(entity, stack, upgrade); computer.addAPI(new PocketAPI(computer)); // Only turn on when initially creating the computer, rather than each tick. @@ -244,7 +251,7 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I public ItemStack withFamily(ItemStack stack, ComputerFamily family) { return create( getComputerID(stack), getLabel(stack), getColour(stack), - family, getUpgrade(stack) + family, getUpgradeWithData(stack) ); } @@ -294,20 +301,27 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I public static @Nullable IPocketUpgrade getUpgrade(ItemStack stack) { var compound = stack.getTag(); - return compound != null && compound.contains(NBT_UPGRADE) - ? PocketUpgrades.instance().get(compound.getString(NBT_UPGRADE)) : null; + if (compound == null || !compound.contains(NBT_UPGRADE)) return null; + return PocketUpgrades.instance().get(compound.getString(NBT_UPGRADE)); } - public static void setUpgrade(ItemStack stack, @Nullable IPocketUpgrade upgrade) { + public static @Nullable UpgradeData getUpgradeWithData(ItemStack stack) { + var compound = stack.getTag(); + if (compound == null || !compound.contains(NBT_UPGRADE)) return null; + var upgrade = PocketUpgrades.instance().get(compound.getString(NBT_UPGRADE)); + return upgrade == null ? null : UpgradeData.of(upgrade, NBTUtil.getCompoundOrEmpty(compound, NBT_UPGRADE_INFO)); + } + + public static void setUpgrade(ItemStack stack, @Nullable UpgradeData upgrade) { var compound = stack.getOrCreateTag(); if (upgrade == null) { compound.remove(NBT_UPGRADE); + compound.remove(NBT_UPGRADE_INFO); } else { - compound.putString(NBT_UPGRADE, upgrade.getUpgradeID().toString()); + compound.putString(NBT_UPGRADE, upgrade.upgrade().getUpgradeID().toString()); + compound.put(NBT_UPGRADE_INFO, upgrade.data().copy()); } - - compound.remove(NBT_UPGRADE_INFO); } public static CompoundTag getUpgradeInfo(ItemStack stack) { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/pocket/recipes/PocketComputerUpgradeRecipe.java b/projects/common/src/main/java/dan200/computercraft/shared/pocket/recipes/PocketComputerUpgradeRecipe.java index 6c74ea7b6..b0b25bd87 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/pocket/recipes/PocketComputerUpgradeRecipe.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/pocket/recipes/PocketComputerUpgradeRecipe.java @@ -5,6 +5,7 @@ package dan200.computercraft.shared.pocket.recipes; import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.impl.PocketUpgrades; import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.pocket.items.PocketComputerItem; @@ -62,7 +63,7 @@ public final class PocketComputerUpgradeRecipe extends CustomRecipe { if (PocketComputerItem.getUpgrade(computer) != null) return ItemStack.EMPTY; // Check for upgrades around the item - IPocketUpgrade upgrade = null; + UpgradeData upgrade = null; for (var y = 0; y < inventory.getHeight(); y++) { for (var x = 0; x < inventory.getWidth(); x++) { var item = inventory.getItem(x + y * inventory.getWidth()); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/blocks/TurtleBlock.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/blocks/TurtleBlock.java index 7e058698e..157bb876b 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/blocks/TurtleBlock.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/blocks/TurtleBlock.java @@ -5,7 +5,9 @@ package dan200.computercraft.shared.turtle.blocks; import dan200.computercraft.annotations.ForgeOverride; +import dan200.computercraft.api.turtle.ITurtleUpgrade; import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.shared.computer.blocks.AbstractComputerBlock; import dan200.computercraft.shared.computer.blocks.AbstractComputerBlockEntity; import dan200.computercraft.shared.computer.core.ComputerFamily; @@ -128,7 +130,7 @@ public class TurtleBlock extends AbstractComputerBlock implem if (stack.getItem() instanceof TurtleItem item) { // Set Upgrades for (var side : TurtleSide.values()) { - turtle.getAccess().setUpgrade(side, item.getUpgrade(stack, side)); + turtle.getAccess().setUpgradeWithData(side, item.getUpgradeWithData(stack, side)); } turtle.getAccess().setFuelLevel(item.getFuelLevel(stack)); @@ -161,11 +163,16 @@ public class TurtleBlock extends AbstractComputerBlock implem var access = turtle.getAccess(); return TurtleItem.create( turtle.getComputerID(), turtle.getLabel(), access.getColour(), turtle.getFamily(), - access.getUpgrade(TurtleSide.LEFT), access.getUpgrade(TurtleSide.RIGHT), + withPersistedData(access.getUpgradeWithData(TurtleSide.LEFT)), + withPersistedData(access.getUpgradeWithData(TurtleSide.RIGHT)), access.getFuelLevel(), turtle.getOverlay() ); } + private static @Nullable UpgradeData withPersistedData(@Nullable UpgradeData upgrade) { + return upgrade == null ? null : UpgradeData.of(upgrade.upgrade(), upgrade.upgrade().getPersistedData(upgrade.data())); + } + @Override @Nullable public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java index ecfc1f787..a5aa15902 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java @@ -13,6 +13,7 @@ import dan200.computercraft.api.turtle.ITurtleUpgrade; import dan200.computercraft.api.turtle.TurtleAnimation; import dan200.computercraft.api.turtle.TurtleCommand; import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.core.computer.ComputerSide; import dan200.computercraft.core.util.Colour; import dan200.computercraft.impl.TurtleUpgrades; @@ -141,17 +142,16 @@ public class TurtleBrain implements TurtleAccessInternal { overlay = nbt.contains(NBT_OVERLAY) ? new ResourceLocation(nbt.getString(NBT_OVERLAY)) : null; // Read upgrades - setUpgradeDirect(TurtleSide.LEFT, nbt.contains(NBT_LEFT_UPGRADE) ? TurtleUpgrades.instance().get(nbt.getString(NBT_LEFT_UPGRADE)) : null); - setUpgradeDirect(TurtleSide.RIGHT, nbt.contains(NBT_RIGHT_UPGRADE) ? TurtleUpgrades.instance().get(nbt.getString(NBT_RIGHT_UPGRADE)) : null); + setUpgradeDirect(TurtleSide.LEFT, readUpgrade(nbt, NBT_LEFT_UPGRADE, NBT_LEFT_UPGRADE_DATA)); + setUpgradeDirect(TurtleSide.RIGHT, readUpgrade(nbt, NBT_RIGHT_UPGRADE, NBT_RIGHT_UPGRADE_DATA)); + } - // NBT - upgradeNBTData.clear(); - if (nbt.contains(NBT_LEFT_UPGRADE_DATA)) { - upgradeNBTData.put(TurtleSide.LEFT, nbt.getCompound(NBT_LEFT_UPGRADE_DATA).copy()); - } - if (nbt.contains(NBT_RIGHT_UPGRADE_DATA)) { - upgradeNBTData.put(TurtleSide.RIGHT, nbt.getCompound(NBT_RIGHT_UPGRADE_DATA).copy()); - } + private @Nullable UpgradeData readUpgrade(CompoundTag tag, String upgradeKey, String dataKey) { + if (!tag.contains(upgradeKey)) return null; + var upgrade = TurtleUpgrades.instance().get(tag.getString(upgradeKey)); + if (upgrade == null) return null; + + return UpgradeData.of(upgrade, tag.getCompound(dataKey)); } private void writeCommon(CompoundTag nbt) { @@ -516,7 +516,7 @@ public class TurtleBrain implements TurtleAccessInternal { } @Override - public void setUpgrade(TurtleSide side, @Nullable ITurtleUpgrade upgrade) { + public void setUpgradeWithData(TurtleSide side, @Nullable UpgradeData upgrade) { if (!setUpgradeDirect(side, upgrade) || owner.getLevel() == null) return; // This is a separate function to avoid updating the block when reading the NBT. We don't need to do this as @@ -529,19 +529,18 @@ public class TurtleBrain implements TurtleAccessInternal { owner.updateInputsImmediately(); } - private boolean setUpgradeDirect(TurtleSide side, @Nullable ITurtleUpgrade upgrade) { + private boolean setUpgradeDirect(TurtleSide side, @Nullable UpgradeData upgrade) { // Remove old upgrade - if (upgrades.containsKey(side)) { - if (upgrades.get(side) == upgrade) return false; - upgrades.remove(side); - } else { - if (upgrade == null) return false; - } - - upgradeNBTData.remove(side); + var oldUpgrade = upgrades.remove(side); + if (oldUpgrade == null && upgrade == null) return false; // Set new upgrade - if (upgrade != null) upgrades.put(side, upgrade); + if (upgrade == null) { + upgradeNBTData.remove(side); + } else { + upgrades.put(side, upgrade.upgrade()); + upgradeNBTData.put(side, upgrade.data().copy()); + } // Notify clients and create peripherals if (owner.getLevel() != null && !owner.getLevel().isClientSide) { @@ -595,7 +594,7 @@ public class TurtleBrain implements TurtleAccessInternal { public float getToolRenderAngle(TurtleSide side, float f) { return (side == TurtleSide.LEFT && animation == TurtleAnimation.SWING_LEFT_TOOL) || - (side == TurtleSide.RIGHT && animation == TurtleAnimation.SWING_RIGHT_TOOL) + (side == TurtleSide.RIGHT && animation == TurtleAnimation.SWING_RIGHT_TOOL) ? 45.0f * (float) Math.sin(getAnimationFraction(f) * Math.PI) : 0.0f; } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleEquipCommand.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleEquipCommand.java index 1a15bb840..ae373f697 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleEquipCommand.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleEquipCommand.java @@ -5,6 +5,7 @@ package dan200.computercraft.shared.turtle.core; import dan200.computercraft.api.turtle.*; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.impl.TurtleUpgrades; import dan200.computercraft.shared.turtle.TurtleUtil; @@ -18,10 +19,10 @@ public class TurtleEquipCommand implements TurtleCommand { @Override public TurtleCommandResult execute(ITurtleAccess turtle) { // Determine the upgrade to replace - var oldUpgrade = turtle.getUpgrade(side); + var oldUpgrade = turtle.getUpgradeWithData(side); // Determine the upgrade to equipLeft - ITurtleUpgrade newUpgrade; + UpgradeData newUpgrade; var selectedStack = turtle.getInventory().getItem(turtle.getSelectedSlot()); if (!selectedStack.isEmpty()) { newUpgrade = TurtleUpgrades.instance().get(selectedStack); @@ -32,8 +33,8 @@ public class TurtleEquipCommand implements TurtleCommand { // Do the swapping: if (newUpgrade != null) turtle.getInventory().removeItem(turtle.getSelectedSlot(), 1); - if (oldUpgrade != null) TurtleUtil.storeItemOrDrop(turtle, oldUpgrade.getCraftingItem().copy()); - turtle.setUpgrade(side, newUpgrade); + if (oldUpgrade != null) TurtleUtil.storeItemOrDrop(turtle, oldUpgrade.getUpgradeItem()); + turtle.setUpgradeWithData(side, newUpgrade); // Animate if (newUpgrade != null || oldUpgrade != null) { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/inventory/UpgradeContainer.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/inventory/UpgradeContainer.java index 22d918626..0dd06e896 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/inventory/UpgradeContainer.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/inventory/UpgradeContainer.java @@ -7,6 +7,7 @@ package dan200.computercraft.shared.turtle.inventory; import dan200.computercraft.api.turtle.ITurtleAccess; import dan200.computercraft.api.turtle.ITurtleUpgrade; import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.impl.TurtleUpgrades; import net.minecraft.core.NonNullList; import net.minecraft.world.Container; @@ -27,7 +28,7 @@ class UpgradeContainer implements Container { private final ITurtleAccess turtle; - private final List lastUpgrade = Arrays.asList(null, null); + private final List> lastUpgrade = Arrays.asList(null, null); private final NonNullList lastStack = NonNullList.withSize(2, ItemStack.EMPTY); UpgradeContainer(ITurtleAccess turtle) { @@ -44,22 +45,25 @@ class UpgradeContainer implements Container { @Override public ItemStack getItem(int slot) { - var upgrade = turtle.getUpgrade(getSide(slot)); + var side = getSide(slot); + var upgrade = turtle.getUpgrade(side); + if (upgrade == null) return ItemStack.EMPTY; // We don't want to return getCraftingItem directly here, as consumers may mutate the stack (they shouldn't!, // but if they do it's a pain to track down). To avoid recreating the stack each tick, we maintain a simple - // cache. - if (upgrade == lastUpgrade.get(slot)) return lastStack.get(slot); + // cache. We use an inlined getUpgradeData here to avoid the additional defensive copy. + var upgradeData = UpgradeData.of(upgrade, turtle.getUpgradeNBTData(side)); + if (upgradeData.equals(lastUpgrade.get(slot))) return lastStack.get(slot); - var stack = upgrade == null ? ItemStack.EMPTY : upgrade.getCraftingItem().copy(); - lastUpgrade.set(slot, upgrade); + var stack = upgradeData.getUpgradeItem(); + lastUpgrade.set(slot, upgradeData.copy()); lastStack.set(slot, stack); return stack; } @Override public void setItem(int slot, ItemStack itemStack) { - turtle.setUpgrade(getSide(slot), TurtleUpgrades.instance().get(itemStack)); + turtle.setUpgradeWithData(getSide(slot), TurtleUpgrades.instance().get(itemStack)); } @Override diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/items/TurtleItem.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/items/TurtleItem.java index 91bc45717..640872b23 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/items/TurtleItem.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/items/TurtleItem.java @@ -8,12 +8,14 @@ import dan200.computercraft.annotations.ForgeOverride; import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.turtle.ITurtleUpgrade; import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.impl.TurtleUpgrades; import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.common.IColouredItem; import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.items.AbstractComputerItem; import dan200.computercraft.shared.turtle.blocks.TurtleBlock; +import dan200.computercraft.shared.util.NBTUtil; import net.minecraft.core.cauldron.CauldronInteraction; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; @@ -32,7 +34,7 @@ public class TurtleItem extends AbstractComputerItem implements IColouredItem { public static ItemStack create( int id, @Nullable String label, int colour, ComputerFamily family, - @Nullable ITurtleUpgrade leftUpgrade, @Nullable ITurtleUpgrade rightUpgrade, + @Nullable UpgradeData leftUpgrade, @Nullable UpgradeData rightUpgrade, int fuelLevel, @Nullable ResourceLocation overlay ) { return switch (family) { @@ -46,7 +48,7 @@ public class TurtleItem extends AbstractComputerItem implements IColouredItem { public ItemStack create( int id, @Nullable String label, int colour, - @Nullable ITurtleUpgrade leftUpgrade, @Nullable ITurtleUpgrade rightUpgrade, + @Nullable UpgradeData leftUpgrade, @Nullable UpgradeData rightUpgrade, int fuelLevel, @Nullable ResourceLocation overlay ) { // Build the stack @@ -58,11 +60,15 @@ public class TurtleItem extends AbstractComputerItem implements IColouredItem { if (overlay != null) stack.getOrCreateTag().putString(NBT_OVERLAY, overlay.toString()); if (leftUpgrade != null) { - stack.getOrCreateTag().putString(NBT_LEFT_UPGRADE, leftUpgrade.getUpgradeID().toString()); + var tag = stack.getOrCreateTag(); + tag.putString(NBT_LEFT_UPGRADE, leftUpgrade.upgrade().getUpgradeID().toString()); + if (!leftUpgrade.data().isEmpty()) tag.put(NBT_LEFT_UPGRADE_DATA, leftUpgrade.data().copy()); } if (rightUpgrade != null) { - stack.getOrCreateTag().putString(NBT_RIGHT_UPGRADE, rightUpgrade.getUpgradeID().toString()); + var tag = stack.getOrCreateTag(); + tag.putString(NBT_RIGHT_UPGRADE, rightUpgrade.upgrade().getUpgradeID().toString()); + if (!rightUpgrade.data().isEmpty()) tag.put(NBT_RIGHT_UPGRADE_DATA, rightUpgrade.data().copy()); } return stack; @@ -117,7 +123,7 @@ public class TurtleItem extends AbstractComputerItem implements IColouredItem { return create( getComputerID(stack), getLabel(stack), getColour(stack), family, - getUpgrade(stack, TurtleSide.LEFT), getUpgrade(stack, TurtleSide.RIGHT), + getUpgradeWithData(stack, TurtleSide.LEFT), getUpgradeWithData(stack, TurtleSide.RIGHT), getFuelLevel(stack), getOverlay(stack) ); } @@ -127,7 +133,20 @@ public class TurtleItem extends AbstractComputerItem implements IColouredItem { if (tag == null) return null; var key = side == TurtleSide.LEFT ? NBT_LEFT_UPGRADE : NBT_RIGHT_UPGRADE; - return tag.contains(key) ? TurtleUpgrades.instance().get(tag.getString(key)) : null; + if (!tag.contains(key)) return null; + return TurtleUpgrades.instance().get(tag.getString(key)); + } + + public @Nullable UpgradeData getUpgradeWithData(ItemStack stack, TurtleSide side) { + var tag = stack.getTag(); + if (tag == null) return null; + + var key = side == TurtleSide.LEFT ? NBT_LEFT_UPGRADE : NBT_RIGHT_UPGRADE; + if (!tag.contains(key)) return null; + var upgrade = TurtleUpgrades.instance().get(tag.getString(key)); + if (upgrade == null) return null; + var dataKey = side == TurtleSide.LEFT ? NBT_LEFT_UPGRADE_DATA : NBT_RIGHT_UPGRADE_DATA; + return UpgradeData.of(upgrade, NBTUtil.getCompoundOrEmpty(tag, dataKey)); } public @Nullable ResourceLocation getOverlay(ItemStack stack) { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleOverlayRecipe.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleOverlayRecipe.java index 4076a3662..7f31bca95 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleOverlayRecipe.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleOverlayRecipe.java @@ -38,8 +38,8 @@ public class TurtleOverlayRecipe extends ShapelessRecipe { turtle.getComputerID(stack), turtle.getLabel(stack), turtle.getColour(stack), - turtle.getUpgrade(stack, TurtleSide.LEFT), - turtle.getUpgrade(stack, TurtleSide.RIGHT), + turtle.getUpgradeWithData(stack, TurtleSide.LEFT), + turtle.getUpgradeWithData(stack, TurtleSide.RIGHT), turtle.getFuelLevel(stack), overlay ); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleUpgradeRecipe.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleUpgradeRecipe.java index 4dcd9e5a2..3ad657e47 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleUpgradeRecipe.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleUpgradeRecipe.java @@ -6,6 +6,7 @@ package dan200.computercraft.shared.turtle.recipes; import dan200.computercraft.api.turtle.ITurtleUpgrade; import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.impl.TurtleUpgrades; import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.turtle.items.TurtleItem; @@ -104,9 +105,10 @@ public final class TurtleUpgradeRecipe extends CustomRecipe { // At this point we have a turtle + 1 or 2 items // Get the turtle we already have var itemTurtle = (TurtleItem) turtle.getItem(); - var upgrades = new ITurtleUpgrade[]{ - itemTurtle.getUpgrade(turtle, TurtleSide.LEFT), - itemTurtle.getUpgrade(turtle, TurtleSide.RIGHT), + @SuppressWarnings({ "unchecked", "rawtypes" }) + UpgradeData[] upgrades = new UpgradeData[]{ + itemTurtle.getUpgradeWithData(turtle, TurtleSide.LEFT), + itemTurtle.getUpgradeWithData(turtle, TurtleSide.RIGHT), }; // Get the upgrades for the new items diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleModem.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleModem.java index 4c514de2d..7235d4431 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleModem.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleModem.java @@ -9,6 +9,7 @@ import dan200.computercraft.api.turtle.*; import dan200.computercraft.shared.peripheral.modem.ModemState; import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemPeripheral; import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; @@ -77,4 +78,9 @@ public class TurtleModem extends AbstractTurtleUpgrade { } } } + + @Override + public CompoundTag getPersistedData(CompoundTag upgradeData) { + return new CompoundTag(); + } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/util/NBTUtil.java b/projects/common/src/main/java/dan200/computercraft/shared/util/NBTUtil.java index 874375033..5fa6f7061 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/util/NBTUtil.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/util/NBTUtil.java @@ -7,6 +7,7 @@ package dan200.computercraft.shared.util; import com.google.common.annotations.VisibleForTesting; import com.google.common.io.BaseEncoding; import dan200.computercraft.core.util.Nullability; +import dan200.computercraft.shared.platform.PlatformHelper; import net.minecraft.nbt.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,6 +20,7 @@ import java.io.OutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -27,9 +29,42 @@ public final class NBTUtil { @VisibleForTesting static final BaseEncoding ENCODING = BaseEncoding.base16().lowerCase(); + private static final CompoundTag EMPTY_TAG; + + static { + // If in a development environment, create a magic immutable compound tag. + // We avoid doing this in prod, as I fear it might mess up the JIT inlining things. + if (PlatformHelper.get().isDevelopmentEnvironment()) { + try { + var ctor = CompoundTag.class.getDeclaredConstructor(Map.class); + ctor.setAccessible(true); + EMPTY_TAG = ctor.newInstance(Collections.emptyMap()); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } else { + EMPTY_TAG = new CompoundTag(); + } + } + private NBTUtil() { } + /** + * Get a singleton empty {@link CompoundTag}. This tag should never be modified. + * + * @return The empty compound tag. + */ + public static CompoundTag emptyTag() { + if (EMPTY_TAG.size() != 0) LOG.error("The empty tag has been modified."); + return EMPTY_TAG; + } + + public static CompoundTag getCompoundOrEmpty(CompoundTag tag, String key) { + var childTag = tag.get(key); + return childTag != null && childTag.getId() == Tag.TAG_COMPOUND ? (CompoundTag) childTag : emptyTag(); + } + private static @Nullable Tag toNBTTag(@Nullable Object object) { if (object == null) return null; if (object instanceof Boolean) return ByteTag.valueOf((byte) ((boolean) (Boolean) object ? 1 : 0)); diff --git a/projects/common/src/test/java/dan200/computercraft/TestPlatformHelper.java b/projects/common/src/test/java/dan200/computercraft/TestPlatformHelper.java index 552e63756..d898d5928 100644 --- a/projects/common/src/test/java/dan200/computercraft/TestPlatformHelper.java +++ b/projects/common/src/test/java/dan200/computercraft/TestPlatformHelper.java @@ -58,6 +58,11 @@ import java.util.function.Predicate; @AutoService({ PlatformHelper.class, dan200.computercraft.impl.PlatformHelper.class, ComputerCraftAPIService.class }) public class TestPlatformHelper extends AbstractComputerCraftAPI implements PlatformHelper { + @Override + public boolean isDevelopmentEnvironment() { + return true; + } + @Override public ConfigFile.Builder createConfigBuilder() { throw new UnsupportedOperationException("Cannot create config file inside tests"); diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java index 677366b30..7f0f5fb30 100644 --- a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java @@ -33,6 +33,7 @@ import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerType; import net.fabricmc.fabric.api.tag.convention.v1.ConventionalItemTags; import net.fabricmc.fabric.api.transfer.v1.item.InventoryStorage; import net.fabricmc.fabric.api.transfer.v1.item.ItemStorage; +import net.fabricmc.loader.api.FabricLoader; import net.minecraft.commands.synchronization.ArgumentTypeInfo; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; @@ -81,6 +82,11 @@ import java.util.function.*; @AutoService(dan200.computercraft.impl.PlatformHelper.class) public class PlatformHelperImpl implements PlatformHelper { + @Override + public boolean isDevelopmentEnvironment() { + return FabricLoader.getInstance().isDevelopmentEnvironment(); + } + @Override public ConfigFile.Builder createConfigBuilder() { return new FabricConfigFile.Builder(); diff --git a/projects/forge/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java b/projects/forge/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java index 41154448c..02a644bb1 100644 --- a/projects/forge/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java +++ b/projects/forge/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java @@ -62,6 +62,7 @@ import net.minecraftforge.common.util.NonNullConsumer; import net.minecraftforge.event.ForgeEventFactory; import net.minecraftforge.eventbus.api.Event; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.fml.loading.FMLLoader; import net.minecraftforge.items.wrapper.InvWrapper; import net.minecraftforge.items.wrapper.SidedInvWrapper; import net.minecraftforge.network.NetworkHooks; @@ -76,6 +77,11 @@ import java.util.function.*; @AutoService(dan200.computercraft.impl.PlatformHelper.class) public class PlatformHelperImpl implements PlatformHelper { + @Override + public boolean isDevelopmentEnvironment() { + return !FMLLoader.isProduction(); + } + @Override public ConfigFile.Builder createConfigBuilder() { return new ForgeConfigFile.Builder(); From d138d9c4a514ab0d95dae9726ab301bf2c3c1cec Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 2 Jul 2023 11:02:10 +0100 Subject: [PATCH 21/32] Preserve item NBT for turtle tools This is a pre-requisite for #1501, and some other refactorings I want to do. Also fix items in the turtle upgrade slots vanishing. We now explicitly invalidate the cache when setting the item. --- .../turtle/inventory/UpgradeContainer.java | 23 +++++++++++-------- .../shared/turtle/upgrades/TurtleTool.java | 21 +++++++++++++++-- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/inventory/UpgradeContainer.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/inventory/UpgradeContainer.java index 0dd06e896..a0f0b6987 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/inventory/UpgradeContainer.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/inventory/UpgradeContainer.java @@ -14,6 +14,7 @@ import net.minecraft.world.Container; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; +import javax.annotation.Nullable; import java.util.Arrays; import java.util.List; @@ -46,24 +47,28 @@ class UpgradeContainer implements Container { @Override public ItemStack getItem(int slot) { var side = getSide(slot); - var upgrade = turtle.getUpgrade(side); + var upgrade = turtle.getUpgradeWithData(side); if (upgrade == null) return ItemStack.EMPTY; - // We don't want to return getCraftingItem directly here, as consumers may mutate the stack (they shouldn't!, - // but if they do it's a pain to track down). To avoid recreating the stack each tick, we maintain a simple - // cache. We use an inlined getUpgradeData here to avoid the additional defensive copy. - var upgradeData = UpgradeData.of(upgrade, turtle.getUpgradeNBTData(side)); - if (upgradeData.equals(lastUpgrade.get(slot))) return lastStack.get(slot); + // We don't want to return getUpgradeItem directly here, as we'd end up recreating the stack each tick. To + // avoid that, we maintain a simple cache. + if (upgrade.equals(lastUpgrade.get(slot))) return lastStack.get(slot); - var stack = upgradeData.getUpgradeItem(); - lastUpgrade.set(slot, upgradeData.copy()); + return setUpgradeStack(slot, upgrade); + } + + private ItemStack setUpgradeStack(int slot, @Nullable UpgradeData upgrade) { + var stack = upgrade == null ? ItemStack.EMPTY : upgrade.getUpgradeItem(); + lastUpgrade.set(slot, upgrade); lastStack.set(slot, stack); return stack; } @Override public void setItem(int slot, ItemStack itemStack) { - turtle.setUpgradeWithData(getSide(slot), TurtleUpgrades.instance().get(itemStack)); + var upgrade = TurtleUpgrades.instance().get(itemStack); + turtle.setUpgradeWithData(getSide(slot), upgrade); + setUpgradeStack(slot, upgrade); } @Override diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java index 9f8b80150..7f75e7456 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java @@ -14,6 +14,7 @@ import dan200.computercraft.shared.util.DropConsumer; import dan200.computercraft.shared.util.WorldUtil; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; import net.minecraft.tags.TagKey; @@ -59,7 +60,7 @@ public class TurtleTool extends AbstractTurtleUpgrade { // Check we've not got anything vaguely interesting on the item. We allow other mods to add their // own NBT, with the understanding such details will be lost to the mist of time. - if (stack.isDamaged() || stack.isEnchanted() || stack.hasCustomHoverName()) return false; + if (stack.isDamaged() || stack.isEnchanted()) return false; if (tag.contains("AttributeModifiers", TAG_LIST) && !tag.getList("AttributeModifiers", TAG_COMPOUND).isEmpty()) { return false; } @@ -67,6 +68,21 @@ public class TurtleTool extends AbstractTurtleUpgrade { return true; } + @Override + public CompoundTag getUpgradeData(ItemStack stack) { + // Just use the current item's tag. + var itemTag = stack.getTag(); + return itemTag == null ? new CompoundTag() : itemTag; + } + + @Override + public ItemStack getUpgradeItem(CompoundTag upgradeData) { + // Copy upgrade data back to the item. + var item = super.getUpgradeItem(upgradeData); + if (!upgradeData.isEmpty()) item.setTag(upgradeData); + return item; + } + @Override public TurtleCommandResult useTool(ITurtleAccess turtle, TurtleSide side, TurtleVerb verb, Direction direction) { return switch (verb) { @@ -177,7 +193,8 @@ public class TurtleTool extends AbstractTurtleUpgrade { } private static boolean isTriviallyBreakable(BlockGetter reader, BlockPos pos, BlockState state) { - return state.is(ComputerCraftTags.Blocks.TURTLE_ALWAYS_BREAKABLE) + return + state.is(ComputerCraftTags.Blocks.TURTLE_ALWAYS_BREAKABLE) // Allow breaking any "instabreak" block. || state.getDestroySpeed(reader, pos) == 0; } From 8708048b6ee882cc52580c35bd361387226ff45f Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 2 Jul 2023 11:46:03 +0100 Subject: [PATCH 22/32] Try to make turtle_test.peripheral_change more robust Replace the arbitrary sleep with a thenWaitUntil. --- .../computercraft/gametest/Turtle_Test.kt | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt index 454e5273d..42d0da4ec 100644 --- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt @@ -39,6 +39,7 @@ import org.hamcrest.Matchers.instanceOf import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotEquals import java.util.* +import java.util.concurrent.CopyOnWriteArrayList import kotlin.time.Duration.Companion.milliseconds class Turtle_Test { @@ -488,7 +489,7 @@ class Turtle_Test { fun Peripheral_change(helper: GameTestHelper) = helper.sequence { val testInfo = (helper as GameTestHelperAccessor).testInfo as GameTestInfoAccessor - val events = mutableListOf>() + val events = CopyOnWriteArrayList>() var running = false thenStartComputer("listen") { running = true @@ -507,15 +508,13 @@ class Turtle_Test { turtle.back().await().assertArrayEquals(true, message = "Moved turtle forward") TestHooks.LOG.info("[{}] Finished turtle at {}", testInfo, testInfo.`computercraft$getTick`()) } - thenIdle(4) // Should happen immediately, but computers might be slow. - thenExecute { - assertEquals( - listOf( - "peripheral_detach" to "right", - "peripheral" to "right", - ), - events, + thenWaitUntil { + val expected = listOf( + "peripheral_detach" to "right", + "peripheral" to "right", ) + + if (events != expected) helper.fail("Expected $expected, but received $events") } } From 0b2bb5e7b5051ae87f1c8dcb2f6299bc8e53dce9 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 2 Jul 2023 12:21:03 +0100 Subject: [PATCH 23/32] Use exclusiveContent for our maven This is a little nasty as we need to include ForgeGradle's repo too, but should still help a bit! --- .../main/kotlin/cc-tweaked.forge.gradle.kts | 3 ++- .../cc-tweaked.java-convention.gradle.kts | 20 ++++++++++++++++--- .../gradle/common/util/runs/RunConfigSetup.kt | 9 +++++---- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/buildSrc/src/main/kotlin/cc-tweaked.forge.gradle.kts b/buildSrc/src/main/kotlin/cc-tweaked.forge.gradle.kts index 9ffe48f81..ece233e6f 100644 --- a/buildSrc/src/main/kotlin/cc-tweaked.forge.gradle.kts +++ b/buildSrc/src/main/kotlin/cc-tweaked.forge.gradle.kts @@ -10,8 +10,9 @@ import cc.tweaked.gradle.IdeaRunConfigurations import cc.tweaked.gradle.MinecraftConfigurations plugins { - id("cc-tweaked.java-convention") id("net.minecraftforge.gradle") + // We must apply java-convention after Forge, as we need the fg extension to be present. + id("cc-tweaked.java-convention") id("org.parchmentmc.librarian.forgegradle") } diff --git a/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts b/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts index d8dfb6b4e..75c739613 100644 --- a/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts @@ -37,9 +37,25 @@ java { repositories { mavenCentral() - maven("https://squiddev.cc/maven") { + + val mainMaven = maven("https://squiddev.cc/maven") { name = "SquidDev" content { + // Until https://github.com/SpongePowered/Mixin/pull/593 is merged + includeModule("org.spongepowered", "mixin") + } + } + + exclusiveContent { + forRepositories(mainMaven) + + // Include the ForgeGradle repository if present. This requires that ForgeGradle is already present, which we + // enforce in our Forge overlay. + val fg = + project.extensions.findByType(net.minecraftforge.gradle.userdev.DependencyManagementExtension::class.java) + if (fg != null) forRepositories(fg.repository) + + filter { includeGroup("org.squiddev") includeGroup("cc.tweaked") // Things we mirror @@ -49,8 +65,6 @@ repositories { includeGroup("me.shedaniel.cloth") includeGroup("mezz.jei") includeModule("com.terraformersmc", "modmenu") - // Until https://github.com/SpongePowered/Mixin/pull/593 is merged - includeModule("org.spongepowered", "mixin") } } } diff --git a/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt b/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt index 0a0324dcf..50e33a84b 100644 --- a/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt +++ b/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt @@ -56,12 +56,13 @@ internal fun setRunConfigInternal(project: Project, spec: JavaExecSpec, config: for ((k, v) in config.lazyTokens) lazyTokens[k] = v lazyTokens.compute( "source_roots", - { key: String, sourceRoots: Supplier? -> + { _, sourceRoots -> Supplier { val modClasses = RunConfigGenerator.mapModClassesToGradle(project, config) - (if (sourceRoots != null) Stream.concat( - sourceRoots.get().split(File.pathSeparator).stream(), modClasses, - ) else modClasses).distinct().collect(Collectors.joining(File.pathSeparator)) + (when (sourceRoots) { + null -> modClasses + else -> Stream.concat(sourceRoots.get().split(File.pathSeparator).stream(), modClasses) + }).distinct().collect(Collectors.joining(File.pathSeparator)) } }, ) From 943a9406b1a5985476fc93fc4994d00da72a6f5a Mon Sep 17 00:00:00 2001 From: PenguinEncounter <49845522+penguinencounter@users.noreply.github.com> Date: Sun, 2 Jul 2023 10:19:09 -0700 Subject: [PATCH 24/32] Fix turtle.refuel example (#1510) --- .../java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java index ece4f31d0..12af726ca 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java @@ -555,7 +555,7 @@ public class TurtleAPI implements ILuaAPI { * @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
+     * if level == "unlimited" then error("Turtle does not need fuel", 0) end
      *
      * local ok, err = turtle.refuel()
      * if ok then

From a91ac6f21483938fef99a2fc356c740d48338e3b Mon Sep 17 00:00:00 2001
From: Jonathan Coates 
Date: Mon, 3 Jul 2023 22:10:11 +0100
Subject: [PATCH 25/32] Make turtle tools a little more flexible

Turtle tools now accept two additional JSON fields

 - allowEnchantments: Whether items with enchantments (or any
   non-standard NBT) can be equipped.
 - consumesDurability: Whether durability will be consumed. This can be
   "never" (the current and default behaviour), "always", and
   "when_enchanted".

Closes #1501.
---
 .../api/turtle/TurtleToolDurability.java      |  48 ++++
 .../api/turtle/TurtleUpgradeDataProvider.java |  30 +++
 .../shared/turtle/core/TurtleBrain.java       |  19 --
 .../turtle/core/TurtlePlaceCommand.java       |  14 +-
 .../turtle/inventory/UpgradeContainer.java    |   2 +-
 .../shared/turtle/upgrades/TurtleTool.java    | 234 ++++++++++++++----
 .../turtle/upgrades/TurtleToolSerialiser.java |  11 +-
 .../shared/platform/FakePlayer.java           |   6 +
 .../shared/platform/FakePlayerExt.java        |   6 +
 9 files changed, 281 insertions(+), 89 deletions(-)
 create mode 100644 projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleToolDurability.java

diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleToolDurability.java b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleToolDurability.java
new file mode 100644
index 000000000..c149a185d
--- /dev/null
+++ b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleToolDurability.java
@@ -0,0 +1,48 @@
+// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
+//
+// SPDX-License-Identifier: MPL-2.0
+
+package dan200.computercraft.api.turtle;
+
+import net.minecraft.util.StringRepresentable;
+import net.minecraft.world.entity.EquipmentSlot;
+import net.minecraft.world.item.ItemStack;
+
+/**
+ * Indicates if an equipped turtle item will consume durability.
+ *
+ * @see TurtleUpgradeDataProvider.ToolBuilder#consumesDurability(TurtleToolDurability)
+ */
+public enum TurtleToolDurability implements StringRepresentable {
+    /**
+     * The equipped tool always consumes durability when using.
+     */
+    ALWAYS("always"),
+
+    /**
+     * The equipped tool consumes durability if it is {@linkplain ItemStack#isEnchanted() enchanted} or has
+     * {@linkplain ItemStack#getAttributeModifiers(EquipmentSlot) custom attribute modifiers}.
+     */
+    WHEN_ENCHANTED("when_enchanted"),
+
+    /**
+     * The equipped tool never consumes durability. Tools which have been damaged cannot be used as upgrades.
+     */
+    NEVER("never");
+
+    private final String serialisedName;
+
+    /**
+     * The codec which may be used for serialising/deserialising {@link TurtleToolDurability}s.
+     */
+    public static final StringRepresentable.EnumCodec CODEC = StringRepresentable.fromEnum(TurtleToolDurability::values);
+
+    TurtleToolDurability(String serialisedName) {
+        this.serialisedName = serialisedName;
+    }
+
+    @Override
+    public String getSerializedName() {
+        return serialisedName;
+    }
+}
diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeDataProvider.java b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeDataProvider.java
index 7aef277d6..a2048e969 100644
--- a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeDataProvider.java
+++ b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeDataProvider.java
@@ -13,8 +13,10 @@ import net.minecraft.data.DataGenerator;
 import net.minecraft.data.PackOutput;
 import net.minecraft.resources.ResourceLocation;
 import net.minecraft.tags.TagKey;
+import net.minecraft.world.entity.EquipmentSlot;
 import net.minecraft.world.entity.ai.attributes.Attributes;
 import net.minecraft.world.item.Item;
+import net.minecraft.world.item.ItemStack;
 import net.minecraft.world.level.block.Block;
 
 import javax.annotation.Nullable;
@@ -61,6 +63,8 @@ public abstract class TurtleUpgradeDataProvider extends UpgradeDataProvider breakable;
+        private boolean allowEnchantments = false;
+        private TurtleToolDurability consumesDurability = TurtleToolDurability.NEVER;
 
         ToolBuilder(ResourceLocation id, TurtleUpgradeSerialiser serialiser, Item toolItem) {
             this.id = id;
@@ -104,6 +108,28 @@ public abstract class TurtleUpgradeDataProvider extends UpgradeDataProvider= 0 && colour <= 0xFFFFFF) {
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java
index 1e9c9357a..d1b9156b3 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java
@@ -74,19 +74,7 @@ public class TurtlePlaceCommand implements TurtleCommand {
         }
     }
 
-    public static boolean deployCopiedItem(
-        ItemStack stack, ITurtleAccess turtle, Direction direction, @Nullable Object[] extraArguments, @Nullable ErrorMessage outErrorMessage
-    ) {
-        // Create a fake player, and orient it appropriately
-        var playerPosition = turtle.getPosition().relative(direction);
-        var turtlePlayer = TurtlePlayer.getWithPosition(turtle, playerPosition, direction);
-        turtlePlayer.loadInventory(stack);
-        var result = deploy(stack, turtle, turtlePlayer, direction, extraArguments, outErrorMessage);
-        turtlePlayer.player().getInventory().clearContent();
-        return result;
-    }
-
-    private static boolean deploy(
+    public static boolean deploy(
         ItemStack stack, ITurtleAccess turtle, TurtlePlayer turtlePlayer, Direction direction,
         @Nullable Object[] extraArguments, @Nullable ErrorMessage outErrorMessage
     ) {
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/inventory/UpgradeContainer.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/inventory/UpgradeContainer.java
index a0f0b6987..9fff2789a 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/inventory/UpgradeContainer.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/inventory/UpgradeContainer.java
@@ -59,7 +59,7 @@ class UpgradeContainer implements Container {
 
     private ItemStack setUpgradeStack(int slot, @Nullable UpgradeData upgrade) {
         var stack = upgrade == null ? ItemStack.EMPTY : upgrade.getUpgradeItem();
-        lastUpgrade.set(slot, upgrade);
+        lastUpgrade.set(slot, UpgradeData.copyOf(upgrade));
         lastStack.set(slot, stack);
         return stack;
     }
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java
index 7f75e7456..564fcbb6c 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java
@@ -6,6 +6,7 @@ package dan200.computercraft.shared.turtle.upgrades;
 
 import dan200.computercraft.api.ComputerCraftTags;
 import dan200.computercraft.api.turtle.*;
+import dan200.computercraft.core.util.Nullability;
 import dan200.computercraft.shared.platform.PlatformHelper;
 import dan200.computercraft.shared.turtle.TurtleUtil;
 import dan200.computercraft.shared.turtle.core.TurtlePlaceCommand;
@@ -17,14 +18,19 @@ import net.minecraft.core.Direction;
 import net.minecraft.nbt.CompoundTag;
 import net.minecraft.resources.ResourceLocation;
 import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
 import net.minecraft.tags.TagKey;
+import net.minecraft.world.InteractionHand;
 import net.minecraft.world.InteractionResult;
 import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.MobType;
 import net.minecraft.world.entity.ai.attributes.Attributes;
 import net.minecraft.world.entity.decoration.ArmorStand;
 import net.minecraft.world.entity.player.Player;
 import net.minecraft.world.item.Item;
 import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.enchantment.EnchantmentHelper;
 import net.minecraft.world.level.BlockGetter;
 import net.minecraft.world.level.Level;
 import net.minecraft.world.level.block.Block;
@@ -33,6 +39,8 @@ import net.minecraft.world.level.block.state.BlockState;
 import net.minecraft.world.phys.EntityHitResult;
 
 import javax.annotation.Nullable;
+import java.util.Objects;
+import java.util.function.Function;
 
 import static net.minecraft.nbt.Tag.TAG_COMPOUND;
 import static net.minecraft.nbt.Tag.TAG_LIST;
@@ -43,31 +51,39 @@ public class TurtleTool extends AbstractTurtleUpgrade {
 
     final ItemStack item;
     final float damageMulitiplier;
-    @Nullable
-    final TagKey breakable;
+    final boolean allowsEnchantments;
+    final TurtleToolDurability consumesDurability;
+    final @Nullable TagKey breakable;
 
-    public TurtleTool(ResourceLocation id, String adjective, Item craftItem, ItemStack toolItem, float damageMulitiplier, @Nullable TagKey breakable) {
+    public TurtleTool(
+        ResourceLocation id, String adjective, Item craftItem, ItemStack toolItem, float damageMulitiplier,
+        boolean allowsEnchantments, TurtleToolDurability consumesDurability, @Nullable TagKey breakable
+    ) {
         super(id, TurtleUpgradeType.TOOL, adjective, new ItemStack(craftItem));
         item = toolItem;
         this.damageMulitiplier = damageMulitiplier;
+        this.allowsEnchantments = allowsEnchantments;
+        this.consumesDurability = consumesDurability;
         this.breakable = breakable;
     }
 
     @Override
     public boolean isItemSuitable(ItemStack stack) {
-        var tag = stack.getTag();
-        if (tag == null || tag.isEmpty()) return true;
-
-        // Check we've not got anything vaguely interesting on the item. We allow other mods to add their
-        // own NBT, with the understanding such details will be lost to the mist of time.
-        if (stack.isDamaged() || stack.isEnchanted()) return false;
-        if (tag.contains("AttributeModifiers", TAG_LIST) && !tag.getList("AttributeModifiers", TAG_COMPOUND).isEmpty()) {
-            return false;
-        }
-
+        if (consumesDurability == TurtleToolDurability.NEVER && stack.isDamaged()) return false;
+        if (!allowsEnchantments && isEnchanted(stack)) return false;
         return true;
     }
 
+    private static boolean isEnchanted(ItemStack stack) {
+        return !stack.isEmpty() && isEnchanted(stack.getTag());
+    }
+
+    private static boolean isEnchanted(@Nullable CompoundTag tag) {
+        if (tag == null || tag.isEmpty()) return false;
+        return (tag.contains(ItemStack.TAG_ENCH, TAG_LIST) && !tag.getList(ItemStack.TAG_ENCH, TAG_COMPOUND).isEmpty())
+               || (tag.contains("AttributeModifiers", TAG_LIST) && !tag.getList("AttributeModifiers", TAG_COMPOUND).isEmpty());
+    }
+
     @Override
     public CompoundTag getUpgradeData(ItemStack stack) {
         // Just use the current item's tag.
@@ -83,11 +99,64 @@ public class TurtleTool extends AbstractTurtleUpgrade {
         return item;
     }
 
+    private ItemStack getToolStack(ITurtleAccess turtle, TurtleSide side) {
+        var item = getCraftingItem();
+        var tag = turtle.getUpgradeNBTData(side);
+        if (!tag.isEmpty()) item.setTag(tag);
+        return item.copy();
+    }
+
+    private void setToolStack(ITurtleAccess turtle, TurtleSide side, ItemStack stack) {
+        var tag = turtle.getUpgradeNBTData(side);
+
+        var useDurability = switch (consumesDurability) {
+            case NEVER -> false;
+            case WHEN_ENCHANTED -> isEnchanted(tag);
+            case ALWAYS -> true;
+        };
+        if (!useDurability) return;
+
+        // If the tool has broken, remove the upgrade!
+        if (stack.isEmpty()) {
+            turtle.setUpgradeWithData(side, null);
+            return;
+        }
+
+        // If the tool has changed, no clue what's going on.
+        if (stack.getItem() != item.getItem()) return;
+
+        var itemTag = stack.getTag();
+
+        // Early return if the item hasn't changed to avoid redundant syncs with the client.
+        if ((itemTag == null && tag.isEmpty()) || Objects.equals(itemTag, tag)) return;
+
+        if (itemTag == null) {
+            tag.getAllKeys().clear();
+        } else {
+            for (var key : itemTag.getAllKeys()) tag.put(key, Nullability.assertNonNull(itemTag.get(key)));
+            tag.getAllKeys().removeIf(x -> !itemTag.contains(x));
+        }
+
+        turtle.updateUpgradeNBTData(side);
+    }
+
+    private  T withEquippedItem(ITurtleAccess turtle, TurtleSide side, Direction direction, Function action) {
+        var turtlePlayer = TurtlePlayer.getWithPosition(turtle, turtle.getPosition(), direction);
+        turtlePlayer.loadInventory(getToolStack(turtle, side));
+
+        var result = action.apply(turtlePlayer);
+
+        setToolStack(turtle, side, turtlePlayer.player().getItemInHand(InteractionHand.MAIN_HAND));
+        turtlePlayer.player().getInventory().clearContent();
+
+        return result;
+    }
+
     @Override
     public TurtleCommandResult useTool(ITurtleAccess turtle, TurtleSide side, TurtleVerb verb, Direction direction) {
         return switch (verb) {
-            case ATTACK -> attack(turtle, direction);
-            case DIG -> dig(turtle, direction);
+            case ATTACK -> attack(turtle, side, direction);
+            case DIG -> dig(turtle, side, direction);
         };
     }
 
@@ -102,16 +171,14 @@ public class TurtleTool extends AbstractTurtleUpgrade {
     }
 
     /**
-     * Attack an entity. This is a very cut down version of {@link Player#attack(Entity)}, which doesn't handle
-     * enchantments, knockback, etc... Unfortunately we can't call attack directly as damage calculations are rather
-     * different (and we don't want to play sounds/particles).
+     * Attack an entity.
      *
      * @param turtle    The current turtle.
+     * @param side      The side the tool is on.
      * @param direction The direction we're attacking in.
      * @return Whether an attack occurred.
-     * @see Player#attack(Entity)
      */
-    private TurtleCommandResult attack(ITurtleAccess turtle, Direction direction) {
+    private TurtleCommandResult attack(ITurtleAccess turtle, TurtleSide side, Direction direction) {
         // Create a fake player, and orient it appropriately
         var world = turtle.getLevel();
         var position = turtle.getPosition();
@@ -123,10 +190,11 @@ public class TurtleTool extends AbstractTurtleUpgrade {
         var turtlePos = player.position();
         var rayDir = player.getViewVector(1.0f);
         var hit = WorldUtil.clip(world, turtlePos, rayDir, 1.5, null);
+        var attacked = false;
         if (hit instanceof EntityHitResult entityHit) {
             // Load up the turtle's inventory
-            var stackCopy = item.copy();
-            turtlePlayer.loadInventory(stackCopy);
+            var stack = getToolStack(turtle, side);
+            turtlePlayer.loadInventory(stack);
 
             var hitEntity = entityHit.getEntity();
 
@@ -134,62 +202,120 @@ public class TurtleTool extends AbstractTurtleUpgrade {
             DropConsumer.set(hitEntity, TurtleUtil.dropConsumer(turtle));
 
             // Attack the entity
-            var attacked = false;
             var result = PlatformHelper.get().canAttackEntity(player, hitEntity);
             if (result.consumesAction()) {
                 attacked = true;
             } else if (result == InteractionResult.PASS && hitEntity.isAttackable() && !hitEntity.skipAttackInteraction(player)) {
-                var damage = (float) player.getAttributeValue(Attributes.ATTACK_DAMAGE) * damageMulitiplier;
-                if (damage > 0.0f) {
-                    var source = player.damageSources().playerAttack(player);
-                    if (hitEntity instanceof ArmorStand) {
-                        // Special case for armor stands: attack twice to guarantee destroy
-                        hitEntity.hurt(source, damage);
-                        if (hitEntity.isAlive()) hitEntity.hurt(source, damage);
-                        attacked = true;
-                    } else {
-                        if (hitEntity.hurt(source, damage)) attacked = true;
-                    }
-                }
+                attacked = attack(player, direction, hitEntity);
             }
 
             // Stop claiming drops
             TurtleUtil.stopConsuming(turtle);
 
-            // Put everything we collected into the turtles inventory, then return
+            // Put everything we collected into the turtles inventory.
+            setToolStack(turtle, side, player.getItemInHand(InteractionHand.MAIN_HAND));
             player.getInventory().clearContent();
-            if (attacked) return TurtleCommandResult.success();
         }
 
-        return TurtleCommandResult.failure("Nothing to attack here");
+        return attacked ? TurtleCommandResult.success() : TurtleCommandResult.failure("Nothing to attack here");
     }
 
-    private TurtleCommandResult dig(ITurtleAccess turtle, Direction direction) {
-        if (PlatformHelper.get().hasToolUsage(item) && TurtlePlaceCommand.deployCopiedItem(item.copy(), turtle, direction, null, null)) {
-            return TurtleCommandResult.success();
+    /**
+     * Attack an entity. This is a copy of {@link Player#attack(Entity)}, with some unwanted features removed (sweeping
+     * edge). This is a little limited.
+     * 

+ * Ideally we'd use attack directly (if other mods mixin to that method, we won't support their features). + * Unfortunately,that doesn't give us any feedback to whether the attack occurred or not (and we don't want to play + * sounds/particles). + * + * @param player The fake player doing the attacking. + * @param direction The direction the turtle is attacking. + * @param entity The entity to attack. + * @return Whether we attacked or not. + * @see Player#attack(Entity) + */ + private boolean attack(ServerPlayer player, Direction direction, Entity entity) { + var baseDamage = (float) player.getAttributeValue(Attributes.ATTACK_DAMAGE) * damageMulitiplier; + var bonusDamage = EnchantmentHelper.getDamageBonus( + player.getItemInHand(InteractionHand.MAIN_HAND), entity instanceof LivingEntity target ? target.getMobType() : MobType.UNDEFINED + ); + var damage = baseDamage + bonusDamage; + if (damage <= 0) return false; + + var knockBack = EnchantmentHelper.getKnockbackBonus(player); + + // We follow the logic in Player.attack of setting the entity on fire before attacking, so it's burning when it + // (possibly) dies. + var fireAspect = EnchantmentHelper.getFireAspect(player); + var onFire = false; + if (entity instanceof LivingEntity target && fireAspect > 0 && !target.isOnFire()) { + onFire = true; + target.setSecondsOnFire(1); } - var level = (ServerLevel) turtle.getLevel(); - var turtlePosition = turtle.getPosition(); + var source = player.damageSources().playerAttack(player); + if (!entity.hurt(source, damage)) { + // If we failed to damage the entity, undo us setting the entity on fire. + if (onFire) entity.clearFire(); + return false; + } - var blockPosition = turtlePosition.relative(direction); + // Special case for armor stands: attack twice to guarantee destroy + if (entity.isAlive() && entity instanceof ArmorStand) entity.hurt(source, damage); + + // Apply knockback + if (knockBack > 0) { + if (entity instanceof LivingEntity target) { + target.knockback(knockBack * 0.5, -direction.getStepX(), -direction.getStepZ()); + } else { + entity.push(direction.getStepX() * knockBack * 0.5, 0.1, direction.getStepZ() * knockBack * 0.5); + } + } + + // Apply remaining enchantments + if (entity instanceof LivingEntity target) EnchantmentHelper.doPostHurtEffects(target, player); + EnchantmentHelper.doPostDamageEffects(player, entity); + + // Damage the original item stack. + if (entity instanceof LivingEntity target) { + player.getItemInHand(InteractionHand.MAIN_HAND).hurtEnemy(target, player); + } + + // Apply fire aspect + if (entity instanceof LivingEntity target && fireAspect > 0 && !target.isOnFire()) { + target.setSecondsOnFire(4 * fireAspect); + } + + return true; + } + + private TurtleCommandResult dig(ITurtleAccess turtle, TurtleSide side, Direction direction) { + var level = (ServerLevel) turtle.getLevel(); + + var blockPosition = turtle.getPosition().relative(direction); if (level.isEmptyBlock(blockPosition) || WorldUtil.isLiquidBlock(level, blockPosition)) { return TurtleCommandResult.failure("Nothing to dig here"); } - var turtlePlayer = TurtlePlayer.getWithPosition(turtle, turtlePosition, direction); - turtlePlayer.loadInventory(item.copy()); + return withEquippedItem(turtle, side, direction, turtlePlayer -> { + var stack = turtlePlayer.player().getItemInHand(InteractionHand.MAIN_HAND); - // Check if we can break the block - var breakable = checkBlockBreakable(level, blockPosition, turtlePlayer); - if (!breakable.isSuccess()) return breakable; + // Right-click the block when using a shovel/hoe. + if (PlatformHelper.get().hasToolUsage(item) && TurtlePlaceCommand.deploy(stack, turtle, turtlePlayer, direction, null, null)) { + return TurtleCommandResult.success(); + } - DropConsumer.set(level, blockPosition, TurtleUtil.dropConsumer(turtle)); - var broken = !turtlePlayer.isBlockProtected(level, blockPosition) && turtlePlayer.player().gameMode.destroyBlock(blockPosition); - TurtleUtil.stopConsuming(turtle); + // Check if we can break the block + var breakable = checkBlockBreakable(level, blockPosition, turtlePlayer); + if (!breakable.isSuccess()) return breakable; - // Check spawn protection - return broken ? TurtleCommandResult.success() : TurtleCommandResult.failure("Cannot break protected block"); + // And break it! + DropConsumer.set(level, blockPosition, TurtleUtil.dropConsumer(turtle)); + var broken = !turtlePlayer.isBlockProtected(level, blockPosition) && turtlePlayer.player().gameMode.destroyBlock(blockPosition); + TurtleUtil.stopConsuming(turtle); + + return broken ? TurtleCommandResult.success() : TurtleCommandResult.failure("Cannot break protected block"); + }); } private static boolean isTriviallyBreakable(BlockGetter reader, BlockPos pos, BlockState state) { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleToolSerialiser.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleToolSerialiser.java index 7a635c214..f46fdfcb2 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleToolSerialiser.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleToolSerialiser.java @@ -5,6 +5,7 @@ package dan200.computercraft.shared.turtle.upgrades; import com.google.gson.JsonObject; +import dan200.computercraft.api.turtle.TurtleToolDurability; import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser; import dan200.computercraft.api.upgrades.UpgradeBase; import dan200.computercraft.shared.platform.RegistryWrappers; @@ -28,6 +29,8 @@ public final class TurtleToolSerialiser implements TurtleUpgradeSerialiser breakable = null; if (object.has("breakable")) { @@ -35,7 +38,7 @@ public final class TurtleToolSerialiser implements TurtleUpgradeSerialiser Date: Tue, 4 Jul 2023 23:03:03 +0000 Subject: [PATCH 26/32] Translations for Russian (ru_ru) Translations for French Co-authored-by: chesiren Co-authored-by: ego-rick --- .../assets/computercraft/lang/fr_fr.json | 12 ++++++++++- .../assets/computercraft/lang/ru_ru.json | 20 ++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/projects/common/src/main/resources/assets/computercraft/lang/fr_fr.json b/projects/common/src/main/resources/assets/computercraft/lang/fr_fr.json index 621ff053a..2ee5b546e 100644 --- a/projects/common/src/main/resources/assets/computercraft/lang/fr_fr.json +++ b/projects/common/src/main/resources/assets/computercraft/lang/fr_fr.json @@ -217,5 +217,15 @@ "gui.computercraft.config.http.enabled.tooltip": "Active l'API \"http\" sur les ordinateurs. Cela désactive également les programmes \"pastebin\" et \"wget\",\nsur lesquels de nombreux utilisateurs comptent. Il est recommandé de laisser cette option activée et\nd'utiliser l'option de configuration \"rules\" pour imposer un contrôle plus précis.", "gui.computercraft.config.peripheral.modem_range.tooltip": "La portée des modems sans fil à basse altitude par temps dégagé, en mètres.\nPlage : 0 ~ 100000", "gui.computercraft.config.peripheral.monitor_bandwidth.tooltip": "La limite de la quantité de données du moniteur pouvant être envoyées *par tick*. Note :\n - La bande passante est mesurée avant la compression, donc les données envoyées\n au client sont plus petites.\n - Cela ignore le nombre de joueurs auxquels un paquet est envoyé. La mise à jour d'un\n moniteur pour un joueur consomme la même limite de bande passante que l'envoi à 20.\n - Un moniteur de taille normale envoie ~25ko de données. Ainsi, la valeur par défaut (1Mo) permet \n à environ 40 moniteurs d'être mis à jour en un seul tick.\nMettre à 0 pour désactiver.\nPlage : > 0", - "gui.computercraft.config.http.rules.tooltip": "Une liste de règles qui contrôlent le comportement de l'API \"http\" pour des domaines\nou des IP spécifiques. Chaque règle est un élément avec un 'hôte' à comparer et une série\nde propriétés. Les règles sont évaluées dans l'ordre, ce qui signifie que les règles antérieures\nremplacent les suivantes.\nL'hôte peut être un nom de domaine (\"pastebin.com\"), un astérisque (\"*.pastebin.com\") ou\nune notation CIDR (\"127.0.0.0/8\").\nS'il n'y a pas de règles, le domaine est bloqué." + "gui.computercraft.config.http.rules.tooltip": "Une liste de règles qui contrôlent le comportement de l'API \"http\" pour des domaines\nou des IP spécifiques. Chaque règle est un élément avec un 'hôte' à comparer et une série\nde propriétés. Les règles sont évaluées dans l'ordre, ce qui signifie que les règles antérieures\nremplacent les suivantes.\nL'hôte peut être un nom de domaine (\"pastebin.com\"), un astérisque (\"*.pastebin.com\") ou\nune notation CIDR (\"127.0.0.0/8\").\nS'il n'y a pas de règles, le domaine est bloqué.", + "gui.computercraft.config.http.proxy.host.tooltip": "Le nom d'hôte ou l'adresse IP du serveur proxy.", + "gui.computercraft.config.http.proxy.tooltip": "Tunnelise les requêtes HTTP et websocket via un serveur proxy. Affecte uniquement\nles règles HTTP avec \"use_proxy\" défini sur true (désactivé par défaut).\nSi l'authentification est requise pour le proxy, créez un fichier \"computercraft-proxy.pw\"\ndans le même dossier que \"computercraft-server.toml\", contenant le\nnom d'utilisateur et mot de passe séparés par deux-points, par ex. \"monutilisateur:monmotdepasse\". Pour\nProxy SOCKS4, seul le nom d'utilisateur est requis.", + "gui.computercraft.config.upload_max_size.tooltip": "La taille limite de téléversement de fichier, en octets. Doit être compris entre 1 Kio et 16 Mio.\nGardez à l'esprit que les téléversements sont traités en un seul clic - les fichiers volumineux ou\nde mauvaises performances réseau peuvent bloquer le thread du réseau. Et attention à l'espace disque !\nPlage : 1024 ~ 16777216", + "gui.computercraft.config.http.proxy": "Proxy", + "gui.computercraft.config.http.proxy.host": "Nom d'hôte", + "gui.computercraft.config.http.proxy.port": "Port", + "gui.computercraft.config.http.proxy.port.tooltip": "Le port du serveur proxy.\nPlage : 1 ~ 65536", + "gui.computercraft.config.http.proxy.type": "Type de proxy", + "gui.computercraft.config.http.proxy.type.tooltip": "Le type de proxy à utiliser.\nValeurs autorisées : HTTP, HTTPS, SOCKS4, SOCKS5", + "gui.computercraft.config.upload_max_size": "Taille limite de téléversement de fichiers (octets)" } diff --git a/projects/common/src/main/resources/assets/computercraft/lang/ru_ru.json b/projects/common/src/main/resources/assets/computercraft/lang/ru_ru.json index 8495d2376..66a7390ca 100644 --- a/projects/common/src/main/resources/assets/computercraft/lang/ru_ru.json +++ b/projects/common/src/main/resources/assets/computercraft/lang/ru_ru.json @@ -177,5 +177,23 @@ "gui.computercraft.config.turtle.need_fuel": "Включить механику топлива", "gui.computercraft.config.turtle.normal_fuel_limit": "Лимит топлива Черепашек", "gui.computercraft.config.turtle.normal_fuel_limit.tooltip": "Лимит топлива для Черепашек.\nОграничение: > 0", - "gui.computercraft.config.turtle.tooltip": "Разные настройки, связанные с черепашками." + "gui.computercraft.config.turtle.tooltip": "Разные настройки, связанные с черепашками.", + "gui.computercraft.config.http.proxy.port": "Порт", + "gui.computercraft.config.http.proxy.port.tooltip": "Порт прокси-сервера.\nДиапазон: 1 ~ 65536", + "gui.computercraft.config.http.proxy.host": "Имя хоста", + "gui.computercraft.config.http.proxy": "Proxy", + "gui.computercraft.config.http.proxy.host.tooltip": "Имя хоста или IP-адрес прокси-сервера.", + "gui.computercraft.config.http.proxy.tooltip": "Туннелирует HTTP-запросы и запросы websocket через прокси-сервер. Влияет только на HTTP\nправила с параметром \"use_proxy\" в значении true (отключено по умолчанию).\nЕсли для прокси-сервера требуется аутентификация, создайте \"computercraft-proxy.pw\"\nфайл в том же каталоге, что и \"computercraft-server.toml\", содержащий имя\nпользователя и пароль, разделенные двоеточием, например \"myuser:mypassword\". Для\nпрокси-серверов SOCKS4 требуется только имя пользователя.", + "gui.computercraft.config.http.proxy.type": "Тип прокси-сервера", + "gui.computercraft.config.http.proxy.type.tooltip": "Тип используемого прокси-сервера.\nДопустимые значения: HTTP, HTTPS, SOCKS4, SOCKS5", + "gui.computercraft.upload.no_response.msg": "Ваш компьютер не использовал переданные вами файлы. Возможно, вам потребуется запустить программу %s и повторить попытку.", + "tracking_field.computercraft.max": "%s (максимальное)", + "tracking_field.computercraft.count": "%s (количество)", + "gui.computercraft.config.http.rules": "Разрешающие/запрещающие правила", + "gui.computercraft.config.http.websocket_enabled": "Включить веб-сокеты", + "gui.computercraft.config.http.websocket_enabled.tooltip": "Включить использование http веб-сокетов. Для этого необходимо, чтобы параметр «http_enable» был true.", + "gui.computercraft.config.log_computer_errors": "Регистрировать ошибки компьютера", + "gui.computercraft.config.log_computer_errors.tooltip": "Регистрировать исключения, вызванные периферийными устройствами и другими объектами Lua. Это облегчает\nдля авторам модов устранение проблем, но может привести к спаму в логах, если люди будут использовать\nглючные методы.", + "gui.computercraft.config.maximum_open_files": "Максимальное количество файлов, открытых на одном компьютере", + "gui.computercraft.config.http.rules.tooltip": "Список правил, которые контролируют поведение «http» API для определенных доменов или\nIP-адресов. Каждое правило представляет собой элемент с «узлом» для сопоставления и набором\nсвойств. Правила оцениваются по порядку, то есть более ранние правила перевешивают\nболее поздние.\nХост может быть доменным именем (\"pastebin.com\"), wildcard-сертификатом (\"*.pastebin.com\") или\nнотацией CIDR (\"127.0.0.0/8\").\nЕсли правил нет, домен блокируется." } From e337a637122b1619d0985a2a578104e77fbcbb2d Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 5 Jul 2023 20:57:56 +0100 Subject: [PATCH 27/32] Bump FG and Loom Also split up CI steps a little, so that it's more clear what failed. --- .github/workflows/main-ci.yml | 39 ++++++++++--------- .../gradle/common/util/runs/RunConfigSetup.kt | 31 +++------------ gradle/libs.versions.toml | 4 +- 3 files changed, 29 insertions(+), 45 deletions(-) diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index 8baff3d9c..d94091eb9 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -8,16 +8,16 @@ jobs: runs-on: ubuntu-latest steps: - - name: Clone repository + - name: 📥 Clone repository uses: actions/checkout@v3 - - name: Set up Java + - name: 📥 Set up Java uses: actions/setup-java@v3 with: java-version: 17 distribution: 'temurin' - - name: Setup Gradle + - name: 📥 Setup Gradle uses: gradle/gradle-build-action@v2 with: cache-read-only: ${{ !startsWith(github.ref, 'refs/heads/mc-') }} @@ -27,42 +27,45 @@ jobs: mkdir -p ~/.gradle echo "org.gradle.daemon=false" >> ~/.gradle/gradle.properties - - name: Build with Gradle + - name: ⚒️ Build run: ./gradlew assemble || ./gradlew assemble - - name: Download assets for game tests + - name: 💡 Lint + uses: pre-commit/action@v3.0.0 + + - name: 🧪 Run tests + run: ./gradlew test validateMixinNames checkChangelog + + - name: 📥 Download assets for game tests run: ./gradlew downloadAssets || ./gradlew downloadAssets - - name: Run tests and linters - run: ./gradlew build + - name: 🧪 Run integration tests + run: ./gradlew runGametest - - name: Run client tests + - name: 🧪 Run client tests run: ./gradlew runGametestClient # Not checkClient, as no point running rendering tests. # These are a little flaky on GH actions: its useful to run them, but don't break the build. continue-on-error: true - - name: Prepare Jars + - name: 🧪 Parse test reports + run: ./tools/parse-reports.py + if: ${{ failure() }} + + - name: 📦 Prepare Jars run: | # Find the main jar and append the git hash onto it. mkdir -p jars find projects/forge/build/libs projects/fabric/build/libs -type f -regex '.*[0-9.]+\(-SNAPSHOT\)?\.jar$' -exec bash -c 'cp {} "jars/$(basename {} .jar)-$(git rev-parse HEAD).jar"' \; - - name: Upload Jar + - name: 📤 Upload Jar uses: actions/upload-artifact@v3 with: name: CC-Tweaked path: ./jars - - name: Upload coverage + - name: 📤 Upload coverage uses: codecov/codecov-action@v3 - - name: Parse test reports - run: ./tools/parse-reports.py - if: ${{ failure() }} - - - name: Run linters - uses: pre-commit/action@v3.0.0 - build-core: strategy: fail-fast: false diff --git a/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt b/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt index 50e33a84b..a74d0008d 100644 --- a/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt +++ b/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt @@ -30,41 +30,22 @@ internal fun setRunConfigInternal(project: Project, spec: JavaExecSpec, config: val originalTask = project.tasks.named(config.taskName, MinecraftRunTask::class.java) // Add argument and JVM argument via providers, to be as lazy as possible with fetching artifacts. - fun lazyTokens(): MutableMap> { - return RunConfigGenerator.configureTokensLazy( - project, config, RunConfigGenerator.mapModClassesToGradle(project, config), - originalTask.get().minecraftArtifacts.files, - originalTask.get().runtimeClasspathArtifacts.files, - ) - } + val lazyTokens = RunConfigGenerator.configureTokensLazy( + project, config, RunConfigGenerator.mapModClassesToGradle(project, config), + originalTask.get().minecraftArtifacts, + originalTask.get().runtimeClasspathArtifacts, + ) spec.argumentProviders.add( CommandLineArgumentProvider { - RunConfigGenerator.getArgsStream(config, lazyTokens(), false).toList() + RunConfigGenerator.getArgsStream(config, lazyTokens, false).toList() }, ) spec.jvmArgumentProviders.add( CommandLineArgumentProvider { - val lazyTokens = lazyTokens() (if (config.isClient) config.jvmArgs + originalTask.get().additionalClientArgs.get() else config.jvmArgs).map { config.replace(lazyTokens, it) } + config.properties.map { (k, v) -> "-D${k}=${config.replace(lazyTokens, v)}" } }, ) - // We can't configure environment variables lazily, so we do these now with a more minimal lazyTokens set. - val lazyTokens = mutableMapOf>() - for ((k, v) in config.tokens) lazyTokens[k] = Supplier { v } - for ((k, v) in config.lazyTokens) lazyTokens[k] = v - lazyTokens.compute( - "source_roots", - { _, sourceRoots -> - Supplier { - val modClasses = RunConfigGenerator.mapModClassesToGradle(project, config) - (when (sourceRoots) { - null -> modClasses - else -> Stream.concat(sourceRoots.get().split(File.pathSeparator).stream(), modClasses) - }).distinct().collect(Collectors.joining(File.pathSeparator)) - } - }, - ) for ((key, value) in config.environment) spec.environment(key, config.replace(lazyTokens, value)) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9152cadaa..d3e1f4fc4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,8 +53,8 @@ checkstyle = "10.3.4" curseForgeGradle = "1.0.14" errorProne-core = "2.18.0" errorProne-plugin = "3.0.1" -fabric-loom = "1.2.7" -forgeGradle = "6.0.6" +fabric-loom = "1.3.7" +forgeGradle = "6.0.8" githubRelease = "2.2.12" ideaExt = "1.1.6" illuaminate = "0.1.0-28-ga7efd71" From cab9c9772ab74dc74b6df9edadec5f7aaba21b88 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Thu, 6 Jul 2023 22:26:39 +0100 Subject: [PATCH 28/32] Add some tests for new turtle tool functionality - Normalise upgrade keys, to be "allowEnchantments" and "consumeDurability". We were previously inconsistent with allow/allows and consumes. - Add tests for durability and enchantments of pickaxes. - Fix a couple of issues with the original upgrade NBT being modified. - Now store the item's tag under a separate key rather than on the root. This makes syncing the NBT between the two much nicer. --- .reuse/dep5 | 1 + .../api/turtle/TurtleToolDurability.java | 2 +- .../api/turtle/TurtleUpgradeDataProvider.java | 10 +- .../shared/turtle/upgrades/TurtleTool.java | 52 +++---- .../turtle/upgrades/TurtleToolSerialiser.java | 10 +- .../computercraft/gametest/Turtle_Test.kt | 84 +++++++++++ .../turtle_upgrades/netherite_pickaxe.json | 6 + .../turtle_upgrades/wooden_pickaxe.json | 5 + .../turtle_test.dig_breaks_tool.snbt | 138 ++++++++++++++++++ .../turtle_test.dig_consume_durability.snbt | 138 ++++++++++++++++++ ...test.dig_enchanted_consume_durability.snbt | 138 ++++++++++++++++++ 11 files changed, 548 insertions(+), 36 deletions(-) create mode 100644 projects/common/src/testMod/resources/data/cctest/computercraft/turtle_upgrades/netherite_pickaxe.json create mode 100644 projects/common/src/testMod/resources/data/cctest/computercraft/turtle_upgrades/wooden_pickaxe.json create mode 100644 projects/common/src/testMod/resources/data/cctest/structures/turtle_test.dig_breaks_tool.snbt create mode 100644 projects/common/src/testMod/resources/data/cctest/structures/turtle_test.dig_consume_durability.snbt create mode 100644 projects/common/src/testMod/resources/data/cctest/structures/turtle_test.dig_enchanted_consume_durability.snbt diff --git a/.reuse/dep5 b/.reuse/dep5 index de4dbc14b..11297f06f 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -6,6 +6,7 @@ Upstream-Contact: Jonathan Coates Files: projects/common/src/main/resources/assets/computercraft/sounds.json projects/common/src/main/resources/assets/computercraft/sounds/empty.ogg + projects/common/src/testMod/resources/data/cctest/computercraft/turtle_upgrades/* projects/common/src/testMod/resources/data/cctest/structures/* projects/fabric/src/generated/* projects/forge/src/generated/* diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleToolDurability.java b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleToolDurability.java index c149a185d..5487dbdea 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleToolDurability.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleToolDurability.java @@ -11,7 +11,7 @@ import net.minecraft.world.item.ItemStack; /** * Indicates if an equipped turtle item will consume durability. * - * @see TurtleUpgradeDataProvider.ToolBuilder#consumesDurability(TurtleToolDurability) + * @see TurtleUpgradeDataProvider.ToolBuilder#consumeDurability(TurtleToolDurability) */ public enum TurtleToolDurability implements StringRepresentable { /** diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeDataProvider.java b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeDataProvider.java index a2048e969..1a8118544 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeDataProvider.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeDataProvider.java @@ -64,7 +64,7 @@ public abstract class TurtleUpgradeDataProvider extends UpgradeDataProvider breakable; private boolean allowEnchantments = false; - private TurtleToolDurability consumesDurability = TurtleToolDurability.NEVER; + private TurtleToolDurability consumeDurability = TurtleToolDurability.NEVER; ToolBuilder(ResourceLocation id, TurtleUpgradeSerialiser serialiser, Item toolItem) { this.id = id; @@ -125,8 +125,8 @@ public abstract class TurtleUpgradeDataProvider extends UpgradeDataProvider breakable; public TurtleTool( ResourceLocation id, String adjective, Item craftItem, ItemStack toolItem, float damageMulitiplier, - boolean allowsEnchantments, TurtleToolDurability consumesDurability, @Nullable TagKey breakable + boolean allowEnchantments, TurtleToolDurability consumeDurability, @Nullable TagKey breakable ) { super(id, TurtleUpgradeType.TOOL, adjective, new ItemStack(craftItem)); item = toolItem; this.damageMulitiplier = damageMulitiplier; - this.allowsEnchantments = allowsEnchantments; - this.consumesDurability = consumesDurability; + this.allowEnchantments = allowEnchantments; + this.consumeDurability = consumeDurability; this.breakable = breakable; } @Override public boolean isItemSuitable(ItemStack stack) { - if (consumesDurability == TurtleToolDurability.NEVER && stack.isDamaged()) return false; - if (!allowsEnchantments && isEnchanted(stack)) return false; + if (consumeDurability == TurtleToolDurability.NEVER && stack.isDamaged()) return false; + if (!allowEnchantments && isEnchanted(stack)) return false; return true; } @@ -86,32 +87,34 @@ public class TurtleTool extends AbstractTurtleUpgrade { @Override public CompoundTag getUpgradeData(ItemStack stack) { - // Just use the current item's tag. + var upgradeData = super.getUpgradeData(stack); + + // Store the item's current tag. var itemTag = stack.getTag(); - return itemTag == null ? new CompoundTag() : itemTag; + if (itemTag != null) upgradeData.put(TAG_ITEM_TAG, itemTag); + + return upgradeData; } @Override public ItemStack getUpgradeItem(CompoundTag upgradeData) { // Copy upgrade data back to the item. - var item = super.getUpgradeItem(upgradeData); - if (!upgradeData.isEmpty()) item.setTag(upgradeData); + var item = super.getUpgradeItem(upgradeData).copy(); + item.setTag(upgradeData.contains(TAG_ITEM_TAG, TAG_COMPOUND) ? upgradeData.getCompound(TAG_ITEM_TAG).copy() : null); return item; } private ItemStack getToolStack(ITurtleAccess turtle, TurtleSide side) { - var item = getCraftingItem(); - var tag = turtle.getUpgradeNBTData(side); - if (!tag.isEmpty()) item.setTag(tag); - return item.copy(); + return getUpgradeItem(turtle.getUpgradeNBTData(side)); } private void setToolStack(ITurtleAccess turtle, TurtleSide side, ItemStack stack) { - var tag = turtle.getUpgradeNBTData(side); + var upgradeData = turtle.getUpgradeNBTData(side); - var useDurability = switch (consumesDurability) { + var useDurability = switch (consumeDurability) { case NEVER -> false; - case WHEN_ENCHANTED -> isEnchanted(tag); + case WHEN_ENCHANTED -> + upgradeData.contains(TAG_ITEM_TAG, TAG_COMPOUND) && isEnchanted(upgradeData.getCompound(TAG_ITEM_TAG)); case ALWAYS -> true; }; if (!useDurability) return; @@ -128,13 +131,12 @@ public class TurtleTool extends AbstractTurtleUpgrade { var itemTag = stack.getTag(); // Early return if the item hasn't changed to avoid redundant syncs with the client. - if ((itemTag == null && tag.isEmpty()) || Objects.equals(itemTag, tag)) return; + if (Objects.equals(itemTag, upgradeData.get(TAG_ITEM_TAG))) return; if (itemTag == null) { - tag.getAllKeys().clear(); + upgradeData.remove(TAG_ITEM_TAG); } else { - for (var key : itemTag.getAllKeys()) tag.put(key, Nullability.assertNonNull(itemTag.get(key))); - tag.getAllKeys().removeIf(x -> !itemTag.contains(x)); + upgradeData.put(TAG_ITEM_TAG, itemTag); } turtle.updateUpgradeNBTData(side); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleToolSerialiser.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleToolSerialiser.java index f46fdfcb2..dc3443879 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleToolSerialiser.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleToolSerialiser.java @@ -29,8 +29,8 @@ public final class TurtleToolSerialiser implements TurtleUpgradeSerialiser breakable = null; if (object.has("breakable")) { @@ -38,7 +38,7 @@ public final class TurtleToolSerialiser implements TurtleUpgradeSerialiser) { + if (!ItemStack.matches(expected, upgrade.upgradeItem)) { + fail("Invalid upgrade item\n Expected => ${expected.tag}\n Actual => ${upgrade.upgradeItem.tag}") + } + + if (!ItemStack.matches(ItemStack(expected.item), upgrade.upgrade.craftingItem)) { + fail("Original upgrade item has changed (is now ${upgrade.upgrade.craftingItem})") + } + } + /** * Checks turtles can place monitors * diff --git a/projects/common/src/testMod/resources/data/cctest/computercraft/turtle_upgrades/netherite_pickaxe.json b/projects/common/src/testMod/resources/data/cctest/computercraft/turtle_upgrades/netherite_pickaxe.json new file mode 100644 index 000000000..42aa7fa85 --- /dev/null +++ b/projects/common/src/testMod/resources/data/cctest/computercraft/turtle_upgrades/netherite_pickaxe.json @@ -0,0 +1,6 @@ +{ + "type": "computercraft:tool", + "item": "minecraft:netherite_pickaxe", + "allowEnchantments": true, + "consumeDurability": "when_enchanted" +} diff --git a/projects/common/src/testMod/resources/data/cctest/computercraft/turtle_upgrades/wooden_pickaxe.json b/projects/common/src/testMod/resources/data/cctest/computercraft/turtle_upgrades/wooden_pickaxe.json new file mode 100644 index 000000000..0516dd1bb --- /dev/null +++ b/projects/common/src/testMod/resources/data/cctest/computercraft/turtle_upgrades/wooden_pickaxe.json @@ -0,0 +1,5 @@ +{ + "type": "computercraft:tool", + "item": "minecraft:wooden_pickaxe", + "consumeDurability": "always" +} diff --git a/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.dig_breaks_tool.snbt b/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.dig_breaks_tool.snbt new file mode 100644 index 000000000..7a1f5a7a8 --- /dev/null +++ b/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.dig_breaks_tool.snbt @@ -0,0 +1,138 @@ +{ + DataVersion: 3218, + size: [5, 5, 5], + data: [ + {pos: [0, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [0, 1, 0], state: "minecraft:air"}, + {pos: [0, 1, 1], state: "minecraft:air"}, + {pos: [0, 1, 2], state: "minecraft:air"}, + {pos: [0, 1, 3], state: "minecraft:air"}, + {pos: [0, 1, 4], state: "minecraft:air"}, + {pos: [1, 1, 0], state: "minecraft:air"}, + {pos: [1, 1, 1], state: "minecraft:air"}, + {pos: [1, 1, 2], state: "minecraft:air"}, + {pos: [1, 1, 3], state: "minecraft:air"}, + {pos: [1, 1, 4], state: "minecraft:air"}, + {pos: [2, 1, 0], state: "minecraft:air"}, + {pos: [2, 1, 1], state: "minecraft:air"}, + {pos: [2, 1, 2], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 80, Items: [], Label: "turtle_test.dig_breaks_tool", LeftUpgrade: "cctest:wooden_pickaxe", LeftUpgradeNbt: {Tag: {Damage: 58}}, On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}}, + {pos: [2, 1, 3], state: "minecraft:stone"}, + {pos: [2, 1, 4], state: "minecraft:air"}, + {pos: [3, 1, 0], state: "minecraft:air"}, + {pos: [3, 1, 1], state: "minecraft:air"}, + {pos: [3, 1, 2], state: "minecraft:air"}, + {pos: [3, 1, 3], state: "minecraft:air"}, + {pos: [3, 1, 4], state: "minecraft:air"}, + {pos: [4, 1, 0], state: "minecraft:air"}, + {pos: [4, 1, 1], state: "minecraft:air"}, + {pos: [4, 1, 2], state: "minecraft:air"}, + {pos: [4, 1, 3], state: "minecraft:air"}, + {pos: [4, 1, 4], state: "minecraft:air"}, + {pos: [0, 2, 0], state: "minecraft:air"}, + {pos: [0, 2, 1], state: "minecraft:air"}, + {pos: [0, 2, 2], state: "minecraft:air"}, + {pos: [0, 2, 3], state: "minecraft:air"}, + {pos: [0, 2, 4], state: "minecraft:air"}, + {pos: [1, 2, 0], state: "minecraft:air"}, + {pos: [1, 2, 1], state: "minecraft:air"}, + {pos: [1, 2, 2], state: "minecraft:air"}, + {pos: [1, 2, 3], state: "minecraft:air"}, + {pos: [1, 2, 4], state: "minecraft:air"}, + {pos: [2, 2, 0], state: "minecraft:air"}, + {pos: [2, 2, 1], state: "minecraft:air"}, + {pos: [2, 2, 2], state: "minecraft:air"}, + {pos: [2, 2, 3], state: "minecraft:air"}, + {pos: [2, 2, 4], state: "minecraft:air"}, + {pos: [3, 2, 0], state: "minecraft:air"}, + {pos: [3, 2, 1], state: "minecraft:air"}, + {pos: [3, 2, 2], state: "minecraft:air"}, + {pos: [3, 2, 3], state: "minecraft:air"}, + {pos: [3, 2, 4], state: "minecraft:air"}, + {pos: [4, 2, 0], state: "minecraft:air"}, + {pos: [4, 2, 1], state: "minecraft:air"}, + {pos: [4, 2, 2], state: "minecraft:air"}, + {pos: [4, 2, 3], state: "minecraft:air"}, + {pos: [4, 2, 4], state: "minecraft:air"}, + {pos: [0, 3, 0], state: "minecraft:air"}, + {pos: [0, 3, 1], state: "minecraft:air"}, + {pos: [0, 3, 2], state: "minecraft:air"}, + {pos: [0, 3, 3], state: "minecraft:air"}, + {pos: [0, 3, 4], state: "minecraft:air"}, + {pos: [1, 3, 0], state: "minecraft:air"}, + {pos: [1, 3, 1], state: "minecraft:air"}, + {pos: [1, 3, 2], state: "minecraft:air"}, + {pos: [1, 3, 3], state: "minecraft:air"}, + {pos: [1, 3, 4], state: "minecraft:air"}, + {pos: [2, 3, 0], state: "minecraft:air"}, + {pos: [2, 3, 1], state: "minecraft:air"}, + {pos: [2, 3, 2], state: "minecraft:air"}, + {pos: [2, 3, 3], state: "minecraft:air"}, + {pos: [2, 3, 4], state: "minecraft:air"}, + {pos: [3, 3, 0], state: "minecraft:air"}, + {pos: [3, 3, 1], state: "minecraft:air"}, + {pos: [3, 3, 2], state: "minecraft:air"}, + {pos: [3, 3, 3], state: "minecraft:air"}, + {pos: [3, 3, 4], state: "minecraft:air"}, + {pos: [4, 3, 0], state: "minecraft:air"}, + {pos: [4, 3, 1], state: "minecraft:air"}, + {pos: [4, 3, 2], state: "minecraft:air"}, + {pos: [4, 3, 3], state: "minecraft:air"}, + {pos: [4, 3, 4], state: "minecraft:air"}, + {pos: [0, 4, 0], state: "minecraft:air"}, + {pos: [0, 4, 1], state: "minecraft:air"}, + {pos: [0, 4, 2], state: "minecraft:air"}, + {pos: [0, 4, 3], state: "minecraft:air"}, + {pos: [0, 4, 4], state: "minecraft:air"}, + {pos: [1, 4, 0], state: "minecraft:air"}, + {pos: [1, 4, 1], state: "minecraft:air"}, + {pos: [1, 4, 2], state: "minecraft:air"}, + {pos: [1, 4, 3], state: "minecraft:air"}, + {pos: [1, 4, 4], state: "minecraft:air"}, + {pos: [2, 4, 0], state: "minecraft:air"}, + {pos: [2, 4, 1], state: "minecraft:air"}, + {pos: [2, 4, 2], state: "minecraft:air"}, + {pos: [2, 4, 3], state: "minecraft:air"}, + {pos: [2, 4, 4], state: "minecraft:air"}, + {pos: [3, 4, 0], state: "minecraft:air"}, + {pos: [3, 4, 1], state: "minecraft:air"}, + {pos: [3, 4, 2], state: "minecraft:air"}, + {pos: [3, 4, 3], state: "minecraft:air"}, + {pos: [3, 4, 4], state: "minecraft:air"}, + {pos: [4, 4, 0], state: "minecraft:air"}, + {pos: [4, 4, 1], state: "minecraft:air"}, + {pos: [4, 4, 2], state: "minecraft:air"}, + {pos: [4, 4, 3], state: "minecraft:air"}, + {pos: [4, 4, 4], state: "minecraft:air"} + ], + entities: [], + palette: [ + "minecraft:polished_andesite", + "minecraft:stone", + "minecraft:air", + "computercraft:turtle_normal{facing:south,waterlogged:false}" + ] +} diff --git a/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.dig_consume_durability.snbt b/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.dig_consume_durability.snbt new file mode 100644 index 000000000..a75b00e45 --- /dev/null +++ b/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.dig_consume_durability.snbt @@ -0,0 +1,138 @@ +{ + DataVersion: 3218, + size: [5, 5, 5], + data: [ + {pos: [0, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [0, 1, 0], state: "minecraft:air"}, + {pos: [0, 1, 1], state: "minecraft:air"}, + {pos: [0, 1, 2], state: "minecraft:air"}, + {pos: [0, 1, 3], state: "minecraft:air"}, + {pos: [0, 1, 4], state: "minecraft:air"}, + {pos: [1, 1, 0], state: "minecraft:air"}, + {pos: [1, 1, 1], state: "minecraft:air"}, + {pos: [1, 1, 2], state: "minecraft:air"}, + {pos: [1, 1, 3], state: "minecraft:air"}, + {pos: [1, 1, 4], state: "minecraft:air"}, + {pos: [2, 1, 0], state: "minecraft:air"}, + {pos: [2, 1, 1], state: "minecraft:air"}, + {pos: [2, 1, 2], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 80, Items: [], Label: "turtle_test.dig_consume_durability", LeftUpgrade: "cctest:wooden_pickaxe", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}}, + {pos: [2, 1, 3], state: "minecraft:stone"}, + {pos: [2, 1, 4], state: "minecraft:air"}, + {pos: [3, 1, 0], state: "minecraft:air"}, + {pos: [3, 1, 1], state: "minecraft:air"}, + {pos: [3, 1, 2], state: "minecraft:air"}, + {pos: [3, 1, 3], state: "minecraft:air"}, + {pos: [3, 1, 4], state: "minecraft:air"}, + {pos: [4, 1, 0], state: "minecraft:air"}, + {pos: [4, 1, 1], state: "minecraft:air"}, + {pos: [4, 1, 2], state: "minecraft:air"}, + {pos: [4, 1, 3], state: "minecraft:air"}, + {pos: [4, 1, 4], state: "minecraft:air"}, + {pos: [0, 2, 0], state: "minecraft:air"}, + {pos: [0, 2, 1], state: "minecraft:air"}, + {pos: [0, 2, 2], state: "minecraft:air"}, + {pos: [0, 2, 3], state: "minecraft:air"}, + {pos: [0, 2, 4], state: "minecraft:air"}, + {pos: [1, 2, 0], state: "minecraft:air"}, + {pos: [1, 2, 1], state: "minecraft:air"}, + {pos: [1, 2, 2], state: "minecraft:air"}, + {pos: [1, 2, 3], state: "minecraft:air"}, + {pos: [1, 2, 4], state: "minecraft:air"}, + {pos: [2, 2, 0], state: "minecraft:air"}, + {pos: [2, 2, 1], state: "minecraft:air"}, + {pos: [2, 2, 2], state: "minecraft:air"}, + {pos: [2, 2, 3], state: "minecraft:air"}, + {pos: [2, 2, 4], state: "minecraft:air"}, + {pos: [3, 2, 0], state: "minecraft:air"}, + {pos: [3, 2, 1], state: "minecraft:air"}, + {pos: [3, 2, 2], state: "minecraft:air"}, + {pos: [3, 2, 3], state: "minecraft:air"}, + {pos: [3, 2, 4], state: "minecraft:air"}, + {pos: [4, 2, 0], state: "minecraft:air"}, + {pos: [4, 2, 1], state: "minecraft:air"}, + {pos: [4, 2, 2], state: "minecraft:air"}, + {pos: [4, 2, 3], state: "minecraft:air"}, + {pos: [4, 2, 4], state: "minecraft:air"}, + {pos: [0, 3, 0], state: "minecraft:air"}, + {pos: [0, 3, 1], state: "minecraft:air"}, + {pos: [0, 3, 2], state: "minecraft:air"}, + {pos: [0, 3, 3], state: "minecraft:air"}, + {pos: [0, 3, 4], state: "minecraft:air"}, + {pos: [1, 3, 0], state: "minecraft:air"}, + {pos: [1, 3, 1], state: "minecraft:air"}, + {pos: [1, 3, 2], state: "minecraft:air"}, + {pos: [1, 3, 3], state: "minecraft:air"}, + {pos: [1, 3, 4], state: "minecraft:air"}, + {pos: [2, 3, 0], state: "minecraft:air"}, + {pos: [2, 3, 1], state: "minecraft:air"}, + {pos: [2, 3, 2], state: "minecraft:air"}, + {pos: [2, 3, 3], state: "minecraft:air"}, + {pos: [2, 3, 4], state: "minecraft:air"}, + {pos: [3, 3, 0], state: "minecraft:air"}, + {pos: [3, 3, 1], state: "minecraft:air"}, + {pos: [3, 3, 2], state: "minecraft:air"}, + {pos: [3, 3, 3], state: "minecraft:air"}, + {pos: [3, 3, 4], state: "minecraft:air"}, + {pos: [4, 3, 0], state: "minecraft:air"}, + {pos: [4, 3, 1], state: "minecraft:air"}, + {pos: [4, 3, 2], state: "minecraft:air"}, + {pos: [4, 3, 3], state: "minecraft:air"}, + {pos: [4, 3, 4], state: "minecraft:air"}, + {pos: [0, 4, 0], state: "minecraft:air"}, + {pos: [0, 4, 1], state: "minecraft:air"}, + {pos: [0, 4, 2], state: "minecraft:air"}, + {pos: [0, 4, 3], state: "minecraft:air"}, + {pos: [0, 4, 4], state: "minecraft:air"}, + {pos: [1, 4, 0], state: "minecraft:air"}, + {pos: [1, 4, 1], state: "minecraft:air"}, + {pos: [1, 4, 2], state: "minecraft:air"}, + {pos: [1, 4, 3], state: "minecraft:air"}, + {pos: [1, 4, 4], state: "minecraft:air"}, + {pos: [2, 4, 0], state: "minecraft:air"}, + {pos: [2, 4, 1], state: "minecraft:air"}, + {pos: [2, 4, 2], state: "minecraft:air"}, + {pos: [2, 4, 3], state: "minecraft:air"}, + {pos: [2, 4, 4], state: "minecraft:air"}, + {pos: [3, 4, 0], state: "minecraft:air"}, + {pos: [3, 4, 1], state: "minecraft:air"}, + {pos: [3, 4, 2], state: "minecraft:air"}, + {pos: [3, 4, 3], state: "minecraft:air"}, + {pos: [3, 4, 4], state: "minecraft:air"}, + {pos: [4, 4, 0], state: "minecraft:air"}, + {pos: [4, 4, 1], state: "minecraft:air"}, + {pos: [4, 4, 2], state: "minecraft:air"}, + {pos: [4, 4, 3], state: "minecraft:air"}, + {pos: [4, 4, 4], state: "minecraft:air"} + ], + entities: [], + palette: [ + "minecraft:polished_andesite", + "minecraft:stone", + "minecraft:air", + "computercraft:turtle_normal{facing:south,waterlogged:false}" + ] +} diff --git a/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.dig_enchanted_consume_durability.snbt b/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.dig_enchanted_consume_durability.snbt new file mode 100644 index 000000000..e461da3ea --- /dev/null +++ b/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.dig_enchanted_consume_durability.snbt @@ -0,0 +1,138 @@ +{ + DataVersion: 3337, + size: [5, 5, 5], + data: [ + {pos: [0, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [0, 1, 0], state: "minecraft:air"}, + {pos: [0, 1, 1], state: "minecraft:air"}, + {pos: [0, 1, 2], state: "minecraft:air"}, + {pos: [0, 1, 3], state: "minecraft:air"}, + {pos: [0, 1, 4], state: "minecraft:air"}, + {pos: [1, 1, 0], state: "minecraft:air"}, + {pos: [1, 1, 1], state: "minecraft:air"}, + {pos: [1, 1, 2], state: "minecraft:air"}, + {pos: [1, 1, 3], state: "minecraft:air"}, + {pos: [1, 1, 4], state: "minecraft:air"}, + {pos: [2, 1, 0], state: "minecraft:air"}, + {pos: [2, 1, 1], state: "minecraft:air"}, + {pos: [2, 1, 2], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 80, Items: [], Label: "turtle_test.dig_enchanted_consume_durability", LeftUpgrade: "cctest:netherite_pickaxe", LeftUpgradeNbt: {Tag: {Damage: 0, Enchantments: [{id: "minecraft:silk_touch", lvl: 1s}], RepairCost: 1}}, On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}}, + {pos: [2, 1, 3], state: "minecraft:stone"}, + {pos: [2, 1, 4], state: "minecraft:air"}, + {pos: [3, 1, 0], state: "minecraft:air"}, + {pos: [3, 1, 1], state: "minecraft:air"}, + {pos: [3, 1, 2], state: "minecraft:air"}, + {pos: [3, 1, 3], state: "minecraft:air"}, + {pos: [3, 1, 4], state: "minecraft:air"}, + {pos: [4, 1, 0], state: "minecraft:air"}, + {pos: [4, 1, 1], state: "minecraft:air"}, + {pos: [4, 1, 2], state: "minecraft:air"}, + {pos: [4, 1, 3], state: "minecraft:air"}, + {pos: [4, 1, 4], state: "minecraft:air"}, + {pos: [0, 2, 0], state: "minecraft:air"}, + {pos: [0, 2, 1], state: "minecraft:air"}, + {pos: [0, 2, 2], state: "minecraft:air"}, + {pos: [0, 2, 3], state: "minecraft:air"}, + {pos: [0, 2, 4], state: "minecraft:air"}, + {pos: [1, 2, 0], state: "minecraft:air"}, + {pos: [1, 2, 1], state: "minecraft:air"}, + {pos: [1, 2, 2], state: "minecraft:air"}, + {pos: [1, 2, 3], state: "minecraft:air"}, + {pos: [1, 2, 4], state: "minecraft:air"}, + {pos: [2, 2, 0], state: "minecraft:air"}, + {pos: [2, 2, 1], state: "minecraft:air"}, + {pos: [2, 2, 2], state: "minecraft:air"}, + {pos: [2, 2, 3], state: "minecraft:air"}, + {pos: [2, 2, 4], state: "minecraft:air"}, + {pos: [3, 2, 0], state: "minecraft:air"}, + {pos: [3, 2, 1], state: "minecraft:air"}, + {pos: [3, 2, 2], state: "minecraft:air"}, + {pos: [3, 2, 3], state: "minecraft:air"}, + {pos: [3, 2, 4], state: "minecraft:air"}, + {pos: [4, 2, 0], state: "minecraft:air"}, + {pos: [4, 2, 1], state: "minecraft:air"}, + {pos: [4, 2, 2], state: "minecraft:air"}, + {pos: [4, 2, 3], state: "minecraft:air"}, + {pos: [4, 2, 4], state: "minecraft:air"}, + {pos: [0, 3, 0], state: "minecraft:air"}, + {pos: [0, 3, 1], state: "minecraft:air"}, + {pos: [0, 3, 2], state: "minecraft:air"}, + {pos: [0, 3, 3], state: "minecraft:air"}, + {pos: [0, 3, 4], state: "minecraft:air"}, + {pos: [1, 3, 0], state: "minecraft:air"}, + {pos: [1, 3, 1], state: "minecraft:air"}, + {pos: [1, 3, 2], state: "minecraft:air"}, + {pos: [1, 3, 3], state: "minecraft:air"}, + {pos: [1, 3, 4], state: "minecraft:air"}, + {pos: [2, 3, 0], state: "minecraft:air"}, + {pos: [2, 3, 1], state: "minecraft:air"}, + {pos: [2, 3, 2], state: "minecraft:air"}, + {pos: [2, 3, 3], state: "minecraft:air"}, + {pos: [2, 3, 4], state: "minecraft:air"}, + {pos: [3, 3, 0], state: "minecraft:air"}, + {pos: [3, 3, 1], state: "minecraft:air"}, + {pos: [3, 3, 2], state: "minecraft:air"}, + {pos: [3, 3, 3], state: "minecraft:air"}, + {pos: [3, 3, 4], state: "minecraft:air"}, + {pos: [4, 3, 0], state: "minecraft:air"}, + {pos: [4, 3, 1], state: "minecraft:air"}, + {pos: [4, 3, 2], state: "minecraft:air"}, + {pos: [4, 3, 3], state: "minecraft:air"}, + {pos: [4, 3, 4], state: "minecraft:air"}, + {pos: [0, 4, 0], state: "minecraft:air"}, + {pos: [0, 4, 1], state: "minecraft:air"}, + {pos: [0, 4, 2], state: "minecraft:air"}, + {pos: [0, 4, 3], state: "minecraft:air"}, + {pos: [0, 4, 4], state: "minecraft:air"}, + {pos: [1, 4, 0], state: "minecraft:air"}, + {pos: [1, 4, 1], state: "minecraft:air"}, + {pos: [1, 4, 2], state: "minecraft:air"}, + {pos: [1, 4, 3], state: "minecraft:air"}, + {pos: [1, 4, 4], state: "minecraft:air"}, + {pos: [2, 4, 0], state: "minecraft:air"}, + {pos: [2, 4, 1], state: "minecraft:air"}, + {pos: [2, 4, 2], state: "minecraft:air"}, + {pos: [2, 4, 3], state: "minecraft:air"}, + {pos: [2, 4, 4], state: "minecraft:air"}, + {pos: [3, 4, 0], state: "minecraft:air"}, + {pos: [3, 4, 1], state: "minecraft:air"}, + {pos: [3, 4, 2], state: "minecraft:air"}, + {pos: [3, 4, 3], state: "minecraft:air"}, + {pos: [3, 4, 4], state: "minecraft:air"}, + {pos: [4, 4, 0], state: "minecraft:air"}, + {pos: [4, 4, 1], state: "minecraft:air"}, + {pos: [4, 4, 2], state: "minecraft:air"}, + {pos: [4, 4, 3], state: "minecraft:air"}, + {pos: [4, 4, 4], state: "minecraft:air"} + ], + entities: [], + palette: [ + "minecraft:polished_andesite", + "minecraft:stone", + "minecraft:air", + "computercraft:turtle_normal{facing:south,waterlogged:false}" + ] +} From cc8c1f38e7e15548c89f5da2f0ead7e1299ad9cc Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Thu, 6 Jul 2023 23:03:22 +0100 Subject: [PATCH 29/32] Small documentation improvements - Document that settings.set doesn't persist values. I think this closes #1512 - haven't heard back from them. - Add missing close reasons to the websocket_closed event. Closes #1493. - Mention what values are preserved by os.queueEvent. This is just the same as modem.transmit. Closes #1490. --- doc/events/websocket_closed.md | 9 ++++ .../dan200/computercraft/core/apis/OSAPI.java | 3 +- .../computercraft/lua/rom/apis/settings.lua | 53 ++++++++++++++----- 3 files changed, 50 insertions(+), 15 deletions(-) diff --git a/doc/events/websocket_closed.md b/doc/events/websocket_closed.md index 06a991fa8..c6870258b 100644 --- a/doc/events/websocket_closed.md +++ b/doc/events/websocket_closed.md @@ -13,6 +13,15 @@ The @{websocket_closed} event is fired when an open WebSocket connection is clos ## Return Values 1. @{string}: The event name. 2. @{string}: The URL of the WebSocket that was closed. +3. @{string}|@{nil}: The [server-provided reason][close_reason] + the websocket was closed. This will be @{nil} if the connection was closed + abnormally. +4. @{number}|@{nil}: The [connection close code][close_code], + indicating why the socket was closed. This will be @{nil} if the connection + was closed abnormally. + +[close_reason]: https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.6 "The WebSocket Connection Close Reason, RFC 6455" +[close_code]: https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5 "The WebSocket Connection Close Code, RFC 6455" ## Example Prints a message when a WebSocket is closed (this may take a minute): diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/OSAPI.java b/projects/core/src/main/java/dan200/computercraft/core/apis/OSAPI.java index 0d2e2347f..86b395e9b 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/OSAPI.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/OSAPI.java @@ -132,7 +132,8 @@ public class OSAPI implements ILuaAPI { * @param name The name of the event to queue. * @param args The parameters of the event. * @cc.tparam string name The name of the event to queue. - * @cc.param ... The parameters of the event. + * @cc.param ... The parameters of the event. These can be any primitive type (boolean, number, string) as well as + * tables. Other types (like functions), as well as metatables, will not be preserved. * @cc.see os.pullEvent To pull the event queued */ @LuaFunction diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/settings.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/settings.lua index e4ebd7cff..d1481401b 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/settings.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/settings.lua @@ -2,13 +2,32 @@ -- -- SPDX-License-Identifier: LicenseRef-CCPL ---- Read and write configuration options for CraftOS and your programs. --- --- By default, the settings API will load its configuration from the --- `/.settings` file. One can then use @{settings.save} to update the file. --- --- @module settings --- @since 1.78 +--[[- Read and write configuration options for CraftOS and your programs. + +When a computer starts, it reads the current value of settings from the +`/.settings` file. These values then may be @{settings.get|read} or +@{settings.set|modified}. + +:::caution +Calling @{settings.set} does _not_ update the settings file by default. You +_must_ call @{settings.save} to persist values. +::: + +@module settings +@since 1.78 +@usage Define an basic setting `123` and read its value. + + settings.define("my.setting", { + description = "An example setting", + default = 123, + type = number, + }) + print("my.setting = " .. settings.get("my.setting")) -- 123 + +You can then use the `set` program to change its value (e.g. `set my.setting 456`), +and then re-run the `example` program to check it has changed. + +]] local expect = dofile("rom/modules/main/cc/expect.lua") local type, expect, field = type, expect.expect, expect.field @@ -92,13 +111,19 @@ local function set_value(name, new) end end ---- Set the value of a setting. --- --- @tparam string name The name of the setting to set --- @param value The setting's value. This cannot be `nil`, and must be --- serialisable by @{textutils.serialize}. --- @throws If this value cannot be serialised --- @see settings.unset +--[[- Set the value of a setting. + +:::caution +Calling @{settings.set} does _not_ update the settings file by default. You +_must_ call @{settings.save} to persist values. +::: + +@tparam string name The name of the setting to set +@param value The setting's value. This cannot be `nil`, and must be +serialisable by @{textutils.serialize}. +@throws If this value cannot be serialised +@see settings.unset +]] function set(name, value) expect(1, name, "string") expect(2, value, "number", "string", "boolean", "table") From 4bbde8c50c00bc572578ab2cff609b3443d10ddf Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 1 Jul 2023 17:00:28 +0100 Subject: [PATCH 30/32] Tighten up the $private HTTP rule - Block multicast and the fd00::/8 address ranges. - Block several cloud metadata providers which sit outside the standard address ranges. --- .../apis/http/options/AddressPredicate.java | 35 ++++++++++++++++--- .../apis/http/options/AddressRuleTest.java | 9 ++++- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressPredicate.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressPredicate.java index e9dae1d88..1dfd09819 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressPredicate.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressPredicate.java @@ -6,9 +6,13 @@ package dan200.computercraft.core.apis.http.options; import com.google.common.net.InetAddresses; +import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; +import java.util.Arrays; +import java.util.Set; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * A predicate on an address. Matches against a domain and an ip address. @@ -107,12 +111,35 @@ interface AddressPredicate { final class PrivatePattern implements AddressPredicate { static final PrivatePattern INSTANCE = new PrivatePattern(); + private static final Set additionalAddresses = Arrays.stream(new String[]{ + // Block various cloud providers internal IPs. + "100.100.100.200", // Alibaba + "192.0.0.192", // Oracle + }).map(InetAddresses::forString).collect(Collectors.toUnmodifiableSet()); + @Override public boolean matches(InetAddress socketAddress) { - return socketAddress.isAnyLocalAddress() - || socketAddress.isLoopbackAddress() - || socketAddress.isLinkLocalAddress() - || socketAddress.isSiteLocalAddress(); + return + socketAddress.isAnyLocalAddress() // 0.0.0.0, ::0 + || socketAddress.isLoopbackAddress() // 127.0.0.0/8, ::1 + || socketAddress.isLinkLocalAddress() // 169.254.0.0/16, fe80::/10 + || socketAddress.isSiteLocalAddress() // 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fec0::/10 + || socketAddress.isMulticastAddress() // 224.0.0.0/4, ff00::/8 + || isUniqueLocalAddress(socketAddress) // fd00::/8 + || additionalAddresses.contains(socketAddress); + } + + /** + * Determine if an IP address lives inside the ULA address range. + * + * @param address The IP address to test. + * @return Whether this address sits in the ULA address range. + * @see Unique local address on Wikipedia + */ + private boolean isUniqueLocalAddress(InetAddress address) { + // ULA is actually defined as fc00::/7 (so both fc00::/8 and fd00::/8). However, only the latter is actually + // defined right now, so let's be conservative. + return address instanceof Inet6Address && (address.getAddress()[0] & 0xff) == 0xfd; } } diff --git a/projects/core/src/test/java/dan200/computercraft/core/apis/http/options/AddressRuleTest.java b/projects/core/src/test/java/dan200/computercraft/core/apis/http/options/AddressRuleTest.java index 4682d45da..a7a7bf488 100644 --- a/projects/core/src/test/java/dan200/computercraft/core/apis/http/options/AddressRuleTest.java +++ b/projects/core/src/test/java/dan200/computercraft/core/apis/http/options/AddressRuleTest.java @@ -31,7 +31,14 @@ public class AddressRuleTest { @ValueSource(strings = { "0.0.0.0", "[::]", "localhost", "127.0.0.1.nip.io", "127.0.0.1", "[::1]", - "172.17.0.1", "192.168.1.114", "[0:0:0:0:0:ffff:c0a8:172]", "10.0.0.1" + "172.17.0.1", "192.168.1.114", "[0:0:0:0:0:ffff:c0a8:172]", "10.0.0.1", + // Multicast + "224.0.0.1", "ff02::1", + // Cloud metadata providers + "100.100.100.200", // Alibaba + "192.0.0.192", // Oracle + "fd00:ec2::254", // AWS + "169.254.169.254", // AWS, Digital Ocean, GCP, etc.. }) public void blocksLocalDomains(String domain) { assertEquals(apply(CoreConfig.httpRules, domain, 80).action, Action.DENY); From 5d71770931b1db8bd5eb452b0f0d904258208cbe Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 5 Jul 2023 11:38:21 +0100 Subject: [PATCH 31/32] Several command permission fixes - Attach permission checks to the first argument (so the literal command name) rather than the last argument. This fixes commands showing up when they shouldn't. - HelpingArgumentBuilder now inherits permissions of its leaf nodes. This only really impacts the "track" subcommand. - Don't autocomplete the computer selector for the "queue" subcommand. As everyone has permission for this command, it's possible to find all computer ids and labels in the world. I'm in mixed minds about this, but don't think this is an exploit - computer ids/labels are sent to in-range players so shouldn't be considered secret - but worth patching none-the-less. --- .../shared/command/CommandComputerCraft.java | 8 ++++- .../shared/command/UserLevel.java | 23 ++++++++++++++ .../command/builder/CommandBuilder.java | 26 ++++++++-------- .../builder/HelpingArgumentBuilder.java | 30 +++++++++++++++++-- 4 files changed, 70 insertions(+), 17 deletions(-) diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java b/projects/common/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java index 7c67aab14..93b96d069 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java @@ -6,9 +6,12 @@ package dan200.computercraft.shared.command; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; import dan200.computercraft.core.computer.ComputerSide; import dan200.computercraft.core.metrics.Metrics; +import dan200.computercraft.shared.command.arguments.ComputersArgumentType; import dan200.computercraft.shared.command.text.TableBuilder; import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.core.ServerComputer; @@ -169,7 +172,10 @@ public final class CommandComputerCraft { .then(command("queue") .requires(UserLevel.ANYONE) - .arg("computer", manyComputers()) + .arg( + RequiredArgumentBuilder.argument("computer", manyComputers()) + .suggests((context, builder) -> Suggestions.empty()) + ) .argManyValue("args", StringArgumentType.string(), Collections.emptyList()) .executes((ctx, args) -> { var computers = getComputersArgument(ctx, "computer"); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/UserLevel.java b/projects/common/src/main/java/dan200/computercraft/shared/command/UserLevel.java index 2d9438121..c8bdbb0f2 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/command/UserLevel.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/command/UserLevel.java @@ -49,6 +49,29 @@ public enum UserLevel implements Predicate { return source.hasPermission(toLevel()); } + /** + * Take the union of two {@link UserLevel}s. + *

+ * This satisfies the property that for all sources {@code s}, {@code a.test(s) || b.test(s) == (a ∪ b).test(s)}. + * + * @param left The first user level to take the union of. + * @param right The second user level to take the union of. + * @return The union of two levels. + */ + public static UserLevel union(UserLevel left, UserLevel right) { + if (left == right) return left; + + // x ∪ ANYONE = ANYONE + if (left == ANYONE || right == ANYONE) return ANYONE; + + // x ∪ OWNER = OWNER + if (left == OWNER) return right; + if (right == OWNER) return left; + + // At this point, we have x != y and x, y ∈ { OP, OWNER_OP }. + return OWNER_OP; + } + private static boolean isOwner(CommandSourceStack source) { var server = source.getServer(); var sender = source.getEntity(); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/builder/CommandBuilder.java b/projects/common/src/main/java/dan200/computercraft/shared/command/builder/CommandBuilder.java index 3fe77ff48..2a395ff7b 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/command/builder/CommandBuilder.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/command/builder/CommandBuilder.java @@ -48,11 +48,15 @@ public class CommandBuilder implements CommandNodeBuilder> { return this; } - public CommandBuilder arg(String name, ArgumentType type) { - args.add(RequiredArgumentBuilder.argument(name, type)); + public CommandBuilder arg(ArgumentBuilder arg) { + args.add(arg); return this; } + public CommandBuilder arg(String name, ArgumentType type) { + return arg(RequiredArgumentBuilder.argument(name, type)); + } + public CommandNodeBuilder>> argManyValue(String name, ArgumentType type, List empty) { return argMany(name, type, () -> empty); } @@ -74,7 +78,7 @@ public class CommandBuilder implements CommandNodeBuilder> { return command -> { // The node for no arguments - var tail = tail(ctx -> command.run(ctx, empty.get())); + var tail = setupTail(ctx -> command.run(ctx, empty.get())); // The node for one or more arguments ArgumentBuilder moreArg = RequiredArgumentBuilder @@ -83,7 +87,7 @@ public class CommandBuilder implements CommandNodeBuilder> { // Chain all of them together! tail.then(moreArg); - return link(tail); + return buildTail(tail); }; } @@ -94,20 +98,16 @@ public class CommandBuilder implements CommandNodeBuilder> { @Override public CommandNode executes(Command command) { - if (args.isEmpty()) throw new IllegalStateException("Cannot have empty arg chain builder"); - - return link(tail(command)); + return buildTail(setupTail(command)); } - private ArgumentBuilder tail(Command command) { - var defaultTail = args.get(args.size() - 1); - defaultTail.executes(command); - if (requires != null) defaultTail.requires(requires); - return defaultTail; + private ArgumentBuilder setupTail(Command command) { + return args.get(args.size() - 1).executes(command); } - private CommandNode link(ArgumentBuilder tail) { + private CommandNode buildTail(ArgumentBuilder tail) { for (var i = args.size() - 2; i >= 0; i--) tail = args.get(i).then(tail); + if (requires != null) tail.requires(requires); return tail.build(); } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java b/projects/common/src/main/java/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java index 20aaa2e90..dfe2fecc3 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java @@ -10,6 +10,7 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.LiteralCommandNode; +import dan200.computercraft.shared.command.UserLevel; import net.minecraft.ChatFormatting; import net.minecraft.commands.CommandSourceStack; import net.minecraft.network.chat.ClickEvent; @@ -18,6 +19,8 @@ import net.minecraft.network.chat.Component; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collection; +import java.util.function.Predicate; +import java.util.stream.Stream; import static dan200.computercraft.core.util.Nullability.assertNonNull; import static dan200.computercraft.shared.command.text.ChatHelpers.coloured; @@ -37,6 +40,29 @@ public final class HelpingArgumentBuilder extends LiteralArgumentBuilder requires(Predicate requirement) { + throw new IllegalStateException("Cannot use requires on a HelpingArgumentBuilder"); + } + + @Override + public Predicate getRequirement() { + // The requirement of this node is the union of all child's requirements. + var requirements = Stream.concat( + children.stream().map(ArgumentBuilder::getRequirement), + getArguments().stream().map(CommandNode::getRequirement) + ).toList(); + + // If all requirements are a UserLevel, take the union of those instead. + var userLevel = UserLevel.OWNER; + for (var requirement : requirements) { + if (!(requirement instanceof UserLevel level)) return x -> requirements.stream().anyMatch(y -> y.test(x)); + userLevel = UserLevel.union(userLevel, level); + } + + return userLevel; + } + @Override public LiteralArgumentBuilder executes(final Command command) { throw new IllegalStateException("Cannot use executes on a HelpingArgumentBuilder"); @@ -80,9 +106,7 @@ public final class HelpingArgumentBuilder extends LiteralArgumentBuilderliteral("help") - .requires(x -> getArguments().stream().anyMatch(y -> y.getRequirement().test(x))) - .executes(helpCommand); + var helpNode = LiteralArgumentBuilder.literal("help").executes(helpCommand); // Add all normal command children to this and the help node for (var child : getArguments()) { From d351bc33c6f53a033f223251a5cb120e6a005568 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Fri, 7 Jul 2023 00:16:48 +0100 Subject: [PATCH 32/32] Bump CC:T to 1.106.0 --- gradle.properties | 2 +- .../computercraft/lua/rom/help/changelog.md | 20 +++++++++++ .../computercraft/lua/rom/help/whatsnew.md | 36 +++++++++---------- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/gradle.properties b/gradle.properties index 31d73d2bb..5d3bda597 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ kotlin.jvm.target.validation.mode=error # Mod properties isUnstable=false -modVersion=1.105.0 +modVersion=1.106.0 # Minecraft properties: We want to configure this here so we can read it in settings.gradle mcVersion=1.19.4 diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/help/changelog.md b/projects/core/src/main/resources/data/computercraft/lua/rom/help/changelog.md index 098267351..ef0af61a8 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/help/changelog.md +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/help/changelog.md @@ -1,3 +1,23 @@ +# New features in CC: Tweaked 1.106.0 + +* Numerous documentation improvements (MCJack123, znepb, penguinencounter). +* Port `fs.find` to Lua. This also allows using `?` as a wildcard. +* Computers cursors now glow in the dark. +* Allow changing turtle upgrades from the GUI. +* Add option to serialize Unicode strings to JSON (MCJack123). +* Small optimisations to the `window` API. +* Turtle upgrades can now preserve NBT from upgrade item stack and when broken. +* Add support for tool enchantments and durability via datapacks. This is disabled for the built-in tools. + +Several bug fixes: +* Fix turtles rendering incorrectly when upside down. +* Fix misplaced calls to IArguments.escapes. +* Lua REPL no longer accepts `)(` as a valid expression. +* Fix several inconsistencies with `require`/`package.path` in the Lua REPL (Wojbie). +* Fix turtle being able to place water buckets outside its reach distance. +* Fix private several IP address ranges not being blocked by the `$private` rule. +* Improve permission checks in the `/computercraft` command. + # New features in CC: Tweaked 1.105.0 * Optimise JSON string parsing. diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md b/projects/core/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md index 1512a2ae6..172ac059e 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md @@ -1,25 +1,21 @@ -New features in CC: Tweaked 1.105.0 +New features in CC: Tweaked 1.106.0 -* Optimise JSON string parsing. -* Add `colors.fromBlit` (Erb3). -* Upload file size limit is now configurable (khankul). -* Wired cables no longer have a distance limit. -* Java methods now coerce values to strings consistently with Lua. -* Add custom timeout support to the HTTP API. -* Support custom proxies for HTTP requests (Lemmmy). -* The `speaker` program now errors when playing HTML files. -* `edit` now shows an error message when editing read-only files. -* Update Ukranian translation (SirEdvin). +* Numerous documentation improvements (MCJack123, znepb, penguinencounter). +* Port `fs.find` to Lua. This also allows using `?` as a wildcard. +* Computers cursors now glow in the dark. +* Allow changing turtle upgrades from the GUI. +* Add option to serialize Unicode strings to JSON (MCJack123). +* Small optimisations to the `window` API. +* Turtle upgrades can now preserve NBT from upgrade item stack and when broken. +* Add support for tool enchantments and durability via datapacks. This is disabled for the built-in tools. Several bug fixes: -* Allow GPS hosts to only be 1 block apart. -* Fix "Turn On"/"Turn Off" buttons being inverted in the computer GUI (Erb3). -* Fix arrow keys not working in the printout UI. -* Several documentation fixes (zyxkad, Lupus590, Commandcracker). -* Fix monitor renderer debug text always being visible on Forge. -* Fix crash when another mod changes the LoggerContext. -* Fix the `monitor_renderer` option not being present in Fabric config files. -* Pasting on MacOS/OSX now uses Cmd+V rather than Ctrl+V. -* Fix turtles placing blocks upside down when at y<0. +* Fix turtles rendering incorrectly when upside down. +* Fix misplaced calls to IArguments.escapes. +* Lua REPL no longer accepts `)(` as a valid expression. +* Fix several inconsistencies with `require`/`package.path` in the Lua REPL (Wojbie). +* Fix turtle being able to place water buckets outside its reach distance. +* Fix private several IP address ranges not being blocked by the `$private` rule. +* Improve permission checks in the `/computercraft` command. Type "help changelog" to see the full version history.