mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-01-05 15:00:29 +00:00
Run integration tests in-game
Name a more iconic duo than @SquidDev and over-engineered test frameworks. This uses Minecraft's test core[1] plus a home-grown framework to run tests against computers in-world. The general idea is: - Build a structure in game. - Save the structure to a file. This will be spawned in every time the test is run. - Write some code which asserts the structure behaves in a particular way. This is done in Kotlin (shock, horror), as coroutines give us a nice way to run asynchronous code while still running on the main thread. As with all my testing efforts, I still haven't actually written any tests! It'd be good to go through some of the historic ones and write some tests though. Turtle block placing and computer redstone interactions are probably a good place to start. [1]: https://www.youtube.com/watch?v=vXaWOJTCYNg
This commit is contained in:
parent
34b5ede326
commit
331031be45
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -1,2 +1,3 @@
|
||||
# Ignore changes in generated files
|
||||
src/generated/resources/data/** linguist-generated
|
||||
src/test/server-files/structures linguist-generated
|
||||
|
5
.github/workflows/main-ci.yml
vendored
5
.github/workflows/main-ci.yml
vendored
@ -26,6 +26,11 @@ jobs:
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build --no-daemon || ./gradlew build --no-daemon
|
||||
|
||||
- name: Run in-game tests
|
||||
run: |
|
||||
./gradlew setupServer --no-daemon
|
||||
./gradlew runTestServerRun --no-daemon
|
||||
|
||||
- name: Upload Jar
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
|
35
build.gradle
35
build.gradle
@ -21,6 +21,7 @@ plugins {
|
||||
id "com.github.hierynomus.license" version "0.15.0"
|
||||
id "com.matthewprenger.cursegradle" version "1.3.0"
|
||||
id "com.github.breadmoirai.github-release" version "2.2.4"
|
||||
id "org.jetbrains.kotlin.jvm" version "1.3.72"
|
||||
}
|
||||
|
||||
apply plugin: 'net.minecraftforge.gradle'
|
||||
@ -51,8 +52,9 @@ minecraft {
|
||||
|
||||
server {
|
||||
workingDirectory project.file("run/server")
|
||||
property 'forge.logging.markers', 'REGISTRIES,REGISTRYDUMP'
|
||||
property 'forge.logging.markers', 'REGISTRIES'
|
||||
property 'forge.logging.console.level', 'debug'
|
||||
arg "--nogui"
|
||||
|
||||
mods {
|
||||
computercraft {
|
||||
@ -63,7 +65,7 @@ minecraft {
|
||||
|
||||
data {
|
||||
workingDirectory project.file('run')
|
||||
property 'forge.logging.markers', 'REGISTRIES,REGISTRYDUMP'
|
||||
property 'forge.logging.markers', 'REGISTRIES'
|
||||
property 'forge.logging.console.level', 'debug'
|
||||
|
||||
args '--mod', 'computercraft', '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/')
|
||||
@ -73,6 +75,24 @@ minecraft {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testServer {
|
||||
workingDirectory project.file('test-files/server')
|
||||
parent runs.server
|
||||
|
||||
mods {
|
||||
cctest {
|
||||
source sourceSets.test
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testServerRun {
|
||||
parent runs.testServer
|
||||
property 'forge.logging.console.level', 'info'
|
||||
property 'cctest.run', 'true'
|
||||
forceExit false
|
||||
}
|
||||
}
|
||||
|
||||
mappings channel: 'official', version: project.mc_version
|
||||
@ -120,6 +140,9 @@ dependencies {
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.7.0'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
|
||||
testImplementation 'org.hamcrest:hamcrest:2.2'
|
||||
testImplementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.72'
|
||||
testImplementation 'org.jetbrains.kotlin:kotlin-reflect:1.3.72'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8'
|
||||
|
||||
deployerJars "org.apache.maven.wagon:wagon-ssh:3.0.0"
|
||||
|
||||
@ -416,6 +439,14 @@ task licenseFormatAPI(type: LicenseFormat);
|
||||
}
|
||||
}
|
||||
|
||||
task setupServer(type: Copy) {
|
||||
from("src/test/server-files") {
|
||||
include "eula.txt"
|
||||
include "server.properties"
|
||||
}
|
||||
into "test-files/server"
|
||||
}
|
||||
|
||||
// Upload tasks
|
||||
|
||||
task checkRelease {
|
||||
|
27
src/test/java/dan200/computercraft/ingame/ComputerTest.kt
Normal file
27
src/test/java/dan200/computercraft/ingame/ComputerTest.kt
Normal file
@ -0,0 +1,27 @@
|
||||
package dan200.computercraft.ingame
|
||||
|
||||
import dan200.computercraft.ingame.api.*
|
||||
import net.minecraft.block.LeverBlock
|
||||
import net.minecraft.block.RedstoneLampBlock
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
|
||||
class ComputerTest {
|
||||
/**
|
||||
* Ensures redstone signals do not travel through computers.
|
||||
*
|
||||
* @see [Issue #548](https://github.com/SquidDev-CC/CC-Tweaked/issues/548)
|
||||
*/
|
||||
@GameTest
|
||||
suspend fun `No through signal`(context: TestContext) {
|
||||
val lamp = BlockPos(2, 0, 4)
|
||||
val lever = BlockPos(2, 0, 0)
|
||||
|
||||
assertFalse(context.getBlock(lamp).getValue(RedstoneLampBlock.LIT), "Lamp should not be lit")
|
||||
|
||||
context.modifyBlock(lever) { x -> x.setValue(LeverBlock.POWERED, true) }
|
||||
context.sleep(3)
|
||||
|
||||
assertFalse(context.getBlock(lamp).getValue(RedstoneLampBlock.LIT), "Lamp should still not be lit")
|
||||
}
|
||||
}
|
10
src/test/java/dan200/computercraft/ingame/TurtleTest.kt
Normal file
10
src/test/java/dan200/computercraft/ingame/TurtleTest.kt
Normal file
@ -0,0 +1,10 @@
|
||||
package dan200.computercraft.ingame
|
||||
|
||||
import dan200.computercraft.ingame.api.GameTest
|
||||
import dan200.computercraft.ingame.api.TestContext
|
||||
import dan200.computercraft.ingame.api.checkComputerOk
|
||||
|
||||
class TurtleTest {
|
||||
@GameTest(required = false)
|
||||
suspend fun `Unequip refreshes peripheral`(context: TestContext) = context.checkComputerOk(1)
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package dan200.computercraft.ingame.api;
|
||||
|
||||
import dan200.computercraft.ingame.mod.TestAPI;
|
||||
import kotlin.coroutines.Continuation;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Assertion state of a computer.
|
||||
*
|
||||
* @see TestAPI For the Lua interface for this.
|
||||
* @see TestExtensionsKt#checkComputerOk(TestContext, int, Continuation)
|
||||
*/
|
||||
public class ComputerState
|
||||
{
|
||||
protected static final Map<Integer, ComputerState> lookup = new ConcurrentHashMap<>();
|
||||
|
||||
protected boolean done;
|
||||
protected String error;
|
||||
|
||||
public boolean isDone()
|
||||
{
|
||||
return done;
|
||||
}
|
||||
|
||||
public void check()
|
||||
{
|
||||
if( !done ) throw new IllegalStateException( "Not yet done" );
|
||||
if( error != null ) throw new AssertionError( error );
|
||||
}
|
||||
|
||||
public static ComputerState get( int id )
|
||||
{
|
||||
return lookup.get( id );
|
||||
}
|
||||
}
|
45
src/test/java/dan200/computercraft/ingame/api/GameTest.java
Normal file
45
src/test/java/dan200/computercraft/ingame/api/GameTest.java
Normal file
@ -0,0 +1,45 @@
|
||||
package dan200.computercraft.ingame.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* A test which manipulates the game. This should applied on an instance function, and should accept a single
|
||||
* {@link TestContext} argument.
|
||||
*
|
||||
* Tests may/should be written as Kotlin coroutines.
|
||||
*/
|
||||
@Retention( RetentionPolicy.RUNTIME )
|
||||
@Target( ElementType.METHOD )
|
||||
public @interface GameTest
|
||||
{
|
||||
/**
|
||||
* Maximum time the test can run, in ticks.
|
||||
*
|
||||
* @return The time the test can run in ticks.
|
||||
*/
|
||||
int timeout() default 200;
|
||||
|
||||
/**
|
||||
* Number of ticks to delay between building the structure and running the test code.
|
||||
*
|
||||
* @return Test delay in ticks.
|
||||
*/
|
||||
long setup() default 5;
|
||||
|
||||
/**
|
||||
* The batch to run tests in. This may be used to run tests which manipulate other bits of state.
|
||||
*
|
||||
* @return This test's batch.
|
||||
*/
|
||||
String batch() default "default";
|
||||
|
||||
/**
|
||||
* If this test must pass. When false, test failures do not cause a build failure.
|
||||
*
|
||||
* @return If this test is required.
|
||||
*/
|
||||
boolean required() default true;
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package dan200.computercraft.ingame.api;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.block.Blocks;
|
||||
import net.minecraft.test.TestTracker;
|
||||
import net.minecraft.test.TestTrackerHolder;
|
||||
import net.minecraft.test.TestUtils;
|
||||
import net.minecraftforge.fml.common.ObfuscationReflectionHelper;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* The context a test is run within.
|
||||
*
|
||||
* @see TestExtensionsKt For additional test helper methods.
|
||||
*/
|
||||
public final class TestContext
|
||||
{
|
||||
private final TestTracker tracker;
|
||||
|
||||
public TestContext( TestTrackerHolder holder )
|
||||
{
|
||||
this.tracker = ObfuscationReflectionHelper.getPrivateValue( TestTrackerHolder.class, holder, "field_229487_a_" );
|
||||
}
|
||||
|
||||
public TestTracker getTracker()
|
||||
{
|
||||
return tracker;
|
||||
}
|
||||
|
||||
public void ok()
|
||||
{
|
||||
try
|
||||
{
|
||||
Method finish = TestTracker.class.getDeclaredMethod( "finish" );
|
||||
finish.setAccessible( true );
|
||||
finish.invoke( tracker );
|
||||
|
||||
Method spawn = TestUtils.class.getDeclaredMethod( "spawnBeacon", TestTracker.class, Block.class );
|
||||
spawn.setAccessible( true );
|
||||
spawn.invoke( null, tracker, Blocks.LIME_STAINED_GLASS );
|
||||
}
|
||||
catch( ReflectiveOperationException e )
|
||||
{
|
||||
throw new RuntimeException( e );
|
||||
}
|
||||
}
|
||||
|
||||
public void fail( Throwable e )
|
||||
{
|
||||
if( !tracker.isDone() ) tracker.fail( e );
|
||||
}
|
||||
|
||||
public boolean isDone()
|
||||
{
|
||||
return tracker.isDone();
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package dan200.computercraft.ingame.api
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
import net.minecraft.block.BlockState
|
||||
import net.minecraft.util.math.BlockPos
|
||||
|
||||
/**
|
||||
* Wait until a predicate matches (or the test times out).
|
||||
*/
|
||||
suspend inline fun TestContext.waitUntil(fn: () -> Boolean) {
|
||||
while (true) {
|
||||
if (isDone) throw CancellationException()
|
||||
if (fn()) return
|
||||
|
||||
delay(50)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until a computer has finished running and check it is OK.
|
||||
*/
|
||||
suspend fun TestContext.checkComputerOk(id: Int) {
|
||||
waitUntil {
|
||||
val computer = ComputerState.get(id)
|
||||
computer != null && computer.isDone
|
||||
}
|
||||
|
||||
ComputerState.get(id).check()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep for a given number of ticks.
|
||||
*/
|
||||
suspend fun TestContext.sleep(ticks: Int = 1) {
|
||||
val target = tracker.level.gameTime + ticks
|
||||
waitUntil { tracker.level.gameTime >= target }
|
||||
}
|
||||
|
||||
private fun TestContext.offset(pos: BlockPos): BlockPos = tracker.testPos.offset(pos.x, pos.y + 2, pos.z)
|
||||
|
||||
/**
|
||||
* Get a block within the test structure.
|
||||
*/
|
||||
fun TestContext.getBlock(pos: BlockPos): BlockState = tracker.level.getBlockState(offset(pos))
|
||||
|
||||
/**
|
||||
* Set a block within the test structure.
|
||||
*/
|
||||
fun TestContext.setBlock(pos: BlockPos, state: BlockState) {
|
||||
tracker.level.setBlockAndUpdate(offset(pos), state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a block state within the test.
|
||||
*/
|
||||
fun TestContext.modifyBlock(pos: BlockPos, modify: (BlockState) -> BlockState) {
|
||||
val level = tracker.level
|
||||
val offset = offset(pos)
|
||||
level.setBlockAndUpdate(offset, modify(level.getBlockState(offset)))
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package dan200.computercraft.ingame.mod;
|
||||
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import dan200.computercraft.utils.Copier;
|
||||
import net.minecraft.command.CommandSource;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
|
||||
import static dan200.computercraft.shared.command.builder.HelpingArgumentBuilder.choice;
|
||||
import static net.minecraft.command.Commands.literal;
|
||||
|
||||
/**
|
||||
* Helper commands for importing/exporting the computer directory.
|
||||
*/
|
||||
class CCTestCommand
|
||||
{
|
||||
public static void register( CommandDispatcher<CommandSource> dispatcher )
|
||||
{
|
||||
dispatcher.register( choice( "cctest" )
|
||||
.then( literal( "import" ).executes( context -> {
|
||||
importFiles( context.getSource().getServer() );
|
||||
return 0;
|
||||
} ) )
|
||||
.then( literal( "export" ).executes( context -> {
|
||||
exportFiles( context.getSource().getServer() );
|
||||
return 0;
|
||||
} ) )
|
||||
);
|
||||
}
|
||||
|
||||
public static void importFiles( MinecraftServer server )
|
||||
{
|
||||
try
|
||||
{
|
||||
Copier.replicate( TestMod.sourceDir.resolve( "computers" ), server.getServerDirectory().toPath().resolve( "world/computercraft" ) );
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
throw new UncheckedIOException( e );
|
||||
}
|
||||
}
|
||||
|
||||
public static void exportFiles( MinecraftServer server )
|
||||
{
|
||||
try
|
||||
{
|
||||
Copier.replicate( server.getServerDirectory().toPath().resolve( "world/computercraft" ), TestMod.sourceDir.resolve( "computers" ) );
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
throw new UncheckedIOException( e );
|
||||
}
|
||||
}
|
||||
}
|
63
src/test/java/dan200/computercraft/ingame/mod/TestAPI.java
Normal file
63
src/test/java/dan200/computercraft/ingame/mod/TestAPI.java
Normal file
@ -0,0 +1,63 @@
|
||||
package dan200.computercraft.ingame.mod;
|
||||
|
||||
import dan200.computercraft.api.lua.IComputerSystem;
|
||||
import dan200.computercraft.api.lua.ILuaAPI;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.ingame.api.ComputerState;
|
||||
import dan200.computercraft.ingame.api.TestContext;
|
||||
import dan200.computercraft.ingame.api.TestExtensionsKt;
|
||||
import kotlin.coroutines.Continuation;
|
||||
|
||||
/**
|
||||
* API exposed to computers to help write tests.
|
||||
*
|
||||
* Note, we extend this API within startup file of computers (see {@code cctest.lua}).
|
||||
*
|
||||
* @see TestExtensionsKt#checkComputerOk(TestContext, int, Continuation) To check tests on the computer have passed.
|
||||
*/
|
||||
public class TestAPI extends ComputerState implements ILuaAPI
|
||||
{
|
||||
private final int id;
|
||||
|
||||
TestAPI( IComputerSystem system )
|
||||
{
|
||||
id = system.getID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startup()
|
||||
{
|
||||
done = false;
|
||||
error = null;
|
||||
lookup.put( id, this );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown()
|
||||
{
|
||||
if( lookup.get( id ) == this ) lookup.remove( id );
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getNames()
|
||||
{
|
||||
return new String[] { "test" };
|
||||
}
|
||||
|
||||
@LuaFunction
|
||||
public final void fail( String message ) throws LuaException
|
||||
{
|
||||
if( done ) throw new LuaException( "Cannot call fail/ok multiple times." );
|
||||
done = true;
|
||||
error = message;
|
||||
throw new LuaException( message );
|
||||
}
|
||||
|
||||
@LuaFunction
|
||||
public final void ok() throws LuaException
|
||||
{
|
||||
if( done ) throw new LuaException( "Cannot call fail/ok multiple times." );
|
||||
done = true;
|
||||
}
|
||||
}
|
119
src/test/java/dan200/computercraft/ingame/mod/TestLoader.java
Normal file
119
src/test/java/dan200/computercraft/ingame/mod/TestLoader.java
Normal file
@ -0,0 +1,119 @@
|
||||
package dan200.computercraft.ingame.mod;
|
||||
|
||||
import com.google.common.base.CaseFormat;
|
||||
import dan200.computercraft.ingame.api.GameTest;
|
||||
import net.minecraft.test.TestFunctionInfo;
|
||||
import net.minecraft.test.TestRegistry;
|
||||
import net.minecraft.test.TestTrackerHolder;
|
||||
import net.minecraftforge.fml.ModList;
|
||||
import net.minecraftforge.fml.common.ObfuscationReflectionHelper;
|
||||
import net.minecraftforge.forgespi.language.ModFileScanData;
|
||||
import org.objectweb.asm.Type;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Loads methods annotated with {@link GameTest} and adds them to the {@link TestRegistry}. This involves some horrible
|
||||
* reflection hacks, as Proguard makes many methods (and constructors) private.
|
||||
*/
|
||||
class TestLoader
|
||||
{
|
||||
private static final Type gameTest = Type.getType( GameTest.class );
|
||||
private static final Collection<TestFunctionInfo> testFunctions = ObfuscationReflectionHelper.getPrivateValue( TestRegistry.class, null, "field_229526_a_" );
|
||||
private static final Set<String> testClassNames = ObfuscationReflectionHelper.getPrivateValue( TestRegistry.class, null, "field_229527_b_" );
|
||||
|
||||
public static void setup()
|
||||
{
|
||||
ModList.get().getAllScanData().stream()
|
||||
.flatMap( x -> x.getAnnotations().stream() )
|
||||
.filter( x -> x.getAnnotationType().equals( gameTest ) )
|
||||
.forEach( TestLoader::loadTest );
|
||||
}
|
||||
|
||||
|
||||
private static void loadTest( ModFileScanData.AnnotationData annotation )
|
||||
{
|
||||
Class<?> klass;
|
||||
Method method;
|
||||
try
|
||||
{
|
||||
klass = TestLoader.class.getClassLoader().loadClass( annotation.getClassType().getClassName() );
|
||||
|
||||
// We don't know the exact signature (could suspend or not), so find something with the correct descriptor instead.
|
||||
String methodName = annotation.getMemberName();
|
||||
method = Arrays.stream( klass.getMethods() ).filter( x -> (x.getName() + Type.getMethodDescriptor( x )).equals( methodName ) ).findFirst()
|
||||
.orElseThrow( () -> new NoSuchMethodException( "No method " + annotation.getClassType().getClassName() + "." + annotation.getMemberName() ) );
|
||||
}
|
||||
catch( ReflectiveOperationException e )
|
||||
{
|
||||
throw new RuntimeException( e );
|
||||
}
|
||||
|
||||
String className = CaseFormat.UPPER_CAMEL.to( CaseFormat.LOWER_UNDERSCORE, klass.getSimpleName() );
|
||||
String name = className + "." + method.getName().toLowerCase().replace( ' ', '_' );
|
||||
|
||||
GameTest test = method.getAnnotation( GameTest.class );
|
||||
|
||||
TestMod.log.info( "Adding test " + name );
|
||||
testClassNames.add( className );
|
||||
testFunctions.add( createTestFunction(
|
||||
test.batch(), name, name,
|
||||
test.required(),
|
||||
new TestRunner( name, method ),
|
||||
test.timeout(),
|
||||
test.setup()
|
||||
) );
|
||||
}
|
||||
|
||||
private static TestFunctionInfo createTestFunction(
|
||||
String batchName,
|
||||
String testName,
|
||||
String structureName,
|
||||
boolean required,
|
||||
Consumer<TestTrackerHolder> function,
|
||||
int maxTicks,
|
||||
long setupTicks
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
Constructor<TestFunctionInfo> ctor = TestFunctionInfo.class.getDeclaredConstructor();
|
||||
ctor.setAccessible( true );
|
||||
|
||||
TestFunctionInfo func = ctor.newInstance();
|
||||
setFinalField( func, "batchName", batchName );
|
||||
setFinalField( func, "testName", testName );
|
||||
setFinalField( func, "structureName", structureName );
|
||||
setFinalField( func, "required", required );
|
||||
setFinalField( func, "function", function );
|
||||
setFinalField( func, "maxTicks", maxTicks );
|
||||
setFinalField( func, "setupTicks", setupTicks );
|
||||
return func;
|
||||
}
|
||||
catch( ReflectiveOperationException e )
|
||||
{
|
||||
throw new RuntimeException( e );
|
||||
}
|
||||
}
|
||||
|
||||
private static void setFinalField( TestFunctionInfo func, String name, Object value ) throws ReflectiveOperationException
|
||||
{
|
||||
Field field = TestFunctionInfo.class.getDeclaredField( name );
|
||||
if( (field.getModifiers() & Modifier.FINAL) != 0 )
|
||||
{
|
||||
Field modifiers = Field.class.getDeclaredField( "modifiers" );
|
||||
modifiers.setAccessible( true );
|
||||
modifiers.set( field, field.getModifiers() & ~Modifier.FINAL );
|
||||
}
|
||||
|
||||
field.setAccessible( true );
|
||||
field.set( func, value );
|
||||
}
|
||||
}
|
117
src/test/java/dan200/computercraft/ingame/mod/TestMod.java
Normal file
117
src/test/java/dan200/computercraft/ingame/mod/TestMod.java
Normal file
@ -0,0 +1,117 @@
|
||||
package dan200.computercraft.ingame.mod;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import net.minecraft.command.CommandSource;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.test.*;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.world.GameRules;
|
||||
import net.minecraft.world.gen.Heightmap;
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.fml.event.server.FMLServerStartedEvent;
|
||||
import net.minecraftforge.fml.event.server.FMLServerStartingEvent;
|
||||
import net.minecraftforge.fml.server.ServerLifecycleHooks;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collection;
|
||||
|
||||
@Mod( TestMod.MOD_ID )
|
||||
public class TestMod
|
||||
{
|
||||
public static final Path sourceDir = Paths.get( "../../src/test/server-files/" ).toAbsolutePath();
|
||||
|
||||
public static final String MOD_ID = "cctest";
|
||||
|
||||
public static final Logger log = LogManager.getLogger( MOD_ID );
|
||||
|
||||
private TestResultList runningTests = null;
|
||||
private int countdown = 20;
|
||||
|
||||
public TestMod()
|
||||
{
|
||||
log.info( "CC: Test initialised" );
|
||||
ComputerCraftAPI.registerAPIFactory( TestAPI::new );
|
||||
TestLoader.setup();
|
||||
|
||||
StructureHelper.testStructuresDir = sourceDir.resolve( "structures" ).toString();
|
||||
|
||||
MinecraftForge.EVENT_BUS.addListener( ( FMLServerStartingEvent event ) -> {
|
||||
log.info( "Starting server, registering command helpers." );
|
||||
TestCommand.register( event.getCommandDispatcher() );
|
||||
CCTestCommand.register( event.getCommandDispatcher() );
|
||||
} );
|
||||
|
||||
MinecraftForge.EVENT_BUS.addListener( ( FMLServerStartedEvent event ) -> {
|
||||
MinecraftServer server = event.getServer();
|
||||
GameRules rules = server.getGameRules();
|
||||
rules.getRule( GameRules.RULE_DAYLIGHT ).set( false, server );
|
||||
rules.getRule( GameRules.RULE_WEATHER_CYCLE ).set( false, server );
|
||||
rules.getRule( GameRules.RULE_DOMOBSPAWNING ).set( false, server );
|
||||
|
||||
log.info( "Cleaning up after last run" );
|
||||
CommandSource source = server.createCommandSourceStack();
|
||||
TestUtils.clearAllTests( source.getLevel(), getStart( source ), TestCollection.singleton, 200 );
|
||||
|
||||
log.info( "Importing files" );
|
||||
CCTestCommand.importFiles( server );
|
||||
} );
|
||||
|
||||
MinecraftForge.EVENT_BUS.addListener( ( TickEvent.ServerTickEvent event ) -> {
|
||||
if( event.phase != TickEvent.Phase.START ) return;
|
||||
|
||||
// Let the world settle a bit before starting tests.
|
||||
countdown--;
|
||||
if( countdown == 0 && System.getProperty( "cctest.run", "false" ).equals( "true" ) ) startTests();
|
||||
|
||||
TestCollection.singleton.tick();
|
||||
MainThread.INSTANCE.tick();
|
||||
|
||||
if( runningTests != null && runningTests.isDone() ) finishTests();
|
||||
} );
|
||||
}
|
||||
|
||||
private void startTests()
|
||||
{
|
||||
MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
|
||||
CommandSource source = server.createCommandSourceStack();
|
||||
Collection<TestFunctionInfo> tests = TestRegistry.getAllTestFunctions();
|
||||
|
||||
log.info( "Running {} tests...", tests.size() );
|
||||
runningTests = new TestResultList( TestUtils.runTests( tests, getStart( source ), source.getLevel(), TestCollection.singleton ) );
|
||||
}
|
||||
|
||||
private void finishTests()
|
||||
{
|
||||
log.info( "Finished tests - {} were run", runningTests.getTotalCount() );
|
||||
if( runningTests.hasFailedRequired() )
|
||||
{
|
||||
log.error( "{} required tests failed", runningTests.getFailedRequiredCount() );
|
||||
}
|
||||
if( runningTests.hasFailedOptional() )
|
||||
{
|
||||
log.warn( "{} optional tests failed", runningTests.getFailedOptionalCount() );
|
||||
}
|
||||
|
||||
if( ServerLifecycleHooks.getCurrentServer().isDedicatedServer() )
|
||||
{
|
||||
log.info( "Stopping server." );
|
||||
|
||||
// We can't exit in the main thread, as Minecraft registers a shutdown hook which results
|
||||
// in a deadlock. So we do this weird janky thing!
|
||||
Thread thread = new Thread( () -> System.exit( runningTests.hasFailedRequired() ? 1 : 0 ) );
|
||||
thread.setDaemon( true );
|
||||
thread.start();
|
||||
}
|
||||
}
|
||||
|
||||
private BlockPos getStart( CommandSource source )
|
||||
{
|
||||
BlockPos pos = new BlockPos( source.getPosition() );
|
||||
return new BlockPos( pos.getX(), source.getLevel().getHeightmapPos( Heightmap.Type.WORLD_SURFACE, pos ).getY(), pos.getZ() + 3 );
|
||||
}
|
||||
}
|
62
src/test/java/dan200/computercraft/ingame/mod/TestRunner.kt
Normal file
62
src/test/java/dan200/computercraft/ingame/mod/TestRunner.kt
Normal file
@ -0,0 +1,62 @@
|
||||
package dan200.computercraft.ingame.mod
|
||||
|
||||
import dan200.computercraft.ingame.api.TestContext
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.minecraft.test.TestCollection
|
||||
import net.minecraft.test.TestTrackerHolder
|
||||
import java.lang.reflect.Method
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentLinkedDeque
|
||||
import java.util.function.Consumer
|
||||
import kotlin.coroutines.AbstractCoroutineContextElement
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.ContinuationInterceptor
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.reflect.full.callSuspend
|
||||
import kotlin.reflect.jvm.kotlinFunction
|
||||
|
||||
internal class TestRunner(private val name: String, private val method: Method) : Consumer<TestTrackerHolder> {
|
||||
override fun accept(t: TestTrackerHolder) {
|
||||
GlobalScope.launch(MainThread + CoroutineName(name)) {
|
||||
val testContext = TestContext(t)
|
||||
try {
|
||||
val instance = method.declaringClass.newInstance()
|
||||
val function = method.kotlinFunction;
|
||||
if (function == null) {
|
||||
method.invoke(instance, testContext)
|
||||
} else {
|
||||
function.callSuspend(instance, testContext)
|
||||
}
|
||||
testContext.ok()
|
||||
} catch (e: Exception) {
|
||||
testContext.fail(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A coroutine scope which runs everything on the main thread.
|
||||
*/
|
||||
internal object MainThread : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
|
||||
private val queue: Queue<() -> Unit> = ConcurrentLinkedDeque()
|
||||
|
||||
fun tick() {
|
||||
while (true) {
|
||||
val q = queue.poll() ?: break;
|
||||
q.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> = MainThreadInterception(continuation)
|
||||
|
||||
private class MainThreadInterception<T>(val cont: Continuation<T>) : Continuation<T> {
|
||||
override val context: CoroutineContext get() = cont.context
|
||||
|
||||
override fun resumeWith(result: Result<T>) {
|
||||
queue.add { cont.resumeWith(result) }
|
||||
}
|
||||
}
|
||||
}
|
49
src/test/java/dan200/computercraft/utils/Copier.java
Normal file
49
src/test/java/dan200/computercraft/utils/Copier.java
Normal file
@ -0,0 +1,49 @@
|
||||
package dan200.computercraft.utils;
|
||||
|
||||
import com.google.common.io.MoreFiles;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
|
||||
public class Copier extends SimpleFileVisitor<Path>
|
||||
{
|
||||
private final Path sourceDir;
|
||||
private final Path targetDir;
|
||||
|
||||
private Copier( Path sourceDir, Path targetDir )
|
||||
{
|
||||
this.sourceDir = sourceDir;
|
||||
this.targetDir = targetDir;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile( Path file, BasicFileAttributes attributes ) throws IOException
|
||||
{
|
||||
Path targetFile = targetDir.resolve( sourceDir.relativize( file ) );
|
||||
Files.copy( file, targetFile );
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory( Path dir, BasicFileAttributes attributes ) throws IOException
|
||||
{
|
||||
Path newDir = targetDir.resolve( sourceDir.relativize( dir ) );
|
||||
Files.createDirectories( newDir );
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
public static void copy( Path from, Path to ) throws IOException
|
||||
{
|
||||
Files.walkFileTree( from, new Copier( from, to ) );
|
||||
}
|
||||
|
||||
public static void replicate( Path from, Path to ) throws IOException
|
||||
{
|
||||
if( Files.exists( to ) ) MoreFiles.deleteRecursively( to );
|
||||
copy( from, to );
|
||||
}
|
||||
}
|
18
src/test/resources/META-INF/mods.toml
Normal file
18
src/test/resources/META-INF/mods.toml
Normal file
@ -0,0 +1,18 @@
|
||||
modLoader="javafml"
|
||||
loaderVersion="[30,)"
|
||||
|
||||
issueTrackerURL="https://github.com/SquidDev-CC/CC-Tweaked/issues"
|
||||
displayURL="https://github.com/SquidDev-CC/CC-Tweaked"
|
||||
logoFile="pack.png"
|
||||
|
||||
credits="Created by Daniel Ratcliffe (@DanTwoHundred)"
|
||||
authors="Daniel Ratcliffe, Aaron Mills, SquidDev"
|
||||
|
||||
[[mods]]
|
||||
modId="cctest"
|
||||
version="1.0.0"
|
||||
displayName="CC: Tweaked test framework"
|
||||
description='''
|
||||
A test framework for ensuring CC: Tweaked works correctly.
|
||||
'''
|
||||
|
@ -0,0 +1,17 @@
|
||||
--- Extend the test API with some convenience functions.
|
||||
--
|
||||
-- It's much easier to declare these in Lua rather than Java.
|
||||
|
||||
function test.assert(ok, ...)
|
||||
if ok then return ... end
|
||||
|
||||
test.fail(... and tostring(...) or "Assertion failed")
|
||||
end
|
||||
|
||||
function test.eq(expected, actual, msg)
|
||||
if expected == actual then return end
|
||||
|
||||
local message = ("Assertion failed:\nExpected %s,\ngot %s"):format(expected, actual)
|
||||
if msg then message = ("%s - %s"):format(msg, message) end
|
||||
test.fail(message)
|
||||
end
|
6
src/test/resources/pack.mcmeta
Executable file
6
src/test/resources/pack.mcmeta
Executable file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"pack": {
|
||||
"pack_format": 4,
|
||||
"description": "CC: Test"
|
||||
}
|
||||
}
|
5
src/test/server-files/computers/computer/1/startup.lua
Normal file
5
src/test/server-files/computers/computer/1/startup.lua
Normal file
@ -0,0 +1,5 @@
|
||||
test.eq("modem", peripheral.getType("right"), "Starts with a modem")
|
||||
turtle.equipRight()
|
||||
test.eq("drive", peripheral.getType("right"), "Unequipping gives a drive")
|
||||
|
||||
test.ok()
|
3
src/test/server-files/computers/ids.json
Normal file
3
src/test/server-files/computers/ids.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"computer": 2
|
||||
}
|
2
src/test/server-files/eula.txt
Normal file
2
src/test/server-files/eula.txt
Normal file
@ -0,0 +1,2 @@
|
||||
# Automatically generated EULA. Please don't use this for a real server.
|
||||
eula=true
|
46
src/test/server-files/server.properties
Normal file
46
src/test/server-files/server.properties
Normal file
@ -0,0 +1,46 @@
|
||||
#Minecraft server properties
|
||||
#Fri Jan 08 18:54:30 GMT 2021
|
||||
allow-flight=false
|
||||
allow-nether=true
|
||||
broadcast-console-to-ops=true
|
||||
broadcast-rcon-to-ops=true
|
||||
difficulty=easy
|
||||
enable-command-block=true
|
||||
enable-query=false
|
||||
enable-rcon=false
|
||||
enforce-whitelist=false
|
||||
force-gamemode=false
|
||||
function-permission-level=2
|
||||
gamemode=creative
|
||||
generate-structures=true
|
||||
generator-settings=
|
||||
hardcore=false
|
||||
level-name=world
|
||||
level-seed=
|
||||
level-type=flat
|
||||
max-build-height=256
|
||||
max-players=20
|
||||
max-tick-time=60000
|
||||
max-world-size=29999984
|
||||
motd=A testing server
|
||||
network-compression-threshold=256
|
||||
online-mode=false
|
||||
op-permission-level=4
|
||||
player-idle-timeout=0
|
||||
prevent-proxy-connections=false
|
||||
pvp=true
|
||||
query.port=25565
|
||||
rcon.password=
|
||||
rcon.port=25575
|
||||
resource-pack=
|
||||
resource-pack-sha1=
|
||||
server-ip=
|
||||
server-port=25565
|
||||
snooper-enabled=true
|
||||
spawn-animals=true
|
||||
spawn-monsters=true
|
||||
spawn-npcs=true
|
||||
spawn-protection=16
|
||||
use-native-transport=true
|
||||
view-distance=10
|
||||
white-list=false
|
@ -0,0 +1,560 @@
|
||||
{
|
||||
size: [5, 5, 5],
|
||||
entities: [],
|
||||
blocks: [
|
||||
{
|
||||
pos: [0, 0, 0],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [1, 0, 0],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [2, 0, 0],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [3, 0, 0],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [4, 0, 0],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [0, 0, 1],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [1, 0, 1],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [2, 0, 1],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [3, 0, 1],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [4, 0, 1],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [0, 0, 2],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [1, 0, 2],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [2, 0, 2],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [3, 0, 2],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [4, 0, 2],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [0, 0, 3],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [1, 0, 3],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [2, 0, 3],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [3, 0, 3],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [4, 0, 3],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [0, 0, 4],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [1, 0, 4],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [2, 0, 4],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [3, 0, 4],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [4, 0, 4],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [2, 1, 4],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
nbt: {
|
||||
id: "computercraft:computer_advanced",
|
||||
ComputerId: 2,
|
||||
On: 1b
|
||||
},
|
||||
pos: [2, 1, 2],
|
||||
state: 2
|
||||
},
|
||||
{
|
||||
pos: [0, 1, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 1, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 1, 0],
|
||||
state: 4
|
||||
},
|
||||
{
|
||||
pos: [3, 1, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 1, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 2, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 2, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 2, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 2, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 2, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 3, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 3, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 3, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 3, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 3, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 4, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 4, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 4, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 4, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 4, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 1, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 1, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 1, 1],
|
||||
state: 5
|
||||
},
|
||||
{
|
||||
pos: [3, 1, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 1, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 2, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 2, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 2, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 2, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 2, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 3, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 3, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 3, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 3, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 3, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 4, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 4, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 4, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 4, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 4, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 1, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 1, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 1, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 1, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 2, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 2, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 2, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 2, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 2, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 3, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 3, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 3, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 3, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 3, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 4, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 4, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 4, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 4, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 4, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 1, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 1, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 1, 3],
|
||||
state: 6
|
||||
},
|
||||
{
|
||||
pos: [3, 1, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 1, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 2, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 2, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 2, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 2, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 2, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 3, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 3, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 3, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 3, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 3, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 4, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 4, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 4, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 4, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 4, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 1, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 1, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 1, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 1, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 2, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 2, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 2, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 2, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 2, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 3, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 3, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 3, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 3, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 3, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 4, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 4, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 4, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 4, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 4, 4],
|
||||
state: 3
|
||||
}
|
||||
],
|
||||
palette: [
|
||||
{
|
||||
Name: "minecraft:polished_andesite"
|
||||
},
|
||||
{
|
||||
Properties: {
|
||||
lit: "false"
|
||||
},
|
||||
Name: "minecraft:redstone_lamp"
|
||||
},
|
||||
{
|
||||
Properties: {
|
||||
facing: "north",
|
||||
state: "blinking"
|
||||
},
|
||||
Name: "computercraft:computer_advanced"
|
||||
},
|
||||
{
|
||||
Name: "minecraft:air"
|
||||
},
|
||||
{
|
||||
Properties: {
|
||||
face: "floor",
|
||||
powered: "false",
|
||||
facing: "south"
|
||||
},
|
||||
Name: "minecraft:lever"
|
||||
},
|
||||
{
|
||||
Properties: {
|
||||
delay: "1",
|
||||
powered: "false",
|
||||
facing: "north",
|
||||
locked: "false"
|
||||
},
|
||||
Name: "minecraft:repeater"
|
||||
},
|
||||
{
|
||||
Properties: {
|
||||
east: "none",
|
||||
south: "none",
|
||||
north: "side",
|
||||
west: "none",
|
||||
power: "0"
|
||||
},
|
||||
Name: "minecraft:redstone_wire"
|
||||
}
|
||||
],
|
||||
DataVersion: 2230
|
||||
}
|
@ -0,0 +1,550 @@
|
||||
{
|
||||
size: [5, 5, 5],
|
||||
entities: [],
|
||||
blocks: [
|
||||
{
|
||||
pos: [0, 0, 0],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [1, 0, 0],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [2, 0, 0],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [3, 0, 0],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [4, 0, 0],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [0, 0, 1],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [1, 0, 1],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [2, 0, 1],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [3, 0, 1],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [4, 0, 1],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [0, 0, 2],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [1, 0, 2],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [2, 0, 2],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [3, 0, 2],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [4, 0, 2],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [0, 0, 3],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [1, 0, 3],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [2, 0, 3],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [3, 0, 3],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [4, 0, 3],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [0, 0, 4],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [1, 0, 4],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [2, 0, 4],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [3, 0, 4],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
pos: [4, 0, 4],
|
||||
state: 0
|
||||
},
|
||||
{
|
||||
nbt: {
|
||||
id: "computercraft:disk_drive"
|
||||
},
|
||||
pos: [1, 1, 2],
|
||||
state: 1
|
||||
},
|
||||
{
|
||||
nbt: {
|
||||
Owner: {
|
||||
UpperId: 4039158846114182220L,
|
||||
LowerId: -6876936588741668278L,
|
||||
Name: "Dev"
|
||||
},
|
||||
RightUpgrade: "computercraft:wireless_modem_normal",
|
||||
Fuel: 0,
|
||||
Label: "Unequip refreshes peripheral",
|
||||
Slot: 0,
|
||||
Items: [],
|
||||
id: "computercraft:turtle_normal",
|
||||
RightUpgradeNbt: {
|
||||
active: 0b
|
||||
},
|
||||
ComputerId: 1,
|
||||
On: 1b
|
||||
},
|
||||
pos: [2, 1, 2],
|
||||
state: 2
|
||||
},
|
||||
{
|
||||
pos: [0, 1, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 1, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 1, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 1, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 1, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 2, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 2, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 2, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 2, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 2, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 3, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 3, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 3, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 3, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 3, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 4, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 4, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 4, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 4, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 4, 0],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 1, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 1, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 1, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 1, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 1, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 2, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 2, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 2, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 2, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 2, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 3, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 3, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 3, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 3, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 3, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 4, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 4, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 4, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 4, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 4, 1],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 1, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 1, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 1, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 2, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 2, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 2, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 2, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 2, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 3, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 3, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 3, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 3, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 3, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 4, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 4, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 4, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 4, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 4, 2],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 1, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 1, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 1, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 1, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 1, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 2, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 2, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 2, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 2, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 2, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 3, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 3, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 3, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 3, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 3, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 4, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 4, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 4, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 4, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 4, 3],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 1, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 1, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 1, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 1, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 1, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 2, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 2, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 2, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 2, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 2, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 3, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 3, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 3, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 3, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 3, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [0, 4, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [1, 4, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [2, 4, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [3, 4, 4],
|
||||
state: 3
|
||||
},
|
||||
{
|
||||
pos: [4, 4, 4],
|
||||
state: 3
|
||||
}
|
||||
],
|
||||
palette: [
|
||||
{
|
||||
Name: "minecraft:polished_andesite"
|
||||
},
|
||||
{
|
||||
Properties: {
|
||||
facing: "north",
|
||||
state: "empty"
|
||||
},
|
||||
Name: "computercraft:disk_drive"
|
||||
},
|
||||
{
|
||||
Properties: {
|
||||
waterlogged: "false",
|
||||
facing: "south"
|
||||
},
|
||||
Name: "computercraft:turtle_normal"
|
||||
},
|
||||
{
|
||||
Name: "minecraft:air"
|
||||
}
|
||||
],
|
||||
DataVersion: 2230
|
||||
}
|
Loading…
Reference in New Issue
Block a user