Merge branch 'mc-1.21.x' into mc-26.2

This commit is contained in:
Jonathan Coates
2026-07-05 11:25:12 +01:00
8 changed files with 308 additions and 34 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ body:
options:
- "1.20.1"
- "1.21.1"
- "26.1"
- "26.2"
validations:
required: true
- type: input
+1
View File
@@ -34,6 +34,7 @@ as documentation for breaking changes and "gotchas" one should look out for betw
- [`math.random`] now uses Lua 5.4's random number generator.
- File handles, HTTP requests and websockets now always use the original bytes rather than encoding/decoding to UTF-8.
Files containing non-ASCII characters will be read back differently if using text mode.
## Minecraft 1.13 {#mc-1.13}
- The "key code" for [`key`] and [`key_up`] events has changed, due to Minecraft updating to LWJGL 3. Make sure you're
@@ -5,17 +5,24 @@
package dan200.computercraft.gametest
import com.mojang.authlib.GameProfile
import dan200.computercraft.gametest.api.GameTest
import dan200.computercraft.gametest.api.Structures
import dan200.computercraft.gametest.api.craftItem
import dan200.computercraft.gametest.api.sequence
import dan200.computercraft.api.pocket.IPocketUpgrade
import dan200.computercraft.api.turtle.ITurtleUpgrade
import dan200.computercraft.gametest.api.*
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.core.component.DataComponentPatch
import net.minecraft.core.component.DataComponents
import net.minecraft.gametest.framework.GameTestHelper
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.ItemStackTemplate
import net.minecraft.world.item.Items
import net.minecraft.world.item.component.ResolvableProfile
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers
import org.junit.jupiter.api.Assertions.assertEquals
import java.util.*
@@ -26,19 +33,155 @@ 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 =
DataComponentPatch.builder().set(DataComponents.PROFILE, ResolvableProfile.createResolved(profile))
.build()
assertEquals(tag, result.componentsPatch, "Expected NBT tags to be the same")
val tag =
DataComponentPatch.builder().set(DataComponents.PROFILE, ResolvableProfile.createResolved(profile))
.build()
assertEquals(tag, result.componentsPatch, "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 {
for (holder in context.level.server.recipeManager.recipes) {
val recipe = holder.value
if (recipe is CustomShapedRecipe || recipe is CustomShapelessRecipe) {
context.assertCraftable(recipe)
}
}
}
/**
* Test that recipes generated by [UpgradeRecipeGenerator] are craftable.
*/
@GameTest(template = Structures.DEFAULT)
fun Upgrade_recipes_craftable(context: GameTestHelper) = context.immediate {
val registries = context.level.registryAccess()
val turtleUpgrades = registries.lookupOrThrow(ITurtleUpgrade.REGISTRY)
val pocketUpgrades = registries.lookupOrThrow(IPocketUpgrade.REGISTRY)
val generator = UpgradeRecipeGenerator(::BasicRecipe, registries)
fun checkUsage(message: String, size: Int, item: ItemStackTemplate) {
val recipes = generator.findRecipesWithInput(item.create())
assertEquals(size, recipes.size, message)
for (recipe in recipes) {
// Ensure the ingredient appears in the recipe.
assertThat("Input is in ingredients", recipe.ingredients, Matchers.hasItem(item))
// 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, ItemStackTemplate(ModRegistry.Items.SPEAKER.get()))
// Check recipes which can be crafted with a turtle
checkUsage(
"Usage: Turtle without upgrades",
(turtleUpgrades.listElements().count() * 2).toInt(),
ItemStackTemplate(ModRegistry.Items.TURTLE_NORMAL.get()),
)
checkUsage(
"Usage: Turtle with one upgrade",
turtleUpgrades.listElements().count().toInt(),
ItemStackTemplate(
ModRegistry.Items.TURTLE_NORMAL.get(),
DataComponentPatch.builder()
.set(
ModRegistry.DataComponents.LEFT_TURTLE_UPGRADE.get(),
TurtleUpgrades.instance().get(registries, ItemStack(ModRegistry.Items.SPEAKER.get()))!!,
)
.build(),
),
)
// Check recipes which can be crafted with a pocket computer
checkUsage(
"Usage: Pocket computer without upgrades",
(pocketUpgrades.listElements().count() * 2).toInt(),
ItemStackTemplate(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get()),
)
fun checkRecipe(message: String, size: Int, item: ItemStackTemplate) {
val recipes = generator.findRecipesWithOutput(item.create())
assertEquals(size, recipes.size, message)
for (recipe in recipes) {
// Ensure the result matches the input.
assertEquals(item, recipe.result, "Recipe has output")
// 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,
ItemStackTemplate(
ModRegistry.Items.TURTLE_NORMAL.get(),
DataComponentPatch.builder().set(
ModRegistry.DataComponents.LEFT_TURTLE_UPGRADE.get(),
TurtleUpgrades.instance().get(registries, ItemStack(ModRegistry.Items.SPEAKER.get()))!!,
).build(),
),
)
checkRecipe(
"Recipe: Turtle with two upgrades",
2,
ItemStackTemplate(
ModRegistry.Items.TURTLE_NORMAL.get(),
DataComponentPatch.builder()
.set(
ModRegistry.DataComponents.LEFT_TURTLE_UPGRADE.get(),
TurtleUpgrades.instance()
.get(registries, ItemStack(ModRegistry.Items.SPEAKER.get()))!!,
)
.set(
ModRegistry.DataComponents.RIGHT_TURTLE_UPGRADE.get(),
TurtleUpgrades.instance()
.get(registries, ItemStack(ModRegistry.Items.WIRELESS_MODEM_NORMAL.get()))!!,
)
.build(),
),
)
// Check recipes which craft a pocket computer
checkRecipe(
"Recipe: Pocket computer with one upgrade",
1,
ItemStackTemplate(
ModRegistry.Items.POCKET_COMPUTER_NORMAL.get(),
DataComponentPatch.builder()
.set(
ModRegistry.DataComponents.BACK_POCKET_UPGRADE.get(),
PocketUpgrades.instance().get(registries, ItemStack(ModRegistry.Items.SPEAKER.get()))!!,
).build(),
),
)
}
}
private class BasicRecipe(val width: Int, val height: Int, val ingredients: List<ItemStackTemplate>, val result: ItemStackTemplate)
private fun GameTestHelper.assertCraftable(recipe: BasicRecipe) {
assertCraftable(
recipe.ingredients.map(ItemStackTemplate::create),
recipe.result.create(),
width = recipe.width,
height = recipe.height,
)
}
@@ -36,7 +36,12 @@ import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.ItemStackTemplate
import net.minecraft.world.item.context.UseOnContext
import net.minecraft.world.item.crafting.CraftingInput
import net.minecraft.world.item.crafting.CraftingRecipe
import net.minecraft.world.item.crafting.RecipeType
import net.minecraft.world.item.crafting.display.RecipeDisplay
import net.minecraft.world.item.crafting.display.ShapedCraftingRecipeDisplay
import net.minecraft.world.item.crafting.display.ShapelessCraftingRecipeDisplay
import net.minecraft.world.item.crafting.display.SlotDisplayContext
import net.minecraft.world.level.GameType
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.entity.BarrelBlockEntity
@@ -238,7 +243,7 @@ fun GameTestHelper.assertContainerExactly(pos: BlockPos, items: List<ItemStack>)
assertContainerExactlyImpl(pos, getContainerAt(pos), items)
/**
* Assert an container contains exactly these items and no more.
* Assert a container contains exactly these items and no more.
*
* @param entity The entity containing these items.
* @param items The list of items this container must contain. This should be equal to the expected contents of the
@@ -247,7 +252,7 @@ fun GameTestHelper.assertContainerExactly(pos: BlockPos, items: List<ItemStack>)
fun <T> GameTestHelper.assertContainerExactly(entity: T, items: List<ItemStack>) where T : Entity, T : Container =
assertContainerExactlyImpl(entity.blockPosition(), entity, items)
private fun ItemStack.toStringFull(): String = if (isEmpty) "<empty>" else "$count x $item$componentsPatch"
fun ItemStack.toStringFull(): String = if (isEmpty) "<empty>" else "$count x $item$componentsPatch"
private fun formatItems(items: List<ItemStack>) = items.joinToString(", ") { it.toStringFull() }
@@ -364,6 +369,62 @@ fun GameTestHelper.placeItemAt(stack: ItemStack, pos: BlockPos, direction: Direc
stack.useOn(UseOnContext(player, InteractionHand.MAIN_HAND, hit))
}
/**
* Assert a [CraftingRecipe] is craftable.
*/
fun GameTestHelper.assertCraftable(recipe: CraftingRecipe) {
val displays = recipe.display()
if (displays.isEmpty()) {
fail("$recipe has no display forms")
}
for (display in displays) assertCraftable(display)
}
/**
* Assert a [RecipeDisplay] is craftable.
*/
fun GameTestHelper.assertCraftable(recipe: RecipeDisplay) {
val context = SlotDisplayContext.fromLevel(level)
when (recipe) {
is ShapedCraftingRecipeDisplay -> {
assertCraftable(
recipe.ingredients.map { it.resolveForFirstStack(context) },
recipe.result().resolveForFirstStack(context),
width = recipe.width,
height = recipe.height,
)
}
is ShapelessCraftingRecipeDisplay -> {
assertCraftable(
recipe.ingredients.map { it.resolveForFirstStack(context) },
recipe.result().resolveForFirstStack(context),
)
}
else -> fail("Unsupported recipe $recipe")
}
}
/**
* Assert a series of items craft the given recipe.
*/
fun GameTestHelper.assertCraftable(items: List<ItemStack>, result: ItemStack, width: Int = 3, height: Int = 3) {
val container = NonNullList.withSize(width * height, ItemStack.EMPTY)
for ((i, item) in items.withIndex()) container[i] = item
val input = CraftingInput.of(width, height, container)
val recipe = level.server.recipeManager.getRecipeFor(RecipeType.CRAFTING, input, level)
if (recipe.isEmpty) fail("Expected recipe to match ${formatItems(items)}")
val actualResult = recipe.get().value.assemble(input)
if (!ItemStack.isSameItemSameComponents(actualResult, result) || actualResult.count != result.count) {
fail("Expected $items to craft ${result.toStringFull()}, got ${actualResult.toStringFull()}")
}
}
/**
* Assert a recipe is not craftable.
*/
@@ -374,7 +435,7 @@ fun GameTestHelper.assertNotCraftable(vararg items: ItemStack) {
val recipe = level.server.recipeManager.getRecipeFor(RecipeType.CRAFTING, input, level)
if (recipe.isPresent) abort("Expected no recipe to match $items")
if (recipe.isPresent) abort("Expected no recipe to match ${items.contentToString()}")
}
/**
@@ -386,7 +447,7 @@ fun GameTestHelper.craftItem(vararg items: ItemStack): ItemStack {
val input = CraftingInput.of(3, 3, container)
val recipe = level.server.recipeManager.getRecipeFor(RecipeType.CRAFTING, input, level).getOrNull()
?: throw assertionException("No recipe matches $items")
?: throw assertionException("No recipe matches ${items.contentToString()}")
return recipe.value.assemble(input)
}
@@ -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): Element {
val testCase = document.createElement("testcase")
testCase.setAttribute("name", testInfo.id().toString())
testCase.setAttribute("classname", testInfo.structure.toString())
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).appendChild(result)
}
override fun onTestSuccess(testInfo: GameTestInfo) {
createTestCase(testInfo)
}
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)
}
}
@@ -1,6 +1,6 @@
# New features in CC: Tweaked 1.120.0
* Support spawning new parallel functions in `parallel.watiForAll`.
* Support spawning new parallel functions in `parallel.waitForAll`.
One bug fix:
* Make HTTP IP filtering stricter.
@@ -331,12 +331,14 @@ Several bug fixes:
* Allow placing seeds into composter barrels with `turtle.place()`.
# New features in CC: Tweaked 1.109.0
Breaking changes:
* Update to Lua 5.2
* `getfenv`/`setfenv` now only work on Lua functions.
* Add support for `goto`.
* Remove support for dumping and loading binary chunks.
* File handles, HTTP requests and websocket messages now use raw bytes rather than converting to UTF-8.
New features:
* Add `allow_repetitions` option to `textutils.serialiseJSON`.
* Track memory allocated by computers.
* Update the version returned by `os.version()` to `CraftOS 1.9`.
@@ -1,6 +1,6 @@
New features in CC: Tweaked 1.120.0
* Support spawning new parallel functions in `parallel.watiForAll`.
* Support spawning new parallel functions in `parallel.waitForAll`.
One bug fix:
* Make HTTP IP filtering stricter.
+1 -1
View File
@@ -192,7 +192,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")