package net.minecraft.gametest.framework; import com.google.common.base.Stopwatch; import com.google.common.collect.Lists; import it.unimi.dsi.fastutil.objects.Object2LongMap; import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectIterator; import it.unimi.dsi.fastutil.objects.Object2LongMap.Entry; import java.util.Collection; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.block.Rotation; import net.minecraft.world.level.block.entity.StructureBlockEntity; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.phys.AABB; import org.jetbrains.annotations.Nullable; public class GameTestInfo { private final TestFunction testFunction; @Nullable private BlockPos structureBlockPos; @Nullable private BlockPos northWestCorner; private final ServerLevel level; private final Collection listeners = Lists.newArrayList(); private final int timeoutTicks; private final Collection sequences = Lists.newCopyOnWriteArrayList(); private final Object2LongMap runAtTickTimeMap = new Object2LongOpenHashMap<>(); private long startTick; private int ticksToWaitForChunkLoading = 20; private boolean placedStructure; private boolean chunksLoaded; private long tickCount; private boolean started; private final RetryOptions retryOptions; private final Stopwatch timer = Stopwatch.createUnstarted(); private boolean done; private final Rotation rotation; @Nullable private Throwable error; @Nullable private StructureBlockEntity structureBlockEntity; public GameTestInfo(TestFunction testFunction, Rotation rotation, ServerLevel level, RetryOptions retryOptions) { this.testFunction = testFunction; this.level = level; this.retryOptions = retryOptions; this.timeoutTicks = testFunction.maxTicks(); this.rotation = testFunction.rotation().getRotated(rotation); } void setStructureBlockPos(BlockPos pos) { this.structureBlockPos = pos; } public GameTestInfo startExecution(int delay) { this.startTick = this.level.getGameTime() + this.testFunction.setupTicks() + delay; this.timer.start(); return this; } public GameTestInfo placeStructure() { if (this.placedStructure) { return this; } else { this.ticksToWaitForChunkLoading = 0; this.placedStructure = true; StructureBlockEntity structureBlockEntity = this.getStructureBlockEntity(); structureBlockEntity.placeStructure(this.level); BoundingBox boundingBox = StructureUtils.getStructureBoundingBox(structureBlockEntity); this.level.getBlockTicks().clearArea(boundingBox); this.level.clearBlockEvents(boundingBox); return this; } } private boolean ensureStructureIsPlaced() { if (this.placedStructure) { return true; } else if (this.ticksToWaitForChunkLoading > 0) { this.ticksToWaitForChunkLoading--; return false; } else { this.placeStructure().startExecution(0); return true; } } public void tick(GameTestRunner runner) { if (!this.isDone()) { if (this.structureBlockEntity == null) { this.fail(new IllegalStateException("Running test without structure block entity")); } if (this.chunksLoaded || StructureUtils.getStructureBoundingBox(this.structureBlockEntity) .intersectingChunks() .allMatch(chunkPos -> this.level.isPositionEntityTicking(chunkPos.getWorldPosition()))) { this.chunksLoaded = true; if (this.ensureStructureIsPlaced()) { this.tickInternal(); if (this.isDone()) { if (this.error != null) { this.listeners.forEach(gameTestListener -> gameTestListener.testFailed(this, runner)); } else { this.listeners.forEach(gameTestListener -> gameTestListener.testPassed(this, runner)); } } } } } } private void tickInternal() { this.tickCount = this.level.getGameTime() - this.startTick; if (this.tickCount >= 0L) { if (!this.started) { this.startTest(); } ObjectIterator> objectIterator = this.runAtTickTimeMap.object2LongEntrySet().iterator(); while (objectIterator.hasNext()) { Entry entry = (Entry)objectIterator.next(); if (entry.getLongValue() <= this.tickCount) { try { ((Runnable)entry.getKey()).run(); } catch (Exception var4) { this.fail(var4); } objectIterator.remove(); } } if (this.tickCount > this.timeoutTicks) { if (this.sequences.isEmpty()) { this.fail(new GameTestTimeoutException("Didn't succeed or fail within " + this.testFunction.maxTicks() + " ticks")); } else { this.sequences.forEach(gameTestSequence -> gameTestSequence.tickAndFailIfNotComplete(this.tickCount)); if (this.error == null) { this.fail(new GameTestTimeoutException("No sequences finished")); } } } else { this.sequences.forEach(gameTestSequence -> gameTestSequence.tickAndContinue(this.tickCount)); } } } private void startTest() { if (!this.started) { this.started = true; try { this.testFunction.run(new GameTestHelper(this)); } catch (Exception var2) { this.fail(var2); } } } public void setRunAtTickTime(long tickTime, Runnable task) { this.runAtTickTimeMap.put(task, tickTime); } public String getTestName() { return this.testFunction.testName(); } @Nullable public BlockPos getStructureBlockPos() { return this.structureBlockPos; } public AABB getStructureBounds() { StructureBlockEntity structureBlockEntity = this.getStructureBlockEntity(); return StructureUtils.getStructureBounds(structureBlockEntity); } public StructureBlockEntity getStructureBlockEntity() { if (this.structureBlockEntity == null) { if (this.structureBlockPos == null) { throw new IllegalStateException("Could not find a structureBlockEntity for this GameTestInfo"); } this.structureBlockEntity = (StructureBlockEntity)this.level.getBlockEntity(this.structureBlockPos); if (this.structureBlockEntity == null) { throw new IllegalStateException("Could not find a structureBlockEntity at the given coordinate " + this.structureBlockPos); } } return this.structureBlockEntity; } public ServerLevel getLevel() { return this.level; } public boolean hasSucceeded() { return this.done && this.error == null; } public boolean hasFailed() { return this.error != null; } public boolean hasStarted() { return this.started; } public boolean isDone() { return this.done; } public long getRunTime() { return this.timer.elapsed(TimeUnit.MILLISECONDS); } private void finish() { if (!this.done) { this.done = true; if (this.timer.isRunning()) { this.timer.stop(); } } } public void succeed() { if (this.error == null) { this.finish(); AABB aABB = this.getStructureBounds(); List list = this.getLevel().getEntitiesOfClass(Entity.class, aABB.inflate(1.0), entity -> !(entity instanceof Player)); list.forEach(entity -> entity.remove(Entity.RemovalReason.DISCARDED)); } } public void fail(Throwable error) { this.error = error; this.finish(); } @Nullable public Throwable getError() { return this.error; } public String toString() { return this.getTestName(); } public void addListener(GameTestListener listener) { this.listeners.add(listener); } public GameTestInfo prepareTestStructure() { BlockPos blockPos = this.getOrCalculateNorthwestCorner(); this.structureBlockEntity = StructureUtils.prepareTestStructure(this, blockPos, this.getRotation(), this.level); this.structureBlockPos = this.structureBlockEntity.getBlockPos(); StructureUtils.addCommandBlockAndButtonToStartTest(this.structureBlockPos, new BlockPos(1, 0, -1), this.getRotation(), this.level); StructureUtils.encaseStructure(this.getStructureBounds(), this.level, !this.testFunction.skyAccess()); this.listeners.forEach(gameTestListener -> gameTestListener.testStructureLoaded(this)); return this; } long getTick() { return this.tickCount; } GameTestSequence createSequence() { GameTestSequence gameTestSequence = new GameTestSequence(this); this.sequences.add(gameTestSequence); return gameTestSequence; } public boolean isRequired() { return this.testFunction.required(); } public boolean isOptional() { return !this.testFunction.required(); } public String getStructureName() { return this.testFunction.structureName(); } public Rotation getRotation() { return this.rotation; } public TestFunction getTestFunction() { return this.testFunction; } public int getTimeoutTicks() { return this.timeoutTicks; } public boolean isFlaky() { return this.testFunction.isFlaky(); } public int maxAttempts() { return this.testFunction.maxAttempts(); } public int requiredSuccesses() { return this.testFunction.requiredSuccesses(); } public RetryOptions retryOptions() { return this.retryOptions; } public Stream getListeners() { return this.listeners.stream(); } public GameTestInfo copyReset() { GameTestInfo gameTestInfo = new GameTestInfo(this.testFunction, this.rotation, this.level, this.retryOptions()); if (this.northWestCorner != null) { gameTestInfo.setNorthWestCorner(this.northWestCorner); } if (this.structureBlockPos != null) { gameTestInfo.setStructureBlockPos(this.structureBlockPos); } return gameTestInfo; } private BlockPos getOrCalculateNorthwestCorner() { if (this.northWestCorner == null) { BoundingBox boundingBox = StructureUtils.getStructureBoundingBox(this.getStructureBlockEntity()); this.northWestCorner = new BlockPos(boundingBox.minX(), boundingBox.minY(), boundingBox.minZ()); } return this.northWestCorner; } public void setNorthWestCorner(BlockPos northWestCorner) { this.northWestCorner = northWestCorner; } }