mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2026-07-20 01:58:53 +00:00
Merge branch 'mc-1.21.x' into mc-26.2
This commit is contained in:
@@ -12,7 +12,7 @@ body:
|
||||
options:
|
||||
- "1.20.1"
|
||||
- "1.21.1"
|
||||
- "26.1"
|
||||
- "26.2"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
+65
-4
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+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): 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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user