mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-10-31 21:52:59 +00:00 
			
		
		
		
	Add a couple of tests for pocket computers
- Ensure they're correctly synced to the client. This definitely isn't comprehensive, but doing anything further probably involves multiple players, which is tricky. - Quick rendering test for in-hand computers.
This commit is contained in:
		| @@ -40,12 +40,6 @@ abstract class ClientJavaExec : JavaExec() { | |||||||
|     @get:Input |     @get:Input | ||||||
|     val useFramebuffer get() = !clientDebug && !project.hasProperty("clientNoFramebuffer") |     val useFramebuffer get() = !clientDebug && !project.hasProperty("clientNoFramebuffer") | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      * The folder screenshots are written to. |  | ||||||
|      */ |  | ||||||
|     @get:OutputDirectory |  | ||||||
|     val screenshots = project.layout.buildDirectory.dir("testScreenshots") |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * The path test results are written to. |      * The path test results are written to. | ||||||
|      */ |      */ | ||||||
| @@ -109,11 +103,6 @@ abstract class ClientJavaExec : JavaExec() { | |||||||
|         } else { |         } else { | ||||||
|             super.exec() |             super.exec() | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|         fsOperations.copy { |  | ||||||
|             from(workingDir.resolve("screenshots")) |  | ||||||
|             into(screenshots) |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @get:Inject |     @get:Inject | ||||||
|   | |||||||
| @@ -49,9 +49,9 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I | |||||||
|     private static final String NBT_UPGRADE = "Upgrade"; |     private static final String NBT_UPGRADE = "Upgrade"; | ||||||
|     private static final String NBT_UPGRADE_INFO = "UpgradeInfo"; |     private static final String NBT_UPGRADE_INFO = "UpgradeInfo"; | ||||||
|     public static final String NBT_LIGHT = "Light"; |     public static final String NBT_LIGHT = "Light"; | ||||||
|     private static final String NBT_ON = "On"; |     public static final String NBT_ON = "On"; | ||||||
| 
 | 
 | ||||||
|     private static final String NBT_INSTANCE = "Instanceid"; |     private static final String NBT_INSTANCE = "InstanceId"; | ||||||
|     private static final String NBT_SESSION = "SessionId"; |     private static final String NBT_SESSION = "SessionId"; | ||||||
| 
 | 
 | ||||||
|     private final ComputerFamily family; |     private final ComputerFamily family; | ||||||
|   | |||||||
| @@ -0,0 +1,15 @@ | |||||||
|  | /* | ||||||
|  |  * This file is part of ComputerCraft - http://www.computercraft.info | ||||||
|  |  * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. | ||||||
|  |  * Send enquiries to dratcliffe@gmail.com | ||||||
|  |  */ | ||||||
|  | package dan200.computercraft.gametest.core; | ||||||
|  | 
 | ||||||
|  | import net.minecraft.client.Minecraft; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Extensions to {@link Minecraft}, injected via mixin. | ||||||
|  |  */ | ||||||
|  | public interface MinecraftExtensions { | ||||||
|  |     boolean computercraft$isRenderingStable(); | ||||||
|  | } | ||||||
| @@ -0,0 +1,54 @@ | |||||||
|  | /* | ||||||
|  |  * This file is part of ComputerCraft - http://www.computercraft.info | ||||||
|  |  * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. | ||||||
|  |  * Send enquiries to dratcliffe@gmail.com | ||||||
|  |  */ | ||||||
|  | package dan200.computercraft.mixin.gametest.client; | ||||||
|  | 
 | ||||||
|  | import dan200.computercraft.gametest.core.MinecraftExtensions; | ||||||
|  | import net.minecraft.client.Minecraft; | ||||||
|  | import net.minecraft.client.multiplayer.ClientLevel; | ||||||
|  | import net.minecraft.client.player.LocalPlayer; | ||||||
|  | import net.minecraft.client.renderer.LevelRenderer; | ||||||
|  | import org.spongepowered.asm.mixin.Final; | ||||||
|  | import org.spongepowered.asm.mixin.Mixin; | ||||||
|  | import org.spongepowered.asm.mixin.Shadow; | ||||||
|  | import org.spongepowered.asm.mixin.Unique; | ||||||
|  | import org.spongepowered.asm.mixin.injection.At; | ||||||
|  | import org.spongepowered.asm.mixin.injection.Inject; | ||||||
|  | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; | ||||||
|  | 
 | ||||||
|  | import javax.annotation.Nullable; | ||||||
|  | import java.util.concurrent.atomic.AtomicBoolean; | ||||||
|  | 
 | ||||||
|  | @Mixin(Minecraft.class) | ||||||
|  | class MinecraftMixin implements MinecraftExtensions { | ||||||
|  |     @Final | ||||||
|  |     @Shadow | ||||||
|  |     public LevelRenderer levelRenderer; | ||||||
|  | 
 | ||||||
|  |     @Shadow | ||||||
|  |     @Nullable | ||||||
|  |     public ClientLevel level; | ||||||
|  | 
 | ||||||
|  |     @Shadow | ||||||
|  |     @Nullable | ||||||
|  |     public LocalPlayer player; | ||||||
|  | 
 | ||||||
|  |     @Unique | ||||||
|  |     private final AtomicBoolean isStable = new AtomicBoolean(false); | ||||||
|  | 
 | ||||||
|  |     @Inject(method = "runTick", at = @At("TAIL")) | ||||||
|  |     private void updateStable(boolean render, CallbackInfo ci) { | ||||||
|  |         isStable.set( | ||||||
|  |             level != null && player != null && | ||||||
|  |                 levelRenderer.isChunkCompiled(player.blockPosition()) && levelRenderer.countRenderedChunks() > 10 && | ||||||
|  |                 levelRenderer.hasRenderedAllChunks() | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean computercraft$isRenderingStable() { | ||||||
|  |         return isStable.get(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -44,7 +44,7 @@ class Inventory_Test { | |||||||
|     /** |     /** | ||||||
|      * Ensures inventory methods check an item is valid before moving it. |      * Ensures inventory methods check an item is valid before moving it. | ||||||
|      * |      * | ||||||
|      * @see <https://github.com/cc-tweaked/cc-restitched/issues/121> |      * @see <https://github.com/cc-tweaked/cc-restitched/issues/122> | ||||||
|      */ |      */ | ||||||
|     @GameTest |     @GameTest | ||||||
|     fun Fails_on_full(helper: GameTestHelper) = helper.sequence { |     fun Fails_on_full(helper: GameTestHelper) = helper.sequence { | ||||||
|   | |||||||
| @@ -0,0 +1,100 @@ | |||||||
|  | package dan200.computercraft.gametest | ||||||
|  | 
 | ||||||
|  | import dan200.computercraft.api.lua.ObjectArguments | ||||||
|  | import dan200.computercraft.client.pocket.ClientPocketComputers | ||||||
|  | import dan200.computercraft.core.apis.TermAPI | ||||||
|  | import dan200.computercraft.gametest.api.* | ||||||
|  | import dan200.computercraft.mixin.gametest.GameTestHelperAccessor | ||||||
|  | import dan200.computercraft.shared.ModRegistry | ||||||
|  | import dan200.computercraft.shared.computer.core.ComputerState | ||||||
|  | import dan200.computercraft.shared.pocket.items.PocketComputerItem | ||||||
|  | import dan200.computercraft.test.core.computer.getApi | ||||||
|  | import net.minecraft.core.BlockPos | ||||||
|  | import net.minecraft.gametest.framework.GameTestHelper | ||||||
|  | import net.minecraft.gametest.framework.GameTestSequence | ||||||
|  | import org.junit.jupiter.api.Assertions.assertEquals | ||||||
|  | import kotlin.random.Random | ||||||
|  | 
 | ||||||
|  | class Pocket_Computer_Test { | ||||||
|  |     /** | ||||||
|  |      * Checks pocket computer state is synced to the holding player. | ||||||
|  |      */ | ||||||
|  |     @ClientGameTest(template = Structures.DEFAULT) | ||||||
|  |     fun Sync_state(context: GameTestHelper) = context.sequence { | ||||||
|  |         // We use a unique label for each test run as computers from previous runs may not have been disposed yet. | ||||||
|  |         val unique = java.lang.Long.toHexString(Random.nextLong()) | ||||||
|  | 
 | ||||||
|  |         // Give the player a pocket computer. | ||||||
|  |         thenExecute { | ||||||
|  |             context.positionAt(BlockPos(2, 2, 2)) | ||||||
|  |             context.givePocketComputer(unique) | ||||||
|  |         } | ||||||
|  |         // Write some text to the computer. | ||||||
|  |         thenOnComputer(unique) { getApi<TermAPI>().write(ObjectArguments("Hello, world!")) } | ||||||
|  |         // And ensure its synced to the client. | ||||||
|  |         thenIdle(4) | ||||||
|  |         thenOnClient { | ||||||
|  |             val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem) | ||||||
|  |             assertEquals(ComputerState.ON, pocketComputer.state) | ||||||
|  | 
 | ||||||
|  |             val term = pocketComputer.terminal | ||||||
|  |             assertEquals("Hello, world!", term.getLine(0).toString().trim(), "Terminal contents is synced") | ||||||
|  |         } | ||||||
|  |         // Update the terminal contents again. | ||||||
|  |         thenOnComputer(unique) { | ||||||
|  |             val term = getApi<TermAPI>() | ||||||
|  |             term.setCursorPos(1, 1) | ||||||
|  |             term.setCursorBlink(true) | ||||||
|  |             term.write(ObjectArguments("Updated text :)")) | ||||||
|  |         } | ||||||
|  |         // And ensure the new computer state and terminal are sent. | ||||||
|  |         thenIdle(4) | ||||||
|  |         thenOnClient { | ||||||
|  |             val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem) | ||||||
|  |             assertEquals(ComputerState.BLINKING, pocketComputer.state) | ||||||
|  | 
 | ||||||
|  |             val term = pocketComputer.terminal | ||||||
|  |             assertEquals("Updated text :)", term.getLine(0).toString().trim(), "Terminal contents is synced") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Checks pocket computers are rendered when being held like a map. | ||||||
|  |      */ | ||||||
|  |     @ClientGameTest(template = Structures.DEFAULT) | ||||||
|  |     fun Renders_map_view(context: GameTestHelper) = context.sequence { | ||||||
|  |         // We use a unique label for each test run as computers from previous runs may not have been disposed yet. | ||||||
|  |         val unique = java.lang.Long.toHexString(Random.nextLong()) | ||||||
|  | 
 | ||||||
|  |         // Give the player a pocket computer. | ||||||
|  |         thenExecute { | ||||||
|  |             context.positionAt(BlockPos(2, 2, 2), xRot = 90.0f) | ||||||
|  |             context.givePocketComputer(unique) | ||||||
|  |         } | ||||||
|  |         thenOnComputer(unique) { | ||||||
|  |             val terminal = getApi<TermAPI>().terminal | ||||||
|  |             terminal.write("Hello, world!") | ||||||
|  |             terminal.setCursorPos(1, 2) | ||||||
|  |             terminal.textColour = 2 | ||||||
|  |             terminal.backgroundColour = 3 | ||||||
|  |             terminal.write("Some coloured text") | ||||||
|  |         } | ||||||
|  |         thenIdle(4) | ||||||
|  |         thenScreenshot(showGui = true) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Give the current player a pocket computer, suitable to be controlled by [GameTestSequence.thenOnComputer]. | ||||||
|  |      */ | ||||||
|  |     private fun GameTestHelper.givePocketComputer(name: String? = null) { | ||||||
|  |         val player = level.randomPlayer!! | ||||||
|  |         player.inventory.clearContent() | ||||||
|  | 
 | ||||||
|  |         val testName = (this as GameTestHelperAccessor).testInfo.testName | ||||||
|  |         val label = testName + (if (name == null) "" else ".$name") | ||||||
|  | 
 | ||||||
|  |         val item = ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get().create(1, label, -1, null) | ||||||
|  |         item.getOrCreateTag().putBoolean(PocketComputerItem.NBT_ON, true) | ||||||
|  |         player.inventory.setItem(0, item) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,5 +1,6 @@ | |||||||
| package dan200.computercraft.gametest.api | package dan200.computercraft.gametest.api | ||||||
| 
 | 
 | ||||||
|  | import dan200.computercraft.gametest.core.MinecraftExtensions | ||||||
| import dan200.computercraft.mixin.gametest.GameTestSequenceAccessor | import dan200.computercraft.mixin.gametest.GameTestSequenceAccessor | ||||||
| import dan200.computercraft.shared.platform.Registries | import dan200.computercraft.shared.platform.Registries | ||||||
| import net.minecraft.client.Minecraft | import net.minecraft.client.Minecraft | ||||||
| @@ -20,9 +21,7 @@ import java.util.concurrent.atomic.AtomicBoolean | |||||||
| /** | /** | ||||||
|  * Attempt to guess whether all chunks have been rendered. |  * Attempt to guess whether all chunks have been rendered. | ||||||
|  */ |  */ | ||||||
| fun Minecraft.isRenderingStable(): Boolean = level != null && player != null && | fun Minecraft.isRenderingStable(): Boolean = (this as MinecraftExtensions).`computercraft$isRenderingStable`() | ||||||
|     levelRenderer.isChunkCompiled(player!!.blockPosition()) && levelRenderer.countRenderedChunks() > 10 && |  | ||||||
|     levelRenderer.hasRenderedAllChunks() |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Run a task on the client. |  * Run a task on the client. | ||||||
| @@ -44,7 +43,7 @@ fun GameTestSequence.thenOnClient(task: ClientTestHelper.() -> Unit): GameTestSe | |||||||
| /** | /** | ||||||
|  * Take a screenshot of the current game state. |  * Take a screenshot of the current game state. | ||||||
|  */ |  */ | ||||||
| fun GameTestSequence.thenScreenshot(name: String? = null): GameTestSequence { | fun GameTestSequence.thenScreenshot(name: String? = null, showGui: Boolean = false): GameTestSequence { | ||||||
|     val suffix = if (name == null) "" else "-$name" |     val suffix = if (name == null) "" else "-$name" | ||||||
|     val test = (this as GameTestSequenceAccessor).parent |     val test = (this as GameTestSequenceAccessor).parent | ||||||
|     val fullName = "${test.testName}$suffix" |     val fullName = "${test.testName}$suffix" | ||||||
| @@ -63,7 +62,7 @@ fun GameTestSequence.thenScreenshot(name: String? = null): GameTestSequence { | |||||||
| 
 | 
 | ||||||
|     // Now disable the GUI, take a screenshot and reenable it. Sleep a little afterwards to ensure the render thread |     // Now disable the GUI, take a screenshot and reenable it. Sleep a little afterwards to ensure the render thread | ||||||
|     // has caught up. |     // has caught up. | ||||||
|     thenOnClient { minecraft.options.hideGui = true } |     thenOnClient { minecraft.options.hideGui = !showGui } | ||||||
|     thenIdle(2) |     thenIdle(2) | ||||||
| 
 | 
 | ||||||
|     // Take a screenshot and wait for it to have finished. |     // Take a screenshot and wait for it to have finished. | ||||||
| @@ -96,12 +95,12 @@ fun GameTestHelper.positionAtArmorStand() { | |||||||
| /** | /** | ||||||
|  * Position the player at a given coordinate. |  * Position the player at a given coordinate. | ||||||
|  */ |  */ | ||||||
| fun GameTestHelper.positionAt(pos: BlockPos) { | fun GameTestHelper.positionAt(pos: BlockPos, yRot: Float = 0.0f, xRot: Float = 0.0f) { | ||||||
|     val absolutePos = absolutePos(pos) |     val absolutePos = absolutePos(pos) | ||||||
|     val player = level.randomPlayer ?: throw GameTestAssertException("Player does not exist") |     val player = level.randomPlayer ?: throw GameTestAssertException("Player does not exist") | ||||||
| 
 | 
 | ||||||
|     player.setupForTest() |     player.setupForTest() | ||||||
|     player.connection.teleport(absolutePos.x + 0.5, absolutePos.y + 0.5, absolutePos.z + 0.5, 0.0f, 0.0f) |     player.connection.teleport(absolutePos.x + 0.5, absolutePos.y + 0.5, absolutePos.z + 0.5, yRot, xRot) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|   | |||||||
| @@ -78,6 +78,7 @@ object TestHooks { | |||||||
|         Loot_Test::class.java, |         Loot_Test::class.java, | ||||||
|         Modem_Test::class.java, |         Modem_Test::class.java, | ||||||
|         Monitor_Test::class.java, |         Monitor_Test::class.java, | ||||||
|  |         Pocket_Computer_Test::class.java, | ||||||
|         Printer_Test::class.java, |         Printer_Test::class.java, | ||||||
|         Recipe_Test::class.java, |         Recipe_Test::class.java, | ||||||
|         Turtle_Test::class.java, |         Turtle_Test::class.java, | ||||||
|   | |||||||
| @@ -13,5 +13,8 @@ | |||||||
|         "GameTestSequenceMixin", |         "GameTestSequenceMixin", | ||||||
|         "SharedConstantsMixin", |         "SharedConstantsMixin", | ||||||
|         "TestCommandAccessor" |         "TestCommandAccessor" | ||||||
|  |     ], | ||||||
|  |     "client": [ | ||||||
|  |         "client.MinecraftMixin" | ||||||
|     ] |     ] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,13 +2,13 @@ | |||||||
| """ | """ | ||||||
| Combines screenshots from the Forge and Fabric tests into a single HTML page. | Combines screenshots from the Forge and Fabric tests into a single HTML page. | ||||||
| """ | """ | ||||||
| import os |  | ||||||
| import os.path |  | ||||||
| from dataclasses import dataclass |  | ||||||
| from typing import TextIO |  | ||||||
| from textwrap import dedent |  | ||||||
| import webbrowser |  | ||||||
| import argparse | import argparse | ||||||
|  | import pathlib | ||||||
|  | import webbrowser | ||||||
|  | from dataclasses import dataclass | ||||||
|  | from datetime import datetime, timedelta, timezone | ||||||
|  | from textwrap import dedent | ||||||
|  | from typing import TextIO | ||||||
|  |  | ||||||
| PROJECT_LOCATIONS = [ | PROJECT_LOCATIONS = [ | ||||||
|     "projects/fabric", |     "projects/fabric", | ||||||
| @@ -68,7 +68,7 @@ def write_images(io: TextIO, images: list[Image]): | |||||||
|             <div class="image"> |             <div class="image"> | ||||||
|                 <img src="../{image.path}" /> |                 <img src="../{image.path}" /> | ||||||
|                 <span class="desc"> |                 <span class="desc"> | ||||||
|                     <span class="desc-prefix">{" » ".join(image.name[:-2])} »</span> |                     <span class="desc-prefix">{" » ".join(image.name[:-1])} »</span> | ||||||
|                     <span class="desc-main">{image.name[-1]}</span> |                     <span class="desc-main">{image.name[-1]}</span> | ||||||
|                 </span> |                 </span> | ||||||
|             </div> |             </div> | ||||||
| @@ -79,6 +79,22 @@ def write_images(io: TextIO, images: list[Image]): | |||||||
|     io.write("</div></body></html>") |     io.write("</div></body></html>") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _normalise_id(name: str) -> str: | ||||||
|  |     """Normalise a test ID so it's more readable.""" | ||||||
|  |     return name[0].upper() + name[1:].replace("_", " ") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _format_timedelta(delta: timedelta) -> str: | ||||||
|  |     if delta.days > 0: | ||||||
|  |         return f"{delta.days} days ago" | ||||||
|  |     elif delta.seconds >= 60 * 60 * 2: | ||||||
|  |         return f"{delta.seconds // (60 * 60)} hours ago" | ||||||
|  |     elif delta.seconds >= 60 * 2: | ||||||
|  |         return f"{delta.seconds // 60} minutes ago" | ||||||
|  |     else: | ||||||
|  |         return f"{delta.seconds} seconds ago" | ||||||
|  |  | ||||||
|  |  | ||||||
| def main(): | def main(): | ||||||
|     spec = argparse.ArgumentParser( |     spec = argparse.ArgumentParser( | ||||||
|         description="Combines screenshots from the Forge and Fabric tests into a single HTML page." |         description="Combines screenshots from the Forge and Fabric tests into a single HTML page." | ||||||
| @@ -96,10 +112,17 @@ def main(): | |||||||
|         "Forge": "projects/forge", |         "Forge": "projects/forge", | ||||||
|         "Fabric": "projects/fabric", |         "Fabric": "projects/fabric", | ||||||
|     }.items(): |     }.items(): | ||||||
|         dir = os.path.join(dir, "build", "testScreenshots") |         for file in sorted(pathlib.Path(dir).glob("build/gametest/*/screenshots/*.png")): | ||||||
|         for file in sorted(os.listdir(dir)): |             name = [project, *(_normalise_id(x) for x in file.stem.split("."))] | ||||||
|             name = [project, *os.path.splitext(file)[0].split(".")] |  | ||||||
|             images.append(Image(name, os.path.join(dir, file))) |             mtime = datetime.fromtimestamp(file.stat().st_mtime, tz=timezone.utc) | ||||||
|  |             delta = datetime.now(tz=timezone.utc) - mtime | ||||||
|  |  | ||||||
|  |             print( | ||||||
|  |                 f"""{" » ".join(name[:-1]):>50} » \x1b[1m{name[-1]:25} \x1b[0;33m({_format_timedelta(delta)})\x1b[0m""" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             images.append(Image(name, str(file))) | ||||||
|  |  | ||||||
|     out_file = "build/screenshots.html" |     out_file = "build/screenshots.html" | ||||||
|     with open(out_file, encoding="utf-8", mode="w") as out: |     with open(out_file, encoding="utf-8", mode="w") as out: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Jonathan Coates
					Jonathan Coates