mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2026-07-20 01:58:53 +00:00
Add tests to check dynamic recipes are well-formed
- Check whether impostor recipes (and other custom shaped/shapeless recipes) match the "real" recipe. - Check whether our upgrade recipe generator generates valid recipes. Reasoning about left/right upgrades (and top/back ones on 26.2) is a bit confusing, so this should prevent any errors.
This commit is contained in:
@@ -17,6 +17,7 @@ import dan200.computercraft.shared.platform.PlatformHelper;
|
||||
import dan200.computercraft.shared.platform.RecipeIngredients;
|
||||
import dan200.computercraft.shared.platform.RegistryWrappers;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
import dan200.computercraft.shared.turtle.core.TurtleBrain;
|
||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
|
||||
import dan200.computercraft.shared.util.ColourUtils;
|
||||
import net.minecraft.advancements.critereon.InventoryChangeTrigger;
|
||||
@@ -182,6 +183,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
|
||||
for (var turtleItem : turtleItems()) {
|
||||
var base = turtleItem.create(-1, null, -1, null, null, 0, null);
|
||||
var name = RegistryWrappers.ITEMS.getKey(turtleItem);
|
||||
var overlayName = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/" + overlay).toString();
|
||||
|
||||
var builder = ShapelessRecipeBuilder.shapeless(RecipeCategory.REDSTONE, base.getItem())
|
||||
.group(name.withSuffix("_overlay").toString())
|
||||
@@ -192,7 +194,8 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
|
||||
.save(
|
||||
RecipeWrapper
|
||||
.wrap(ModRegistry.RecipeSerializers.TURTLE_OVERLAY.get(), add)
|
||||
.withExtraData(x -> x.addProperty("overlay", new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/" + overlay).toString())),
|
||||
.withExtraData(x -> x.addProperty("overlay", overlayName))
|
||||
.withResultTag(x -> x.putString(TurtleBrain.NBT_OVERLAY, overlayName)),
|
||||
name.withSuffix("_overlays/" + overlay)
|
||||
);
|
||||
}
|
||||
|
||||
+5
-6
@@ -28,7 +28,6 @@ import dan200.computercraft.shared.util.IDAssigner;
|
||||
import dan200.computercraft.shared.util.InventoryUtil;
|
||||
import dan200.computercraft.shared.util.NBTUtil;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
@@ -378,11 +377,11 @@ public class PocketComputerItem extends Item implements IComputerItem, IColoured
|
||||
compound.remove(NBT_UPGRADE_INFO);
|
||||
} else {
|
||||
compound.putString(NBT_UPGRADE, upgrade.upgrade().getUpgradeID().toString());
|
||||
compound.put(NBT_UPGRADE_INFO, upgrade.data().copy());
|
||||
if (upgrade.data().isEmpty()) {
|
||||
compound.remove(NBT_UPGRADE_INFO);
|
||||
} else {
|
||||
compound.put(NBT_UPGRADE_INFO, upgrade.data().copy());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static CompoundTag getUpgradeInfo(ItemStack stack) {
|
||||
return stack.getOrCreateTagElement(NBT_UPGRADE_INFO);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,22 @@
|
||||
package dan200.computercraft.gametest
|
||||
|
||||
import com.mojang.authlib.GameProfile
|
||||
import dan200.computercraft.api.ComputerCraftAPI
|
||||
import dan200.computercraft.gametest.api.Structures
|
||||
import dan200.computercraft.gametest.api.assertCraftable
|
||||
import dan200.computercraft.gametest.api.craftItem
|
||||
import dan200.computercraft.gametest.api.sequence
|
||||
import dan200.computercraft.gametest.api.immediate
|
||||
import dan200.computercraft.impl.PocketUpgrades
|
||||
import dan200.computercraft.impl.TurtleUpgrades
|
||||
import dan200.computercraft.shared.ModRegistry
|
||||
import dan200.computercraft.shared.integration.UpgradeRecipeGenerator
|
||||
import dan200.computercraft.shared.recipe.CustomShapedRecipe
|
||||
import dan200.computercraft.shared.recipe.CustomShapelessRecipe
|
||||
import net.minecraft.gametest.framework.GameTest
|
||||
import net.minecraft.gametest.framework.GameTestHelper
|
||||
import net.minecraft.nbt.CompoundTag
|
||||
import net.minecraft.nbt.NbtUtils
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu
|
||||
import net.minecraft.world.inventory.MenuType
|
||||
@@ -28,22 +36,147 @@ class Recipe_Test {
|
||||
* Mostly useful for Fabric, where we need a mixin for this.
|
||||
*/
|
||||
@GameTest(template = Structures.DEFAULT)
|
||||
fun Craft_result_has_nbt(context: GameTestHelper) = context.sequence {
|
||||
thenExecute {
|
||||
val result = context.craftItem(
|
||||
ItemStack(Items.SKELETON_SKULL),
|
||||
ItemStack(ModRegistry.Items.COMPUTER_ADVANCED.get()),
|
||||
)
|
||||
fun Craft_result_has_nbt(context: GameTestHelper) = context.immediate {
|
||||
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")
|
||||
val profile = GameProfile(UUID.fromString("f3c8d69b-0776-4512-8434-d1b2165909eb"), "dan200")
|
||||
|
||||
val tag = CompoundTag()
|
||||
tag.put("SkullOwner", NbtUtils.writeGameProfile(CompoundTag(), profile))
|
||||
val tag = CompoundTag()
|
||||
tag.put("SkullOwner", NbtUtils.writeGameProfile(CompoundTag(), profile))
|
||||
|
||||
assertEquals(tag, result.tag, "Expected NBT tags to be the same")
|
||||
assertEquals(tag, result.tag, "Expected NBT tags to be the same")
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that all impostor recipes are craftable.
|
||||
*/
|
||||
@GameTest(template = Structures.DEFAULT)
|
||||
fun Imposter_recipes_craftable(context: GameTestHelper) = context.immediate {
|
||||
// FIXME: Skip these recipes, as the NBT does not match.
|
||||
val skipRecipes = setOf(
|
||||
ResourceLocation(ComputerCraftAPI.MOD_ID, "printed_pages"),
|
||||
ResourceLocation(ComputerCraftAPI.MOD_ID, "printed_book"),
|
||||
)
|
||||
|
||||
for (recipe in context.level.server.recipeManager.recipes) {
|
||||
if (recipe is CustomShapedRecipe || recipe is CustomShapelessRecipe) {
|
||||
context.assertCraftable(recipe, matchNbt = !skipRecipes.contains(recipe.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that recipes generated by [UpgradeRecipeGenerator] are craftable.
|
||||
*/
|
||||
@GameTest(template = Structures.DEFAULT)
|
||||
fun Upgrade_recipes_craftable(context: GameTestHelper) = context.immediate {
|
||||
val generator = UpgradeRecipeGenerator { it }
|
||||
|
||||
fun checkUsage(message: String, size: Int, item: ItemStack) {
|
||||
val recipes = generator.findRecipesWithInput(item)
|
||||
assertEquals(size, recipes.size, message)
|
||||
|
||||
for (recipe in recipes) {
|
||||
// Ensure the ingredient appears in the recipe.
|
||||
if (!recipe.ingredients.any { ItemStack.isSameItemSameTags(it.items[0], item) }) {
|
||||
context.fail("Cannot find item ${item}${item.tag} in ${recipe.ingredients}")
|
||||
}
|
||||
|
||||
// Ensure the recipe can actually be crafted using the main recipe manager.
|
||||
context.assertCraftable(recipe)
|
||||
}
|
||||
}
|
||||
|
||||
// Speakers should be an input to 4 recipes (2xPocket, 2xTurtle).
|
||||
checkUsage("Usage: Speaker", 4, ItemStack(ModRegistry.Items.SPEAKER.get()))
|
||||
|
||||
// Check recipes which can be crafted with a turtle
|
||||
checkUsage(
|
||||
"Usage: Turtle without upgrades",
|
||||
TurtleUpgrades.instance().upgrades.size * 2,
|
||||
ItemStack(ModRegistry.Items.TURTLE_NORMAL.get()),
|
||||
)
|
||||
checkUsage(
|
||||
"Usage: Turtle with one upgrade",
|
||||
TurtleUpgrades.instance().upgrades.size,
|
||||
ModRegistry.Items.TURTLE_NORMAL.get().create(
|
||||
-1,
|
||||
"",
|
||||
-1,
|
||||
TurtleUpgrades.instance().get(ItemStack(ModRegistry.Items.SPEAKER.get()))!!,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
),
|
||||
)
|
||||
|
||||
// Check recipes which can be crafted with a pocket computer
|
||||
checkUsage(
|
||||
"Usage: Pocket computer without upgrades",
|
||||
PocketUpgrades.instance().upgrades.size,
|
||||
ItemStack(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get()),
|
||||
)
|
||||
|
||||
fun checkRecipe(message: String, size: Int, item: ItemStack) {
|
||||
val recipes = generator.findRecipesWithOutput(item)
|
||||
assertEquals(size, recipes.size, message)
|
||||
|
||||
for (recipe in recipes) {
|
||||
// Ensure the result matches the input.
|
||||
val result = recipe.getResultItem(context.level.registryAccess())
|
||||
if (!ItemStack.isSameItemSameTags(result, item) || result.count != item.count) {
|
||||
context.fail("Expected to craft ${item}${item.tag} but crafted ${result}${result.tag}")
|
||||
}
|
||||
|
||||
// Ensure the recipe can actually be crafted using the main recipe manager.
|
||||
context.assertCraftable(recipe)
|
||||
}
|
||||
}
|
||||
|
||||
// Check recipes which craft a turtle
|
||||
checkRecipe(
|
||||
"Recipe: Turtle with one upgrade",
|
||||
1,
|
||||
ModRegistry.Items.TURTLE_NORMAL.get().create(
|
||||
-1,
|
||||
"",
|
||||
-1,
|
||||
TurtleUpgrades.instance().get(ItemStack(ModRegistry.Items.SPEAKER.get()))!!,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
),
|
||||
)
|
||||
checkRecipe(
|
||||
"Recipe: Turtle with two upgrades",
|
||||
2,
|
||||
ModRegistry.Items.TURTLE_NORMAL.get().create(
|
||||
-1,
|
||||
"",
|
||||
-1,
|
||||
TurtleUpgrades.instance().get(ItemStack(ModRegistry.Items.SPEAKER.get()))!!,
|
||||
TurtleUpgrades.instance().get(ItemStack(ModRegistry.Items.WIRELESS_MODEM_NORMAL.get()))!!,
|
||||
0,
|
||||
null,
|
||||
),
|
||||
)
|
||||
|
||||
// Check recipes which craft a pocket computer
|
||||
checkRecipe(
|
||||
"Recipe: Pocket computer with one upgrade",
|
||||
1,
|
||||
ModRegistry.Items.POCKET_COMPUTER_NORMAL.get().create(
|
||||
-1,
|
||||
"",
|
||||
-1,
|
||||
PocketUpgrades.instance().get(ItemStack(ModRegistry.Items.SPEAKER.get()))!!,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
object DummyMenu : AbstractContainerMenu(MenuType.GENERIC_9x1, 0) {
|
||||
override fun quickMoveStack(player: Player, slot: Int): ItemStack = ItemStack.EMPTY
|
||||
override fun stillValid(p0: Player): Boolean = true
|
||||
|
||||
+46
@@ -27,7 +27,9 @@ 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.CraftingRecipe
|
||||
import net.minecraft.world.item.crafting.RecipeType
|
||||
import net.minecraft.world.item.crafting.ShapedRecipe
|
||||
import net.minecraft.world.level.block.Blocks
|
||||
import net.minecraft.world.level.block.entity.BarrelBlockEntity
|
||||
import net.minecraft.world.level.block.entity.BlockEntity
|
||||
@@ -346,6 +348,50 @@ fun GameTestHelper.placeItemAt(stack: ItemStack, pos: BlockPos, direction: Direc
|
||||
stack.useOn(UseOnContext(player, InteractionHand.MAIN_HAND, hit))
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a recipe is craftable.
|
||||
*/
|
||||
fun GameTestHelper.assertCraftable(recipe: CraftingRecipe, matchNbt: Boolean = true) {
|
||||
val (width, height) = if (recipe is ShapedRecipe) {
|
||||
(recipe.width to recipe.height)
|
||||
} else {
|
||||
(3 to 3)
|
||||
}
|
||||
|
||||
assertCraftable(
|
||||
recipe.ingredients.map {
|
||||
if (it.isEmpty) ItemStack.EMPTY else it.items[0]
|
||||
},
|
||||
recipe.getResultItem(level.registryAccess()),
|
||||
width = width,
|
||||
height = height,
|
||||
matchNbt = matchNbt,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a series of items craft the given recipe.
|
||||
*/
|
||||
fun GameTestHelper.assertCraftable(items: List<ItemStack>, result: ItemStack, width: Int = 3, height: Int = 3, matchNbt: Boolean = true) {
|
||||
val container = TransientCraftingContainer(DummyMenu, width, height)
|
||||
for ((i, item) in items.withIndex()) container.setItem(i, item)
|
||||
|
||||
val recipe = level.server.recipeManager.getRecipeFor(RecipeType.CRAFTING, container, level)
|
||||
if (recipe.isEmpty) fail("Expected recipe to match $items")
|
||||
|
||||
val actualResult = recipe.get().assemble(container, level.registryAccess())
|
||||
|
||||
if (matchNbt) {
|
||||
if (!ItemStack.isSameItemSameTags(actualResult, result) || actualResult.count != result.count) {
|
||||
fail("Expected $items to craft ${result}${result.tag}, got ${actualResult}${actualResult.tag}")
|
||||
}
|
||||
} else {
|
||||
if (!ItemStack.isSameItem(actualResult, result) || actualResult.count != result.count) {
|
||||
fail("Expected $items to craft $result, got $actualResult")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a recipe is not craftable.
|
||||
*/
|
||||
|
||||
+77
-10
@@ -4,13 +4,22 @@
|
||||
|
||||
package dan200.computercraft.gametest.core
|
||||
|
||||
import com.google.common.base.Stopwatch
|
||||
import net.minecraft.gametest.framework.GameTestInfo
|
||||
import net.minecraft.gametest.framework.JUnitLikeTestReporter
|
||||
import net.minecraft.gametest.framework.TestReporter
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.nio.file.Files
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Element
|
||||
import java.io.*
|
||||
import java.nio.file.Files.createDirectories
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.transform.TransformerException
|
||||
import javax.xml.transform.TransformerFactory
|
||||
import javax.xml.transform.dom.DOMSource
|
||||
import javax.xml.transform.stream.StreamResult
|
||||
|
||||
/**
|
||||
* A test reporter which delegates to a list of other reporters.
|
||||
@@ -33,15 +42,73 @@ class MultiTestReporter(private val reporters: List<TestReporter>) : TestReporte
|
||||
|
||||
/**
|
||||
* Reports tests to a JUnit XML file. This is equivalent to [JUnitLikeTestReporter], except it ensures the destination
|
||||
* directory exists.
|
||||
* directory exists and includes the stack trace in the error.
|
||||
*/
|
||||
class JunitTestReporter(destination: File) : JUnitLikeTestReporter(destination) {
|
||||
override fun save(file: File) {
|
||||
open class JunitTestReporter(private val destination: File) : TestReporter {
|
||||
private val document: Document
|
||||
private val testSuite: Element
|
||||
private val stopwatch: Stopwatch = Stopwatch.createStarted()
|
||||
|
||||
init {
|
||||
document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()
|
||||
|
||||
testSuite = document.createElement("testsuite")
|
||||
testSuite.setAttribute("timestamp", DateTimeFormatter.ISO_INSTANT.format(Instant.now()))
|
||||
|
||||
val container = document.createElement("testsuite")
|
||||
container.appendChild(testSuite)
|
||||
document.appendChild(container)
|
||||
}
|
||||
|
||||
private fun createTestCase(testInfo: GameTestInfo, name: String?): Element {
|
||||
val testCase = document.createElement("testcase")
|
||||
testCase.setAttribute("name", name)
|
||||
testCase.setAttribute("classname", testInfo.structureName)
|
||||
testCase.setAttribute("time", (testInfo.runTime.toDouble() / 1000.0).toString())
|
||||
testSuite.appendChild(testCase)
|
||||
return testCase
|
||||
}
|
||||
|
||||
override fun onTestFailed(testInfo: GameTestInfo) {
|
||||
val error = testInfo.error!!
|
||||
val result: Element
|
||||
if (testInfo.isRequired) {
|
||||
result = document.createElement("failure")
|
||||
result.setAttribute("message", error.message)
|
||||
result.setAttribute("type", error.javaClass.name)
|
||||
|
||||
val writer = StringWriter()
|
||||
error.printStackTrace(PrintWriter(writer))
|
||||
result.textContent = writer.toString()
|
||||
} else {
|
||||
result = document.createElement("skipped")
|
||||
result.setAttribute("message", error.message)
|
||||
}
|
||||
|
||||
createTestCase(testInfo, testInfo.testName).appendChild(result)
|
||||
}
|
||||
|
||||
override fun onTestSuccess(testInfo: GameTestInfo) {
|
||||
createTestCase(testInfo, testInfo.testName)
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
stopwatch.stop()
|
||||
testSuite.setAttribute(
|
||||
"time",
|
||||
(stopwatch.elapsed(TimeUnit.MILLISECONDS).toDouble() / 1000.0).toString(),
|
||||
)
|
||||
|
||||
try {
|
||||
Files.createDirectories(file.toPath().parent)
|
||||
} catch (e: IOException) {
|
||||
throw TransformerException("Failed to create parent directory", e)
|
||||
try {
|
||||
createDirectories(destination.toPath().parent)
|
||||
} catch (e: IOException) {
|
||||
throw UncheckedIOException("Failed to create parent directory", e)
|
||||
}
|
||||
|
||||
TransformerFactory.newInstance().newTransformer().transform(DOMSource(document), StreamResult(destination))
|
||||
} catch (transformerException: TransformerException) {
|
||||
throw Error("Couldn't save test report", transformerException)
|
||||
}
|
||||
super.save(file)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ loom {
|
||||
|
||||
systemProperties.put("fabric-api.gametest", "true")
|
||||
systemProperties.put(
|
||||
"fabric-api.gametest.report-file",
|
||||
"cctest.gametest-report",
|
||||
layout.buildDirectory.dir("test-results/runGametest.xml").getAbsolutePath(),
|
||||
)
|
||||
runDirectory = layout.projectDirectory.dir("run/gametest")
|
||||
|
||||
+4
-1
@@ -13,5 +13,8 @@
|
||||
{"item": "computercraft:turtle_advanced"}
|
||||
],
|
||||
"overlay": "computercraft:block/turtle_rainbow_overlay",
|
||||
"result": {"item": "computercraft:turtle_advanced"}
|
||||
"result": {
|
||||
"item": "computercraft:turtle_advanced",
|
||||
"nbt": "{Overlay:\"computercraft:block/turtle_rainbow_overlay\"}"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,5 +10,5 @@
|
||||
{"item": "computercraft:turtle_advanced"}
|
||||
],
|
||||
"overlay": "computercraft:block/turtle_trans_overlay",
|
||||
"result": {"item": "computercraft:turtle_advanced"}
|
||||
"result": {"item": "computercraft:turtle_advanced", "nbt": "{Overlay:\"computercraft:block/turtle_trans_overlay\"}"}
|
||||
}
|
||||
|
||||
+1
-1
@@ -13,5 +13,5 @@
|
||||
{"item": "computercraft:turtle_normal"}
|
||||
],
|
||||
"overlay": "computercraft:block/turtle_rainbow_overlay",
|
||||
"result": {"item": "computercraft:turtle_normal"}
|
||||
"result": {"item": "computercraft:turtle_normal", "nbt": "{Overlay:\"computercraft:block/turtle_rainbow_overlay\"}"}
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,5 +10,5 @@
|
||||
{"item": "computercraft:turtle_normal"}
|
||||
],
|
||||
"overlay": "computercraft:block/turtle_trans_overlay",
|
||||
"result": {"item": "computercraft:turtle_normal"}
|
||||
"result": {"item": "computercraft:turtle_normal", "nbt": "{Overlay:\"computercraft:block/turtle_trans_overlay\"}"}
|
||||
}
|
||||
|
||||
+4
-1
@@ -13,5 +13,8 @@
|
||||
{"item": "computercraft:turtle_advanced"}
|
||||
],
|
||||
"overlay": "computercraft:block/turtle_rainbow_overlay",
|
||||
"result": {"item": "computercraft:turtle_advanced"}
|
||||
"result": {
|
||||
"item": "computercraft:turtle_advanced",
|
||||
"nbt": "{Overlay:\"computercraft:block/turtle_rainbow_overlay\"}"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,5 +10,5 @@
|
||||
{"item": "computercraft:turtle_advanced"}
|
||||
],
|
||||
"overlay": "computercraft:block/turtle_trans_overlay",
|
||||
"result": {"item": "computercraft:turtle_advanced"}
|
||||
"result": {"item": "computercraft:turtle_advanced", "nbt": "{Overlay:\"computercraft:block/turtle_trans_overlay\"}"}
|
||||
}
|
||||
|
||||
+1
-1
@@ -13,5 +13,5 @@
|
||||
{"item": "computercraft:turtle_normal"}
|
||||
],
|
||||
"overlay": "computercraft:block/turtle_rainbow_overlay",
|
||||
"result": {"item": "computercraft:turtle_normal"}
|
||||
"result": {"item": "computercraft:turtle_normal", "nbt": "{Overlay:\"computercraft:block/turtle_rainbow_overlay\"}"}
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,5 +10,5 @@
|
||||
{"item": "computercraft:turtle_normal"}
|
||||
],
|
||||
"overlay": "computercraft:block/turtle_trans_overlay",
|
||||
"result": {"item": "computercraft:turtle_normal"}
|
||||
"result": {"item": "computercraft:turtle_normal", "nbt": "{Overlay:\"computercraft:block/turtle_trans_overlay\"}"}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user