From f8785a092fe4eb6971a490f262b3bded64c738a8 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 2 Mar 2025 21:14:27 +0000 Subject: [PATCH 1/8] Fix particle texture for turtle colour model This is never actually used in practice, but let's avoid any missing texture reference warnings! Embarrassing that I hadn't noticed this before! --- .../src/main/kotlin/cc/tweaked/gradle/XmlUtil.kt | 16 ---------------- .../models/block/turtle_colour.json | 2 +- 2 files changed, 1 insertion(+), 17 deletions(-) delete mode 100644 buildSrc/src/main/kotlin/cc/tweaked/gradle/XmlUtil.kt diff --git a/buildSrc/src/main/kotlin/cc/tweaked/gradle/XmlUtil.kt b/buildSrc/src/main/kotlin/cc/tweaked/gradle/XmlUtil.kt deleted file mode 100644 index af97f55e2..000000000 --- a/buildSrc/src/main/kotlin/cc/tweaked/gradle/XmlUtil.kt +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package cc.tweaked.gradle - -import groovy.util.Node -import groovy.util.NodeList - -object XmlUtil { - fun findChild(node: Node, name: String): Node? = when (val child = node.get(name)) { - is Node -> child - is NodeList -> child.singleOrNull() as Node? - else -> null - } -} diff --git a/projects/common/src/main/resources/assets/computercraft/models/block/turtle_colour.json b/projects/common/src/main/resources/assets/computercraft/models/block/turtle_colour.json index 652791dae..3d5de561e 100644 --- a/projects/common/src/main/resources/assets/computercraft/models/block/turtle_colour.json +++ b/projects/common/src/main/resources/assets/computercraft/models/block/turtle_colour.json @@ -1,7 +1,7 @@ { "parent": "computercraft:block/turtle_base", "textures": { - "texture": "computercraft:block/turtle_colour", + "particle": "computercraft:block/turtle_colour_body_front", "body_back": "computercraft:block/turtle_colour_body_back", "body_backpack": "computercraft:block/turtle_colour_body_backpack", "body_bottom": "computercraft:block/turtle_colour_body_bottom", From a892739f8eb87fb078dedae07d8c8614b9d6da4b Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Mon, 3 Mar 2025 10:09:54 +0000 Subject: [PATCH 2/8] Specify precision in monitor fragment shader Some people run Minecraft on OpenGL ES GPUs via the gl4es translation bridge. This sets the default precision for floats and ints, but not usamplerBuffer. Using lowp should be fine here (we don't need to encode much info!), but we use mediump just in case. Have run this through the Mali Offline compiler, and it seems fine with it. Fixes #2127. --- .../assets/minecraft/shaders/core/computercraft/monitor_tbo.fsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/common/src/main/resources/assets/minecraft/shaders/core/computercraft/monitor_tbo.fsh b/projects/common/src/main/resources/assets/minecraft/shaders/core/computercraft/monitor_tbo.fsh index 85e91499b..d65cb78d6 100644 --- a/projects/common/src/main/resources/assets/minecraft/shaders/core/computercraft/monitor_tbo.fsh +++ b/projects/common/src/main/resources/assets/minecraft/shaders/core/computercraft/monitor_tbo.fsh @@ -10,7 +10,7 @@ #define FONT_HEIGHT 9.0 uniform sampler2D Sampler0; // Font -uniform usamplerBuffer Tbo; +uniform mediump usamplerBuffer Tbo; layout(std140) uniform MonitorData { vec3 Palette[16]; From 0cff73e2fcc988839ebfbd9e056e70d3812b3ac9 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Mon, 3 Mar 2025 21:30:19 +0000 Subject: [PATCH 3/8] Add turtle.getEquipped{Left,Right} These just return details about the currently equipped *item*. This allows us to expose information about the currently equipped upgrade, without having to invent a whole new format. Docs are a bit consise, but didn't really know how to flesh them out any further. Fixes #964, fixes #1613, closes #1692. --- gradle/libs.versions.toml | 2 +- .../shared/turtle/apis/TurtleAPI.java | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6c3e81ff7..e7e634de1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -57,7 +57,7 @@ junitPlatform = "1.11.4" jmh = "1.37" # Build tools -cctJavadoc = "1.8.3" +cctJavadoc = "1.8.4" checkstyle = "10.21.2" errorProne-core = "2.36.0" errorProne-plugin = "4.1.0" 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 029f681bb..0e052045a 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 @@ -13,7 +13,9 @@ import dan200.computercraft.core.metrics.Metrics; import dan200.computercraft.core.metrics.MetricsObserver; import dan200.computercraft.shared.peripheral.generic.methods.AbstractInventoryMethods; import dan200.computercraft.shared.turtle.core.*; +import org.jspecify.annotations.Nullable; +import java.util.Map; import java.util.Optional; /** @@ -659,6 +661,7 @@ public class TurtleAPI implements ILuaAPI { * @cc.treturn [2] string The reason equipping this item failed. * @cc.since 1.6 * @see #equipRight() + * @see #getEquippedLeft() */ @LuaFunction public final MethodResult equipLeft() { @@ -678,12 +681,43 @@ public class TurtleAPI implements ILuaAPI { * @cc.treturn [2] string The reason equipping this item failed. * @cc.since 1.6 * @see #equipLeft() + * @see #getEquippedRight() */ @LuaFunction public final MethodResult equipRight() { return trackCommand(new TurtleEquipCommand(TurtleSide.RIGHT)); } + /** + * Get the upgrade currently equipped on the left of the turtle. + *

+ * This returns information about the currently equipped item, in the same form as + * {@link #getItemDetail(ILuaContext, Optional, Optional)}. + * + * @return Details about the currently equipped item, or {@code nil} if no upgrade is equipped. + * @see #equipLeft() + */ + @LuaFunction(mainThread = true) + public final @Nullable Map getEquippedLeft() { + var upgrade = turtle.getUpgradeWithData(TurtleSide.LEFT); + return upgrade == null ? null : VanillaDetailRegistries.ITEM_STACK.getDetails(upgrade.getUpgradeItem()); + } + + /** + * Get the upgrade currently equipped on the right of the turtle. + *

+ * This returns information about the currently equipped item, in the same form as + * {@link #getItemDetail(ILuaContext, Optional, Optional)}. + * + * @return Details about the currently equipped item, or {@code nil} if no upgrade is equipped. + * @see #equipRight() + */ + @LuaFunction(mainThread = true) + public final @Nullable Map getEquippedRight() { + var upgrade = turtle.getUpgradeWithData(TurtleSide.RIGHT); + return upgrade == null ? null : VanillaDetailRegistries.ITEM_STACK.getDetails(upgrade.getUpgradeItem()); + } + /** * Get information about the block in front of the turtle. * From b42bc0a01acd9b9c049ab202b5511e7f412438c3 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 5 Mar 2025 18:45:55 +0000 Subject: [PATCH 4/8] Bump Loom and vanilla-extract versions --- gradle/libs.versions.toml | 4 ++-- .../dan200/computercraft/shared/turtle/apis/TurtleAPI.java | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e7e634de1..40eafeaf3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -61,7 +61,7 @@ cctJavadoc = "1.8.4" checkstyle = "10.21.2" errorProne-core = "2.36.0" errorProne-plugin = "4.1.0" -fabric-loom = "1.9.2" +fabric-loom = "1.10.3" githubRelease = "2.5.2" gradleVersions = "0.50.0" ideaExt = "1.1.7" @@ -74,7 +74,7 @@ shadow = "8.3.1" spotless = "6.23.3" taskTree = "2.1.1" teavm = "0.11.0-SQUID.1" -vanillaExtract = "0.2.0" +vanillaExtract = "0.2.1" versionCatalogUpdate = "0.8.1" [libraries] 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 0e052045a..dad9dffba 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 @@ -696,6 +696,7 @@ public class TurtleAPI implements ILuaAPI { * * @return Details about the currently equipped item, or {@code nil} if no upgrade is equipped. * @see #equipLeft() + * @cc.since 1.116.0 */ @LuaFunction(mainThread = true) public final @Nullable Map getEquippedLeft() { @@ -711,6 +712,7 @@ public class TurtleAPI implements ILuaAPI { * * @return Details about the currently equipped item, or {@code nil} if no upgrade is equipped. * @see #equipRight() + * @cc.since 1.116.0 */ @LuaFunction(mainThread = true) public final @Nullable Map getEquippedRight() { From 1b8344d0a30c9d810c2782c94fdf7bc7ca62bedd Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 5 Mar 2025 19:01:32 +0000 Subject: [PATCH 5/8] Ignore shader loading errors Another go at fixing #2127. In a892739f8eb87fb078dedae07d8c8614b9d6da4b we set the precision on the Tbo uniform. However, this is stripped in the shader pre-processing Pojav/gl4es does, and so has no effect. As a (terrible) workaround, we now just ignore shader loading errors. This probably does leak memory (we'll never clean up the program), but there's not much we can do about that. --- .../computercraft/client/ClientRegistry.java | 17 ++++++++++++++++- .../client/render/RenderTypes.java | 11 +++++++---- .../shaders/core/computercraft/monitor_tbo.fsh | 2 +- 3 files changed, 24 insertions(+), 6 deletions(-) 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 9fe909ee9..7f54840cf 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java +++ b/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java @@ -47,6 +47,8 @@ import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.ItemLike; import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; @@ -63,6 +65,8 @@ import java.util.function.Supplier; * @see ModRegistry The common registry for actual game objects. */ public final class ClientRegistry { + private static final Logger LOG = LoggerFactory.getLogger(ClientRegistry.class); + private ClientRegistry() { } @@ -182,7 +186,18 @@ public final class ClientRegistry { } public static void registerShaders(ResourceProvider resources, BiConsumer> load) throws IOException { - RenderTypes.registerShaders(resources, load); + RenderTypes.registerShaders(resources, (name, create, onLoaded) -> { + ShaderInstance shader; + try { + shader = create.get(); + } catch (Exception e) { + LOG.error("Failed to load {}", name, e); + onLoaded.accept(null); + return; + } + + load.accept(shader, onLoaded); + }); } private record UnclampedPropertyFunction( diff --git a/projects/common/src/client/java/dan200/computercraft/client/render/RenderTypes.java b/projects/common/src/client/java/dan200/computercraft/client/render/RenderTypes.java index 327038690..01f4cf151 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/render/RenderTypes.java +++ b/projects/common/src/client/java/dan200/computercraft/client/render/RenderTypes.java @@ -16,11 +16,11 @@ import net.minecraft.client.renderer.RenderType; import net.minecraft.client.renderer.ShaderInstance; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.ResourceProvider; +import org.apache.commons.io.function.IOSupplier; import org.jspecify.annotations.Nullable; import java.io.IOException; import java.util.Objects; -import java.util.function.BiConsumer; import java.util.function.Consumer; /** @@ -68,9 +68,12 @@ public class RenderTypes { return Objects.requireNonNull(GameRenderer.getRendertypeTextShader(), "Text shader has not been registered"); } - public static void registerShaders(ResourceProvider resources, BiConsumer> load) throws IOException { - load.accept( - new MonitorTextureBufferShader( + public interface ShaderLoader { + void tryLoad(String name, IOSupplier create, Consumer<@Nullable ShaderInstance> accept) throws IOException; + } + + public static void registerShaders(ResourceProvider resources, ShaderLoader load) throws IOException { + load.tryLoad("monitor shader", () -> new MonitorTextureBufferShader( resources, ComputerCraftAPI.MOD_ID + "/monitor_tbo", MONITOR_TBO.format() diff --git a/projects/common/src/main/resources/assets/minecraft/shaders/core/computercraft/monitor_tbo.fsh b/projects/common/src/main/resources/assets/minecraft/shaders/core/computercraft/monitor_tbo.fsh index d65cb78d6..85e91499b 100644 --- a/projects/common/src/main/resources/assets/minecraft/shaders/core/computercraft/monitor_tbo.fsh +++ b/projects/common/src/main/resources/assets/minecraft/shaders/core/computercraft/monitor_tbo.fsh @@ -10,7 +10,7 @@ #define FONT_HEIGHT 9.0 uniform sampler2D Sampler0; // Font -uniform mediump usamplerBuffer Tbo; +uniform usamplerBuffer Tbo; layout(std140) uniform MonitorData { vec3 Palette[16]; From b97634b7177e4855f56de44f80a882313654866a Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 8 Mar 2025 23:39:11 +0000 Subject: [PATCH 6/8] Flesh out LuaTable a bit Add a whole buncha helper methods for parsing values, much like IArguments. This allows us to remove TableHelper. Gosh, that dates back to 2018! --- .../computercraft/api/lua/IArguments.java | 3 + .../computercraft/api/lua/LuaTable.java | 379 +++++++++++++++++- .../computercraft/api/lua/LuaValues.java | 14 +- .../computercraft/core/apis/HTTPAPI.java | 26 +- .../computercraft/core/apis/TableHelper.java | 156 ------- 5 files changed, 394 insertions(+), 184 deletions(-) delete mode 100644 projects/core/src/main/java/dan200/computercraft/core/apis/TableHelper.java diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/lua/IArguments.java b/projects/core-api/src/main/java/dan200/computercraft/api/lua/IArguments.java index e70527785..f554b05e5 100644 --- a/projects/core-api/src/main/java/dan200/computercraft/api/lua/IArguments.java +++ b/projects/core-api/src/main/java/dan200/computercraft/api/lua/IArguments.java @@ -223,6 +223,9 @@ public interface IArguments { /** * Get an argument as a table. + *

+ * The returned table may be converted into a {@link LuaTable} (using {@link ObjectLuaTable}) for easier parsing of + * table keys. * * @param index The argument number. * @return The argument's value. diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/lua/LuaTable.java b/projects/core-api/src/main/java/dan200/computercraft/api/lua/LuaTable.java index 006f6f51d..fa42b1395 100644 --- a/projects/core-api/src/main/java/dan200/computercraft/api/lua/LuaTable.java +++ b/projects/core-api/src/main/java/dan200/computercraft/api/lua/LuaTable.java @@ -7,15 +7,18 @@ package dan200.computercraft.api.lua; import org.jspecify.annotations.Nullable; import java.util.Map; +import java.util.Optional; import static dan200.computercraft.api.lua.LuaValues.*; /** - * A view of a Lua table, which may be able to access table elements in a more optimised manner than - * {@link IArguments#getTable(int)}. + * A view of a Lua table. + *

+ * Much like {@link IArguments}, this allows for convenient parsing of fields from a Lua table. * * @param The type of keys in a table, will typically be a wildcard. * @param The type of values in a table, will typically be a wildcard. + * @see ObjectArguments */ public interface LuaTable extends Map { /** @@ -29,19 +32,47 @@ public interface LuaTable extends Map { return size; } + /** + * Get an array entry as a double. + * + * @param index The index in the table, starting at 1. + * @return The entry's value. + * @throws LuaException If the value is not a number. + * @see #getFiniteDouble(int) if you require this to be finite (i.e. not infinite or NaN). + * @since 1.116 + */ + default double getDouble(int index) throws LuaException { + Object value = get((double) index); + if (!(value instanceof Number number)) throw badTableItem(index, "number", getType(value)); + return number.doubleValue(); + } + + /** + * Get a table entry as a double. + * + * @param key The name of the field in the table. + * @return The field's value. + * @throws LuaException If the value is not a number. + * @see #getFiniteDouble(String) if you require this to be finite (i.e. not infinite or NaN). + * @since 1.116 + */ + default double getDouble(String key) throws LuaException { + Object value = get(key); + if (!(value instanceof Number number)) throw badField(key, "number", getType(value)); + return number.doubleValue(); + } + /** * Get an array entry as an integer. * * @param index The index in the table, starting at 1. - * @return The table's value. + * @return The entry's value. * @throws LuaException If the value is not an integer. */ default long getLong(int index) throws LuaException { Object value = get((double) index); if (!(value instanceof Number number)) throw badTableItem(index, "number", getType(value)); - - var asDouble = number.doubleValue(); - if (!Double.isFinite(asDouble)) throw badTableItem(index, "number", getNumericType(asDouble)); + checkFiniteIndex(index, number.doubleValue()); return number.longValue(); } @@ -49,15 +80,13 @@ public interface LuaTable extends Map { * Get a table entry as an integer. * * @param key The name of the field in the table. - * @return The table's value. + * @return The field's value. * @throws LuaException If the value is not an integer. */ default long getLong(String key) throws LuaException { Object value = get(key); if (!(value instanceof Number number)) throw badField(key, "number", getType(value)); - - var asDouble = number.doubleValue(); - if (!Double.isFinite(asDouble)) throw badField(key, "number", getNumericType(asDouble)); + checkFiniteField(key, number.doubleValue()); return number.longValue(); } @@ -65,7 +94,7 @@ public interface LuaTable extends Map { * Get an array entry as an integer. * * @param index The index in the table, starting at 1. - * @return The table's value. + * @return The entry's value. * @throws LuaException If the value is not an integer. */ default int getInt(int index) throws LuaException { @@ -76,13 +105,339 @@ public interface LuaTable extends Map { * Get a table entry as an integer. * * @param key The name of the field in the table. - * @return The table's value. + * @return The field's value. * @throws LuaException If the value is not an integer. */ default int getInt(String key) throws LuaException { return (int) getLong(key); } + /** + * Get an argument as a finite number (not infinite or NaN). + * + * @param index The index in the table, starting at 1. + * @return The entry's value. + * @throws LuaException If the value is not finite. + * @since 1.116 + */ + default double getFiniteDouble(int index) throws LuaException { + return checkFiniteIndex(index, getDouble(index)); + } + + /** + * Get an argument as a finite number (not infinite or NaN). + * + * @param key The name of the field in the table. + * @return The field's value. + * @throws LuaException If the value is not finite. + * @since 1.116 + */ + default double getFiniteDouble(String key) throws LuaException { + return checkFiniteField(key, getDouble(key)); + } + + /** + * Get an array entry as a boolean. + * + * @param index The index in the table, starting at 1. + * @return The entry's value. + * @throws LuaException If the value is not a boolean. + * @since 1.116 + */ + default boolean getBoolean(int index) throws LuaException { + Object value = get((double) index); + if (!(value instanceof Boolean bool)) throw badTableItem(index, "boolean", getType(value)); + return bool; + } + + /** + * Get a table entry as a boolean. + * + * @param key The name of the field in the table. + * @return The field's value. + * @throws LuaException If the value is not a boolean. + * @since 1.116 + */ + default boolean getBoolean(String key) throws LuaException { + Object value = get(key); + if (!(value instanceof Boolean bool)) throw badField(key, "boolean", getType(value)); + return bool; + } + + /** + * Get an array entry as a string. + * + * @param index The index in the table, starting at 1. + * @return The entry's value. + * @throws LuaException If the value is not a string. + * @since 1.116 + */ + default String getString(int index) throws LuaException { + Object value = get((double) index); + if (!(value instanceof String string)) throw badTableItem(index, "string", getType(value)); + return string; + } + + /** + * Get a table entry as a string. + * + * @param key The name of the field in the table. + * @return The field's value. + * @throws LuaException If the value is not a string. + * @since 1.116 + */ + default String getString(String key) throws LuaException { + Object value = get(key); + if (!(value instanceof String string)) throw badField(key, "string", getType(value)); + return string; + } + + /** + * Get an array entry as a table. + *

+ * The returned table may be converted into a {@link LuaTable} (using {@link ObjectLuaTable}) for easier parsing of + * table keys. + * + * @param index The index in the table, starting at 1. + * @return The entry's value. + * @throws LuaException If the value is not a table. + * @since 1.116 + */ + default Map getTable(int index) throws LuaException { + Object value = get((double) index); + if (!(value instanceof Map table)) throw badTableItem(index, "table", getType(value)); + return table; + } + + /** + * Get a table entry as a table. + *

+ * The returned table may be converted into a {@link LuaTable} (using {@link ObjectLuaTable}) for easier parsing of + * table keys. + * + * @param key The name of the field in the table. + * @return The field's value. + * @throws LuaException If the value is not a table. + * @since 1.116 + */ + default Map getTable(String key) throws LuaException { + Object value = get(key); + if (!(value instanceof Map table)) throw badField(key, "table", getType(value)); + return table; + } + + /** + * Get an array entry as a double. + * + * @param index The index in the table, starting at 1. + * @return The entry's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not a number. + * @see #getFiniteDouble(int) if you require this to be finite (i.e. not infinite or NaN). + * @since 1.116 + */ + default Optional optDouble(int index) throws LuaException { + Object value = get((double) index); + if (value == null) return Optional.empty(); + if (!(value instanceof Number number)) throw badTableItem(index, "number", getType(value)); + return Optional.of(number.doubleValue()); + } + + /** + * Get a table entry as a double. + * + * @param key The name of the field in the table. + * @return The field's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not a number. + * @see #getFiniteDouble(String) if you require this to be finite (i.e. not infinite or NaN). + * @since 1.116 + */ + default Optional optDouble(String key) throws LuaException { + Object value = get(key); + if (value == null) return Optional.empty(); + if (!(value instanceof Number number)) throw badField(key, "number", getType(value)); + return Optional.of(number.doubleValue()); + } + + /** + * Get an array entry as an integer. + * + * @param index The index in the table, starting at 1. + * @return The entry's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not an integer. + * @since 1.116 + */ + default Optional optLong(int index) throws LuaException { + Object value = get((double) index); + if (value == null) return Optional.empty(); + if (!(value instanceof Number number)) throw badTableItem(index, "number", getType(value)); + checkFiniteIndex(index, number.doubleValue()); + return Optional.of(number.longValue()); + } + + /** + * Get a table entry as an integer. + * + * @param key The name of the field in the table. + * @return The field's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not an integer. + * @since 1.116 + */ + default Optional optLong(String key) throws LuaException { + Object value = get(key); + if (value == null) return Optional.empty(); + if (!(value instanceof Number number)) throw badField(key, "number", getType(value)); + checkFiniteField(key, number.doubleValue()); + return Optional.of(number.longValue()); + } + + /** + * Get an array entry as an integer. + * + * @param index The index in the table, starting at 1. + * @return The entry's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not an integer. + * @since 1.116 + */ + default Optional optInt(int index) throws LuaException { + return optLong(index).map(Long::intValue); + } + + /** + * Get a table entry as an integer. + * + * @param key The name of the field in the table. + * @return The field's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not an integer. + * @since 1.116 + */ + default Optional optInt(String key) throws LuaException { + return optLong(key).map(Long::intValue); + } + + /** + * Get an argument as a finite number (not infinite or NaN). + * + * @param index The index in the table, starting at 1. + * @return The entry's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not finite. + * @since 1.116 + */ + default Optional optFiniteDouble(int index) throws LuaException { + var value = optDouble(index); + if (value.isPresent()) checkFiniteIndex(index, value.get()); + return value; + } + + /** + * Get an argument as a finite number (not infinite or NaN). + * + * @param key The name of the field in the table. + * @return The field's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not finite. + * @since 1.116 + */ + default Optional optFiniteDouble(String key) throws LuaException { + var value = optDouble(key); + if (value.isPresent()) checkFiniteField(key, value.get()); + return value; + } + + /** + * Get an array entry as a boolean. + * + * @param index The index in the table, starting at 1. + * @return The entry's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not a boolean. + * @since 1.116 + */ + default Optional optBoolean(int index) throws LuaException { + Object value = get((double) index); + if (value == null) return Optional.empty(); + if (!(value instanceof Boolean bool)) throw badTableItem(index, "boolean", getType(value)); + return Optional.of(bool); + } + + /** + * Get a table entry as a boolean. + * + * @param key The name of the field in the table. + * @return The field's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not a boolean. + * @since 1.116 + */ + default Optional optBoolean(String key) throws LuaException { + Object value = get(key); + if (value == null) return Optional.empty(); + if (!(value instanceof Boolean bool)) throw badField(key, "boolean", getType(value)); + return Optional.of(bool); + } + + /** + * Get an array entry as a double. + * + * @param index The index in the table, starting at 1. + * @return The entry's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not a string. + * @since 1.116 + */ + default Optional optString(int index) throws LuaException { + Object value = get((double) index); + if (value == null) return Optional.empty(); + if (!(value instanceof String string)) throw badTableItem(index, "string", getType(value)); + return Optional.of(string); + } + + /** + * Get a table entry as a string. + * + * @param key The name of the field in the table. + * @return The field's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not a string. + * @since 1.116 + */ + default Optional optString(String key) throws LuaException { + Object value = get(key); + if (value == null) return Optional.empty(); + if (!(value instanceof String string)) throw badField(key, "string", getType(value)); + return Optional.of(string); + } + + /** + * Get an array entry as a table. + *

+ * The returned table may be converted into a {@link LuaTable} (using {@link ObjectLuaTable}) for easier parsing of + * table keys. + * + * @param index The index in the table, starting at 1. + * @return The entry's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not a table. + * @since 1.116 + */ + default Optional> optTable(int index) throws LuaException { + Object value = get((double) index); + if (value == null) return Optional.empty(); + if (!(value instanceof Map table)) throw badTableItem(index, "table", getType(value)); + return Optional.of(table); + } + + /** + * Get a table entry as a table. + *

+ * The returned table may be converted into a {@link LuaTable} (using {@link ObjectLuaTable}) for easier parsing of + * table keys. + * + * @param key The name of the field in the table. + * @return The field's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not a table. + * @since 1.116 + */ + default Optional> optTable(String key) throws LuaException { + Object value = get(key); + if (value == null) return Optional.empty(); + if (!(value instanceof Map table)) throw badField(key, "table", getType(value)); + return Optional.of(table); + } + @Nullable @Override default V put(K o, V o2) { diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/lua/LuaValues.java b/projects/core-api/src/main/java/dan200/computercraft/api/lua/LuaValues.java index 61ec3635e..523ba84e6 100644 --- a/projects/core-api/src/main/java/dan200/computercraft/api/lua/LuaValues.java +++ b/projects/core-api/src/main/java/dan200/computercraft/api/lua/LuaValues.java @@ -97,7 +97,7 @@ public final class LuaValues { * @return The constructed exception, which should be thrown immediately. */ public static LuaException badTableItem(int index, String expected, String actual) { - return new LuaException("table item #" + index + " is not " + expected + " (got " + actual + ")"); + return new LuaException("bad item #" + index + " (" + expected + " expected, got " + actual + ")"); } /** @@ -109,7 +109,7 @@ public final class LuaValues { * @return The constructed exception, which should be thrown immediately. */ public static LuaException badField(String key, String expected, String actual) { - return new LuaException("field " + key + " is not " + expected + " (got " + actual + ")"); + return new LuaException("bad field '" + key + "' (" + expected + " expected, got " + actual + ")"); } /** @@ -138,6 +138,16 @@ public final class LuaValues { return value; } + static double checkFiniteIndex(int index, double value) throws LuaException { + if (!Double.isFinite(value)) throw badTableItem(index, "number", getNumericType(value)); + return value; + } + + static double checkFiniteField(String key, double value) throws LuaException { + if (!Double.isFinite(value)) throw badField(key, "number", getNumericType(value)); + return value; + } + /** * Ensure a string is a valid enum value. * diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java b/projects/core/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java index 4e198b4cc..3cce0fafa 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java @@ -20,7 +20,6 @@ import java.util.Locale; import java.util.Map; import java.util.Optional; -import static dan200.computercraft.core.apis.TableHelper.*; import static dan200.computercraft.core.util.ArgumentHelpers.assertBetween; /** @@ -78,15 +77,14 @@ public class HTTPAPI implements ILuaAPI { Optional timeoutArg; if (args.get(0) instanceof Map) { - var options = args.getTable(0); - address = getStringField(options, "url"); - var postString = optStringField(options, "body", null); - postBody = postString == null ? null : LuaValues.encode(postString); - headerTable = optTableField(options, "headers", Map.of()); - binary = optBooleanField(options, "binary", false); - requestMethod = optStringField(options, "method", null); - redirect = optBooleanField(options, "redirect", true); - timeoutArg = optRealField(options, "timeout"); + var options = new ObjectLuaTable(args.getTable(0)); + address = options.getString("url"); + postBody = options.optString("body").map(LuaValues::encode).orElse(null); + headerTable = options.optTable("headers").orElse(Map.of()); + binary = options.optBoolean("binary").orElse(false); + requestMethod = options.optString("method").orElse(null); + redirect = options.optBoolean("redirect").orElse(true); + timeoutArg = options.optFiniteDouble("timeout"); } else { // Get URL and post information address = args.getString(0); @@ -151,10 +149,10 @@ public class HTTPAPI implements ILuaAPI { Optional timeoutArg; if (args.get(0) instanceof Map) { - var options = args.getTable(0); - address = getStringField(options, "url"); - headerTable = optTableField(options, "headers", Map.of()); - timeoutArg = optRealField(options, "timeout"); + var options = new ObjectLuaTable(args.getTableUnsafe(0)); + address = options.getString("url"); + headerTable = options.optTable("headers").orElse(Map.of()); + timeoutArg = options.optFiniteDouble("timeout"); } else { address = args.getString(0); headerTable = args.optTable(1, Map.of()); diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/TableHelper.java b/projects/core/src/main/java/dan200/computercraft/core/apis/TableHelper.java deleted file mode 100644 index 9ccb5f39e..000000000 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/TableHelper.java +++ /dev/null @@ -1,156 +0,0 @@ -// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.core.apis; - -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.lua.LuaValues; -import org.jspecify.annotations.Nullable; - -import java.util.Map; -import java.util.Optional; - -import static dan200.computercraft.api.lua.LuaValues.getNumericType; - -/** - * Various helpers for tables. - */ -public final class TableHelper { - private TableHelper() { - throw new IllegalStateException("Cannot instantiate singleton " + getClass().getName()); - } - - public static LuaException badKey(String key, String expected, @Nullable Object actual) { - return badKey(key, expected, LuaValues.getType(actual)); - } - - public static LuaException badKey(String key, String expected, String actual) { - return new LuaException("bad field '" + key + "' (" + expected + " expected, got " + actual + ")"); - } - - public static double getNumberField(Map table, String key) throws LuaException { - var value = table.get(key); - if (value instanceof Number) { - return ((Number) value).doubleValue(); - } else { - throw badKey(key, "number", value); - } - } - - public static int getIntField(Map table, String key) throws LuaException { - var value = table.get(key); - if (value instanceof Number) { - return (int) ((Number) value).longValue(); - } else { - throw badKey(key, "number", value); - } - } - - public static double getRealField(Map table, String key) throws LuaException { - return checkReal(key, getNumberField(table, key)); - } - - public static boolean getBooleanField(Map table, String key) throws LuaException { - var value = table.get(key); - if (value instanceof Boolean) { - return (Boolean) value; - } else { - throw badKey(key, "boolean", value); - } - } - - public static String getStringField(Map table, String key) throws LuaException { - var value = table.get(key); - if (value instanceof String) { - return (String) value; - } else { - throw badKey(key, "string", value); - } - } - - @SuppressWarnings("unchecked") - public static Map getTableField(Map table, String key) throws LuaException { - var value = table.get(key); - if (value instanceof Map) { - return (Map) value; - } else { - throw badKey(key, "table", value); - } - } - - public static double optNumberField(Map table, String key, double def) throws LuaException { - var value = table.get(key); - if (value == null) { - return def; - } else if (value instanceof Number) { - return ((Number) value).doubleValue(); - } else { - throw badKey(key, "number", value); - } - } - - public static int optIntField(Map table, String key, int def) throws LuaException { - var value = table.get(key); - if (value == null) { - return def; - } else if (value instanceof Number) { - return (int) ((Number) value).longValue(); - } else { - throw badKey(key, "number", value); - } - } - - public static Optional optRealField(Map table, String key) throws LuaException { - var value = table.get(key); - if (value == null) { - return Optional.empty(); - } else { - return Optional.of(getRealField(table, key)); - } - } - - public static double optRealField(Map table, String key, double def) throws LuaException { - return checkReal(key, optNumberField(table, key, def)); - } - - public static boolean optBooleanField(Map table, String key, boolean def) throws LuaException { - var value = table.get(key); - if (value == null) { - return def; - } else if (value instanceof Boolean) { - return (Boolean) value; - } else { - throw badKey(key, "boolean", value); - } - } - - @Nullable - public static String optStringField(Map table, String key, @Nullable String def) throws LuaException { - var value = table.get(key); - if (value == null) { - return def; - } else if (value instanceof String) { - return (String) value; - } else { - throw badKey(key, "string", value); - } - } - - @SuppressWarnings("unchecked") - public static Map optTableField(Map table, String key, Map def) throws LuaException { - var value = table.get(key); - if (value == null) { - return def; - } else if (value instanceof Map) { - return (Map) value; - } else { - throw badKey(key, "table", value); - } - } - - private static double checkReal(String key, double value) throws LuaException { - if (!Double.isFinite(value)) throw badKey(key, "number", getNumericType(value)); - return value; - } -} From 749b3df227fc3ca51a32ac114d1bcc9a34304c6a Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 9 Mar 2025 11:46:36 +0000 Subject: [PATCH 7/8] Remove PrintoutItem.getType Kinda surprised this is still around! Not sure why I kept it post the-flattening really, it's been redundant for a while. --- .../client/render/CustomLecternRenderer.java | 5 +++-- .../client/render/PrintoutItemRenderer.java | 3 ++- .../dan200/computercraft/shared/ModRegistry.java | 6 +++--- .../shared/media/items/PrintoutItem.java | 15 +-------------- .../shared/media/recipes/PrintoutRecipe.java | 2 +- .../peripheral/printer/PrinterBlockEntity.java | 5 ++--- .../dan200/computercraft/gametest/Turtle_Test.kt | 4 ++-- 7 files changed, 14 insertions(+), 26 deletions(-) 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 index c926ee55b..bb82d2754 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/render/CustomLecternRenderer.java +++ b/projects/common/src/client/java/dan200/computercraft/client/render/CustomLecternRenderer.java @@ -12,6 +12,7 @@ import dan200.computercraft.client.pocket.ClientPocketComputers; import dan200.computercraft.client.render.text.FixedWidthFontRenderer; import dan200.computercraft.core.terminal.Terminal; import dan200.computercraft.core.util.Colour; +import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.lectern.CustomLecternBlockEntity; import dan200.computercraft.shared.media.items.PrintoutItem; import dan200.computercraft.shared.pocket.items.PocketComputerItem; @@ -57,9 +58,9 @@ public class CustomLecternRenderer implements BlockEntityRenderer new TreasureDiskItem(properties().stacksTo(1))); public static final RegistryEntry PRINTED_PAGE = REGISTRY.register("printed_page", - () -> new PrintoutItem(properties().stacksTo(1), PrintoutItem.Type.PAGE)); + () -> new PrintoutItem(properties().stacksTo(1))); public static final RegistryEntry PRINTED_PAGES = REGISTRY.register("printed_pages", - () -> new PrintoutItem(properties().stacksTo(1), PrintoutItem.Type.PAGES)); + () -> new PrintoutItem(properties().stacksTo(1))); public static final RegistryEntry PRINTED_BOOK = REGISTRY.register("printed_book", - () -> new PrintoutItem(properties().stacksTo(1), PrintoutItem.Type.BOOK)); + () -> new PrintoutItem(properties().stacksTo(1))); public static final RegistryEntry SPEAKER = ofBlock(Blocks.SPEAKER, BlockItem::new); public static final RegistryEntry DISK_DRIVE = ofBlock(Blocks.DISK_DRIVE, BlockItem::new); 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 7c8fbf0b2..b2c85672c 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 @@ -33,17 +33,8 @@ public class PrintoutItem extends Item { public static final int LINE_MAX_LENGTH = 25; public static final int MAX_PAGES = 16; - public enum Type { - PAGE, - PAGES, - BOOK - } - - private final Type type; - - public PrintoutItem(Properties settings, Type type) { + public PrintoutItem(Properties settings) { super(settings); - this.type = type; } @Override @@ -103,10 +94,6 @@ public class PrintoutItem extends Item { return ModRegistry.Items.PRINTED_BOOK.get().createFromTitleAndText(title, text, colours); } - public Type getType() { - return type; - } - public static String getTitle(ItemStack stack) { var nbt = stack.getTag(); return nbt != null && nbt.contains(NBT_TITLE) ? nbt.getString(NBT_TITLE) : ""; diff --git a/projects/common/src/main/java/dan200/computercraft/shared/media/recipes/PrintoutRecipe.java b/projects/common/src/main/java/dan200/computercraft/shared/media/recipes/PrintoutRecipe.java index 1f59b5e4f..6601c75fe 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/media/recipes/PrintoutRecipe.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/media/recipes/PrintoutRecipe.java @@ -58,7 +58,7 @@ public final class PrintoutRecipe extends CustomRecipe { for (var x = 0; x < inventory.getWidth(); x++) { var stack = inventory.getItem(x + y * inventory.getWidth()); if (!stack.isEmpty()) { - if (stack.getItem() instanceof PrintoutItem printout && printout.getType() != PrintoutItem.Type.BOOK) { + if (!stack.is(ModRegistry.Items.PRINTED_BOOK.get())) { if (printouts == null) printouts = new ItemStack[9]; printouts[numPrintouts] = stack; numPages += PrintoutItem.getPageCount(stack); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/printer/PrinterBlockEntity.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/printer/PrinterBlockEntity.java index c6fcc8efc..3deb94fce 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/printer/PrinterBlockEntity.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/printer/PrinterBlockEntity.java @@ -5,6 +5,7 @@ package dan200.computercraft.shared.peripheral.printer; import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.common.AbstractContainerBlockEntity; import dan200.computercraft.shared.computer.terminal.NetworkedTerminal; import dan200.computercraft.shared.container.BasicWorldlyContainer; @@ -159,9 +160,7 @@ public final class PrinterBlockEntity extends AbstractContainerBlockEntity imple } static boolean isPaper(ItemStack stack) { - var item = stack.getItem(); - return item == Items.PAPER - || (item instanceof PrintoutItem printout && printout.getType() == PrintoutItem.Type.PAGE); + return stack.is(Items.PAPER) || stack.is(ModRegistry.Items.PRINTED_PAGE.get()); } private boolean canInputPage() { 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 5310f52cd..869084ddc 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 @@ -439,14 +439,14 @@ class Turtle_Test { object : BasicItemDetailProvider("printout", PrintoutItem::class.java) { override fun provideDetails(data: MutableMap, stack: ItemStack, item: PrintoutItem) { - data["type"] = item.type.toString().lowercase() + data["pages"] = PrintoutItem.getPageCount(stack) } }, ) } thenOnComputer { val details = getTurtleItemDetail(detailed = true) - assertEquals(mapOf("type" to "page"), details["printout"]) { + assertEquals(mapOf("pages" to 1), details["printout"]) { "Printout information is returned (whole map is $details)" } } From 63ba3fe2745a0d08e9492e0d6faf6d8385b19923 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 9 Mar 2025 12:19:59 +0000 Subject: [PATCH 8/8] Fix printout crafting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced by the previous commit — I'd made one of the checks too lax. Add some tests for this, so it doesn't happen again, though this code does get a complete rewrite in 1.21 anyway >_>. --- .../shared/media/recipes/PrintoutRecipe.java | 2 +- .../test/shared/ItemStackMatcher.java | 5 ++ .../computercraft/gametest/Printout_Test.kt | 56 +++++++++++++++++++ .../computercraft/gametest/Recipe_Test.kt | 17 ++---- .../gametest/api/TestExtensions.kt | 29 ++++++++++ 5 files changed, 96 insertions(+), 13 deletions(-) diff --git a/projects/common/src/main/java/dan200/computercraft/shared/media/recipes/PrintoutRecipe.java b/projects/common/src/main/java/dan200/computercraft/shared/media/recipes/PrintoutRecipe.java index 6601c75fe..d3a19d33c 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/media/recipes/PrintoutRecipe.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/media/recipes/PrintoutRecipe.java @@ -58,7 +58,7 @@ public final class PrintoutRecipe extends CustomRecipe { for (var x = 0; x < inventory.getWidth(); x++) { var stack = inventory.getItem(x + y * inventory.getWidth()); if (!stack.isEmpty()) { - if (!stack.is(ModRegistry.Items.PRINTED_BOOK.get())) { + if (stack.is(ModRegistry.Items.PRINTED_PAGE.get()) || stack.is(ModRegistry.Items.PRINTED_PAGES.get())) { if (printouts == null) printouts = new ItemStack[9]; printouts[numPrintouts] = stack; numPages += PrintoutItem.getPageCount(stack); diff --git a/projects/common/src/testFixtures/java/dan200/computercraft/test/shared/ItemStackMatcher.java b/projects/common/src/testFixtures/java/dan200/computercraft/test/shared/ItemStackMatcher.java index 2c12f3019..dc6b50285 100644 --- a/projects/common/src/testFixtures/java/dan200/computercraft/test/shared/ItemStackMatcher.java +++ b/projects/common/src/testFixtures/java/dan200/computercraft/test/shared/ItemStackMatcher.java @@ -27,6 +27,11 @@ public class ItemStackMatcher extends TypeSafeMatcher { description.appendValue(stack).appendValue(stack.getTag()); } + @Override + protected void describeMismatchSafely(ItemStack item, Description description) { + description.appendText("was ").appendValue(stack).appendValue(stack.getTag()); + } + public static Matcher isStack(ItemStack stack) { return new ItemStackMatcher(stack); } diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Printout_Test.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Printout_Test.kt index 27c605d52..6685db073 100644 --- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Printout_Test.kt +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Printout_Test.kt @@ -5,9 +5,17 @@ package dan200.computercraft.gametest import dan200.computercraft.gametest.api.* +import dan200.computercraft.shared.ModRegistry +import dan200.computercraft.shared.media.items.PrintoutItem +import dan200.computercraft.test.shared.ItemStackMatcher.isStack +import net.minecraft.gametest.framework.GameTest import net.minecraft.gametest.framework.GameTestGenerator import net.minecraft.gametest.framework.GameTestHelper import net.minecraft.gametest.framework.TestFunction +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import org.hamcrest.MatcherAssert.assertThat +import org.junit.jupiter.api.Assertions.assertEquals class Printout_Test { /** @@ -56,4 +64,52 @@ class Printout_Test { thenExecute { helper.level.dayTime = Times.NOON } } + + @GameTest(template = "default") + fun Craft_pages(helper: GameTestHelper) = helper.immediate { + // Assert that crafting with only one page fails + helper.assertNotCraftable(ItemStack(ModRegistry.Items.PRINTED_PAGE.get()), ItemStack(Items.STRING)) + + // Assert that crafting with no pages fails + helper.assertNotCraftable(ItemStack(Items.PAPER), ItemStack(Items.PAPER), ItemStack(Items.STRING)) + + // Assert that crafting with a book fails + helper.assertNotCraftable(ItemStack(ModRegistry.Items.PRINTED_PAGE.get()), ItemStack(ModRegistry.Items.PRINTED_BOOK.get()), ItemStack(Items.STRING)) + + assertThat( + helper.craftItem( + PrintoutItem.createSingleFromTitleAndText("First", createPagesOf(' '), createPagesOf('b')), + PrintoutItem.createMultipleFromTitleAndText("Second", createPagesOf(' '), createPagesOf('b')), + ItemStack(Items.STRING), + ), + isStack(PrintoutItem.createMultipleFromTitleAndText("First", createPagesOf(' ', 2), createPagesOf('b', 2))), + ) + } + + @GameTest(template = "default") + fun Craft_book(helper: GameTestHelper) = helper.immediate { + // Assert that crafting with no pages fails + helper.assertNotCraftable(ItemStack(Items.PAPER), ItemStack(Items.PAPER), ItemStack(Items.STRING), ItemStack(Items.LEATHER)) + + // Assert that crafting with only one page works + assertEquals( + ModRegistry.Items.PRINTED_BOOK.get(), + helper.craftItem(ItemStack(ModRegistry.Items.PRINTED_PAGE.get()), ItemStack(Items.STRING), ItemStack(Items.LEATHER)).item, + ) + + assertThat( + helper.craftItem( + PrintoutItem.createSingleFromTitleAndText("First", createPagesOf(' '), createPagesOf('b')), + PrintoutItem.createMultipleFromTitleAndText("Second", createPagesOf(' '), createPagesOf('b')), + ItemStack(Items.STRING), + ItemStack(Items.LEATHER), + ), + isStack(PrintoutItem.createBookFromTitleAndText("First", createPagesOf(' ', 2), createPagesOf('b', 2))), + ) + } + + private fun createPagesOf(c: Char, pages: Int = 1): Array { + val line = c.toString().repeat(PrintoutItem.LINE_MAX_LENGTH) + return Array(PrintoutItem.LINES_PER_PAGE * pages) { line } + } } diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Recipe_Test.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Recipe_Test.kt index 2c65cfe92..98890c0e8 100644 --- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Recipe_Test.kt +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Recipe_Test.kt @@ -6,20 +6,18 @@ package dan200.computercraft.gametest import com.mojang.authlib.GameProfile import dan200.computercraft.gametest.api.Structures +import dan200.computercraft.gametest.api.craftItem import dan200.computercraft.gametest.api.sequence import dan200.computercraft.shared.ModRegistry import net.minecraft.gametest.framework.GameTest -import net.minecraft.gametest.framework.GameTestAssertException import net.minecraft.gametest.framework.GameTestHelper import net.minecraft.nbt.CompoundTag import net.minecraft.nbt.NbtUtils import net.minecraft.world.entity.player.Player import net.minecraft.world.inventory.AbstractContainerMenu import net.minecraft.world.inventory.MenuType -import net.minecraft.world.inventory.TransientCraftingContainer import net.minecraft.world.item.ItemStack import net.minecraft.world.item.Items -import net.minecraft.world.item.crafting.RecipeType import org.junit.jupiter.api.Assertions.assertEquals import java.util.* @@ -32,15 +30,10 @@ class Recipe_Test { @GameTest(template = Structures.DEFAULT) fun Craft_result_has_nbt(context: GameTestHelper) = context.sequence { thenExecute { - val container = TransientCraftingContainer(DummyMenu, 3, 3) - container.setItem(0, ItemStack(Items.SKELETON_SKULL)) - container.setItem(1, ItemStack(ModRegistry.Items.COMPUTER_ADVANCED.get())) - - val recipe = context.level.server.recipeManager - .getRecipeFor(RecipeType.CRAFTING, container, context.level) - .orElseThrow { GameTestAssertException("No recipe matches") } - - val result = recipe.assemble(container, context.level.registryAccess()) + val result = context.craftItem( + ItemStack(Items.SKELETON_SKULL), + ItemStack(ModRegistry.Items.COMPUTER_ADVANCED.get()), + ) val profile = GameProfile(UUID.fromString("f3c8d69b-0776-4512-8434-d1b2165909eb"), "dan200") 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 de0c2e0f5..75699004a 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 @@ -5,6 +5,7 @@ package dan200.computercraft.gametest.api import dan200.computercraft.api.peripheral.IPeripheral +import dan200.computercraft.gametest.Recipe_Test.DummyMenu import dan200.computercraft.gametest.core.ManagedComputers import dan200.computercraft.mixin.gametest.GameTestHelperAccessor import dan200.computercraft.mixin.gametest.GameTestInfoAccessor @@ -22,9 +23,11 @@ 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.inventory.TransientCraftingContainer import net.minecraft.world.item.Item import net.minecraft.world.item.ItemStack import net.minecraft.world.item.context.UseOnContext +import net.minecraft.world.item.crafting.RecipeType import net.minecraft.world.level.block.Blocks import net.minecraft.world.level.block.entity.BarrelBlockEntity import net.minecraft.world.level.block.entity.BlockEntity @@ -343,6 +346,32 @@ fun GameTestHelper.placeItemAt(stack: ItemStack, pos: BlockPos, direction: Direc stack.useOn(UseOnContext(player, InteractionHand.MAIN_HAND, hit)) } +/** + * Assert a recipe is not craftable. + */ +fun GameTestHelper.assertNotCraftable(vararg items: ItemStack) { + val container = TransientCraftingContainer(DummyMenu, 3, 3) + for ((i, item) in items.withIndex()) container.setItem(i, item) + + val recipe = level.server.recipeManager.getRecipeFor(RecipeType.CRAFTING, container, level) + + if (recipe.isPresent) fail("Expected no recipe to match $items") +} + +/** + * Attempt to craft an item. + */ +fun GameTestHelper.craftItem(vararg items: ItemStack): ItemStack { + val container = TransientCraftingContainer(DummyMenu, 3, 3) + for ((i, item) in items.withIndex()) container.setItem(i, item) + + val recipe = level.server.recipeManager + .getRecipeFor(RecipeType.CRAFTING, container, level) + .orElseThrow { GameTestAssertException("No recipe matches $items") } + + return recipe.assemble(container, level.registryAccess()) +} + /** * Run a function multiple times until it succeeds. */