mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2024-12-14 20:20:30 +00:00
Client-side tests
This spins up a Minecraft instance (much like we do for the server) and instructs the client to take screenshots at particular times. We then compare those screenshots and assert they match (modulo some small delta).
This commit is contained in:
parent
4b33306940
commit
0ff6b0ca70
11
.github/workflows/main-ci.yml
vendored
11
.github/workflows/main-ci.yml
vendored
@ -32,7 +32,7 @@ jobs:
|
||||
run: |
|
||||
./gradlew assemble || ./gradlew assemble
|
||||
./gradlew downloadAssets || ./gradlew downloadAssets
|
||||
./gradlew build
|
||||
xvfb-run ./gradlew build
|
||||
|
||||
- name: Upload Jar
|
||||
uses: actions/upload-artifact@v2
|
||||
@ -40,6 +40,15 @@ jobs:
|
||||
name: CC-Tweaked
|
||||
path: build/libs
|
||||
|
||||
- name: Upload Screnshots
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Screenshots
|
||||
path: test-files/client/screenshots
|
||||
if-no-files-found: ignore
|
||||
retention-days: 5
|
||||
if: failure()
|
||||
|
||||
- name: Upload Coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
|
||||
|
40
build.gradle
40
build.gradle
@ -80,6 +80,17 @@ minecraft {
|
||||
args '--mod', 'computercraft', '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/')
|
||||
}
|
||||
|
||||
testClient {
|
||||
workingDirectory project.file('test-files/client')
|
||||
parent runs.client
|
||||
|
||||
mods {
|
||||
cctest {
|
||||
source sourceSets.testMod
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testServer {
|
||||
workingDirectory project.file('test-files/server')
|
||||
parent runs.server
|
||||
@ -352,14 +363,15 @@ task setupServer(type: Copy) {
|
||||
into "test-files/server"
|
||||
}
|
||||
|
||||
tasks.register('testInGame', JavaExec.class).configure {
|
||||
it.group('test server')
|
||||
it.description("Runs tests on a temporary Minecraft server.")
|
||||
it.dependsOn(setupServer, 'prepareRunTestServer', 'cleanTestInGame', 'compileTestModJava')
|
||||
["Client", "Server"].forEach {name ->
|
||||
tasks.register("test$name", JavaExec.class).configure {
|
||||
it.group('In-game tests')
|
||||
it.description("Runs tests on a temporary Minecraft instance.")
|
||||
it.dependsOn(setupServer, "prepareRunTest$name", "cleanTest$name", 'compileTestModJava')
|
||||
|
||||
// Copy from runTestServer. We do it in this slightly odd way as runTestServer
|
||||
// isn't created until the task is configured (which is no good for us).
|
||||
JavaExec exec = tasks.getByName('runTestServer')
|
||||
JavaExec exec = tasks.getByName("runTest$name")
|
||||
exec.copyTo(it)
|
||||
it.setClasspath(exec.getClasspath())
|
||||
it.mainClass = exec.mainClass
|
||||
@ -371,7 +383,7 @@ tasks.register('testInGame', JavaExec.class).configure {
|
||||
// Jacoco and modlauncher don't play well together as the classes loaded in-game don't
|
||||
// match up with those written to disk. We get Jacoco to dump all classes to disk, and
|
||||
// use that when generating the report.
|
||||
def coverageOut = new File(buildDir, 'jacocoClassDump/testInGame')
|
||||
def coverageOut = new File(buildDir, "jacocoClassDump/test$name")
|
||||
jacoco.applyTo(it)
|
||||
it.jacoco.setIncludes(["dan200.computercraft.*"])
|
||||
it.jacoco.setClassDumpDir(coverageOut)
|
||||
@ -381,21 +393,23 @@ tasks.register('testInGame', JavaExec.class).configure {
|
||||
it.jacoco.setIncludeNoLocationClasses(true)
|
||||
}
|
||||
|
||||
tasks.register('jacocoTestInGameReport', JacocoReport.class).configure {
|
||||
it.group('test server')
|
||||
it.description('Generate coverage reports for in-game tests (testInGame)')
|
||||
it.dependsOn('testInGame')
|
||||
tasks.register("jacocoTest${name}Report", JacocoReport.class).configure {
|
||||
it.group('In-game')
|
||||
it.description("Generate coverage reports for test$name")
|
||||
it.dependsOn("test$name")
|
||||
|
||||
it.executionData(new File(buildDir, 'jacoco/testInGame.exec'))
|
||||
it.executionData(new File(buildDir, "jacoco/test${name}.exec"))
|
||||
it.sourceDirectories.from(sourceSets.main.allJava.srcDirs)
|
||||
it.classDirectories.from(new File(buildDir, 'jacocoClassDump/testInGame'))
|
||||
it.classDirectories.from(new File(buildDir, "jacocoClassDump/$name"))
|
||||
|
||||
it.reports {
|
||||
xml.enabled true
|
||||
html.enabled true
|
||||
}
|
||||
}
|
||||
check.dependsOn('jacocoTestInGameReport')
|
||||
|
||||
check.dependsOn("jacocoTest${name}Report")
|
||||
}
|
||||
|
||||
|
||||
// Upload tasks
|
||||
|
@ -1,6 +1,7 @@
|
||||
package dan200.computercraft.ingame
|
||||
|
||||
import dan200.computercraft.ingame.api.*
|
||||
import dan200.computercraft.shared.Capabilities
|
||||
import dan200.computercraft.shared.Registry
|
||||
import dan200.computercraft.shared.peripheral.monitor.TileMonitor
|
||||
import net.minecraft.block.Blocks
|
||||
@ -40,4 +41,25 @@ class Monitor_Test {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@GameTest(batch = "client:Monitor_Test.Looks_acceptable", timeoutTicks = 400)
|
||||
fun Looks_acceptable(helper: GameTestHelper) = helper.sequence {
|
||||
this
|
||||
.thenExecute { helper.normaliseScene() }
|
||||
.thenExecute {
|
||||
helper.positionAtArmorStand()
|
||||
|
||||
// Get the monitor and peripheral. This forces us to create a server monitor at this location.
|
||||
val monitor = helper.getBlockEntity(BlockPos(2, 2, 2)) as TileMonitor
|
||||
monitor.getCapability(Capabilities.CAPABILITY_PERIPHERAL)
|
||||
|
||||
val terminal = monitor.cachedServerMonitor.terminal
|
||||
terminal.write("Hello, world!")
|
||||
terminal.setCursorPos(1, 2)
|
||||
terminal.textColour = 2
|
||||
terminal.backgroundColour = 3
|
||||
terminal.write("Some coloured text")
|
||||
}
|
||||
.thenScreenshot()
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ public class ComputerState
|
||||
public void check( @Nonnull String marker )
|
||||
{
|
||||
if( !markers.contains( marker ) ) throw new IllegalStateException( "Not yet at " + marker );
|
||||
if( error != null ) throw new AssertionError( error );
|
||||
if( error != null ) throw new RuntimeException( error );
|
||||
}
|
||||
|
||||
public static ComputerState get( int id )
|
||||
|
@ -1,9 +1,22 @@
|
||||
package dan200.computercraft.ingame.api
|
||||
|
||||
import dan200.computercraft.ComputerCraft
|
||||
import dan200.computercraft.ingame.mod.ImageUtils
|
||||
import dan200.computercraft.ingame.mod.TestMod
|
||||
import net.minecraft.block.BlockState
|
||||
import net.minecraft.block.Blocks
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.command.arguments.BlockStateInput
|
||||
import net.minecraft.entity.item.ArmorStandEntity
|
||||
import net.minecraft.util.ScreenShotHelper
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import net.minecraft.world.gen.Heightmap
|
||||
import java.nio.file.Files
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.function.Supplier
|
||||
import javax.imageio.ImageIO
|
||||
|
||||
|
||||
/**
|
||||
* Wait until a computer has finished running and check it is OK.
|
||||
@ -16,6 +29,74 @@ fun GameTestSequence.thenComputerOk(id: Int, marker: String = ComputerState.DONE
|
||||
ComputerState.get(id).check(marker)
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a task on the client
|
||||
*/
|
||||
fun GameTestSequence.thenOnClient(task: ClientTestHelper.() -> Unit): GameTestSequence {
|
||||
var future: CompletableFuture<Unit>? = null
|
||||
return this
|
||||
.thenExecute { future = Minecraft.getInstance().submit(Supplier { task(ClientTestHelper()) }) }
|
||||
.thenWaitUntil { if (!future!!.isDone) throw GameTestAssertException("Not done task yet") }
|
||||
.thenExecute {
|
||||
try {
|
||||
future!!.get()
|
||||
} catch (e: ExecutionException) {
|
||||
throw e.cause ?: e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Idle for one tick to allow the client to catch up, then take a screenshot.
|
||||
*/
|
||||
fun GameTestSequence.thenScreenshot(name: String? = null): GameTestSequence {
|
||||
val suffix = if (name == null) "" else "-$name"
|
||||
val fullName = "${parent.testName}$suffix"
|
||||
|
||||
val counter = AtomicInteger()
|
||||
return this
|
||||
// Wait until all chunks have been rendered and we're idle for an extended period.
|
||||
.thenExecute { counter.set(0) }
|
||||
.thenWaitUntil {
|
||||
if (Minecraft.getInstance().levelRenderer.hasRenderedAllChunks()) {
|
||||
val idleFor = counter.getAndIncrement()
|
||||
if (idleFor <= 20) throw GameTestAssertException("Only idle for $idleFor ticks")
|
||||
} else {
|
||||
counter.set(0)
|
||||
throw GameTestAssertException("Waiting for client to finish rendering")
|
||||
}
|
||||
}
|
||||
// Now disable the GUI, take a screenshot and reenable it. We sleep either side to give the client time to do
|
||||
// its thing.
|
||||
.thenExecute { Minecraft.getInstance().options.hideGui = true }
|
||||
.thenIdle(5) // Some delay before/after to ensure the render thread has caught up.
|
||||
.thenOnClient { screenshot("$fullName.png") }
|
||||
.thenIdle(2)
|
||||
.thenExecute {
|
||||
Minecraft.getInstance().options.hideGui = false
|
||||
|
||||
val screenshotsPath = Minecraft.getInstance().gameDirectory.toPath().resolve("screenshots")
|
||||
val screenshotPath = screenshotsPath.resolve("$fullName.png")
|
||||
val originalPath = TestMod.sourceDir.resolve("screenshots").resolve("$fullName.png")
|
||||
|
||||
if (!Files.exists(originalPath)) throw GameTestAssertException("$fullName does not exist. Use `/cctest promote' to create it.");
|
||||
|
||||
val screenshot = ImageIO.read(screenshotPath.toFile())
|
||||
val original = ImageIO.read(originalPath.toFile())
|
||||
|
||||
if (screenshot.width != original.width || screenshot.height != original.height) {
|
||||
throw GameTestAssertException("$fullName screenshot is ${screenshot.width}x${screenshot.height} but original is ${original.width}x${original.height}")
|
||||
}
|
||||
|
||||
if (ImageUtils.areSame(screenshot, original)) return@thenExecute
|
||||
|
||||
ImageUtils.writeDifference(screenshotsPath.resolve("$fullName.diff.png"), screenshot, original)
|
||||
throw GameTestAssertException("Images are different.")
|
||||
}
|
||||
}
|
||||
|
||||
val GameTestHelper.testName: String get() = tracker.testName
|
||||
|
||||
/**
|
||||
* Modify a block state within the test.
|
||||
*/
|
||||
@ -32,14 +113,45 @@ fun GameTestHelper.sequence(run: GameTestSequence.() -> GameTestSequence) {
|
||||
*/
|
||||
fun GameTestHelper.setBlock(pos: BlockPos, state: BlockStateInput) = state.place(level, absolutePos(pos), 3)
|
||||
|
||||
/**
|
||||
* "Normalise" the current world in preparation for screenshots.
|
||||
*
|
||||
* Basically removes any dirt and replaces it with concrete.
|
||||
*/
|
||||
fun GameTestHelper.normaliseScene() {
|
||||
val y = level.getHeightmapPos(Heightmap.Type.WORLD_SURFACE, absolutePos(BlockPos.ZERO))
|
||||
for (x in -100..100) {
|
||||
for (z in -100..100) {
|
||||
val pos = y.offset(x, -3, z)
|
||||
val block = level.getBlockState(pos).block
|
||||
if (block == Blocks.DIRT || block == Blocks.GRASS_BLOCK) {
|
||||
level.setBlock(pos, Blocks.WHITE_CONCRETE.defaultBlockState(), 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun GameTestSequence.thenExecuteSafe(runnable: Runnable) {
|
||||
try {
|
||||
runnable.run()
|
||||
} catch (e: GameTestAssertException) {
|
||||
throw e
|
||||
} catch (e: RuntimeException) {
|
||||
ComputerCraft.log.error("Error in test", e)
|
||||
throw GameTestAssertException(e.toString())
|
||||
/**
|
||||
* Position the player at an armor stand.
|
||||
*/
|
||||
fun GameTestHelper.positionAtArmorStand() {
|
||||
val entities = level.getEntities(null, bounds) { it.name.string == testName }
|
||||
if (entities.size <= 0 || entities[0] !is ArmorStandEntity) throw IllegalStateException("Cannot find armor stand")
|
||||
|
||||
val stand = entities[0] as ArmorStandEntity
|
||||
val player = level.randomPlayer ?: throw NullPointerException("Player does not exist")
|
||||
|
||||
player.connection.teleport(stand.x, stand.y, stand.z, stand.yRot, stand.xRot)
|
||||
}
|
||||
|
||||
|
||||
class ClientTestHelper {
|
||||
val minecraft: Minecraft = Minecraft.getInstance()
|
||||
|
||||
fun screenshot(name: String) {
|
||||
ScreenShotHelper.grab(
|
||||
minecraft.gameDirectory, name,
|
||||
minecraft.window.width, minecraft.window.height, minecraft.mainRenderTarget
|
||||
) { TestMod.log.info(it.string) }
|
||||
}
|
||||
}
|
||||
|
@ -7,14 +7,23 @@ package dan200.computercraft.ingame.mod;
|
||||
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.command.CommandSource;
|
||||
import net.minecraft.data.NBTToSNBTConverter;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityType;
|
||||
import net.minecraft.entity.item.ArmorStandEntity;
|
||||
import net.minecraft.entity.player.ServerPlayerEntity;
|
||||
import net.minecraft.nbt.CompoundNBT;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.test.*;
|
||||
import net.minecraft.tileentity.StructureBlockTileEntity;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.text.StringTextComponent;
|
||||
import net.minecraft.util.text.TextFormatting;
|
||||
import net.minecraft.world.storage.FolderName;
|
||||
import net.minecraftforge.fml.loading.FMLLoader;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
@ -58,6 +67,37 @@ class CCTestCommand
|
||||
result.addFailureListener( x -> TestRegistry.rememberFailedTest( x.getTestFunction() ) );
|
||||
return 0;
|
||||
} ) )
|
||||
|
||||
.then( literal( "promote" ).executes( context -> {
|
||||
if( !FMLLoader.getDist().isClient() ) return error( context.getSource(), "Cannot run on server" );
|
||||
|
||||
promote();
|
||||
return 0;
|
||||
} ) )
|
||||
.then( literal( "marker" ).executes( context -> {
|
||||
ServerPlayerEntity player = context.getSource().getPlayerOrException();
|
||||
BlockPos pos = StructureHelper.findNearestStructureBlock( player.blockPosition(), 15, player.getLevel() );
|
||||
if( pos == null ) return error( context.getSource(), "No nearby test" );
|
||||
|
||||
StructureBlockTileEntity structureBlock = (StructureBlockTileEntity) player.getLevel().getBlockEntity( pos );
|
||||
TestFunctionInfo info = TestRegistry.getTestFunction( structureBlock.getStructurePath() );
|
||||
|
||||
// Kill the existing armor stand
|
||||
player
|
||||
.getLevel().getEntities()
|
||||
.filter( x -> x.isAlive() && x instanceof ArmorStandEntity && x.getName().getString().equals( info.getTestName() ) )
|
||||
.forEach( Entity::remove );
|
||||
|
||||
// And create a new one
|
||||
CompoundNBT nbt = new CompoundNBT();
|
||||
nbt.putBoolean( "Marker", true );
|
||||
nbt.putBoolean( "Invisible", true );
|
||||
ArmorStandEntity armorStand = EntityType.ARMOR_STAND.create( player.getLevel() );
|
||||
armorStand.readAdditionalSaveData( nbt );
|
||||
armorStand.copyPosition( player );
|
||||
armorStand.setCustomName( new StringTextComponent( info.getTestName() ) );
|
||||
return 0;
|
||||
} ) )
|
||||
);
|
||||
}
|
||||
|
||||
@ -73,7 +113,7 @@ class CCTestCommand
|
||||
}
|
||||
}
|
||||
|
||||
public static void exportFiles( MinecraftServer server )
|
||||
static void exportFiles( MinecraftServer server )
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -85,12 +125,28 @@ class CCTestCommand
|
||||
}
|
||||
}
|
||||
|
||||
private static void promote()
|
||||
{
|
||||
try
|
||||
{
|
||||
Copier.replicate(
|
||||
Minecraft.getInstance().gameDirectory.toPath().resolve( "screenshots" ),
|
||||
TestMod.sourceDir.resolve( "screenshots" ),
|
||||
x -> !x.toFile().getName().endsWith( ".diff.png" )
|
||||
);
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
throw new UncheckedIOException( e );
|
||||
}
|
||||
}
|
||||
|
||||
private static class Callback implements ITestCallback
|
||||
{
|
||||
private final CommandSource source;
|
||||
private final TestResultList result;
|
||||
|
||||
public Callback( CommandSource source, TestResultList result )
|
||||
Callback( CommandSource source, TestResultList result )
|
||||
{
|
||||
this.source = source;
|
||||
this.result = result;
|
||||
@ -106,7 +162,13 @@ class CCTestCommand
|
||||
{
|
||||
if( !tracker.isDone() ) return;
|
||||
|
||||
source.sendFailure( new StringTextComponent( result.getFailedRequiredCount() + " required tests failed" ).withStyle( TextFormatting.RED ) );
|
||||
error( source, result.getFailedRequiredCount() + " required tests failed" );
|
||||
}
|
||||
}
|
||||
|
||||
private static int error( CommandSource source, String message )
|
||||
{
|
||||
source.sendFailure( new StringTextComponent( message ).withStyle( TextFormatting.RED ) );
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,101 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.ingame.mod;
|
||||
|
||||
import net.minecraft.block.Blocks;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.screen.MainMenuScreen;
|
||||
import net.minecraft.client.settings.CloudOption;
|
||||
import net.minecraft.client.settings.ParticleStatus;
|
||||
import net.minecraft.client.tutorial.TutorialSteps;
|
||||
import net.minecraft.util.datafix.codec.DatapackCodec;
|
||||
import net.minecraft.util.registry.DynamicRegistries;
|
||||
import net.minecraft.util.registry.Registry;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.biome.Biome;
|
||||
import net.minecraft.world.biome.Biomes;
|
||||
import net.minecraft.world.gen.FlatChunkGenerator;
|
||||
import net.minecraft.world.gen.FlatGenerationSettings;
|
||||
import net.minecraft.world.gen.FlatLayerInfo;
|
||||
import net.minecraft.world.gen.settings.DimensionGeneratorSettings;
|
||||
import net.minecraft.world.gen.settings.DimensionStructuresSettings;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.client.event.GuiScreenEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
|
||||
import static net.minecraft.world.gen.settings.DimensionGeneratorSettings.withOverworld;
|
||||
|
||||
@Mod.EventBusSubscriber( modid = TestMod.MOD_ID, value = Dist.CLIENT )
|
||||
public final class ClientHooks
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger( TestHooks.class );
|
||||
|
||||
private static boolean triggered = false;
|
||||
|
||||
private ClientHooks()
|
||||
{
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onGuiInit( GuiScreenEvent.InitGuiEvent event )
|
||||
{
|
||||
if( triggered || !(event.getGui() instanceof MainMenuScreen) ) return;
|
||||
triggered = true;
|
||||
|
||||
ClientHooks.openWorld();
|
||||
}
|
||||
|
||||
private static void openWorld()
|
||||
{
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
|
||||
// Clear some options before we get any further.
|
||||
minecraft.options.autoJump = false;
|
||||
minecraft.options.renderClouds = CloudOption.OFF;
|
||||
minecraft.options.particles = ParticleStatus.MINIMAL;
|
||||
minecraft.options.tutorialStep = TutorialSteps.NONE;
|
||||
minecraft.options.renderDistance = 6;
|
||||
minecraft.options.gamma = 1.0;
|
||||
|
||||
if( minecraft.getLevelSource().levelExists( "test" ) )
|
||||
{
|
||||
LOG.info( "World exists, loading it" );
|
||||
Minecraft.getInstance().loadLevel( "test" );
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.info( "World does not exist, creating it for the first time" );
|
||||
|
||||
DynamicRegistries.Impl registries = DynamicRegistries.builtin();
|
||||
|
||||
Registry<DimensionType> dimensions = registries.registryOrThrow( Registry.DIMENSION_TYPE_REGISTRY );
|
||||
Registry<Biome> biomes = registries.registryOrThrow( Registry.BIOME_REGISTRY );
|
||||
DimensionGeneratorSettings generator = new DimensionGeneratorSettings( 0, false, false, withOverworld(
|
||||
dimensions,
|
||||
DimensionType.defaultDimensions( dimensions, biomes, registries.registryOrThrow( Registry.NOISE_GENERATOR_SETTINGS_REGISTRY ), 0 ),
|
||||
new FlatChunkGenerator( new FlatGenerationSettings(
|
||||
biomes,
|
||||
new DimensionStructuresSettings( Optional.empty(), Collections.emptyMap() ),
|
||||
Collections.singletonList( new FlatLayerInfo( 4, Blocks.WHITE_CONCRETE ) ),
|
||||
false, false,
|
||||
Optional.of( () -> biomes.getOrThrow( Biomes.DESERT ) )
|
||||
) )
|
||||
) );
|
||||
|
||||
WorldSettings settings = new WorldSettings(
|
||||
"test", GameType.CREATIVE, false, Difficulty.PEACEFUL, true,
|
||||
new GameRules(), DatapackCodec.DEFAULT
|
||||
);
|
||||
Minecraft.getInstance().createLevel( "test", settings, registries, generator );
|
||||
}
|
||||
}
|
||||
}
|
@ -13,23 +13,25 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public final class Copier extends SimpleFileVisitor<Path>
|
||||
{
|
||||
private final Path sourceDir;
|
||||
private final Path targetDir;
|
||||
private final Predicate<Path> predicate;
|
||||
|
||||
private Copier( Path sourceDir, Path targetDir )
|
||||
private Copier( Path sourceDir, Path targetDir, Predicate<Path> predicate )
|
||||
{
|
||||
this.sourceDir = sourceDir;
|
||||
this.targetDir = targetDir;
|
||||
this.predicate = predicate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile( Path file, BasicFileAttributes attributes ) throws IOException
|
||||
{
|
||||
Path targetFile = targetDir.resolve( sourceDir.relativize( file ) );
|
||||
Files.copy( file, targetFile );
|
||||
if( predicate.test( file ) ) Files.copy( file, targetDir.resolve( sourceDir.relativize( file ) ) );
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@ -43,12 +45,17 @@ public final class Copier extends SimpleFileVisitor<Path>
|
||||
|
||||
public static void copy( Path from, Path to ) throws IOException
|
||||
{
|
||||
Files.walkFileTree( from, new Copier( from, to ) );
|
||||
Files.walkFileTree( from, new Copier( from, to, p -> true ) );
|
||||
}
|
||||
|
||||
public static void replicate( Path from, Path to ) throws IOException
|
||||
{
|
||||
replicate( from, to, p -> true );
|
||||
}
|
||||
|
||||
public static void replicate( Path from, Path to, Predicate<Path> check ) throws IOException
|
||||
{
|
||||
if( Files.exists( to ) ) MoreFiles.deleteRecursively( to );
|
||||
copy( from, to );
|
||||
Files.walkFileTree( from, new Copier( from, to, check ) );
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.ingame.mod;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public final class ImageUtils
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger( ImageUtils.class );
|
||||
|
||||
/**
|
||||
* Allow 0.05% of pixels to fail. This allows for slight differences at the edges.
|
||||
*/
|
||||
private static final double PIXEL_THRESHOLD = 0.0005;
|
||||
|
||||
/**
|
||||
* Maximum possible distance between two colours. Floating point differences means we need some fuzziness here.
|
||||
*/
|
||||
public static final int DISTANCE_THRESHOLD = 5;
|
||||
|
||||
private ImageUtils()
|
||||
{
|
||||
}
|
||||
|
||||
public static boolean areSame( BufferedImage left, BufferedImage right )
|
||||
{
|
||||
int width = left.getWidth(), height = left.getHeight();
|
||||
if( width != right.getWidth() || height != right.getHeight() ) return false;
|
||||
|
||||
int failed = 0, threshold = (int) (width * height * PIXEL_THRESHOLD);
|
||||
for( int x = 0; x < width; x++ )
|
||||
{
|
||||
for( int y = 0; y < height; y++ )
|
||||
{
|
||||
int l = left.getRGB( x, y ), r = right.getRGB( x, y );
|
||||
if( (l & 0xFFFFFF) != (r & 0xFFFFFF) && distance( l, r, 0 ) + distance( l, r, 8 ) + distance( l, r, 16 ) >= DISTANCE_THRESHOLD )
|
||||
{
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( failed > 0 ) LOG.warn( "{} pixels failed comparing (threshold is {})", failed, threshold );
|
||||
return failed <= threshold;
|
||||
}
|
||||
|
||||
public static void writeDifference( Path path, BufferedImage left, BufferedImage right ) throws IOException
|
||||
{
|
||||
int width = left.getWidth(), height = left.getHeight();
|
||||
|
||||
BufferedImage copy = new BufferedImage( width, height, left.getType() );
|
||||
for( int x = 0; x < width; x++ )
|
||||
{
|
||||
for( int y = 0; y < height; y++ )
|
||||
{
|
||||
int l = left.getRGB( x, y ), r = right.getRGB( x, y );
|
||||
copy.setRGB( x, y, difference( l, r, 0 ) | difference( l, r, 8 ) | difference( l, r, 16 ) | 0xFF000000 );
|
||||
}
|
||||
}
|
||||
|
||||
ImageIO.write( copy, "png", path.toFile() );
|
||||
}
|
||||
|
||||
private static int difference( int l, int r, int shift )
|
||||
{
|
||||
return Math.abs( ((l >> shift) & 0xFF) - ((r >> shift) & 0xFF) ) << shift;
|
||||
}
|
||||
|
||||
private static int distance( int l, int r, int shift )
|
||||
{
|
||||
return Math.abs( ((l >> shift) & 0xFF) - ((r >> shift) & 0xFF) );
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
@ -0,0 +1,657 @@
|
||||
{
|
||||
size: [5, 5, 5],
|
||||
entities: [
|
||||
{
|
||||
nbt: {
|
||||
Brain: {
|
||||
memories: {}
|
||||
},
|
||||
HurtByTimestamp: 0,
|
||||
Attributes: [
|
||||
{
|
||||
Base: 0.699999988079071d,
|
||||
Name: "minecraft:generic.movement_speed"
|
||||
}
|
||||
],
|
||||
Invulnerable: 0b,
|
||||
FallFlying: 0b,
|
||||
ShowArms: 0b,
|
||||
PortalCooldown: 0,
|
||||
AbsorptionAmount: 0.0f,
|
||||
FallDistance: 0.0f,
|
||||
DisabledSlots: 0,
|
||||
CanUpdate: 1b,
|
||||
DeathTime: 0s,
|
||||
Pose: {},
|
||||
id: "minecraft:armor_stand",
|
||||
Invisible: 1b,
|
||||
UUID: [I; -1245769654, -1089124211, -1971323071, 221540869],
|
||||
Motion: [0.0d, 0.0d, 0.0d],
|
||||
Small: 0b,
|
||||
Health: 20.0f,
|
||||
Air: 300s,
|
||||
OnGround: 1b,
|
||||
Marker: 1b,
|
||||
Rotation: [-0.2999616f, 16.965813f],
|
||||
HandItems: [
|
||||
{},
|
||||
{}
|
||||
],
|
||||
CustomName: '{"text":"monitor_test.looks_acceptable"}',
|
||||
Pos: [121.57308543720325d, 6.0d, 139.40392497295767d],
|
||||
Fire: 0s,
|
||||
ArmorItems: [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
],
|
||||
NoBasePlate: 0b,
|
||||
HurtTime: 0s
|
||||
},
|
||||
blockPos: [2, 1, 0],
|
||||
pos: [2.573085437203247d, 1.0d, 0.40392497295766816d]
|
||||
}
|
||||
],
|
||||
blocks: [
|
||||
{
|
||||
pos: [0, 0, 0],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [0, 0, 1],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [0, 0, 2],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [0, 0, 3],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [0, 0, 4],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [1, 0, 0],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [1, 0, 1],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [1, 0, 2],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [1, 0, 3],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [1, 0, 4],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [2, 0, 0],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [2, 0, 1],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [2, 0, 2],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [2, 0, 3],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [2, 0, 4],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [3, 0, 0],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [3, 0, 1],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [3, 0, 2],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [3, 0, 3],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [3, 0, 4],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [4, 0, 0],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [4, 0, 1],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [4, 0, 2],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [4, 0, 3],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [4, 0, 4],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [0, 1, 0],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [0, 1, 1],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [0, 1, 2],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [0, 1, 3],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [0, 1, 4],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [1, 1, 0],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [1, 1, 1],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [1, 1, 3],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [1, 1, 4],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [2, 1, 0],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [2, 1, 1],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [2, 1, 3],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [2, 1, 4],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [3, 1, 0],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [3, 1, 1],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [3, 1, 3],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [3, 1, 4],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [4, 1, 0],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [4, 1, 1],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [4, 1, 2],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [4, 1, 3],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [4, 1, 4],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [0, 2, 0],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [0, 2, 1],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [0, 2, 2],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [0, 2, 3],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [0, 2, 4],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [1, 2, 0],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [1, 2, 1],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [1, 2, 3],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [1, 2, 4],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [2, 2, 0],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [2, 2, 1],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [2, 2, 3],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [2, 2, 4],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [3, 2, 0],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [3, 2, 1],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [3, 2, 3],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [3, 2, 4],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [4, 2, 0],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [4, 2, 1],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [4, 2, 2],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [4, 2, 3],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [4, 2, 4],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [0, 3, 0],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [0, 3, 1],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [0, 3, 2],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [0, 3, 3],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [0, 3, 4],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [1, 3, 0],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [1, 3, 1],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [1, 3, 2],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [1, 3, 3],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [1, 3, 4],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [2, 3, 0],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [2, 3, 1],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [2, 3, 2],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [2, 3, 3],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [2, 3, 4],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [3, 3, 0],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [3, 3, 1],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [3, 3, 2],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [3, 3, 3],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [3, 3, 4],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [4, 3, 0],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [4, 3, 1],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [4, 3, 2],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [4, 3, 3],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [4, 3, 4],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [0, 4, 0],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [0, 4, 1],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [0, 4, 2],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [0, 4, 3],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [0, 4, 4],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [1, 4, 0],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [1, 4, 1],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [1, 4, 2],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [1, 4, 3],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [1, 4, 4],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [2, 4, 0],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [2, 4, 1],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [2, 4, 2],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [2, 4, 3],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [2, 4, 4],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [3, 4, 0],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [3, 4, 1],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [3, 4, 2],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [3, 4, 3],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [3, 4, 4],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [4, 4, 0],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [4, 4, 1],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [4, 4, 2],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [4, 4, 3],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
pos: [4, 4, 4],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
nbt: {
|
||||
XIndex: 2,
|
||||
Height: 2,
|
||||
id: "computercraft:monitor_advanced",
|
||||
Width: 3,
|
||||
YIndex: 0
|
||||
},
|
||||
pos: [1, 1, 2],
|
||||
state: 2
|
||||
},
|
||||
{
|
||||
nbt: {
|
||||
XIndex: 1,
|
||||
Height: 2,
|
||||
id: "computercraft:monitor_advanced",
|
||||
Width: 3,
|
||||
YIndex: 0
|
||||
},
|
||||
pos: [2, 1, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
nbt: {
|
||||
XIndex: 0,
|
||||
Height: 2,
|
||||
id: "computercraft:monitor_advanced",
|
||||
Width: 3,
|
||||
YIndex: 0
|
||||
},
|
||||
pos: [3, 1, 2],
|
||||
state: 4
|
||||
},
|
||||
{
|
||||
nbt: {
|
||||
XIndex: 2,
|
||||
Height: 2,
|
||||
id: "computercraft:monitor_advanced",
|
||||
Width: 3,
|
||||
YIndex: 1
|
||||
},
|
||||
pos: [1, 2, 2],
|
||||
state: 5
|
||||
},
|
||||
{
|
||||
nbt: {
|
||||
XIndex: 1,
|
||||
Height: 2,
|
||||
id: "computercraft:monitor_advanced",
|
||||
Width: 3,
|
||||
YIndex: 1
|
||||
},
|
||||
pos: [2, 2, 2],
|
||||
state: 6
|
||||
},
|
||||
{
|
||||
nbt: {
|
||||
XIndex: 0,
|
||||
Height: 2,
|
||||
id: "computercraft:monitor_advanced",
|
||||
Width: 3,
|
||||
YIndex: 1
|
||||
},
|
||||
pos: [3, 2, 2],
|
||||
state: 7
|
||||
}
|
||||
],
|
||||
palette: [
|
||||
{
|
||||
Name: "minecraft:polished_andesite"
|
||||
},
|
||||
{
|
||||
Name: "minecraft:air"
|
||||
},
|
||||
{
|
||||
Properties: {
|
||||
orientation: "north",
|
||||
facing: "north",
|
||||
state: "lu"
|
||||
},
|
||||
Name: "computercraft:monitor_advanced"
|
||||
},
|
||||
{
|
||||
Properties: {
|
||||
orientation: "north",
|
||||
facing: "north",
|
||||
state: "lru"
|
||||
},
|
||||
Name: "computercraft:monitor_advanced"
|
||||
},
|
||||
{
|
||||
Properties: {
|
||||
orientation: "north",
|
||||
facing: "north",
|
||||
state: "ru"
|
||||
},
|
||||
Name: "computercraft:monitor_advanced"
|
||||
},
|
||||
{
|
||||
Properties: {
|
||||
orientation: "north",
|
||||
facing: "north",
|
||||
state: "ld"
|
||||
},
|
||||
Name: "computercraft:monitor_advanced"
|
||||
},
|
||||
{
|
||||
Properties: {
|
||||
orientation: "north",
|
||||
facing: "north",
|
||||
state: "lrd"
|
||||
},
|
||||
Name: "computercraft:monitor_advanced"
|
||||
},
|
||||
{
|
||||
Properties: {
|
||||
orientation: "north",
|
||||
facing: "north",
|
||||
state: "rd"
|
||||
},
|
||||
Name: "computercraft:monitor_advanced"
|
||||
}
|
||||
],
|
||||
DataVersion: 2586
|
||||
}
|
Loading…
Reference in New Issue
Block a user