From c47718b09d69dbc29a93e76daf9a5fceb86eea15 Mon Sep 17 00:00:00 2001 From: Andrew Kvapil Date: Sun, 4 Aug 2024 14:57:09 +0200 Subject: [PATCH 01/26] Fix getNextVersion --- buildSrc/src/main/kotlin/cc/tweaked/gradle/Extensions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/cc/tweaked/gradle/Extensions.kt b/buildSrc/src/main/kotlin/cc/tweaked/gradle/Extensions.kt index dada28b64..beca80746 100644 --- a/buildSrc/src/main/kotlin/cc/tweaked/gradle/Extensions.kt +++ b/buildSrc/src/main/kotlin/cc/tweaked/gradle/Extensions.kt @@ -143,7 +143,7 @@ fun getNextVersion(version: String): String { val lastIndex = mainVersion.lastIndexOf('.') if (lastIndex < 0) throw IllegalArgumentException("Cannot parse version format \"$version\"") val lastVersion = try { - version.substring(lastIndex + 1).toInt() + mainVersion.substring(lastIndex + 1).toInt() } catch (e: NumberFormatException) { throw IllegalArgumentException("Cannot parse version format \"$version\"", e) } From 5abab982c767259124377805abefe72db57f1f28 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 11 Aug 2024 11:47:03 +0100 Subject: [PATCH 02/26] Allow registering more generic detail providers Allow registering details providers matching any super type, not just the exact type. This is mostly useful for 1.21, where we can have providers for any DataComponentHolder, not just item stacks. --- .../api/detail/BasicItemDetailProvider.java | 23 ++++++++----------- .../api/detail/DetailRegistry.java | 2 +- .../impl/detail/DetailRegistryImpl.java | 4 ++-- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/detail/BasicItemDetailProvider.java b/projects/common-api/src/main/java/dan200/computercraft/api/detail/BasicItemDetailProvider.java index bf706f0db..c709b79f6 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/detail/BasicItemDetailProvider.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/detail/BasicItemDetailProvider.java @@ -13,7 +13,7 @@ import java.util.Objects; /** - * An item detail provider for {@link ItemStack}'s whose {@link Item} has a specific type. + * An item detail provider for {@link ItemStack}s whose {@link Item} has a specific type. * * @param The type the stack's item must have. */ @@ -22,7 +22,7 @@ public abstract class BasicItemDetailProvider implements DetailProvider itemType) { } /** - * Create a new item detail provider. Meta will be inserted directly into the results. + * Create a new item detail provider. Details will be inserted directly into the results. * * @param itemType The type the stack's item must have. */ @@ -53,21 +53,18 @@ public BasicItemDetailProvider(Class itemType) { * @param stack The item stack to provide details for. * @param item The item to provide details for. */ - public abstract void provideDetails( - Map data, ItemStack stack, T item - ); + public abstract void provideDetails(Map data, ItemStack stack, T item); @Override - public void provideDetails(Map data, ItemStack stack) { + public final void provideDetails(Map data, ItemStack stack) { var item = stack.getItem(); if (!itemType.isInstance(item)) return; - // If `namespace` is specified, insert into a new data map instead of the existing one. - Map child = namespace == null ? data : new HashMap<>(); - - provideDetails(child, stack, itemType.cast(item)); - - if (namespace != null) { + if (namespace == null) { + provideDetails(data, stack, itemType.cast(item)); + } else { + Map child = new HashMap<>(); + provideDetails(child, stack, itemType.cast(item)); data.put(namespace, child); } } diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/detail/DetailRegistry.java b/projects/common-api/src/main/java/dan200/computercraft/api/detail/DetailRegistry.java index 7c355e8b0..4f21ff81f 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/detail/DetailRegistry.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/detail/DetailRegistry.java @@ -26,7 +26,7 @@ public interface DetailRegistry { * @param provider The detail provider to register. * @see DetailProvider */ - void addProvider(DetailProvider provider); + void addProvider(DetailProvider provider); /** * Compute basic details about an object. This is cheaper than computing all details operation, and so is suitable diff --git a/projects/common/src/main/java/dan200/computercraft/impl/detail/DetailRegistryImpl.java b/projects/common/src/main/java/dan200/computercraft/impl/detail/DetailRegistryImpl.java index 3f16b6893..31d42bba2 100644 --- a/projects/common/src/main/java/dan200/computercraft/impl/detail/DetailRegistryImpl.java +++ b/projects/common/src/main/java/dan200/computercraft/impl/detail/DetailRegistryImpl.java @@ -15,7 +15,7 @@ * @param The type of object that this registry provides details for. */ public class DetailRegistryImpl implements DetailRegistry { - private final Collection> providers = new ArrayList<>(); + private final Collection> providers = new ArrayList<>(); private final DetailProvider basic; public DetailRegistryImpl(DetailProvider basic) { @@ -24,7 +24,7 @@ public DetailRegistryImpl(DetailProvider basic) { } @Override - public synchronized void addProvider(DetailProvider provider) { + public synchronized void addProvider(DetailProvider provider) { Objects.requireNonNull(provider, "provider cannot be null"); if (!providers.contains(provider)) providers.add(provider); } From 77af4bc2139a39c79f094c4e86caa54e477a4d99 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 11 Aug 2024 11:50:24 +0100 Subject: [PATCH 03/26] Fix a couple of typos in fluid method docs --- .../peripheral/generic/methods/AbstractFluidMethods.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/AbstractFluidMethods.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/AbstractFluidMethods.java index e409a558c..c5aa2877a 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/AbstractFluidMethods.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/AbstractFluidMethods.java @@ -70,10 +70,10 @@ public abstract int pushFluid( ) throws LuaException; /** - * Move a fluid from a connected fluid container into this oneone. + * Move a fluid from a connected fluid container into this one. *

* This allows you to pull fluid in the current fluid container from another container on the same wired - * network. Both containers must attached to wired modems which are connected via a cable. + * network. Both containers must be attached to wired modems which are connected via a cable. * * @param to Container to move fluid to. * @param computer The current computer. From 216f0adb3c3f88f4f09a2e0e604bbe55b667cd39 Mon Sep 17 00:00:00 2001 From: JackMacWindows Date: Sun, 11 Aug 2024 11:53:47 +0100 Subject: [PATCH 04/26] Fix a couple of typos in fluid method docs Also mention ffmpeg can now encode/decode DFPWM. --- .../computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua index 73783ee1c..5b5e05244 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua @@ -13,7 +13,7 @@ Typically DFPWM audio is read from [the filesystem][`fs.ReadHandle`] or a [a web and converted a format suitable for [`speaker.playAudio`]. ## Encoding and decoding files -This modules exposes two key functions, [`make_decoder`] and [`make_encoder`], which construct a new decoder or encoder. +This module exposes two key functions, [`make_decoder`] and [`make_encoder`], which construct a new decoder or encoder. The returned encoder/decoder is itself a function, which converts between the two kinds of data. These encoders and decoders have lots of hidden state, so you should be careful to use the same encoder or decoder for @@ -21,9 +21,9 @@ a specific audio stream. Typically you will want to create a decoder for each st for each one you write. ## Converting audio to DFPWM -DFPWM is not a popular file format and so standard audio processing tools will not have an option to export to it. +DFPWM is not a popular file format and so standard audio processing tools may not have an option to export to it. Instead, you can convert audio files online using [music.madefor.cc], the [LionRay Wav Converter][LionRay] Java -application or development builds of [FFmpeg]. +application or [FFmpeg] 5.1 or later. [music.madefor.cc]: https://music.madefor.cc/ "DFPWM audio converter for Computronics and CC: Tweaked" [LionRay]: https://github.com/gamax92/LionRay/ "LionRay Wav Converter " From bfb28b471024680418f5e05c5678b39c649f3919 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 11 Aug 2024 12:03:48 +0100 Subject: [PATCH 05/26] Log current block entity in TickScheduler This check should be impossible (the BE has not been removed, but is no longer present in the world), but we've had one instance where it has happened (#1925). I don't have a good solution here, so at least let's print both BEs for now. --- .../java/dan200/computercraft/shared/util/TickScheduler.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/projects/common/src/main/java/dan200/computercraft/shared/util/TickScheduler.java b/projects/common/src/main/java/dan200/computercraft/shared/util/TickScheduler.java index f54baf78e..1ae910185 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/util/TickScheduler.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/util/TickScheduler.java @@ -109,8 +109,9 @@ private static State tickToken(Token token) { return State.UNLOADED; } else { // This should be impossible: either the block entity is at the above position, or it has been removed. - if (level.getBlockEntity(pos) != blockEntity) { - throw new IllegalStateException("Expected " + blockEntity + " at " + pos); + var currentBlockEntity = level.getBlockEntity(pos); + if (currentBlockEntity != blockEntity) { + throw new IllegalStateException("Expected " + blockEntity + " at " + pos + ", got " + currentBlockEntity); } // Otherwise schedule a tick and remove it from the queue. From be59f1a8752c178473a743e130f13a975d6ef86b Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 11 Aug 2024 12:25:28 +0100 Subject: [PATCH 06/26] Clarify some quicks of JSON serialisation There's a mismatch between how Lua and JSON's values are defined, which means that serialisation is a little confusing at times. This commit attempts to document them a little better. Closes #1885, closes #1920 --- .../computercraft/lua/rom/apis/textutils.lua | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua index 8c52a8a2c..ed9727a69 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua @@ -851,13 +851,32 @@ unserialise = unserialize -- GB version --[[- Returns a JSON representation of the given data. -This function attempts to guess whether a table is a JSON array or -object. However, empty tables are assumed to be empty objects - use -[`textutils.empty_json_array`] to mark an empty array. - This is largely intended for interacting with various functions from the [`commands`] API, though may also be used in making [`http`] requests. +Lua has a rather different data model to Javascript/JSON. As a result, some Lua +values do not serialise cleanly into JSON. + + - Lua tables can contain arbitrary key-value pairs, but JSON only accepts arrays, + and objects (which require a string key). When serialising a table, if it only + has numeric keys, then it will be treated as an array. Otherwise, the table will + be serialised to an object using the string keys. Non-string keys (such as numbers + or tables) will be dropped. + + A consequence of this is that an empty table will always be serialised to an object, + not an array. [`textutils.empty_json_array`] may be used to express an empty array. + + - Lua strings are an a sequence of raw bytes, and do not have any specific encoding. + However, JSON strings must be valid unicode. By default, non-ASCII characters in a + string are serialised to their unicode code point (for instance, `"\xfe"` is + converted to `"\u00fe"`). The `unicode_strings` option may be set to treat all input + strings as UTF-8. + + - Lua does not distinguish between missing keys (`undefined` in JS) and ones explicitly + set to `null`. As a result `{ x = nil }` is serialised to `{}`. [`textutils.json_null`] + may be used to get an explicit null value (`{ x = textutils.json_null }` will serialise + to `{"x": null}`). + @param[1] t The value to serialise. Like [`textutils.serialise`], this should not contain recursive tables or functions. @tparam[1,opt] { From 9484315d37f9cb8acb8e36622f4bdde6b16d90f4 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 11 Aug 2024 14:11:14 +0100 Subject: [PATCH 07/26] Fix return type of Vector.dot Closes #1932 --- .../main/resources/data/computercraft/lua/rom/apis/vector.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/vector.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/vector.lua index 9501f53c1..d68289418 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/vector.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/vector.lua @@ -114,7 +114,7 @@ local vector = { -- -- @tparam Vector self The first vector to compute the dot product of. -- @tparam Vector o The second vector to compute the dot product of. - -- @treturn Vector The dot product of `self` and `o`. + -- @treturn number The dot product of `self` and `o`. -- @usage v1:dot(v2) dot = function(self, o) if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end From bb97c465d963ca325806a81b0de0d41b021af085 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 14 Aug 2024 21:12:30 +0100 Subject: [PATCH 08/26] Fix computers/turtles not being dropped on explosion Computer drops are currently[^1] implemented via a dynamic drop. To support this, we need to inject the dynamic drop into the loot parameters. We currently do this by implementing our own drop logic in playerWillDestroy[^2], manually creating the loot params and adding our additional drop. However, if the item is dropped via some other method (such as via explosions), we'll go through vanilla's drop logic and so never add the dynamic drop! The correct way to do this is to override getDrops to add the dynamic drop instead. I don't know why we didn't always do this -- the code in question was first written for MC 1.14[^3], when things were very different. [^1]: This is no longer the case on 1.21, where we can just copy capabilities. [^2]: We need to override vanilla's drop behaviour to ensure items are dropped in creative mode. [^3]: See 594bc4203c6470e624a5f5e5edb2436590d1706c. Which probably means the bug has been around for 5 years :/. --- .../blocks/AbstractComputerBlock.java | 32 ++-- .../gametest/GameTestHelperAccessor.java | 5 - .../computercraft/gametest/Computer_Test.kt | 16 ++ .../computer_test.drops_on_explosion.snbt | 138 ++++++++++++++++++ 4 files changed, 168 insertions(+), 23 deletions(-) create mode 100644 projects/common/src/testMod/resources/data/cctest/structures/computer_test.drops_on_explosion.snbt diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/AbstractComputerBlock.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/AbstractComputerBlock.java index 7647b36e6..b9e61a7aa 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/AbstractComputerBlock.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/AbstractComputerBlock.java @@ -35,9 +35,9 @@ import net.minecraft.world.level.storage.loot.LootParams; import net.minecraft.world.level.storage.loot.parameters.LootContextParams; import net.minecraft.world.phys.BlockHitResult; -import net.minecraft.world.phys.Vec3; import javax.annotation.Nullable; +import java.util.List; public abstract class AbstractComputerBlock extends HorizontalDirectionalBlock implements IBundledRedstoneBlock, EntityBlock { private static final ResourceLocation DROP = new ResourceLocation(ComputerCraftAPI.MOD_ID, "computer"); @@ -110,9 +110,19 @@ public ItemStack getCloneItemStack(BlockGetter world, BlockPos pos, BlockState s return super.getCloneItemStack(world, pos, state); } + @Override + @Deprecated + public List getDrops(BlockState state, LootParams.Builder params) { + if (params.getOptionalParameter(LootContextParams.BLOCK_ENTITY) instanceof AbstractComputerBlockEntity computer) { + params = params.withDynamicDrop(DROP, out -> out.accept(getItem(computer))); + } + + return super.getDrops(state, params); + } + @Override public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity tile, ItemStack tool) { - // Don't drop blocks here - see onBlockHarvested. + // Don't drop blocks here - see playerWillDestroy. player.awardStat(Stats.BLOCK_MINED.get(this)); player.causeFoodExhaustion(0.005F); } @@ -120,25 +130,11 @@ public void playerDestroy(Level world, Player player, BlockPos pos, BlockState s @Override public void playerWillDestroy(Level world, BlockPos pos, BlockState state, Player player) { super.playerWillDestroy(world, pos, state, player); - if (!(world instanceof ServerLevel serverWorld)) return; + if (!(world instanceof ServerLevel serverLevel)) return; // We drop the item here instead of doing it in the harvest method, as we should // drop computers for creative players too. - - var tile = world.getBlockEntity(pos); - if (tile instanceof AbstractComputerBlockEntity computer) { - var context = new LootParams.Builder(serverWorld) - .withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(pos)) - .withParameter(LootContextParams.TOOL, player.getMainHandItem()) - .withParameter(LootContextParams.THIS_ENTITY, player) - .withParameter(LootContextParams.BLOCK_ENTITY, tile) - .withDynamicDrop(DROP, out -> out.accept(getItem(computer))); - for (var item : state.getDrops(context)) { - popResource(world, pos, item); - } - - state.spawnAfterBreak(serverWorld, pos, player.getMainHandItem(), true); - } + dropResources(state, serverLevel, pos, world.getBlockEntity(pos)); } @Override diff --git a/projects/common/src/testMod/java/dan200/computercraft/mixin/gametest/GameTestHelperAccessor.java b/projects/common/src/testMod/java/dan200/computercraft/mixin/gametest/GameTestHelperAccessor.java index 591e35af4..72ebf4e2f 100644 --- a/projects/common/src/testMod/java/dan200/computercraft/mixin/gametest/GameTestHelperAccessor.java +++ b/projects/common/src/testMod/java/dan200/computercraft/mixin/gametest/GameTestHelperAccessor.java @@ -6,16 +6,11 @@ import net.minecraft.gametest.framework.GameTestHelper; import net.minecraft.gametest.framework.GameTestInfo; -import net.minecraft.world.phys.AABB; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; -import org.spongepowered.asm.mixin.gen.Invoker; @Mixin(GameTestHelper.class) public interface GameTestHelperAccessor { - @Invoker - AABB callGetBounds(); - @Accessor GameTestInfo getTestInfo(); } diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Computer_Test.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Computer_Test.kt index a4ebf599e..0b981cce8 100644 --- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Computer_Test.kt +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Computer_Test.kt @@ -19,9 +19,11 @@ import net.minecraft.gametest.framework.GameTest import net.minecraft.gametest.framework.GameTestHelper import net.minecraft.world.item.ItemStack import net.minecraft.world.item.Items +import net.minecraft.world.level.Level import net.minecraft.world.level.block.Blocks import net.minecraft.world.level.block.LeverBlock import net.minecraft.world.level.block.RedstoneLampBlock +import net.minecraft.world.phys.Vec3 import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.lwjgl.glfw.GLFW @@ -115,6 +117,20 @@ class Computer_Test { thenOnComputer { callPeripheral("right", "size").assertArrayEquals(54) } } + /** + * Tests a computer item is dropped on explosion. + */ + @GameTest + fun Drops_on_explosion(context: GameTestHelper) = context.sequence { + thenExecute { + val pos = BlockPos(2, 2, 2) + val explosionPos = Vec3.atCenterOf(context.absolutePos(pos)) + context.level.explode(null, explosionPos.x, explosionPos.y, explosionPos.z, 2.0f, Level.ExplosionInteraction.TNT) + + context.assertItemEntityPresent(ModRegistry.Items.COMPUTER_NORMAL.get(), pos, 1.0) + } + } + /** * Check the client can open the computer UI and interact with it. */ diff --git a/projects/common/src/testMod/resources/data/cctest/structures/computer_test.drops_on_explosion.snbt b/projects/common/src/testMod/resources/data/cctest/structures/computer_test.drops_on_explosion.snbt new file mode 100644 index 000000000..bc64fcd09 --- /dev/null +++ b/projects/common/src/testMod/resources/data/cctest/structures/computer_test.drops_on_explosion.snbt @@ -0,0 +1,138 @@ +{ + DataVersion: 3465, + size: [5, 5, 5], + data: [ + {pos: [0, 0, 0], state: "minecraft:obsidian"}, + {pos: [0, 0, 1], state: "minecraft:obsidian"}, + {pos: [0, 0, 2], state: "minecraft:obsidian"}, + {pos: [0, 0, 3], state: "minecraft:obsidian"}, + {pos: [0, 0, 4], state: "minecraft:obsidian"}, + {pos: [1, 0, 0], state: "minecraft:obsidian"}, + {pos: [1, 0, 1], state: "minecraft:obsidian"}, + {pos: [1, 0, 2], state: "minecraft:obsidian"}, + {pos: [1, 0, 3], state: "minecraft:obsidian"}, + {pos: [1, 0, 4], state: "minecraft:obsidian"}, + {pos: [2, 0, 0], state: "minecraft:obsidian"}, + {pos: [2, 0, 1], state: "minecraft:obsidian"}, + {pos: [2, 0, 2], state: "minecraft:obsidian"}, + {pos: [2, 0, 3], state: "minecraft:obsidian"}, + {pos: [2, 0, 4], state: "minecraft:obsidian"}, + {pos: [3, 0, 0], state: "minecraft:obsidian"}, + {pos: [3, 0, 1], state: "minecraft:obsidian"}, + {pos: [3, 0, 2], state: "minecraft:obsidian"}, + {pos: [3, 0, 3], state: "minecraft:obsidian"}, + {pos: [3, 0, 4], state: "minecraft:obsidian"}, + {pos: [4, 0, 0], state: "minecraft:obsidian"}, + {pos: [4, 0, 1], state: "minecraft:obsidian"}, + {pos: [4, 0, 2], state: "minecraft:obsidian"}, + {pos: [4, 0, 3], state: "minecraft:obsidian"}, + {pos: [4, 0, 4], state: "minecraft:obsidian"}, + {pos: [0, 1, 0], state: "minecraft:barrier"}, + {pos: [0, 1, 1], state: "minecraft:barrier"}, + {pos: [0, 1, 2], state: "minecraft:barrier"}, + {pos: [0, 1, 3], state: "minecraft:barrier"}, + {pos: [0, 1, 4], state: "minecraft:barrier"}, + {pos: [1, 1, 0], state: "minecraft:barrier"}, + {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:barrier"}, + {pos: [2, 1, 0], state: "minecraft:barrier"}, + {pos: [2, 1, 1], state: "minecraft:air"}, + {pos: [2, 1, 2], state: "computercraft:computer_normal{facing:east,state:off}", nbt: {On: 0b, id: "computercraft:computer_normal"}}, + {pos: [2, 1, 3], state: "minecraft:air"}, + {pos: [2, 1, 4], state: "minecraft:barrier"}, + {pos: [3, 1, 0], state: "minecraft:barrier"}, + {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:barrier"}, + {pos: [4, 1, 0], state: "minecraft:barrier"}, + {pos: [4, 1, 1], state: "minecraft:barrier"}, + {pos: [4, 1, 2], state: "minecraft:barrier"}, + {pos: [4, 1, 3], state: "minecraft:barrier"}, + {pos: [4, 1, 4], state: "minecraft:barrier"}, + {pos: [0, 2, 0], state: "minecraft:barrier"}, + {pos: [0, 2, 1], state: "minecraft:barrier"}, + {pos: [0, 2, 2], state: "minecraft:air"}, + {pos: [0, 2, 3], state: "minecraft:barrier"}, + {pos: [0, 2, 4], state: "minecraft:barrier"}, + {pos: [1, 2, 0], state: "minecraft:barrier"}, + {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:barrier"}, + {pos: [2, 2, 0], state: "minecraft:barrier"}, + {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:barrier"}, + {pos: [3, 2, 0], state: "minecraft:barrier"}, + {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:barrier"}, + {pos: [4, 2, 0], state: "minecraft:barrier"}, + {pos: [4, 2, 1], state: "minecraft:barrier"}, + {pos: [4, 2, 2], state: "minecraft:barrier"}, + {pos: [4, 2, 3], state: "minecraft:barrier"}, + {pos: [4, 2, 4], state: "minecraft:barrier"}, + {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:obsidian", + "minecraft:barrier", + "minecraft:air", + "computercraft:computer_normal{facing:east,state:off}" + ] +} From 87dfad026ed17fde83cfe7f1115bfb928cd12f0e Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 14 Aug 2024 22:41:31 +0100 Subject: [PATCH 09/26] Add a test for exploding turtles There's been a couple of bug reports in the past where the game would crash if a turtle is destroyed while breaking a block (typically due to the block exploding). This commit adds a test, to ensure that this is handled gracefully. I'm not entirely sure this is testing the right thing. Looking at the issues in question, it doesn't look like I ever managed to reproduce the bug. However, it's hopefully at least a quick sanity test to check we never break this case. --- .../computercraft/gametest/Turtle_Test.kt | 14 ++ .../computercraft/gametest/core/TestHooks.kt | 25 ++++ .../turtle_test.breaks_exploding_block.snbt | 139 ++++++++++++++++++ .../computercraft/gametest/core/TestMod.java | 2 + .../computercraft/gametest/core/TestMod.java | 5 + 5 files changed, 185 insertions(+) create mode 100644 projects/common/src/testMod/resources/data/cctest/structures/turtle_test.breaks_exploding_block.snbt 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 c02d84013..227ece5d0 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 @@ -693,6 +693,20 @@ class Turtle_Test { } } + /** + * Tests a turtle can break a block that explodes, causing the turtle itself to explode. + * + * This attempts to test [#585](https://github.com/cc-tweaked/CC-Tweaked/issues/585) and other similar issues. It's + * not clear if this is a good test case, as that bug does not seem reliably reproducible, but it's at least a good + * sanity check. + */ + @GameTest + fun Breaks_exploding_block(context: GameTestHelper) = context.sequence { + thenOnComputer { turtle.dig(Optional.empty()) } + thenIdle(2) + thenExecute { context.assertItemEntityPresent(ModRegistry.Items.TURTLE_NORMAL.get(), BlockPos(2, 2, 2), 1.0) } + } + /** * Render turtles as an item. */ diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/core/TestHooks.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/core/TestHooks.kt index e6ac3895d..149f947ce 100644 --- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/core/TestHooks.kt +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/core/TestHooks.kt @@ -13,7 +13,13 @@ import dan200.computercraft.shared.computer.core.ServerContext import net.minecraft.core.BlockPos import net.minecraft.gametest.framework.* import net.minecraft.server.MinecraftServer +import net.minecraft.server.level.ServerLevel import net.minecraft.world.level.GameRules +import net.minecraft.world.level.Level +import net.minecraft.world.level.LevelAccessor +import net.minecraft.world.level.block.Blocks +import net.minecraft.world.level.block.state.BlockState +import net.minecraft.world.phys.Vec3 import org.slf4j.Logger import org.slf4j.LoggerFactory import java.io.File @@ -173,4 +179,23 @@ object TestHooks { throw RuntimeException(e) } } + + /** + * Adds a hook that makes breaking a bone block spawn an explosion. + * + * It would be more Correct to register a custom block, but that's quite a lot of work, and doesn't seem worth it + * for test code. + * + * See also [Turtle_Test.Breaks_exploding_block]. + */ + @JvmStatic + fun onBeforeDestroyBlock(level: LevelAccessor, pos: BlockPos, state: BlockState): Boolean { + if (state.block === Blocks.BONE_BLOCK && level is ServerLevel) { + val explosionPos = Vec3.atCenterOf(pos) + level.explode(null, explosionPos.x, explosionPos.y, explosionPos.z, 4.0f, Level.ExplosionInteraction.TNT) + return true + } + + return false + } } diff --git a/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.breaks_exploding_block.snbt b/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.breaks_exploding_block.snbt new file mode 100644 index 000000000..2c20fc77a --- /dev/null +++ b/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.breaks_exploding_block.snbt @@ -0,0 +1,139 @@ +{ + DataVersion: 3465, + size: [5, 5, 5], + data: [ + {pos: [0, 0, 0], state: "minecraft:obsidian"}, + {pos: [0, 0, 1], state: "minecraft:obsidian"}, + {pos: [0, 0, 2], state: "minecraft:obsidian"}, + {pos: [0, 0, 3], state: "minecraft:obsidian"}, + {pos: [0, 0, 4], state: "minecraft:obsidian"}, + {pos: [1, 0, 0], state: "minecraft:obsidian"}, + {pos: [1, 0, 1], state: "minecraft:obsidian"}, + {pos: [1, 0, 2], state: "minecraft:obsidian"}, + {pos: [1, 0, 3], state: "minecraft:obsidian"}, + {pos: [1, 0, 4], state: "minecraft:obsidian"}, + {pos: [2, 0, 0], state: "minecraft:obsidian"}, + {pos: [2, 0, 1], state: "minecraft:obsidian"}, + {pos: [2, 0, 2], state: "minecraft:obsidian"}, + {pos: [2, 0, 3], state: "minecraft:obsidian"}, + {pos: [2, 0, 4], state: "minecraft:obsidian"}, + {pos: [3, 0, 0], state: "minecraft:obsidian"}, + {pos: [3, 0, 1], state: "minecraft:obsidian"}, + {pos: [3, 0, 2], state: "minecraft:obsidian"}, + {pos: [3, 0, 3], state: "minecraft:obsidian"}, + {pos: [3, 0, 4], state: "minecraft:obsidian"}, + {pos: [4, 0, 0], state: "minecraft:obsidian"}, + {pos: [4, 0, 1], state: "minecraft:obsidian"}, + {pos: [4, 0, 2], state: "minecraft:obsidian"}, + {pos: [4, 0, 3], state: "minecraft:obsidian"}, + {pos: [4, 0, 4], state: "minecraft:obsidian"}, + {pos: [0, 1, 0], state: "minecraft:barrier"}, + {pos: [0, 1, 1], state: "minecraft:barrier"}, + {pos: [0, 1, 2], state: "minecraft:barrier"}, + {pos: [0, 1, 3], state: "minecraft:barrier"}, + {pos: [0, 1, 4], state: "minecraft:barrier"}, + {pos: [1, 1, 0], state: "minecraft:barrier"}, + {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:barrier"}, + {pos: [2, 1, 0], state: "minecraft:barrier"}, + {pos: [2, 1, 1], state: "minecraft:air"}, + {pos: [2, 1, 2], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [], Label: "turtle_test.breaks_exploding_block", LeftUpgrade: "minecraft:diamond_pickaxe", LeftUpgradeNbt: {Tag: {Damage: 0}}, On: 1b, Owner: {LowerId: -5670393268852517359L, Name: "Player172", UpperId: 3578583684139923613L}, Slot: 0, id: "computercraft:turtle_normal"}}, + {pos: [2, 1, 3], state: "minecraft:bone_block{axis:y}"}, + {pos: [2, 1, 4], state: "minecraft:barrier"}, + {pos: [3, 1, 0], state: "minecraft:barrier"}, + {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:barrier"}, + {pos: [4, 1, 0], state: "minecraft:barrier"}, + {pos: [4, 1, 1], state: "minecraft:barrier"}, + {pos: [4, 1, 2], state: "minecraft:barrier"}, + {pos: [4, 1, 3], state: "minecraft:barrier"}, + {pos: [4, 1, 4], state: "minecraft:barrier"}, + {pos: [0, 2, 0], state: "minecraft:barrier"}, + {pos: [0, 2, 1], state: "minecraft:barrier"}, + {pos: [0, 2, 2], state: "minecraft:air"}, + {pos: [0, 2, 3], state: "minecraft:barrier"}, + {pos: [0, 2, 4], state: "minecraft:barrier"}, + {pos: [1, 2, 0], state: "minecraft:barrier"}, + {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:barrier"}, + {pos: [2, 2, 0], state: "minecraft:barrier"}, + {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:barrier"}, + {pos: [3, 2, 0], state: "minecraft:barrier"}, + {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:barrier"}, + {pos: [4, 2, 0], state: "minecraft:barrier"}, + {pos: [4, 2, 1], state: "minecraft:barrier"}, + {pos: [4, 2, 2], state: "minecraft:barrier"}, + {pos: [4, 2, 3], state: "minecraft:barrier"}, + {pos: [4, 2, 4], state: "minecraft:barrier"}, + {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:obsidian", + "minecraft:barrier", + "minecraft:bone_block{axis:y}", + "minecraft:air", + "computercraft:turtle_normal{facing:south,waterlogged:false}" + ] +} diff --git a/projects/fabric/src/testMod/java/dan200/computercraft/gametest/core/TestMod.java b/projects/fabric/src/testMod/java/dan200/computercraft/gametest/core/TestMod.java index 4a2ba819d..ac9068030 100644 --- a/projects/fabric/src/testMod/java/dan200/computercraft/gametest/core/TestMod.java +++ b/projects/fabric/src/testMod/java/dan200/computercraft/gametest/core/TestMod.java @@ -14,6 +14,7 @@ import net.fabricmc.fabric.api.event.Event; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents; import net.minecraft.gametest.framework.GameTestRegistry; import net.minecraft.resources.ResourceLocation; @@ -26,6 +27,7 @@ public void onInitialize() { ServerLifecycleEvents.SERVER_STARTED.addPhaseOrdering(Event.DEFAULT_PHASE, phase); ServerLifecycleEvents.SERVER_STARTED.register(phase, TestHooks::onServerStarted); CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> CCTestCommand.register(dispatcher)); + PlayerBlockBreakEvents.BEFORE.register((level, player, pos, state, blockEntity) -> !TestHooks.onBeforeDestroyBlock(level, pos, state)); TestHooks.loadTests(GameTestRegistry::register); } diff --git a/projects/forge/src/testMod/java/dan200/computercraft/gametest/core/TestMod.java b/projects/forge/src/testMod/java/dan200/computercraft/gametest/core/TestMod.java index ad295ffd1..67cf999bd 100644 --- a/projects/forge/src/testMod/java/dan200/computercraft/gametest/core/TestMod.java +++ b/projects/forge/src/testMod/java/dan200/computercraft/gametest/core/TestMod.java @@ -12,6 +12,7 @@ import net.minecraftforge.event.RegisterCommandsEvent; import net.minecraftforge.event.RegisterGameTestsEvent; import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.level.BlockEvent; import net.minecraftforge.event.server.ServerStartedEvent; import net.minecraftforge.eventbus.api.EventPriority; import net.minecraftforge.fml.DistExecutor; @@ -26,6 +27,10 @@ public TestMod() { var bus = MinecraftForge.EVENT_BUS; bus.addListener(EventPriority.LOW, (ServerStartedEvent e) -> TestHooks.onServerStarted(e.getServer())); bus.addListener((RegisterCommandsEvent e) -> CCTestCommand.register(e.getDispatcher())); + bus.addListener((BlockEvent.BreakEvent e) -> { + if (TestHooks.onBeforeDestroyBlock(e.getLevel(), e.getPos(), e.getState())) e.setCanceled(true); + }); + DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> TestMod::onInitializeClient); var modBus = FMLJavaModLoadingContext.get().getModEventBus(); From ed283155f7183b01ceb5d23846e9e07a784f88ef Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Thu, 15 Aug 2024 08:49:46 +0100 Subject: [PATCH 10/26] Update to Gradle 8.10 --- REUSE.toml | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 43504 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 5 ++++- gradlew.bat | 2 ++ 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/REUSE.toml b/REUSE.toml index 7bacbc129..401bf6c60 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -100,7 +100,7 @@ SPDX-License-Identifier = "CC0-1.0" path = ".github/**" [[annotations]] -path = ["gradle/wrapper/**", "gradlew", "gradlew.bat"] +path = ["gradle/wrapper/**"] SPDX-FileCopyrightText = "Gradle Inc" SPDX-License-Identifier = "Apache-2.0" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136f3d4ba8a0da8d277868979cfbc8ad796..2c3521197d7c4586c843d1d3e9090525f1898cde 100644 GIT binary patch delta 8703 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 Ojj7^59?Sdh$^Qpp*~F>< delta 8662 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 09523c0e5..9355b4155 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index b740cf133..f5feea6d6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 25da30dbd..9d21a2183 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## From 356c8e8aebdcd9ad9361a546c300effd8e8bb532 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Thu, 15 Aug 2024 09:03:33 +0100 Subject: [PATCH 11/26] Fix disk drives not setting/clearing removed flag This was originally noticed on 1.21, as it causes disk drives to not be detected as peripherals. However, things will still be broken (albeit more subtly) on 1.20, so worth fixing here. --- .../diskdrive/DiskDriveBlockEntity.java | 2 + .../cc/tweaked/linter/ExtraMustCallSuper.kt | 75 +++++++++++++++++++ ...m.google.errorprone.bugpatterns.BugChecker | 1 + 3 files changed, 78 insertions(+) create mode 100644 projects/lints/src/main/kotlin/cc/tweaked/linter/ExtraMustCallSuper.kt diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDriveBlockEntity.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDriveBlockEntity.java index 337ef87fa..bcd006c74 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDriveBlockEntity.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDriveBlockEntity.java @@ -96,11 +96,13 @@ public IPeripheral peripheral() { @Override public void clearRemoved() { + super.clearRemoved(); updateMedia(); } @Override public void setRemoved() { + super.setRemoved(); if (recordPlaying) stopRecord(); } diff --git a/projects/lints/src/main/kotlin/cc/tweaked/linter/ExtraMustCallSuper.kt b/projects/lints/src/main/kotlin/cc/tweaked/linter/ExtraMustCallSuper.kt new file mode 100644 index 000000000..8b93e49c2 --- /dev/null +++ b/projects/lints/src/main/kotlin/cc/tweaked/linter/ExtraMustCallSuper.kt @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +@file:Suppress("JAVA_MODULE_DOES_NOT_EXPORT_PACKAGE") + +package cc.tweaked.linter + +import com.google.errorprone.BugPattern +import com.google.errorprone.VisitorState +import com.google.errorprone.bugpatterns.BugChecker +import com.google.errorprone.matchers.Description +import com.google.errorprone.util.ASTHelpers +import com.sun.source.tree.* +import com.sun.source.util.TreeScanner +import com.sun.tools.javac.code.Symbol.MethodSymbol +import javax.lang.model.element.Modifier + +@BugPattern( + summary = "Checks that a methods invoke their super method.", + explanation = """ + This extends ErrorProne's built in "MustCallSuper" with several additional Minecraft-specific methods. + """, + severity = BugPattern.SeverityLevel.ERROR, + tags = [BugPattern.StandardTags.LIKELY_ERROR], +) +class ExtraMustCallSuper : BugChecker(), BugChecker.MethodTreeMatcher { + companion object { + private val REQUIRED_METHODS = setOf( + MethodReference("net.minecraft.world.level.block.entity.BlockEntity", "setRemoved"), + MethodReference("net.minecraft.world.level.block.entity.BlockEntity", "clearRemoved"), + ) + } + + override fun matchMethod(tree: MethodTree, state: VisitorState): Description { + val methodSym: MethodSymbol = ASTHelpers.getSymbol(tree) + if (methodSym.modifiers.contains(Modifier.ABSTRACT)) return Description.NO_MATCH + + val superMethod: MethodReference = findRequiredSuper(methodSym, state) ?: return Description.NO_MATCH + val foundSuper = SuperScanner(superMethod.method).scan(tree, Unit) ?: false + if (foundSuper) return Description.NO_MATCH + + return buildDescription(tree) + .setMessage("This method overrides %s#%s but does not call the super method.".format(superMethod.owner, superMethod.method)) + .build() + } + + private fun findRequiredSuper(method: MethodSymbol, state: VisitorState): MethodReference? { + for (superMethod in ASTHelpers.findSuperMethods(method, state.types)) { + val superName = MethodReference(superMethod.owner.qualifiedName.toString(), superMethod.name.toString()) + if (REQUIRED_METHODS.contains(superName)) return superName + } + return null + } + + private data class MethodReference(val owner: String, val method: String) + + private class SuperScanner(private val methodName: String) : TreeScanner() { + // Skip visiting other elements. + override fun visitClass(tree: ClassTree, state: Unit): Boolean = false + override fun visitLambdaExpression(tree: LambdaExpressionTree, state: Unit): Boolean = false + + override fun visitMethodInvocation(tree: MethodInvocationTree, state: Unit): Boolean? { + val methodSelect: ExpressionTree = tree.methodSelect + if (methodSelect.kind == Tree.Kind.MEMBER_SELECT) { + val memberSelect = methodSelect as MemberSelectTree + if (ASTHelpers.isSuper(memberSelect.expression) && memberSelect.identifier.contentEquals(methodName)) return true + } + + return super.visitMethodInvocation(tree, state) + } + + override fun reduce(r1: Boolean?, r2: Boolean?): Boolean = (r1 ?: false) || (r2 ?: false) + } +} diff --git a/projects/lints/src/main/resources/META-INF/services/com.google.errorprone.bugpatterns.BugChecker b/projects/lints/src/main/resources/META-INF/services/com.google.errorprone.bugpatterns.BugChecker index db10e66d9..388a1372a 100644 --- a/projects/lints/src/main/resources/META-INF/services/com.google.errorprone.bugpatterns.BugChecker +++ b/projects/lints/src/main/resources/META-INF/services/com.google.errorprone.bugpatterns.BugChecker @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers # # SPDX-License-Identifier: MPL-2.0 +cc.tweaked.linter.ExtraMustCallSuper cc.tweaked.linter.LoaderOverride cc.tweaked.linter.MissingLoaderOverride cc.tweaked.linter.SideChecker From b7a8432cfb7106475f0511b58e89690c526868b9 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Thu, 15 Aug 2024 10:32:54 +0100 Subject: [PATCH 12/26] Fix turtles capturing their own drops when broken There's a whole load of gnarly issues that occur when a turtle is broken mid-dig/attack (normally due to an explosion). We fixed most of these in 24af36743d08fcdb58439c52bf587b33ed828263, but not perfectly. Part of the fix here was to not capture drops if the turtle BE has been removed. However, on removal, turtles drop their items *before* removing the BE. This meant that the drop consumer still triggered, and attempted to insert items back into the turtle. This bug only triggers if the turtle contains a stack larger than 10 (ish, I think) items, which is possibly why I'd never reproduced before. We now drop items after removing the BE, which resolves the issue. Fixes #1936. --- .../shared/turtle/blocks/TurtleBlock.java | 10 +++++++--- .../dan200/computercraft/gametest/Turtle_Test.kt | 9 +++++---- .../structures/turtle_test.breaks_exploding_block.snbt | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) 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 4694a8183..2456a76ae 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 @@ -125,11 +125,15 @@ public BlockState updateShape(BlockState state, Direction side, BlockState other public final void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) { if (state.is(newState.getBlock())) return; - if (!level.isClientSide && level.getBlockEntity(pos) instanceof TurtleBlockEntity turtle && !turtle.hasMoved()) { - Containers.dropContents(level, pos, turtle); - } + // Most blocks drop items and then remove the BE. However, if a turtle is consuming drops right now, that can + // lead to loops where it tries to insert an item back into the inventory. To prevent this, take a reference to + // the turtle BE now, remove it, and then drop the items. + var turtle = !level.isClientSide && level.getBlockEntity(pos) instanceof TurtleBlockEntity t && !t.hasMoved() + ? t : null; super.onRemove(state, level, pos, newState, isMoving); + + if (turtle != null) Containers.dropContents(level, pos, turtle); } @Override 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 227ece5d0..e1619029f 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 @@ -696,15 +696,16 @@ class Turtle_Test { /** * Tests a turtle can break a block that explodes, causing the turtle itself to explode. * - * This attempts to test [#585](https://github.com/cc-tweaked/CC-Tweaked/issues/585) and other similar issues. It's - * not clear if this is a good test case, as that bug does not seem reliably reproducible, but it's at least a good - * sanity check. + * @see [#585](https://github.com/cc-tweaked/CC-Tweaked/issues/585). */ @GameTest fun Breaks_exploding_block(context: GameTestHelper) = context.sequence { thenOnComputer { turtle.dig(Optional.empty()) } thenIdle(2) - thenExecute { context.assertItemEntityPresent(ModRegistry.Items.TURTLE_NORMAL.get(), BlockPos(2, 2, 2), 1.0) } + thenExecute { + context.assertItemEntityCountIs(ModRegistry.Items.TURTLE_NORMAL.get(), BlockPos(2, 2, 2), 1.0, 1) + context.assertItemEntityCountIs(Items.BONE_BLOCK, BlockPos(2, 2, 2), 1.0, 65) + } } /** diff --git a/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.breaks_exploding_block.snbt b/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.breaks_exploding_block.snbt index 2c20fc77a..3e315e552 100644 --- a/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.breaks_exploding_block.snbt +++ b/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.breaks_exploding_block.snbt @@ -39,7 +39,7 @@ {pos: [1, 1, 4], state: "minecraft:barrier"}, {pos: [2, 1, 0], state: "minecraft:barrier"}, {pos: [2, 1, 1], state: "minecraft:air"}, - {pos: [2, 1, 2], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [], Label: "turtle_test.breaks_exploding_block", LeftUpgrade: "minecraft:diamond_pickaxe", LeftUpgradeNbt: {Tag: {Damage: 0}}, On: 1b, Owner: {LowerId: -5670393268852517359L, Name: "Player172", UpperId: 3578583684139923613L}, Slot: 0, id: "computercraft:turtle_normal"}}, + {pos: [2, 1, 2], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [], Label: "turtle_test.breaks_exploding_block", LeftUpgrade: "minecraft:diamond_pickaxe", LeftUpgradeNbt: {Tag: {Damage: 0}}, On: 1b, Owner: {LowerId: -5670393268852517359L, Name: "Player172", UpperId: 3578583684139923613L}, Slot: 0, Items: [{Count: 64b, Slot: 0b, id: "minecraft:bone_block"}], id: "computercraft:turtle_normal"}}, {pos: [2, 1, 3], state: "minecraft:bone_block{axis:y}"}, {pos: [2, 1, 4], state: "minecraft:barrier"}, {pos: [3, 1, 0], state: "minecraft:barrier"}, From 7e53c19d74153b4d59ec72d0eaf66b5ce2689c99 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 27 Jul 2024 14:21:09 +0100 Subject: [PATCH 13/26] Add a specialised menu for printouts Rather than having a general "held-item" container, we now have a specialised one for printouts. This now is a little more general, supporting any container (not just the player inventory), and syncs the current page via a data slot. Currently this isn't especially useful, but should make it a little easier to add lectern support in the future. --- .../client/gui/PrintoutScreen.java | 107 ++++++++++---- .../computercraft/shared/ModRegistry.java | 10 +- .../shared/common/HeldItemMenu.java | 68 --------- .../shared/media/PrintoutMenu.java | 136 ++++++++++++++++++ .../shared/media/items/PrintoutItem.java | 13 +- .../container/HeldItemContainerData.java | 37 ----- 6 files changed, 229 insertions(+), 142 deletions(-) delete mode 100644 projects/common/src/main/java/dan200/computercraft/shared/common/HeldItemMenu.java create mode 100644 projects/common/src/main/java/dan200/computercraft/shared/media/PrintoutMenu.java delete mode 100644 projects/common/src/main/java/dan200/computercraft/shared/network/container/HeldItemContainerData.java diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/PrintoutScreen.java b/projects/common/src/client/java/dan200/computercraft/client/gui/PrintoutScreen.java index 8ed3b8f64..626ee07d0 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/gui/PrintoutScreen.java +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/PrintoutScreen.java @@ -6,15 +6,22 @@ import com.mojang.blaze3d.vertex.Tesselator; import dan200.computercraft.core.terminal.TextBuffer; -import dan200.computercraft.shared.common.HeldItemMenu; +import dan200.computercraft.shared.ModRegistry; +import dan200.computercraft.shared.media.PrintoutMenu; import dan200.computercraft.shared.media.items.PrintoutItem; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerListener; +import net.minecraft.world.item.ItemStack; import org.lwjgl.glfw.GLFW; +import java.util.Arrays; +import java.util.Objects; + import static dan200.computercraft.client.render.PrintoutRenderer.*; import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMAP; @@ -23,40 +30,75 @@ * * @see dan200.computercraft.client.render.PrintoutRenderer */ -public class PrintoutScreen extends AbstractContainerScreen { - private final boolean book; - private final int pages; - private final TextBuffer[] text; - private final TextBuffer[] colours; - private int page; - - public PrintoutScreen(HeldItemMenu container, Inventory player, Component title) { - super(container, player, title); +public final class PrintoutScreen extends AbstractContainerScreen implements ContainerListener { + private PrintoutInfo printout = PrintoutInfo.DEFAULT; + private int page = 0; + public PrintoutScreen(PrintoutMenu container, Inventory player, Component title) { + super(container, player, title); imageHeight = Y_SIZE; + } + + private void setPrintout(ItemStack stack) { + var text = PrintoutItem.getText(stack); + var textBuffers = new TextBuffer[text.length]; + for (var i = 0; i < textBuffers.length; i++) textBuffers[i] = new TextBuffer(text[i]); + + var colours = PrintoutItem.getColours(stack); + var colourBuffers = new TextBuffer[colours.length]; + for (var i = 0; i < colours.length; i++) colourBuffers[i] = new TextBuffer(colours[i]); + + var pages = Math.max(text.length / PrintoutItem.LINES_PER_PAGE, 1); + var book = stack.is(ModRegistry.Items.PRINTED_BOOK.get()); + + printout = new PrintoutInfo(pages, book, textBuffers, colourBuffers); + } - var text = PrintoutItem.getText(container.getStack()); - this.text = new TextBuffer[text.length]; - for (var i = 0; i < this.text.length; i++) this.text[i] = new TextBuffer(text[i]); + @Override + protected void init() { + super.init(); + menu.addSlotListener(this); + } - var colours = PrintoutItem.getColours(container.getStack()); - this.colours = new TextBuffer[colours.length]; - for (var i = 0; i < this.colours.length; i++) this.colours[i] = new TextBuffer(colours[i]); + @Override + public void removed() { + menu.removeSlotListener(this); + } - page = 0; - pages = Math.max(this.text.length / PrintoutItem.LINES_PER_PAGE, 1); - book = ((PrintoutItem) container.getStack().getItem()).getType() == PrintoutItem.Type.BOOK; + @Override + public void slotChanged(AbstractContainerMenu menu, int slot, ItemStack stack) { + if (slot == 0) setPrintout(stack); + } + + @Override + public void dataChanged(AbstractContainerMenu menu, int slot, int data) { + if (slot == PrintoutMenu.DATA_CURRENT_PAGE) page = data; + } + + private void setPage(int page) { + this.page = page; + + var gameMode = Objects.requireNonNull(Objects.requireNonNull(minecraft).gameMode); + gameMode.handleInventoryButtonClick(menu.containerId, PrintoutMenu.PAGE_BUTTON_OFFSET + page); + } + + private void previousPage() { + if (page > 0) setPage(page - 1); + } + + private void nextPage() { + if (page < printout.pages() - 1) setPage(page + 1); } @Override public boolean keyPressed(int key, int scancode, int modifiers) { if (key == GLFW.GLFW_KEY_RIGHT) { - if (page < pages - 1) page++; + nextPage(); return true; } if (key == GLFW.GLFW_KEY_LEFT) { - if (page > 0) page--; + previousPage(); return true; } @@ -68,13 +110,13 @@ public boolean mouseScrolled(double x, double y, double delta) { if (super.mouseScrolled(x, y, delta)) return true; if (delta < 0) { // Scroll up goes to the next page - if (page < pages - 1) page++; + nextPage(); return true; } if (delta > 0) { // Scroll down goes to the previous page - if (page > 0) page--; + previousPage(); return true; } @@ -85,8 +127,9 @@ public boolean mouseScrolled(double x, double y, double delta) { protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) { // Draw the printout var renderer = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); - drawBorder(graphics.pose(), renderer, leftPos, topPos, 0, page, pages, book, FULL_BRIGHT_LIGHTMAP); - drawText(graphics.pose(), renderer, leftPos + X_TEXT_MARGIN, topPos + Y_TEXT_MARGIN, PrintoutItem.LINES_PER_PAGE * page, FULL_BRIGHT_LIGHTMAP, text, colours); + + drawBorder(graphics.pose(), renderer, leftPos, topPos, 0, page, printout.pages(), printout.book(), FULL_BRIGHT_LIGHTMAP); + drawText(graphics.pose(), renderer, leftPos + X_TEXT_MARGIN, topPos + Y_TEXT_MARGIN, PrintoutItem.LINES_PER_PAGE * page, FULL_BRIGHT_LIGHTMAP, printout.text(), printout.colour()); renderer.endBatch(); } @@ -105,4 +148,18 @@ public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTi protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { // Skip rendering labels. } + + record PrintoutInfo(int pages, boolean book, TextBuffer[] text, TextBuffer[] colour) { + public static final PrintoutInfo DEFAULT; + + static { + var textLines = new TextBuffer[PrintoutItem.LINES_PER_PAGE]; + Arrays.fill(textLines, new TextBuffer(" ".repeat(PrintoutItem.LINE_MAX_LENGTH))); + + var colourLines = new TextBuffer[PrintoutItem.LINES_PER_PAGE]; + Arrays.fill(colourLines, new TextBuffer("f".repeat(PrintoutItem.LINE_MAX_LENGTH))); + + DEFAULT = new PrintoutInfo(1, false, textLines, colourLines); + } + } } 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 12446bb61..9d16b6ee3 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java @@ -23,7 +23,6 @@ import dan200.computercraft.shared.common.ClearColourRecipe; import dan200.computercraft.shared.common.ColourableRecipe; import dan200.computercraft.shared.common.DefaultBundledRedstoneProvider; -import dan200.computercraft.shared.common.HeldItemMenu; import dan200.computercraft.shared.computer.apis.CommandAPI; import dan200.computercraft.shared.computer.blocks.CommandComputerBlock; import dan200.computercraft.shared.computer.blocks.ComputerBlock; @@ -41,6 +40,7 @@ import dan200.computercraft.shared.details.BlockDetails; import dan200.computercraft.shared.details.ItemDetails; import dan200.computercraft.shared.integration.PermissionRegistry; +import dan200.computercraft.shared.media.PrintoutMenu; import dan200.computercraft.shared.media.items.DiskItem; import dan200.computercraft.shared.media.items.PrintoutItem; import dan200.computercraft.shared.media.items.RecordMedia; @@ -49,7 +49,6 @@ import dan200.computercraft.shared.media.recipes.PrintoutRecipe; import dan200.computercraft.shared.network.container.ComputerContainerData; import dan200.computercraft.shared.network.container.ContainerData; -import dan200.computercraft.shared.network.container.HeldItemContainerData; import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveBlock; import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveBlockEntity; import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveMenu; @@ -309,11 +308,8 @@ public static class Menus { public static final RegistryEntry> PRINTER = REGISTRY.register("printer", () -> new MenuType<>(PrinterMenu::new, FeatureFlags.VANILLA_SET)); - public static final RegistryEntry> PRINTOUT = REGISTRY.register("printout", - () -> ContainerData.toType( - HeldItemContainerData::new, - (id, inventory, data) -> new HeldItemMenu(Menus.PRINTOUT.get(), id, inventory.player, data.getHand()) - )); + public static final RegistryEntry> PRINTOUT = REGISTRY.register("printout", + () -> new MenuType<>((i, c) -> PrintoutMenu.createRemote(i), FeatureFlags.VANILLA_SET)); } static class ArgumentTypes { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/common/HeldItemMenu.java b/projects/common/src/main/java/dan200/computercraft/shared/common/HeldItemMenu.java deleted file mode 100644 index 7a4c5895d..000000000 --- a/projects/common/src/main/java/dan200/computercraft/shared/common/HeldItemMenu.java +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. -// -// SPDX-License-Identifier: LicenseRef-CCPL - -package dan200.computercraft.shared.common; - -import net.minecraft.network.chat.Component; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.MenuProvider; -import net.minecraft.world.entity.player.Inventory; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.inventory.AbstractContainerMenu; -import net.minecraft.world.inventory.MenuType; -import net.minecraft.world.item.ItemStack; - -import javax.annotation.Nullable; - -public class HeldItemMenu extends AbstractContainerMenu { - private final ItemStack stack; - private final InteractionHand hand; - - public HeldItemMenu(MenuType type, int id, Player player, InteractionHand hand) { - super(type, id); - - this.hand = hand; - stack = player.getItemInHand(hand).copy(); - } - - public ItemStack getStack() { - return stack; - } - - @Override - public ItemStack quickMoveStack(Player player, int slot) { - return ItemStack.EMPTY; - } - - @Override - public boolean stillValid(Player player) { - if (!player.isAlive()) return false; - - var stack = player.getItemInHand(hand); - return stack == this.stack || !stack.isEmpty() && !this.stack.isEmpty() && stack.getItem() == this.stack.getItem(); - } - - public static class Factory implements MenuProvider { - private final MenuType type; - private final Component name; - private final InteractionHand hand; - - public Factory(MenuType type, ItemStack stack, InteractionHand hand) { - this.type = type; - name = stack.getHoverName(); - this.hand = hand; - } - - @Override - public Component getDisplayName() { - return name; - } - - @Nullable - @Override - public AbstractContainerMenu createMenu(int id, Inventory inventory, Player player) { - return new HeldItemMenu(type, id, player, hand); - } - } -} diff --git a/projects/common/src/main/java/dan200/computercraft/shared/media/PrintoutMenu.java b/projects/common/src/main/java/dan200/computercraft/shared/media/PrintoutMenu.java new file mode 100644 index 000000000..ffa7097bb --- /dev/null +++ b/projects/common/src/main/java/dan200/computercraft/shared/media/PrintoutMenu.java @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.media; + +import dan200.computercraft.shared.ModRegistry; +import dan200.computercraft.shared.container.InvisibleSlot; +import dan200.computercraft.shared.media.items.PrintoutItem; +import net.minecraft.util.Mth; +import net.minecraft.world.Container; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.*; +import net.minecraft.world.item.ItemStack; + +import java.util.function.Predicate; + +/** + * The menus for {@linkplain PrintoutItem printouts}. + *

+ * This is a somewhat similar design to {@link LecternMenu}, which is used to read written books. + *

+ * This holds a single slot (containing the printout), and a single data slot ({@linkplain #DATA_CURRENT_PAGE holding + * the current page}). The page is set by the client by sending a {@linkplain #clickMenuButton(Player, int) button + * press} with an index of {@link #PAGE_BUTTON_OFFSET} plus the current page. + *

+ * The client-side screen uses {@linkplain ContainerListener container listeners} to subscribe to item and page changes. + * However, listeners aren't fired on the client, so we copy {@link LecternMenu}'s hack and call + * {@link #broadcastChanges()} whenever an item or data value are changed. + */ +public class PrintoutMenu extends AbstractContainerMenu { + public static final int DATA_CURRENT_PAGE = 0; + private static final int DATA_SIZE = 1; + + public static final int PAGE_BUTTON_OFFSET = 100; + + private final Predicate valid; + private final ContainerData currentPage; + + public PrintoutMenu( + int containerId, Container container, int slotIdx, Predicate valid, ContainerData currentPage + ) { + super(ModRegistry.Menus.PRINTOUT.get(), containerId); + this.valid = valid; + this.currentPage = currentPage; + + addSlot(new InvisibleSlot(container, slotIdx) { + @Override + public void setChanged() { + super.setChanged(); + slotsChanged(container); // Trigger listeners on the client. + } + }); + addDataSlots(currentPage); + } + + /** + * Create {@link PrintoutMenu} for use a remote (client). + * + * @param containerId The current container id. + * @return The constructed container. + */ + public static PrintoutMenu createRemote(int containerId) { + return new PrintoutMenu(containerId, new SimpleContainer(1), 0, p -> true, new SimpleContainerData(DATA_SIZE)); + } + + /** + * Create a {@link PrintoutMenu} for the printout in the current player's hand. + * + * @param containerId The current container id. + * @param player The player to open the container. + * @param hand The hand containing the item. + * @return The constructed container. + */ + public static PrintoutMenu createInHand(int containerId, Player player, InteractionHand hand) { + var currentStack = player.getItemInHand(hand); + var currentItem = currentStack.getItem(); + + var slot = switch (hand) { + case MAIN_HAND -> player.getInventory().selected; + case OFF_HAND -> Inventory.SLOT_OFFHAND; + }; + return new PrintoutMenu( + containerId, player.getInventory(), slot, + p -> player.getItemInHand(hand).getItem() == currentItem, new SimpleContainerData(DATA_SIZE) + ); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + return ItemStack.EMPTY; + } + + @Override + public boolean stillValid(Player player) { + return valid.test(player); + } + + @Override + public boolean clickMenuButton(Player player, int id) { + if (id >= PAGE_BUTTON_OFFSET) { + var page = Mth.clamp(id - PAGE_BUTTON_OFFSET, 0, PrintoutItem.getPageCount(getPrintout()) - 1); + setData(DATA_CURRENT_PAGE, page); + return true; + } + + return super.clickMenuButton(player, id); + } + + /** + * Get the current printout. + * + * @return The current printout. + */ + public ItemStack getPrintout() { + return getSlot(0).getItem(); + } + + /** + * Get the current page. + * + * @return The current page. + */ + public int getPage() { + return currentPage.get(DATA_CURRENT_PAGE); + } + + @Override + public void setData(int id, int data) { + super.setData(id, data); + broadcastChanges(); // Trigger listeners on the client. + } +} diff --git a/projects/common/src/main/java/dan200/computercraft/shared/media/items/PrintoutItem.java b/projects/common/src/main/java/dan200/computercraft/shared/media/items/PrintoutItem.java index c99f2b756..31d2a6e86 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/media/items/PrintoutItem.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/media/items/PrintoutItem.java @@ -4,13 +4,14 @@ package dan200.computercraft.shared.media.items; +import com.google.common.base.Strings; import dan200.computercraft.shared.ModRegistry; -import dan200.computercraft.shared.common.HeldItemMenu; -import dan200.computercraft.shared.network.container.HeldItemContainerData; +import dan200.computercraft.shared.media.PrintoutMenu; import net.minecraft.network.chat.Component; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.SimpleMenuProvider; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; @@ -51,11 +52,13 @@ public void appendHoverText(ItemStack stack, @Nullable Level world, List use(Level world, Player player, InteractionHand hand) { + var stack = player.getItemInHand(hand); if (!world.isClientSide) { - new HeldItemContainerData(hand) - .open(player, new HeldItemMenu.Factory(ModRegistry.Menus.PRINTOUT.get(), player.getItemInHand(hand), hand)); + var title = getTitle(stack); + var displayTitle = Strings.isNullOrEmpty(title) ? stack.getDisplayName() : Component.literal(title); + player.openMenu(new SimpleMenuProvider((id, playerInventory, p) -> PrintoutMenu.createInHand(id, p, hand), displayTitle)); } - return new InteractionResultHolder<>(InteractionResult.sidedSuccess(world.isClientSide), player.getItemInHand(hand)); + return new InteractionResultHolder<>(InteractionResult.sidedSuccess(world.isClientSide), stack); } private ItemStack createFromTitleAndText(@Nullable String title, @Nullable String[] text, @Nullable String[] colours) { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/network/container/HeldItemContainerData.java b/projects/common/src/main/java/dan200/computercraft/shared/network/container/HeldItemContainerData.java deleted file mode 100644 index 8bbb6522b..000000000 --- a/projects/common/src/main/java/dan200/computercraft/shared/network/container/HeldItemContainerData.java +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.shared.network.container; - -import dan200.computercraft.shared.common.HeldItemMenu; -import dan200.computercraft.shared.media.items.PrintoutItem; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.world.InteractionHand; - -/** - * Opens a printout GUI based on the currently held item. - * - * @see HeldItemMenu - * @see PrintoutItem - */ -public class HeldItemContainerData implements ContainerData { - private final InteractionHand hand; - - public HeldItemContainerData(InteractionHand hand) { - this.hand = hand; - } - - public HeldItemContainerData(FriendlyByteBuf buffer) { - hand = buffer.readEnum(InteractionHand.class); - } - - @Override - public void toBytes(FriendlyByteBuf buf) { - buf.writeEnum(hand); - } - - public InteractionHand getHand() { - return hand; - } -} From aa8078ddebf8c6b54fc33b090eefd43b34bc3fe7 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Thu, 15 Aug 2024 21:17:37 +0100 Subject: [PATCH 14/26] Allow placing printouts in lecterns - Add a new custom lectern block, that is used to hold the printed pages. We have to roll quite a lot of custom logic, so this is much cleaner than trying to mixin to the existing lectern code. - Add a new (entity) model for printed pages and books placed on a lectern. I did originally think about just rendering the item (or the in-hand/map version), but I think this is a bit more consistent with vanilla. However, we do still need to sync the item to the client (mostly to get the current page count!). There is a risk of chunkbanning here, but I think it's much harder than vanilla, due to the significantly reduced page limit. --- .../computercraft/client/ClientRegistry.java | 2 + .../client/model/LecternPrintoutModel.java | 117 +++++++++++ .../client/render/CustomLecternRenderer.java | 51 +++++ .../data/client/ClientDataProviders.java | 4 +- .../computercraft/blockstates/lectern.json | 8 + .../assets/minecraft/atlases/blocks.json | 3 +- .../loot_tables/blocks/lectern.json | 12 ++ .../data/BlockModelProvider.java | 6 + .../computercraft/data/LanguageProvider.java | 4 +- .../computercraft/data/LootTableProvider.java | 3 + .../computercraft/data/TagProvider.java | 6 + .../computercraft/shared/ModRegistry.java | 10 + .../shared/container/BasicContainer.java | 5 +- .../shared/lectern/CustomLecternBlock.java | 142 +++++++++++++ .../lectern/CustomLecternBlockEntity.java | 193 ++++++++++++++++++ .../shared/media/items/PrintoutItem.java | 20 ++ .../textures/entity/printout.png | Bin 0 -> 212 bytes .../textures/entity/printout.png.license | 3 + .../minecraft/tags/items/lectern_books.json | 4 + .../minecraft/tags/items/lectern_books.json | 1 + 20 files changed, 589 insertions(+), 5 deletions(-) create mode 100644 projects/common/src/client/java/dan200/computercraft/client/model/LecternPrintoutModel.java create mode 100644 projects/common/src/client/java/dan200/computercraft/client/render/CustomLecternRenderer.java create mode 100644 projects/common/src/generated/resources/assets/computercraft/blockstates/lectern.json create mode 100644 projects/common/src/generated/resources/data/computercraft/loot_tables/blocks/lectern.json create mode 100644 projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlock.java create mode 100644 projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlockEntity.java create mode 100644 projects/common/src/main/resources/assets/computercraft/textures/entity/printout.png create mode 100644 projects/common/src/main/resources/assets/computercraft/textures/entity/printout.png.license create mode 100644 projects/fabric/src/generated/resources/data/minecraft/tags/items/lectern_books.json create mode 100644 projects/forge/src/generated/resources/data/minecraft/tags/items/lectern_books.json diff --git a/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java b/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java index 405f810ab..b1560568c 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java +++ b/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java @@ -13,6 +13,7 @@ import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller; import dan200.computercraft.client.gui.*; import dan200.computercraft.client.pocket.ClientPocketComputers; +import dan200.computercraft.client.render.CustomLecternRenderer; import dan200.computercraft.client.render.RenderTypes; import dan200.computercraft.client.render.TurtleBlockEntityRenderer; import dan200.computercraft.client.render.monitor.MonitorBlockEntityRenderer; @@ -73,6 +74,7 @@ public static void register() { BlockEntityRenderers.register(ModRegistry.BlockEntities.MONITOR_ADVANCED.get(), MonitorBlockEntityRenderer::new); BlockEntityRenderers.register(ModRegistry.BlockEntities.TURTLE_NORMAL.get(), TurtleBlockEntityRenderer::new); BlockEntityRenderers.register(ModRegistry.BlockEntities.TURTLE_ADVANCED.get(), TurtleBlockEntityRenderer::new); + BlockEntityRenderers.register(ModRegistry.BlockEntities.LECTERN.get(), CustomLecternRenderer::new); } /** diff --git a/projects/common/src/client/java/dan200/computercraft/client/model/LecternPrintoutModel.java b/projects/common/src/client/java/dan200/computercraft/client/model/LecternPrintoutModel.java new file mode 100644 index 000000000..5e9930094 --- /dev/null +++ b/projects/common/src/client/java/dan200/computercraft/client/model/LecternPrintoutModel.java @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.client.model; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.client.render.CustomLecternRenderer; +import dan200.computercraft.shared.media.items.PrintoutItem; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.model.geom.PartPose; +import net.minecraft.client.model.geom.builders.CubeListBuilder; +import net.minecraft.client.model.geom.builders.MeshDefinition; +import net.minecraft.client.resources.model.Material; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.inventory.InventoryMenu; + +import java.util.List; + +/** + * A model for {@linkplain PrintoutItem printouts} placed on a lectern. + *

+ * This provides two models, {@linkplain #renderPages(PoseStack, VertexConsumer, int, int, int) one for a variable + * number of pages}, and {@linkplain #renderBook(PoseStack, VertexConsumer, int, int) one for books}. + * + * @see CustomLecternRenderer + */ +public class LecternPrintoutModel { + public static final ResourceLocation TEXTURE = new ResourceLocation(ComputerCraftAPI.MOD_ID, "entity/printout"); + public static final Material MATERIAL = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE); + + private static final int TEXTURE_WIDTH = 32; + private static final int TEXTURE_HEIGHT = 32; + + private static final String PAGE_1 = "page_1"; + private static final String PAGE_2 = "page_2"; + private static final String PAGE_3 = "page_3"; + private static final List PAGES = List.of(PAGE_1, PAGE_2, PAGE_3); + + private final ModelPart pagesRoot; + private final ModelPart bookRoot; + private final ModelPart[] pages; + + public LecternPrintoutModel() { + pagesRoot = buildPages(); + bookRoot = buildBook(); + pages = PAGES.stream().map(pagesRoot::getChild).toArray(ModelPart[]::new); + } + + private static ModelPart buildPages() { + var mesh = new MeshDefinition(); + var parts = mesh.getRoot(); + parts.addOrReplaceChild( + PAGE_1, + CubeListBuilder.create().texOffs(0, 0).addBox(-0.005f, -4.0f, -2.5f, 1f, 8.0f, 5.0f), + PartPose.ZERO + ); + + parts.addOrReplaceChild( + PAGE_2, + CubeListBuilder.create().texOffs(12, 0).addBox(-0.005f, -4.0f, -2.5f, 1f, 8.0f, 5.0f), + PartPose.offsetAndRotation(-0.125f, 0, 1.5f, (float) Math.PI * (1f / 16), 0, 0) + ); + parts.addOrReplaceChild( + PAGE_3, + CubeListBuilder.create().texOffs(12, 0).addBox(-0.005f, -4.0f, -2.5f, 1f, 8.0f, 5.0f), + PartPose.offsetAndRotation(-0.25f, 0, -1.5f, (float) -Math.PI * (2f / 16), 0, 0) + ); + + return mesh.getRoot().bake(TEXTURE_WIDTH, TEXTURE_HEIGHT); + } + + private static ModelPart buildBook() { + var mesh = new MeshDefinition(); + var parts = mesh.getRoot(); + + parts.addOrReplaceChild( + "spine", + CubeListBuilder.create().texOffs(12, 15).addBox(-0.005f, -5.0f, -0.5f, 0, 10, 1.0f), + PartPose.ZERO + ); + + var angle = (float) Math.toRadians(5); + parts.addOrReplaceChild( + "left", + CubeListBuilder.create() + .texOffs(0, 10).addBox(0, -5.0f, -6.0f, 0, 10, 6.0f) + .texOffs(0, 0).addBox(0.005f, -4.0f, -5.0f, 1.0f, 8.0f, 5.0f), + PartPose.offsetAndRotation(-0.005f, 0, -0.5f, 0, -angle, 0) + ); + + parts.addOrReplaceChild( + "right", + CubeListBuilder.create() + .texOffs(14, 10).addBox(0, -5.0f, 0, 0, 10, 6.0f) + .texOffs(0, 0).addBox(0.005f, -4.0f, 0, 1.0f, 8.0f, 5.0f), + PartPose.offsetAndRotation(-0.005f, 0, 0.5f, 0, angle, 0) + ); + + return mesh.getRoot().bake(TEXTURE_WIDTH, TEXTURE_HEIGHT); + } + + public void renderBook(PoseStack poseStack, VertexConsumer buffer, int packedLight, int packedOverlay) { + bookRoot.render(poseStack, buffer, packedLight, packedOverlay, 1, 1, 1, 1); + } + + public void renderPages(PoseStack poseStack, VertexConsumer buffer, int packedLight, int packedOverlay, int pageCount) { + if (pageCount > pages.length) pageCount = pages.length; + var i = 0; + for (; i < pageCount; i++) pages[i].visible = true; + for (; i < pages.length; i++) pages[i].visible = false; + + pagesRoot.render(poseStack, buffer, packedLight, packedOverlay, 1, 1, 1, 1); + } +} diff --git a/projects/common/src/client/java/dan200/computercraft/client/render/CustomLecternRenderer.java b/projects/common/src/client/java/dan200/computercraft/client/render/CustomLecternRenderer.java new file mode 100644 index 000000000..1743d2bfd --- /dev/null +++ b/projects/common/src/client/java/dan200/computercraft/client/render/CustomLecternRenderer.java @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.client.render; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import dan200.computercraft.client.model.LecternPrintoutModel; +import dan200.computercraft.shared.lectern.CustomLecternBlockEntity; +import dan200.computercraft.shared.media.items.PrintoutItem; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.blockentity.LecternRenderer; +import net.minecraft.world.level.block.LecternBlock; + +/** + * A block entity renderer for our {@linkplain CustomLecternBlockEntity lectern}. + *

+ * This largely follows {@link LecternRenderer}, but with support for multiple types of item. + */ +public class CustomLecternRenderer implements BlockEntityRenderer { + private final LecternPrintoutModel printoutModel; + + public CustomLecternRenderer(BlockEntityRendererProvider.Context context) { + printoutModel = new LecternPrintoutModel(); + } + + @Override + public void render(CustomLecternBlockEntity lectern, float partialTick, PoseStack poseStack, MultiBufferSource buffer, int packedLight, int packedOverlay) { + poseStack.pushPose(); + poseStack.translate(0.5f, 1.0625f, 0.5f); + poseStack.mulPose(Axis.YP.rotationDegrees(-lectern.getBlockState().getValue(LecternBlock.FACING).getClockWise().toYRot())); + poseStack.mulPose(Axis.ZP.rotationDegrees(67.5f)); + poseStack.translate(0, -0.125f, 0); + + var item = lectern.getItem(); + if (item.getItem() instanceof PrintoutItem printout) { + var vertexConsumer = LecternPrintoutModel.MATERIAL.buffer(buffer, RenderType::entitySolid); + if (printout.getType() == PrintoutItem.Type.BOOK) { + printoutModel.renderBook(poseStack, vertexConsumer, packedLight, packedOverlay); + } else { + printoutModel.renderPages(poseStack, vertexConsumer, packedLight, packedOverlay, PrintoutItem.getPageCount(item)); + } + } + + poseStack.popPose(); + } +} diff --git a/projects/common/src/client/java/dan200/computercraft/data/client/ClientDataProviders.java b/projects/common/src/client/java/dan200/computercraft/data/client/ClientDataProviders.java index 76891e250..b318826d0 100644 --- a/projects/common/src/client/java/dan200/computercraft/data/client/ClientDataProviders.java +++ b/projects/common/src/client/java/dan200/computercraft/data/client/ClientDataProviders.java @@ -5,6 +5,7 @@ package dan200.computercraft.data.client; import dan200.computercraft.client.gui.GuiSprites; +import dan200.computercraft.client.model.LecternPrintoutModel; import dan200.computercraft.data.DataProviders; import dan200.computercraft.shared.turtle.inventory.UpgradeSlot; import net.minecraft.client.renderer.texture.atlas.SpriteSource; @@ -30,7 +31,8 @@ public static void add(DataProviders.GeneratorSink generator) { generator.addFromCodec("Block atlases", PackType.CLIENT_RESOURCES, "atlases", SpriteSources.FILE_CODEC, out -> { out.accept(new ResourceLocation("blocks"), List.of( new SingleFile(UpgradeSlot.LEFT_UPGRADE, Optional.empty()), - new SingleFile(UpgradeSlot.RIGHT_UPGRADE, Optional.empty()) + new SingleFile(UpgradeSlot.RIGHT_UPGRADE, Optional.empty()), + new SingleFile(LecternPrintoutModel.TEXTURE, Optional.empty()) )); out.accept(GuiSprites.SPRITE_SHEET, Stream.of( // Buttons diff --git a/projects/common/src/generated/resources/assets/computercraft/blockstates/lectern.json b/projects/common/src/generated/resources/assets/computercraft/blockstates/lectern.json new file mode 100644 index 000000000..c760753f3 --- /dev/null +++ b/projects/common/src/generated/resources/assets/computercraft/blockstates/lectern.json @@ -0,0 +1,8 @@ +{ + "variants": { + "facing=east": {"model": "minecraft:block/lectern", "y": 90}, + "facing=north": {"model": "minecraft:block/lectern", "y": 0}, + "facing=south": {"model": "minecraft:block/lectern", "y": 180}, + "facing=west": {"model": "minecraft:block/lectern", "y": 270} + } +} diff --git a/projects/common/src/generated/resources/assets/minecraft/atlases/blocks.json b/projects/common/src/generated/resources/assets/minecraft/atlases/blocks.json index d9d236296..eb21f8f98 100644 --- a/projects/common/src/generated/resources/assets/minecraft/atlases/blocks.json +++ b/projects/common/src/generated/resources/assets/minecraft/atlases/blocks.json @@ -1,6 +1,7 @@ { "sources": [ {"type": "minecraft:single", "resource": "computercraft:gui/turtle_upgrade_left"}, - {"type": "minecraft:single", "resource": "computercraft:gui/turtle_upgrade_right"} + {"type": "minecraft:single", "resource": "computercraft:gui/turtle_upgrade_right"}, + {"type": "minecraft:single", "resource": "computercraft:entity/printout"} ] } diff --git a/projects/common/src/generated/resources/data/computercraft/loot_tables/blocks/lectern.json b/projects/common/src/generated/resources/data/computercraft/loot_tables/blocks/lectern.json new file mode 100644 index 000000000..b5876ca42 --- /dev/null +++ b/projects/common/src/generated/resources/data/computercraft/loot_tables/blocks/lectern.json @@ -0,0 +1,12 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [{"condition": "minecraft:survives_explosion"}], + "entries": [{"type": "minecraft:item", "name": "minecraft:lectern"}], + "rolls": 1.0 + } + ], + "random_sequence": "computercraft:blocks/lectern" +} diff --git a/projects/common/src/main/java/dan200/computercraft/data/BlockModelProvider.java b/projects/common/src/main/java/dan200/computercraft/data/BlockModelProvider.java index c0c9f3491..274557ad2 100644 --- a/projects/common/src/main/java/dan200/computercraft/data/BlockModelProvider.java +++ b/projects/common/src/main/java/dan200/computercraft/data/BlockModelProvider.java @@ -23,6 +23,7 @@ import net.minecraft.data.models.blockstates.*; import net.minecraft.data.models.model.*; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.properties.BlockStateProperties; import net.minecraft.world.level.block.state.properties.BooleanProperty; import net.minecraft.world.level.block.state.properties.Property; @@ -100,6 +101,11 @@ public static void addBlockModels(BlockModelGenerators generators) { registerTurtleUpgrade(generators, "block/turtle_speaker", "block/turtle_speaker_face"); registerTurtleModem(generators, "block/turtle_modem_normal", "block/wireless_modem_normal_face"); registerTurtleModem(generators, "block/turtle_modem_advanced", "block/wireless_modem_advanced_face"); + + generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant( + ModRegistry.Blocks.LECTERN.get(), + Variant.variant().with(VariantProperties.MODEL, ModelLocationUtils.getModelLocation(Blocks.LECTERN)) + ).with(createHorizontalFacingDispatch())); } private static void registerDiskDrive(BlockModelGenerators generators) { diff --git a/projects/common/src/main/java/dan200/computercraft/data/LanguageProvider.java b/projects/common/src/main/java/dan200/computercraft/data/LanguageProvider.java index 585f77b1e..2dc57e405 100644 --- a/projects/common/src/main/java/dan200/computercraft/data/LanguageProvider.java +++ b/projects/common/src/main/java/dan200/computercraft/data/LanguageProvider.java @@ -284,7 +284,9 @@ private Stream getExpectedKeys() { return Stream.of( RegistryWrappers.BLOCKS.stream() .filter(x -> RegistryWrappers.BLOCKS.getKey(x).getNamespace().equals(ComputerCraftAPI.MOD_ID)) - .map(Block::getDescriptionId), + .map(Block::getDescriptionId) + // Exclude blocks that just reuse vanilla translations, such as the lectern. + .filter(x -> !x.startsWith("block.minecraft.")), RegistryWrappers.ITEMS.stream() .filter(x -> RegistryWrappers.ITEMS.getKey(x).getNamespace().equals(ComputerCraftAPI.MOD_ID)) .map(Item::getDescriptionId), diff --git a/projects/common/src/main/java/dan200/computercraft/data/LootTableProvider.java b/projects/common/src/main/java/dan200/computercraft/data/LootTableProvider.java index 33c9d239b..5f0f5b5ac 100644 --- a/projects/common/src/main/java/dan200/computercraft/data/LootTableProvider.java +++ b/projects/common/src/main/java/dan200/computercraft/data/LootTableProvider.java @@ -15,6 +15,7 @@ import net.minecraft.advancements.critereon.StatePropertiesPredicate; import net.minecraft.data.loot.LootTableProvider.SubProviderEntry; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Items; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.storage.loot.LootPool; import net.minecraft.world.level.storage.loot.LootTable; @@ -57,6 +58,8 @@ private static void registerBlocks(BiConsumer WIRED_MODEM_FULL = REGISTRY.register("wired_modem_full", () -> new WiredModemFullBlock(modemProperties().mapColor(MapColor.STONE))); public static final RegistryEntry CABLE = REGISTRY.register("cable", () -> new CableBlock(modemProperties().mapColor(MapColor.STONE))); + + public static final RegistryEntry LECTERN = REGISTRY.register("lectern", () -> new CustomLecternBlock( + BlockBehaviour.Properties.of().mapColor(MapColor.WOOD).instrument(NoteBlockInstrument.BASS).strength(2.5F).sound(SoundType.WOOD).ignitedByLava() + )); } public static class BlockEntities { @@ -212,6 +220,8 @@ private static RegistryEntry> ofBlock ofBlock(Blocks.WIRELESS_MODEM_NORMAL, (p, s) -> new WirelessModemBlockEntity(BlockEntities.WIRELESS_MODEM_NORMAL.get(), p, s, false)); public static final RegistryEntry> WIRELESS_MODEM_ADVANCED = ofBlock(Blocks.WIRELESS_MODEM_ADVANCED, (p, s) -> new WirelessModemBlockEntity(BlockEntities.WIRELESS_MODEM_ADVANCED.get(), p, s, true)); + + public static final RegistryEntry> LECTERN = ofBlock(Blocks.LECTERN, CustomLecternBlockEntity::new); } public static final class Items { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/container/BasicContainer.java b/projects/common/src/main/java/dan200/computercraft/shared/container/BasicContainer.java index 6680725da..7dd51b29f 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/container/BasicContainer.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/container/BasicContainer.java @@ -4,16 +4,17 @@ package dan200.computercraft.shared.container; -import net.minecraft.core.NonNullList; import net.minecraft.world.Container; import net.minecraft.world.ContainerHelper; import net.minecraft.world.item.ItemStack; +import java.util.List; + /** * A basic implementation of {@link Container} which operates on a {@linkplain #getContents() list of stacks}. */ public interface BasicContainer extends Container { - NonNullList getContents(); + List getContents(); @Override default int getContainerSize() { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlock.java b/projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlock.java new file mode 100644 index 000000000..f79417f2f --- /dev/null +++ b/projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlock.java @@ -0,0 +1,142 @@ +// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.lectern; + +import dan200.computercraft.shared.ModRegistry; +import dan200.computercraft.shared.media.items.PrintoutItem; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.stats.Stats; +import net.minecraft.util.RandomSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.LecternBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; + +/** + * Extends {@link LecternBlock} with support for {@linkplain PrintoutItem printouts}. + *

+ * Unlike the vanilla lectern, this block is never empty. If the book is removed from the lectern, it converts back to + * its vanilla version (see {@link #clearLectern(Level, BlockPos, BlockState)}). + * + * @see PrintoutItem#useOn(UseOnContext) Placing books into a lectern. + */ +public class CustomLecternBlock extends LecternBlock { + public CustomLecternBlock(Properties properties) { + super(properties); + registerDefaultState(defaultBlockState().setValue(HAS_BOOK, true)); + } + + /** + * Replace a vanilla lectern with a custom one. + * + * @param level The current level. + * @param pos The position of the lectern. + * @param blockState The current state of the lectern. + * @param item The item to place in the custom lectern. + */ + public static void replaceLectern(Level level, BlockPos pos, BlockState blockState, ItemStack item) { + level.setBlockAndUpdate(pos, ModRegistry.Blocks.LECTERN.get().defaultBlockState() + .setValue(HAS_BOOK, true) + .setValue(FACING, blockState.getValue(FACING)) + .setValue(POWERED, blockState.getValue(POWERED))); + + if (level.getBlockEntity(pos) instanceof CustomLecternBlockEntity be) be.setItem(item.split(1)); + } + + /** + * Remove a custom lectern and replace it with an empty vanilla one. + * + * @param level The current level. + * @param pos The position of the lectern. + * @param blockState The current state of the lectern. + */ + static void clearLectern(Level level, BlockPos pos, BlockState blockState) { + level.setBlockAndUpdate(pos, Blocks.LECTERN.defaultBlockState() + .setValue(HAS_BOOK, false) + .setValue(FACING, blockState.getValue(FACING)) + .setValue(POWERED, blockState.getValue(POWERED))); + } + + @Override + @Deprecated + public ItemStack getCloneItemStack(BlockGetter level, BlockPos pos, BlockState state) { + return new ItemStack(Items.LECTERN); + } + + @Override + public void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { + // If we've no lectern, remove it. + if (level.getBlockEntity(pos) instanceof CustomLecternBlockEntity lectern && lectern.getItem().isEmpty()) { + clearLectern(level, pos, state); + return; + } + + super.tick(state, level, pos, random); + } + + @Override + public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) { + if (state.is(newState.getBlock())) return; + + if (level.getBlockEntity(pos) instanceof CustomLecternBlockEntity lectern) { + dropItem(level, pos, state, lectern.getItem().copy()); + } + + super.onRemove(state, level, pos, newState, isMoving); + } + + private static void dropItem(Level level, BlockPos pos, BlockState state, ItemStack stack) { + if (stack.isEmpty()) return; + + var direction = state.getValue(FACING); + var dx = 0.25 * direction.getStepX(); + var dz = 0.25 * direction.getStepZ(); + var entity = new ItemEntity(level, pos.getX() + 0.5 + dx, pos.getY() + 1, pos.getZ() + 0.5 + dz, stack); + entity.setDefaultPickUpDelay(); + level.addFreshEntity(entity); + } + + @Override + public String getDescriptionId() { + return Blocks.LECTERN.getDescriptionId(); + } + + @Override + public CustomLecternBlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new CustomLecternBlockEntity(pos, state); + } + + @Override + public int getAnalogOutputSignal(BlockState blockState, Level level, BlockPos pos) { + return level.getBlockEntity(pos) instanceof CustomLecternBlockEntity lectern ? lectern.getRedstoneSignal() : 0; + } + + @Override + public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + if (!level.isClientSide && level.getBlockEntity(pos) instanceof CustomLecternBlockEntity lectern) { + if (player.isSecondaryUseActive()) { + // When shift+clicked with an empty hand, drop the item and replace with the normal lectern. + clearLectern(level, pos, state); + } else { + // Otherwise open the screen. + player.openMenu(lectern); + } + + player.awardStat(Stats.INTERACT_WITH_LECTERN); + } + + return InteractionResult.sidedSuccess(level.isClientSide); + } +} diff --git a/projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlockEntity.java b/projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlockEntity.java new file mode 100644 index 000000000..ee124ed15 --- /dev/null +++ b/projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlockEntity.java @@ -0,0 +1,193 @@ +// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.lectern; + +import dan200.computercraft.shared.ModRegistry; +import dan200.computercraft.shared.container.BasicContainer; +import dan200.computercraft.shared.container.SingleContainerData; +import dan200.computercraft.shared.media.PrintoutMenu; +import dan200.computercraft.shared.media.items.PrintoutItem; +import dan200.computercraft.shared.util.BlockEntityHelpers; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; +import net.minecraft.util.Mth; +import net.minecraft.world.Container; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.LecternBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.LecternBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +import java.util.AbstractList; +import java.util.List; + +/** + * The block entity for our {@link CustomLecternBlock}. + * + * @see LecternBlockEntity + */ +public final class CustomLecternBlockEntity extends BlockEntity implements MenuProvider { + private static final String NBT_ITEM = "Item"; + private static final String NBT_PAGE = "Page"; + + private ItemStack item = ItemStack.EMPTY; + private int page, pageCount; + + public CustomLecternBlockEntity(BlockPos pos, BlockState blockState) { + super(ModRegistry.BlockEntities.LECTERN.get(), pos, blockState); + } + + public ItemStack getItem() { + return item; + } + + void setItem(ItemStack item) { + this.item = item; + itemChanged(); + BlockEntityHelpers.updateBlock(this); + } + + int getRedstoneSignal() { + if (item.getItem() instanceof PrintoutItem) { + var progress = pageCount > 1 ? (float) page / (pageCount - 1) : 1F; + return Mth.floor(progress * 14f) + 1; + } + + return 15; + } + + /** + * Called after the item has changed. This sets up the state for the new item. + */ + private void itemChanged() { + if (item.getItem() instanceof PrintoutItem) { + pageCount = PrintoutItem.getPageCount(item); + page = Mth.clamp(page, 0, pageCount - 1); + } else { + pageCount = page = 0; + } + } + + /** + * Set the current page, emitting a redstone pulse if needed. + * + * @param page The new page. + */ + private void setPage(int page) { + if (this.page == page) return; + + this.page = page; + setChanged(); + if (getLevel() != null) LecternBlock.signalPageChange(getLevel(), getBlockPos(), getBlockState()); + } + + @Override + public void load(CompoundTag tag) { + super.load(tag); + + item = tag.contains(NBT_ITEM, Tag.TAG_COMPOUND) ? ItemStack.of(tag.getCompound(NBT_ITEM)) : ItemStack.EMPTY; + page = tag.getInt(NBT_PAGE); + itemChanged(); + } + + @Override + protected void saveAdditional(CompoundTag tag) { + super.saveAdditional(tag); + + if (!item.isEmpty()) tag.put(NBT_ITEM, item.save(new CompoundTag())); + if (item.getItem() instanceof PrintoutItem) tag.putInt(NBT_PAGE, page); + } + + @Override + public Packet getUpdatePacket() { + return ClientboundBlockEntityDataPacket.create(this); + } + + @Override + public CompoundTag getUpdateTag() { + var tag = super.getUpdateTag(); + tag.put(NBT_ITEM, item.save(new CompoundTag())); + return tag; + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) { + var item = getItem(); + if (item.getItem() instanceof PrintoutItem) { + return new PrintoutMenu( + containerId, new LecternContainer(), 0, + p -> Container.stillValidBlockEntity(this, player, Container.DEFAULT_DISTANCE_LIMIT), + new PrintoutContainerData() + ); + } + + return null; + } + + @Override + public Component getDisplayName() { + return getItem().getDisplayName(); + } + + /** + * A read-only container storing the lectern's contents. + */ + private final class LecternContainer implements BasicContainer { + private final List itemView = new AbstractList<>() { + @Override + public ItemStack get(int index) { + if (index != 0) throw new IndexOutOfBoundsException("Inventory only has one slot"); + return item; + } + + @Override + public int size() { + return 1; + } + }; + + @Override + public List getContents() { + return itemView; + } + + @Override + public void setChanged() { + // Should never happen, so a no-op. + } + + @Override + public boolean stillValid(Player player) { + return !isRemoved(); + } + } + + /** + * {@link ContainerData} for a {@link PrintoutMenu}. This provides a read/write view of the current page. + */ + private final class PrintoutContainerData implements SingleContainerData { + @Override + public int get() { + return page; + } + + @Override + public void set(int index, int value) { + if (index == 0) setPage(value); + } + } +} diff --git a/projects/common/src/main/java/dan200/computercraft/shared/media/items/PrintoutItem.java b/projects/common/src/main/java/dan200/computercraft/shared/media/items/PrintoutItem.java index 31d2a6e86..6aed3babf 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/media/items/PrintoutItem.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/media/items/PrintoutItem.java @@ -6,6 +6,7 @@ import com.google.common.base.Strings; import dan200.computercraft.shared.ModRegistry; +import dan200.computercraft.shared.lectern.CustomLecternBlock; import dan200.computercraft.shared.media.PrintoutMenu; import net.minecraft.network.chat.Component; import net.minecraft.world.InteractionHand; @@ -16,7 +17,10 @@ import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.item.context.UseOnContext; import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.LecternBlock; import javax.annotation.Nullable; import java.util.List; @@ -50,6 +54,22 @@ public void appendHoverText(ItemStack stack, @Nullable Level world, List use(Level world, Player player, InteractionHand hand) { var stack = player.getItemInHand(hand); diff --git a/projects/common/src/main/resources/assets/computercraft/textures/entity/printout.png b/projects/common/src/main/resources/assets/computercraft/textures/entity/printout.png new file mode 100644 index 0000000000000000000000000000000000000000..204b0483ff4109af001a30b59daae94412e06dc3 GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vJfv^#Gp`S0HVxFJfmPlIkr!Z{EBE z2M!!La^&L0i#M-byLt2G-Me?+ym|BW)2HbDFQtJ>8B2ovf*Bm1-ADs+qCH(4Lp07O zCnO{kCL}aW4*qBv0T+11qol8Uy9on(w0xJW< Date: Sat, 17 Aug 2024 11:39:14 +0100 Subject: [PATCH 15/26] Fix several repeated words Depressing how many of these there are. Some come from Dan though (including one in the LICENSE!), so at least it's not just me! --- .../main/java/dan200/computercraft/api/turtle/TurtleSide.java | 2 +- .../computercraft/shared/command/text/TableBuilder.java | 2 +- .../shared/computer/blocks/AbstractComputerBlockEntity.java | 2 +- .../dan200/computercraft/shared/turtle/apis/TurtleAPI.java | 2 +- .../java/dan200/computercraft/api/peripheral/IPeripheral.java | 2 +- .../src/main/java/dan200/computercraft/core/apis/OSAPI.java | 2 +- .../core/apis/http/options/AddressPredicate.java | 2 +- .../main/java/dan200/computercraft/core/asm/Generator.java | 2 +- .../resources/data/computercraft/lua/rom/help/changelog.md | 4 ++-- .../computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua | 2 +- .../test-rom/spec/modules/cc/internal/syntax/lexer_spec.md | 4 ++-- 11 files changed, 13 insertions(+), 13 deletions(-) diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleSide.java b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleSide.java index b8aac57d5..66f36ec95 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleSide.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleSide.java @@ -5,7 +5,7 @@ package dan200.computercraft.api.turtle; /** - * An enum representing the two sides of the turtle that a turtle turtle might reside. + * An enum representing the two sides of the turtle that a turtle upgrade might reside. */ public enum TurtleSide { /** diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/text/TableBuilder.java b/projects/common/src/main/java/dan200/computercraft/shared/command/text/TableBuilder.java index 2428e5637..dbf719d12 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/command/text/TableBuilder.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/command/text/TableBuilder.java @@ -63,7 +63,7 @@ public String getId() { /** * Get the number of columns for this table. *

- * This will be the same as {@link #getHeaders()}'s length if it is is non-{@code null}, + * This will be the same as {@link #getHeaders()}'s length if it is non-{@code null}, * otherwise the length of the first column. * * @return The number of columns. diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/AbstractComputerBlockEntity.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/AbstractComputerBlockEntity.java index 07db76314..e0f5e688b 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/AbstractComputerBlockEntity.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/AbstractComputerBlockEntity.java @@ -110,7 +110,7 @@ protected void serverTick() { fresh = false; computerID = computer.getID(); - // If the on state has changed, mark as as dirty. + // If the on state has changed, mark as dirty. var newOn = computer.isOn(); if (on != newOn) { on = newOn; 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 885235d37..b1ad1af39 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 @@ -345,7 +345,7 @@ public final int getItemCount(Optional slot) throws LuaException { * For instance, if a slot contains 13 blocks of dirt, it has room for another 51. * * @param slot The slot we wish to check. Defaults to the {@link #select selected slot}. - * @return The space left in in this slot. + * @return The space left in this slot. * @throws LuaException If the slot is out of range. */ @LuaFunction diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/peripheral/IPeripheral.java b/projects/core-api/src/main/java/dan200/computercraft/api/peripheral/IPeripheral.java index 1a857d8d0..ca3123a61 100644 --- a/projects/core-api/src/main/java/dan200/computercraft/api/peripheral/IPeripheral.java +++ b/projects/core-api/src/main/java/dan200/computercraft/api/peripheral/IPeripheral.java @@ -38,7 +38,7 @@ default Set getAdditionalTypes() { } /** - * Is called when when a computer is attaching to the peripheral. + * Is called when a computer is attaching to the peripheral. *

* This will occur when a peripheral is placed next to an active computer, when a computer is turned on next to a * peripheral, when a turtle travels into a square next to a peripheral, or when a wired modem adjacent to this 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 c0d8b047d..e31f76d73 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 @@ -298,7 +298,7 @@ public final double clock() { * textutils.formatTime(os.time()) * } * @cc.since 1.2 - * @cc.changed 1.80pr1 Add support for getting the local local and UTC time. + * @cc.changed 1.80pr1 Add support for getting the local and UTC time. * @cc.changed 1.82.0 Arguments are now case insensitive. * @cc.changed 1.83.0 {@link #time(IArguments)} now accepts table arguments and converts them to UNIX timestamps. * @see #date To get a date table that can be converted with this function. 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 eb1d284ef..46bce96be 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 @@ -57,7 +57,7 @@ public static HostRange parse(String addressStr, String prefixSizeStr) { prefixSize = Integer.parseInt(prefixSizeStr); } catch (NumberFormatException e) { throw new InvalidRuleException(String.format( - "Invalid host host '%s': Cannot extract size of CIDR mask from '%s'.", + "Invalid host '%s': Cannot extract size of CIDR mask from '%s'.", addressStr + '/' + prefixSizeStr, prefixSizeStr )); } 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 8124a54c4..3272b3bc2 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 @@ -270,7 +270,7 @@ private Optional build(Method method, MethodHandle handle, List paramet } // Fold over the original method's arguments, excluding the target in reverse. For each argument, we reduce - // a method of type type (target, args..., arg_n, context..., IArguments) -> _ to (target, args..., context..., IArguments) -> _ + // a method of type (target, args..., arg_n, context..., IArguments) -> _ to (target, args..., context..., IArguments) -> _ // until eventually we've flattened the whole list. for (var i = parameterTypes.size() - 1; i >= 0; i--) { handle = MethodHandles.foldArguments(handle, i + 1, argSelectors.get(i)); 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 3342468a4..c15e79772 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 @@ -800,7 +800,7 @@ And several bug fixes: # New features in CC: Tweaked 1.86.2 * Fix peripheral.getMethods returning an empty table. -* Update to Minecraft 1.15.2. This is currently alpha-quality and so is missing missing features and may be unstable. +* Update to Minecraft 1.15.2. This is currently alpha-quality and so is missing features and may be unstable. # New features in CC: Tweaked 1.86.1 @@ -1416,7 +1416,7 @@ And several bug fixes: * Turtles can now compare items in their inventories * Turtles can place signs with text on them with `turtle.place( [signText] )` * Turtles now optionally require fuel items to move, and can refuel themselves -* The size of the the turtle inventory has been increased to 16 +* The size of the turtle inventory has been increased to 16 * The size of the turtle screen has been increased * New turtle functions: `turtle.compareTo( [slotNum] )`, `turtle.craft()`, `turtle.attack()`, `turtle.attackUp()`, `turtle.attackDown()`, `turtle.dropUp()`, `turtle.dropDown()`, `turtle.getFuelLevel()`, `turtle.refuel()` * New disk function: disk.getID() diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua index 5b5e05244..0161c6b09 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua @@ -211,7 +211,7 @@ end --[[- A convenience function for encoding a complete file of audio at once. -This should only be used for complete pieces of audio. If you are writing writing multiple chunks to the same place, +This should only be used for complete pieces of audio. If you are writing multiple chunks to the same place, you should use an encoder returned by [`make_encoder`] instead. @tparam { number... } input The table of amplitude data. diff --git a/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/lexer_spec.md b/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/lexer_spec.md index c6e983566..8cb2e646f 100644 --- a/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/lexer_spec.md +++ b/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/lexer_spec.md @@ -13,13 +13,13 @@ correct tokens and positions, and that it can report sensible error messages. We can lex some basic comments: ```lua --- A basic singleline comment comment +-- A basic singleline comment --[ Not a multiline comment --[= Also not a multiline comment! ``` ```txt -1:1-1:37 COMMENT -- A basic singleline comment comment +1:1-1:37 COMMENT -- A basic singleline comment 2:1-2:27 COMMENT --[ Not a multiline comment 3:1-3:34 COMMENT --[= Also not a multiline comment! ``` From cdcd82679c8e286738afd6f19d1d9ed845dcd2aa Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 18 Aug 2024 10:20:54 +0100 Subject: [PATCH 16/26] Don't share singleton collections CC tries to preserve sharing of objects when crossing the Lua/Java boundary. For instance, if you queue (or send over a modem) `{ tbl, tbl }`, then the returned table will have `x[1] == x[2]`. However, this sharing causes issues with Java singletons. If some code uses a singleton collection (such as List.of()) in multiple places, then the same Lua table will be used in all those locations. It's incredibly easy to accidentally, especially when using using Stream.toList. For now, we special case these collections and don't de-duplicate them. I'm not wild about this (it's a bit of a hack!), but I think it's probably the easiest solution for now. Fixes #1940 --- .../computercraft/api/lua/MethodResult.java | 10 +++-- .../core/lua/CobaltLuaMachine.java | 44 ++++++++++++------- .../computercraft/core/util/LuaUtil.java | 24 +++++++++- .../core/computer/ComputerTest.java | 23 ++++++++++ .../resources/test-rom/spec/apis/os_spec.lua | 27 ++++++++++++ .../modules/cc/internal/syntax/lexer_spec.md | 2 +- 6 files changed, 110 insertions(+), 20 deletions(-) diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/lua/MethodResult.java b/projects/core-api/src/main/java/dan200/computercraft/api/lua/MethodResult.java index aec0f8615..8b834f54c 100644 --- a/projects/core-api/src/main/java/dan200/computercraft/api/lua/MethodResult.java +++ b/projects/core-api/src/main/java/dan200/computercraft/api/lua/MethodResult.java @@ -8,9 +8,7 @@ import javax.annotation.Nullable; import java.nio.ByteBuffer; -import java.util.Collection; -import java.util.Map; -import java.util.Objects; +import java.util.*; /** * The result of invoking a Lua method. @@ -55,6 +53,12 @@ public static MethodResult of() { *

* In order to provide a custom object with methods, one may return a {@link IDynamicLuaObject}, or an arbitrary * class with {@link LuaFunction} annotations. Anything else will be converted to {@code nil}. + *

+ * Shared objects in a {@link MethodResult} will preserve their sharing when converted to Lua values. For instance, + * {@code Map m = new HashMap(); return MethodResult.of(m, m); } will return two values {@code a}, {@code b} + * where {@code a == b}. The one exception to this is Java's singleton collections ({@link List#of()}, + * {@link Set#of()} and {@link Map#of()}), which are always converted to new table. This is not true for other + * singleton collections, such as those provided by {@link Collections} or Guava. * * @param value The value to return to the calling Lua function. * @return A method result which returns immediately with the given value. 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 2b97f00fb..5d51e55e0 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 @@ -13,6 +13,7 @@ import dan200.computercraft.core.computer.TimeoutState; import dan200.computercraft.core.methods.LuaMethod; import dan200.computercraft.core.methods.MethodSupplier; +import dan200.computercraft.core.util.LuaUtil; import dan200.computercraft.core.util.Nullability; import dan200.computercraft.core.util.SanitisedError; import org.slf4j.Logger; @@ -183,10 +184,35 @@ private LuaValue toValue(@Nullable Object object, @Nullable IdentityHashMap(1); var result = values.get(object); if (result != null) return result; + var wrapped = toValueWorker(object, values); + if (wrapped == null) { + LOG.warn(Logging.JAVA_ERROR, "Received unknown type '{}', returning nil.", object.getClass().getName()); + return Constants.NIL; + } + + values.put(object, wrapped); + return wrapped; + } + + /** + * Convert a complex Java object (such as a collection or Lua object) to a Lua value. + *

+ * This is a worker function for {@link #toValue(Object, IdentityHashMap)}, which handles the actual construction + * of values, without reading/writing from the value map. + * + * @param object The object to convert. + * @param values The map of Java to Lua values. + * @return The converted value, or {@code null} if it could not be converted. + * @throws LuaError If the value could not be converted. + */ + private @Nullable LuaValue toValueWorker(Object object, IdentityHashMap values) throws LuaError { if (object instanceof ILuaFunction) { return new ResultInterpreterFunction(this, FUNCTION_METHOD, object, context, object.toString()); } @@ -194,15 +220,12 @@ private LuaValue toValue(@Nullable Object object, @Nullable IdentityHashMap map) { var table = new LuaTable(); - values.put(object, table); - - for (Map.Entry pair : map.entrySet()) { + for (var pair : map.entrySet()) { var key = toValue(pair.getKey(), values); var value = toValue(pair.getValue(), values); if (!key.isNil() && !value.isNil()) table.rawset(key, value); @@ -212,27 +235,18 @@ private LuaValue toValue(@Nullable Object object, @Nullable IdentityHashMap objects) { var table = new LuaTable(objects.size(), 0); - values.put(object, table); var i = 0; - for (Object child : objects) table.rawset(++i, toValue(child, values)); + for (var child : objects) table.rawset(++i, toValue(child, values)); return table; } if (object instanceof Object[] objects) { var table = new LuaTable(objects.length, 0); - values.put(object, table); for (var i = 0; i < objects.length; i++) table.rawset(i + 1, toValue(objects[i], values)); return table; } - var wrapped = wrapLuaObject(object); - if (wrapped != null) { - values.put(object, wrapped); - return wrapped; - } - - LOG.warn(Logging.JAVA_ERROR, "Received unknown type '{}', returning nil.", object.getClass().getName()); - return Constants.NIL; + return wrapLuaObject(object); } Varargs toValues(@Nullable Object[] objects) throws LuaError { diff --git a/projects/core/src/main/java/dan200/computercraft/core/util/LuaUtil.java b/projects/core/src/main/java/dan200/computercraft/core/util/LuaUtil.java index 86205d319..0ba8dd1a0 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/util/LuaUtil.java +++ b/projects/core/src/main/java/dan200/computercraft/core/util/LuaUtil.java @@ -4,9 +4,18 @@ package dan200.computercraft.core.util; +import dan200.computercraft.core.lua.ILuaMachine; + import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; public class LuaUtil { + private static final List EMPTY_LIST = List.of(); + private static final Set EMPTY_SET = Set.of(); + private static final Map EMPTY_MAP = Map.of(); + public static Object[] consArray(Object value, Collection rest) { if (rest.isEmpty()) return new Object[]{ value }; @@ -14,7 +23,20 @@ public static Object[] consArray(Object value, Collection rest) { var out = new Object[rest.size() + 1]; out[0] = value; var i = 1; - for (Object additionalType : rest) out[i++] = additionalType; + for (var additionalType : rest) out[i++] = additionalType; return out; } + + /** + * Determine whether a value is a singleton collection, such as one created with {@link List#of()}. + *

+ * These collections are treated specially by {@link ILuaMachine} implementations: we skip sharing for them, and + * create a new table each time. + * + * @param value The value to test. + * @return Whether this is a singleton collection. + */ + public static boolean isSingletonCollection(Object value) { + return value == EMPTY_LIST || value == EMPTY_SET || value == EMPTY_MAP; + } } diff --git a/projects/core/src/test/java/dan200/computercraft/core/computer/ComputerTest.java b/projects/core/src/test/java/dan200/computercraft/core/computer/ComputerTest.java index 98124fc89..674816828 100644 --- a/projects/core/src/test/java/dan200/computercraft/core/computer/ComputerTest.java +++ b/projects/core/src/test/java/dan200/computercraft/core/computer/ComputerTest.java @@ -5,11 +5,14 @@ package dan200.computercraft.core.computer; import com.google.common.io.CharStreams; +import dan200.computercraft.api.lua.ILuaAPI; +import dan200.computercraft.api.lua.LuaFunction; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.util.List; import java.util.Objects; import static java.time.Duration.ofSeconds; @@ -30,6 +33,26 @@ public void testTimeout() { }); } + @Test + public void testDuplicateObjects() { + class CustomApi implements ILuaAPI { + @Override + public String[] getNames() { + return new String[]{ "custom" }; + } + + @LuaFunction + public final Object[] getObjects() { + return new Object[]{ List.of(), List.of() }; + } + } + + ComputerBootstrap.run(""" + local x, y = custom.getObjects() + assert(x ~= y) + """, i -> i.addApi(new CustomApi()), 50); + } + public static void main(String[] args) throws Exception { var stream = ComputerTest.class.getClassLoader().getResourceAsStream("benchmark.lua"); try (var reader = new InputStreamReader(Objects.requireNonNull(stream), StandardCharsets.UTF_8)) { diff --git a/projects/core/src/test/resources/test-rom/spec/apis/os_spec.lua b/projects/core/src/test/resources/test-rom/spec/apis/os_spec.lua index 562dcc7ff..95a50b369 100644 --- a/projects/core/src/test/resources/test-rom/spec/apis/os_spec.lua +++ b/projects/core/src/test/resources/test-rom/spec/apis/os_spec.lua @@ -188,4 +188,31 @@ describe("The os library", function() expect.error(os.loadAPI, nil):eq("bad argument #1 (string expected, got nil)") end) end) + + describe("os.queueEvent", function() + local function roundtrip(...) + local event_name = ("event_%08x"):format(math.random(1, 0x7FFFFFFF)) + os.queueEvent(event_name, ...) + return select(2, os.pullEvent(event_name)) + end + + it("preserves references in tables", function() + local tbl = {} + local xs = roundtrip({ tbl, tbl }) + expect(xs[1]):eq(xs[2]) + end) + + it("does not preserve references in separate args", function() + -- I'm not sure I like this behaviour, but it is what CC has always done. + local tbl = {} + local xs, ys = roundtrip(tbl, tbl) + expect(xs):ne(ys) + end) + + it("clones objects", function() + local tbl = {} + local xs = roundtrip(tbl) + expect(xs):ne(tbl) + end) + end) end) diff --git a/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/lexer_spec.md b/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/lexer_spec.md index 8cb2e646f..9d5af0531 100644 --- a/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/lexer_spec.md +++ b/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/lexer_spec.md @@ -19,7 +19,7 @@ We can lex some basic comments: ``` ```txt -1:1-1:37 COMMENT -- A basic singleline comment +1:1-1:29 COMMENT -- A basic singleline comment 2:1-2:27 COMMENT --[ Not a multiline comment 3:1-3:34 COMMENT --[= Also not a multiline comment! ``` From b89e2615db63778d971d39a119eea96c054370f0 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 18 Aug 2024 10:28:16 +0100 Subject: [PATCH 17/26] Don't add lore to item details when empty --- .../dan200/computercraft/shared/details/ItemDetails.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/common/src/main/java/dan200/computercraft/shared/details/ItemDetails.java b/projects/common/src/main/java/dan200/computercraft/shared/details/ItemDetails.java index 650ad3bb9..d29249773 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/details/ItemDetails.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/details/ItemDetails.java @@ -50,12 +50,12 @@ public static void fill(Map data, ItemStack stack) { if (tag != null && tag.contains("display", Tag.TAG_COMPOUND)) { var displayTag = tag.getCompound("display"); if (displayTag.contains("Lore", Tag.TAG_LIST)) { - var loreTag = displayTag.getList("Lore", Tag.TAG_STRING); - data.put("lore", loreTag.stream() + var lore = displayTag.getList("Lore", Tag.TAG_STRING).stream() .map(ItemDetails::parseTextComponent) .filter(Objects::nonNull) .map(Component::getString) - .toList()); + .toList(); + if (!lore.isEmpty()) data.put("lore", lore); } } From 3299d0e72a44b59e82ba2071157fdf3412a35883 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 18 Aug 2024 10:45:16 +0100 Subject: [PATCH 18/26] Search for items in the whole gametest structure This fixes some flakiness where items get thrown outside a 1 block radius. --- .../dan200/computercraft/gametest/Computer_Test.kt | 5 ++--- .../dan200/computercraft/gametest/Turtle_Test.kt | 4 ++-- .../computercraft/gametest/api/TestExtensions.kt | 11 +++++++++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Computer_Test.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Computer_Test.kt index 0b981cce8..7a21696de 100644 --- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Computer_Test.kt +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Computer_Test.kt @@ -123,11 +123,10 @@ class Computer_Test { @GameTest fun Drops_on_explosion(context: GameTestHelper) = context.sequence { thenExecute { - val pos = BlockPos(2, 2, 2) - val explosionPos = Vec3.atCenterOf(context.absolutePos(pos)) + val explosionPos = Vec3.atCenterOf(context.absolutePos(BlockPos(2, 2, 2))) context.level.explode(null, explosionPos.x, explosionPos.y, explosionPos.z, 2.0f, Level.ExplosionInteraction.TNT) - context.assertItemEntityPresent(ModRegistry.Items.COMPUTER_NORMAL.get(), pos, 1.0) + context.assertItemEntityCountIs(ModRegistry.Items.COMPUTER_NORMAL.get(), 1) } } 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 e1619029f..3f2922c70 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 @@ -703,8 +703,8 @@ class Turtle_Test { thenOnComputer { turtle.dig(Optional.empty()) } thenIdle(2) thenExecute { - context.assertItemEntityCountIs(ModRegistry.Items.TURTLE_NORMAL.get(), BlockPos(2, 2, 2), 1.0, 1) - context.assertItemEntityCountIs(Items.BONE_BLOCK, BlockPos(2, 2, 2), 1.0, 65) + context.assertItemEntityCountIs(ModRegistry.Items.TURTLE_NORMAL.get(), 1) + context.assertItemEntityCountIs(Items.BONE_BLOCK, 65) } } diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/api/TestExtensions.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/api/TestExtensions.kt index 8da752f68..4191ecb03 100644 --- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/api/TestExtensions.kt +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/api/TestExtensions.kt @@ -22,6 +22,7 @@ import net.minecraft.world.Container import net.minecraft.world.InteractionHand import net.minecraft.world.entity.Entity import net.minecraft.world.entity.EntityType +import net.minecraft.world.item.Item import net.minecraft.world.item.ItemStack import net.minecraft.world.item.context.UseOnContext import net.minecraft.world.level.block.Blocks @@ -252,6 +253,16 @@ fun GameTestHelper.assertExactlyItems(vararg expected: ItemStack, message: Strin } } +/** + * Similar to [GameTestHelper.assertItemEntityCountIs], but searching anywhere in the structure bounds. + */ +fun GameTestHelper.assertItemEntityCountIs(expected: Item, count: Int) { + val actualCount = getEntities(EntityType.ITEM).sumOf { if (it.item.`is`(expected)) it.item.count else 0 } + if (actualCount != count) { + throw GameTestAssertException("Expected $count ${expected.description.string} items to exist (found $actualCount)") + } +} + private fun getName(type: BlockEntityType<*>): ResourceLocation = RegistryWrappers.BLOCK_ENTITY_TYPES.getKey(type)!! /** From 34a2fd039f419cc53efc99968190e193eb263628 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 18 Aug 2024 11:03:17 +0100 Subject: [PATCH 19/26] Clarify behaviour of readAll at the end of a file This should return an empty string, to match PUC Lua. --- .../core/apis/handles/AbstractHandle.java | 4 ++-- .../resources/test-rom/spec/apis/fs_spec.lua | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/handles/AbstractHandle.java b/projects/core/src/main/java/dan200/computercraft/core/apis/handles/AbstractHandle.java index 9e7d37f4b..b46a2b1ba 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/handles/AbstractHandle.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/handles/AbstractHandle.java @@ -177,9 +177,9 @@ public Object[] read(Optional countArg) throws LuaException { /** * Read the remainder of the file. * - * @return The file, or {@code null} if at the end of it. + * @return The remaining contents of the file, or {@code null} in the event of an error. * @throws LuaException If the file has been closed. - * @cc.treturn string|nil The remaining contents of the file, or {@code nil} if we are at the end. + * @cc.treturn string|nil The remaining contents of the file, or {@code nil} in the event of an error. * @cc.since 1.80pr1 */ @Nullable diff --git a/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua b/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua index 37d6d5b15..f9fe7aaa6 100644 --- a/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua +++ b/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua @@ -200,6 +200,14 @@ describe("The fs library", function() handle.close() end) + it("reading an empty file returns nil", function() + local file = create_test_file "" + + local handle = fs.open(file, mode) + expect(handle.read()):eq(nil) + handle.close() + end) + it("can read a line of text", function() local file = create_test_file "some\nfile\r\ncontents\n\n" @@ -223,6 +231,16 @@ describe("The fs library", function() expect(handle.readLine(true)):eq(nil) handle.close() end) + + it("readAll always returns a string", function() + local contents = "some\nfile\ncontents" + local file = create_test_file "some\nfile\ncontents" + + local handle = fs.open(file, mode) + expect(handle.readAll()):eq(contents) + expect(handle.readAll()):eq("") + handle.close() + end) end describe("reading", function() From e57b6fede2d1f7da0340d4fbe6b1288f819ca23e Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 18 Aug 2024 11:38:10 +0100 Subject: [PATCH 20/26] Test behaviour of fs.getName/getDir with relative paths It's not entirely clear what the correct behaviour of fs.getDir("..") should be, and there's not much consensus between various languages. I think the intended behaviour of this function is to move "up" one directory level in the path, meaning it should return "../..". --- .../core/filesystem/FileSystem.java | 4 +++ .../resources/test-rom/spec/apis/fs_spec.lua | 35 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java index 811b7df1b..75a4ce1f8 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java +++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java @@ -122,6 +122,10 @@ public static String getDirectory(String path) { } var lastSlash = path.lastIndexOf('/'); + + // If the trailing segment is a "..", then just append another one. + if (path.substring(lastSlash < 0 ? 0 : lastSlash + 1).equals("..")) return path + "/.."; + if (lastSlash >= 0) { return path.substring(0, lastSlash); } else { diff --git a/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua b/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua index f9fe7aaa6..e6af8d55c 100644 --- a/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua +++ b/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua @@ -160,6 +160,41 @@ describe("The fs library", function() end) end) + describe("fs.getName", function() + it("returns 'root' for the empty path", function() + expect(fs.getName("")):eq("root") + expect(fs.getName("foo/..")):eq("root") + end) + + it("returns the file name", function() + expect(fs.getName("foo/bar")):eq("bar") + expect(fs.getName("foo/bar/")):eq("bar") + expect(fs.getName("../foo")):eq("foo") + end) + + it("returns '..' for parent directories", function() + expect(fs.getName("..")):eq("..") + end) + end) + + describe("fs.getDir", function() + it("returns '..' for the empty path", function() + expect(fs.getDir("")):eq("..") + expect(fs.getDir("foo/..")):eq("..") + end) + + it("returns the directory name", function() + expect(fs.getDir("foo/bar")):eq("foo") + expect(fs.getDir("foo/bar/")):eq("foo") + expect(fs.getDir("../foo")):eq("..") + end) + + it("returns '..' for parent directories", function() + expect(fs.getDir("..")):eq("../..") + expect(fs.getDir("../..")):eq("../../..") + end) + end) + describe("fs.getSize", function() it("fails on non-existent nodes", function() expect.error(fs.getSize, "rom/x"):eq("/rom/x: No such file") From 80c7a54ad4d236d610139c04174b6d0ff46eee9a Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 18 Aug 2024 12:49:33 +0100 Subject: [PATCH 21/26] Test path manipulation methods sanitise correctly There's some nuance here with pattern vs non-pattern characters, so useful to test for that. --- .../dan200/computercraft/core/apis/FSAPI.java | 2 +- .../core/filesystem/FileSystem.java | 8 +++---- .../resources/test-rom/spec/apis/fs_spec.lua | 24 +++++++++++++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/FSAPI.java b/projects/core/src/main/java/dan200/computercraft/core/apis/FSAPI.java index 794d8b7c4..09c37ba9c 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/FSAPI.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/FSAPI.java @@ -101,7 +101,7 @@ private FileSystem getFileSystem() { * } */ @LuaFunction - public final String[] list(String path) throws LuaException { + public final List list(String path) throws LuaException { try (var ignored = environment.time(Metrics.FS_OPS)) { return getFileSystem().list(path); } catch (FileSystemException e) { diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java index 75a4ce1f8..509ee63f8 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java +++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java @@ -149,7 +149,7 @@ public synchronized BasicFileAttributes getAttributes(String path) throws FileSy return getMount(sanitizePath(path)).getAttributes(sanitizePath(path)); } - public synchronized String[] list(String path) throws FileSystemException { + public synchronized List list(String path) throws FileSystemException { path = sanitizePath(path); var mount = getMount(path); @@ -165,10 +165,8 @@ public synchronized String[] list(String path) throws FileSystemException { } // Return list - var array = new String[list.size()]; - list.toArray(array); - Arrays.sort(array); - return array; + list.sort(Comparator.naturalOrder()); + return list; } public synchronized boolean exists(String path) throws FileSystemException { diff --git a/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua b/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua index e6af8d55c..084511173 100644 --- a/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua +++ b/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua @@ -158,6 +158,14 @@ describe("The fs library", function() expect(fs.combine("", "a")):eq("a") expect(fs.combine("a", "", "b", "c")):eq("a/b/c") end) + + it("preserves pattern characters", function() + expect(fs.combine("foo*?")):eq("foo*?") + end) + + it("sanitises paths", function() + expect(fs.combine("foo\":<>|")):eq("foo") + end) end) describe("fs.getName", function() @@ -175,6 +183,14 @@ describe("The fs library", function() it("returns '..' for parent directories", function() expect(fs.getName("..")):eq("..") end) + + it("preserves pattern characters", function() + expect(fs.getName("foo*?")):eq("foo*?") + end) + + it("sanitises paths", function() + expect(fs.getName("foo\":<>|")):eq("foo") + end) end) describe("fs.getDir", function() @@ -193,6 +209,14 @@ describe("The fs library", function() expect(fs.getDir("..")):eq("../..") expect(fs.getDir("../..")):eq("../../..") end) + + it("preserves pattern characters", function() + expect(fs.getDir("foo*?/x")):eq("foo*?") + end) + + it("sanitises paths", function() + expect(fs.getDir("foo\":<>|/x")):eq("foo") + end) end) describe("fs.getSize", function() From 43770fa9bd0ab99d7ea800e65e368c58a30a21d3 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 18 Aug 2024 12:56:36 +0100 Subject: [PATCH 22/26] Remove usage of deprecated legacy Java Date API I've been staring at this warning for years, and ignored it thinking it would be a pain to fix. I'm a fool! --- .../src/main/java/dan200/computercraft/core/apis/OSAPI.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e31f76d73..49c75e8cb 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 @@ -122,7 +122,7 @@ private static int getDayForCalendar(Calendar c) { } private static long getEpochForCalendar(Calendar c) { - return c.getTime().getTime(); + return c.getTimeInMillis(); } /** From 9b2f974a8121fd87d329a2d095d1887ff401e96e Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Mon, 19 Aug 2024 08:05:19 +0100 Subject: [PATCH 23/26] Some tweaks to the turtle docs - Mention only diamond tools can be used as upgrades, and be clearer that only the pickaxe and sword are actually useful. We probably could be more explicit here, but struggled to find a way to do that. - Expliitly list which peripherals can be equipped. - Add turtle recipes. --- .../shared/turtle/apis/TurtleAPI.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) 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 b1ad1af39..5eab546fa 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 @@ -47,18 +47,24 @@ *

* ## Turtle upgrades * While a normal turtle can move about the world and place blocks, its functionality is limited. Thankfully, turtles - * can be upgraded with *tools* and [peripherals][`peripheral`]. Turtles have two upgrade slots, one on the left and right - * sides. Upgrades can be equipped by crafting a turtle with the upgrade, or calling the [`turtle.equipLeft`]/[`turtle.equipRight`] - * functions. + * can be upgraded with upgrades. Turtles have two upgrade slots, one on the left and right sides. Upgrades can be + * equipped by crafting a turtle with the upgrade, or calling the [`turtle.equipLeft`]/[`turtle.equipRight`] functions. *

- * Turtle tools allow you to break blocks ([`turtle.dig`]) and attack entities ([`turtle.attack`]). Some tools are more - * suitable to a task than others. For instance, a diamond pickaxe can break every block, while a sword does more - * damage. Other tools have more niche use-cases, for instance hoes can til dirt. + * By default, any diamond tool may be used as an upgrade (though more may be added with [datapacks]). The diamond + * pickaxe may be used to break blocks (with [`turtle.dig`]), while the sword can attack entities ([`turtle.attack`]). + * Other tools have more niche use-cases, for instance hoes can til dirt. *

- * Peripherals (such as the [wireless modem][`modem`] or [`speaker`]) can also be equipped as upgrades. These are then - * accessible by accessing the `"left"` or `"right"` peripheral. + * Some peripherals (namely [speakers][`speaker`] and Ender and Wireless [modems][`modem`]) can also be equipped as + * upgrades. These are then accessible by accessing the `"left"` or `"right"` peripheral. + *

+ * ## Recipes + *

+ * + * + *
*

* [Turtle Graphics]: https://en.wikipedia.org/wiki/Turtle_graphics "Turtle graphics" + * [datapacks]: https://datapacks.madefor.cc "" * * @cc.module turtle * @cc.since 1.3 From d7cea55e2ab15a064d890ade511022780131575f Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Mon, 19 Aug 2024 08:10:50 +0100 Subject: [PATCH 24/26] Add recipes for pocket computers too This is a little daft (recipes feel a little clumsy and tacked on), but it's better than them being nowhere. --- .../dan200/computercraft/shared/pocket/apis/PocketAPI.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 67b171c37..d127f50f7 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 @@ -30,6 +30,12 @@ * print("On something else") * end * } + *

+ * ## Recipes + *

+ * + * + *
* * @cc.module pocket */ From 8080dcdd9e0e1fb32b75dde73f4aa65162620e2c Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Mon, 19 Aug 2024 17:28:02 +0100 Subject: [PATCH 25/26] Fix pocket computers not being active in the off-hand While Item.inventoryTick is passed a slot number, apparently that slot corresponds to the offset within a particular inventory compartment (such as the main inventory or armour)[^1], rather than the inventory as a whole. In the case of the off-hand, this means the pocket computer is set to be in slot 0. When we next tick the computer (to send terminal updates), we then assume the item has gone missing, and so skip sending updates. Fixes #1945. [^1]: A fun side effect of this is that the "selected" flag is true for the off-hand iff the player has slot 0 active. This whole thing feels like a vanilla bug, but who knows! --- .../pocket/items/PocketComputerItem.java | 8 ++++-- .../shared/util/InventoryUtil.java | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) 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 08c698751..348c96dec 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 @@ -128,12 +128,16 @@ private boolean updateItem(ItemStack stack, PocketBrain brain) { } @Override - public void inventoryTick(ItemStack stack, Level world, Entity entity, int slotNum, boolean selected) { + public void inventoryTick(ItemStack stack, Level world, Entity entity, int compartmentSlot, boolean selected) { // This (in vanilla at least) is only called for players. Don't bother to handle other entities. if (world.isClientSide || !(entity instanceof ServerPlayer player)) return; + // Find the actual slot the item exists in, aborting if it can't be found. + var slot = InventoryUtil.getInventorySlotFromCompartment(player, compartmentSlot, stack); + if (slot < 0) return; + // If we're in the inventory, create a computer and keep it alive. - var holder = new PocketHolder.PlayerHolder(player, slotNum); + var holder = new PocketHolder.PlayerHolder(player, slot); var brain = getOrCreateBrain((ServerLevel) world, holder, stack); brain.computer().keepAlive(); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/util/InventoryUtil.java b/projects/common/src/main/java/dan200/computercraft/shared/util/InventoryUtil.java index 35ed3adbd..3e833566f 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/util/InventoryUtil.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/util/InventoryUtil.java @@ -9,9 +9,12 @@ import net.minecraft.server.level.ServerLevel; import net.minecraft.world.Container; import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; import net.minecraft.world.phys.EntityHitResult; import net.minecraft.world.phys.Vec3; @@ -35,6 +38,28 @@ public static int getHandSlot(Player player, InteractionHand hand) { }; } + /** + * Map a slot inside a player's compartment to a slot in the full player's inventory. + *

+ * {@link Inventory#tick()} passes in a slot to {@link Item#inventoryTick(ItemStack, Level, Entity, int, boolean)}. + * However, this slot corresponds to the index within the current compartment (items, armour, offhand) and not + * the actual slot. + *

+ * This method searches the relevant compartments (inventory and offhand, skipping armour) for the stack, returning + * its slot if found. + * + * @param player The player holding the item. + * @param slot The slot inside the compartment. + * @param stack The stack being ticked. + * @return The inventory slot, or {@code -1} if the item could not be found in the inventory. + */ + public static int getInventorySlotFromCompartment(Player player, int slot, ItemStack stack) { + if (stack.isEmpty()) throw new IllegalArgumentException("Cannot search for empty stack"); + if (player.getInventory().getItem(slot) == stack) return slot; + if (player.getInventory().getItem(Inventory.SLOT_OFFHAND) == stack) return Inventory.SLOT_OFFHAND; + return -1; + } + public static @Nullable Container getEntityContainer(ServerLevel level, BlockPos pos, Direction side) { var vecStart = new Vec3( pos.getX() + 0.5 + 0.6 * side.getStepX(), From d24984c1d561aeb12f624e75f1fa5649aa8ec766 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Mon, 19 Aug 2024 18:28:22 +0100 Subject: [PATCH 26/26] Bump CC:T to 1.113.0 --- gradle.properties | 2 +- .../data/computercraft/lua/rom/help/changelog.md | 10 ++++++++++ .../data/computercraft/lua/rom/help/whatsnew.md | 14 ++++++-------- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/gradle.properties b/gradle.properties index 93d5e0dba..c2fb7cb55 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ kotlin.jvm.target.validation.mode=error # Mod properties isUnstable=false -modVersion=1.112.0 +modVersion=1.113.0 # Minecraft properties: We want to configure this here so we can read it in settings.gradle mcVersion=1.20.1 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 c15e79772..e2f5f2b34 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,13 @@ +# New features in CC: Tweaked 1.113.0 + +* Allow placing printed pages and books in lecterns. + +Several bug fixes: +* Various documentation fixes (MCJack123) +* Fix computers and turtles not being dropped when exploded with TNT. +* Fix crash when turtles are broken while mining a block. +* Fix pocket computer terminals not updating when in the off-hand. + # New features in CC: Tweaked 1.112.0 * Report a custom error when using `!` instead of `not`. 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 65e718be5..05c2bdfc3 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,13 +1,11 @@ -New features in CC: Tweaked 1.112.0 +New features in CC: Tweaked 1.113.0 -* Report a custom error when using `!` instead of `not`. -* Update several translations (zyxkad, MineKID-LP). -* Add `cc.strings.split` function. +* Allow placing printed pages and books in lecterns. Several bug fixes: -* Fix `drive.getAudioTitle` returning `nil` when no disk is inserted. -* Preserve item data when upgrading pocket computers. -* Add missing bounds check to `cc.strings.wrap` (Lupus950). -* Fix modems not moving with Create contraptions. +* Various documentation fixes (MCJack123) +* Fix computers and turtles not being dropped when exploded with TNT. +* Fix crash when turtles are broken while mining a block. +* Fix pocket computer terminals not updating when in the off-hand. Type "help changelog" to see the full version history.