From 8d7463352641dc3101fa8a9eda861abd008442ff Mon Sep 17 00:00:00 2001 From: CursedFlames <18627001+CursedFlames@users.noreply.github.com> Date: Thu, 22 Feb 2024 20:17:10 +1300 Subject: [PATCH] implement CloTrackingView --- .../cubicchunks/mixin/GlobalSet.java | 8 + .../common/server/level/MixinChunkHolder.java | 2 +- .../common/server/level/MixinChunkMap.java | 3 +- .../server/level/MixinCloTrackingView.java | 10 + .../server/level/CloTrackingView.java | 256 ++++++++++++++++++ .../server/level/CubicChunkHolder.java | 2 + .../resources/cubicchunks.mixins.core.json | 1 + .../server/level/TestCloTrackingView.java | 112 ++++++++ .../cubicchunks/testutils/BaseTest.java | 2 + 9 files changed, 393 insertions(+), 3 deletions(-) create mode 100644 src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/common/server/level/MixinCloTrackingView.java create mode 100644 src/main/java/io/github/opencubicchunks/cubicchunks/server/level/CloTrackingView.java create mode 100644 src/test/java/io/github/opencubicchunks/cubicchunks/test/server/level/TestCloTrackingView.java diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/GlobalSet.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/GlobalSet.java index 81fbcc9b..600581a1 100644 --- a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/GlobalSet.java +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/GlobalSet.java @@ -18,12 +18,14 @@ import io.github.notstirred.dasm.api.annotations.selector.FieldSig; import io.github.notstirred.dasm.api.annotations.selector.MethodSig; import io.github.notstirred.dasm.api.annotations.selector.Ref; +import io.github.opencubicchunks.cubicchunks.server.level.CloTrackingView; import io.github.opencubicchunks.cubicchunks.server.level.CubicChunkHolder; import io.github.opencubicchunks.cubicchunks.server.level.CubicTicketType; import io.github.opencubicchunks.cubicchunks.server.level.progress.CubicChunkProgressListener; import io.github.opencubicchunks.cubicchunks.world.level.chunklike.CloAccess; import io.github.opencubicchunks.cubicchunks.world.level.chunklike.CloPos; import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkTrackingView; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ThreadedLevelLightEngine; import net.minecraft.server.level.TicketType; @@ -90,4 +92,10 @@ public abstract CompletableFuture future); + public native void cc_addSaveDependency(String source, CompletableFuture future); @AddTransformToSets(GeneralSet.class) @TransformFromMethod( value = @MethodSig("updateChunkToSave(Ljava/util/concurrent/CompletableFuture;Ljava/lang/String;)V")) diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/common/server/level/MixinChunkMap.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/common/server/level/MixinChunkMap.java index a2964efc..09046024 100644 --- a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/common/server/level/MixinChunkMap.java +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/common/server/level/MixinChunkMap.java @@ -208,7 +208,7 @@ public String toString() { ); for (ChunkHolder holder : cloHolders) { -// holder.addSaveDependency("getChunkRangeFuture " + pos + " " + radius, combinedFuture); + ((CubicChunkHolder) holder).cc_addSaveDependency("getChunkRangeFuture " + pos + " " + radius, combinedFuture); } cir.setReturnValue(combinedFuture); @@ -259,7 +259,6 @@ private boolean cc_updateChunkScheduling_onForgeHook(ServerLevel level, long pos @AddTransformToSets(GeneralSet.class) @TransformFromMethod(@MethodSig("scheduleChunkGeneration(Lnet/minecraft/server/level/ChunkHolder;Lnet/minecraft/world/level/chunk/ChunkStatus;)Ljava/util/concurrent/CompletableFuture;")) private native CompletableFuture> cc_scheduleChunkGeneration(ChunkHolder pChunkHolder, ChunkStatus pChunkStatus); - // FIXME requires redirect from ticket type to cubic ticket type @AddTransformToSets(GeneralSet.class) @TransformFromMethod(@MethodSig("releaseLightTicket(Lnet/minecraft/world/level/ChunkPos;)V")) protected native void cc_releaseLightTicket(ChunkPos pChunkPos); diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/common/server/level/MixinCloTrackingView.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/common/server/level/MixinCloTrackingView.java new file mode 100644 index 00000000..39809aa0 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/common/server/level/MixinCloTrackingView.java @@ -0,0 +1,10 @@ +package io.github.opencubicchunks.cubicchunks.mixin.core.common.server.level; + +import io.github.opencubicchunks.cubicchunks.server.level.CloTrackingView; +import org.spongepowered.asm.mixin.Mixin; + +// Needed for DASM to apply +// TODO won't be necessary once we have dasm.json +@Mixin(CloTrackingView.class) +public interface MixinCloTrackingView { +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/server/level/CloTrackingView.java b/src/main/java/io/github/opencubicchunks/cubicchunks/server/level/CloTrackingView.java new file mode 100644 index 00000000..332faff6 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/server/level/CloTrackingView.java @@ -0,0 +1,256 @@ +package io.github.opencubicchunks.cubicchunks.server.level; + +import java.util.function.Consumer; + +import io.github.notstirred.dasm.api.annotations.Dasm; +import io.github.notstirred.dasm.api.annotations.redirect.redirects.AddFieldToSets; +import io.github.notstirred.dasm.api.annotations.redirect.redirects.AddMethodToSets; +import io.github.notstirred.dasm.api.annotations.selector.FieldSig; +import io.github.notstirred.dasm.api.annotations.selector.MethodSig; +import io.github.notstirred.dasm.api.annotations.selector.Ref; +import io.github.opencubicchunks.cc_core.api.CubicConstants; +import io.github.opencubicchunks.cc_core.utils.Coords; +import io.github.opencubicchunks.cubicchunks.mixin.GeneralSet; +import io.github.opencubicchunks.cubicchunks.mixin.GlobalSet; +import io.github.opencubicchunks.cubicchunks.world.level.chunklike.CloPos; +import net.minecraft.server.level.ChunkTrackingView; +import net.minecraft.world.level.ChunkPos; + +@Dasm(GeneralSet.class) +public interface CloTrackingView extends ChunkTrackingView { + @AddFieldToSets(sets = GlobalSet.class, owner = @Ref(ChunkTrackingView.class), field = @FieldSig(type = @Ref(ChunkTrackingView.class), name = "EMPTY")) + CloTrackingView EMPTY = new CloTrackingView() { + @Override public boolean cc_contains(int x, int y, int z, boolean pSearchAllChunks) { + return false; + } + + @Override public void cc_forEach(Consumer pAction) { + } + + @Override + public boolean contains(int x, int z, boolean pSearchAllChunks) { + return false; + } + + @Override + public void forEach(Consumer pAction) { + } + }; + + @AddMethodToSets(sets = GlobalSet.class, owner = @Ref(ChunkTrackingView.class), method = @MethodSig("of(Lnet/minecraft/world/level/ChunkPos;I)Lnet/minecraft/server/level/ChunkTrackingView;")) + static CloTrackingView cc_of(CloPos pCenter, int pViewDistance) { + return new CloTrackingView.Positioned(pCenter, pViewDistance); + } + + @AddMethodToSets(sets = GlobalSet.class, owner = @Ref(ChunkTrackingView.class), method = @MethodSig("difference(Ljava/util/function/Consumer;Ljava/util/function/Consumer;)V")) + static void cc_difference(CloTrackingView pOldCloTrackingView, CloTrackingView pNewCloTrackingView, Consumer pChunkDropper, Consumer pChunkMarker) { + if (!pOldCloTrackingView.equals(pNewCloTrackingView)) { + if (pOldCloTrackingView instanceof Positioned oldPositioned + && pNewCloTrackingView instanceof Positioned newPositioned) { + if (oldPositioned.cc_cubeIntersects(newPositioned)) { + int minX = Math.min(oldPositioned.minX(), newPositioned.minX()); + int minY = Math.min(oldPositioned.minY(), newPositioned.minY()); + int minZ = Math.min(oldPositioned.minZ(), newPositioned.minZ()); + int maxX = Math.max(oldPositioned.maxX(), newPositioned.maxX()); + int maxY = Math.max(oldPositioned.maxY(), newPositioned.maxY()); + int maxZ = Math.max(oldPositioned.maxZ(), newPositioned.maxZ()); + + for(int x = minX; x <= maxX; ++x) { + for (int z = minZ; z <= maxZ; ++z) { + for (int dx = 0; dx < CubicConstants.DIAMETER_IN_SECTIONS; dx++) { + for (int dz = 0; dz < CubicConstants.DIAMETER_IN_SECTIONS; dz++) { + int chunkX = Coords.cubeToSection(x, dx); + int chunkZ = Coords.cubeToSection(z, dz); + boolean oldHas = oldPositioned.contains(chunkX, chunkZ); + boolean newHas = newPositioned.contains(chunkX, chunkZ); + if (oldHas != newHas) { + if (newHas) { + pChunkDropper.accept(CloPos.chunk(chunkX, chunkZ)); + } else { + pChunkMarker.accept(CloPos.chunk(chunkX, chunkZ)); + } + } + } + } + for (int y = minY; y <= maxY; ++y) { + boolean oldHas = oldPositioned.cc_contains(x, y, z); + boolean newHas = newPositioned.cc_contains(x, y, z); + if (oldHas != newHas) { + if (newHas) { + pChunkDropper.accept(CloPos.cube(x, y, z)); + } else { + pChunkMarker.accept(CloPos.cube(x, y, z)); + } + } + } + } + } + return; + } else if (oldPositioned.cc_chunkIntersects(newPositioned)) { + int minX = Math.min(oldPositioned.minX(), newPositioned.minX()); + int minZ = Math.min(oldPositioned.minZ(), newPositioned.minZ()); + int maxX = Math.max(oldPositioned.maxX(), newPositioned.maxX()); + int maxZ = Math.max(oldPositioned.maxZ(), newPositioned.maxZ()); + + for(int x = minX; x <= maxX; ++x) { + for (int z = minZ; z <= maxZ; ++z) { + for (int dx = 0; dx < CubicConstants.DIAMETER_IN_SECTIONS; dx++) { + for (int dz = 0; dz < CubicConstants.DIAMETER_IN_SECTIONS; dz++) { + int chunkX = Coords.cubeToSection(x, dx); + int chunkZ = Coords.cubeToSection(z, dz); + boolean oldHas = oldPositioned.contains(chunkX, chunkZ); + boolean newHas = newPositioned.contains(chunkX, chunkZ); + if (oldHas != newHas) { + if (newHas) { + pChunkDropper.accept(CloPos.chunk(chunkX, chunkZ)); + } else { + pChunkMarker.accept(CloPos.chunk(chunkX, chunkZ)); + } + } + } + } + } + } + + oldPositioned.cc_forEachCubesOnly(pChunkMarker); + newPositioned.cc_forEachCubesOnly(pChunkDropper); + return; + } + } + + pOldCloTrackingView.cc_forEach(pChunkMarker); + pNewCloTrackingView.cc_forEach(pChunkDropper); + } + } + + @AddMethodToSets(sets = GlobalSet.class, owner = @Ref(ChunkTrackingView.class), method = @MethodSig("contains(Lnet/minecraft/world/level/ChunkPos;)Z")) + default boolean cc_contains(CloPos cloPos) { + if (cloPos.isCube()) { + return this.cc_contains(cloPos.getX(), cloPos.getY(), cloPos.getZ()); + } else { + return this.contains(cloPos.getX(), cloPos.getZ()); + } + } + + default boolean cc_contains(int x, int y, int z) { + return this.cc_contains(x, y, z, true); + } + + boolean cc_contains(int x, int y, int z, boolean pSearchAllChunks); + + @AddMethodToSets(sets = GlobalSet.class, owner = @Ref(ChunkTrackingView.class), method = @MethodSig("forEach(Ljava/util/function/Consumer;)V")) + void cc_forEach(Consumer pAction); + + default boolean cc_isInViewDistance(int x, int y, int z) { + return this.cc_contains(x, y, z, false); + } + + static boolean cc_isInViewDistance(int centerX, int centerY, int centerZ, int viewDistance, int x, int y, int z) { + return cc_isWithinDistance(centerX, centerY, centerZ, viewDistance, x, y, z, false); + } + + static boolean cc_isWithinDistance(int centerX, int centerY, int centerZ, int viewDistance, int x, int y, int z, boolean increaseRadiusByOne) { + // Mojang does some weird jank, but it's almost identical to just increasing the view distance by 1 - so we do that instead + if (increaseRadiusByOne) viewDistance++; + int dx = Math.max(0, Math.abs(x - centerX) - 1); + int dy = Math.max(0, Math.abs(y - centerY) - 1); + int dz = Math.max(0, Math.abs(z - centerZ) - 1); + return dx*dx + dy*dy + dz*dz < viewDistance * viewDistance; + } + + static boolean cc_isWithinDistanceCubeColumn(int centerX, int centerZ, int viewDistance, int x, int z, boolean increaseRadiusByOne) { + return cc_isWithinDistance(centerX, 0, centerZ, viewDistance, x, 0, z, increaseRadiusByOne); + } + + record Positioned(CloPos center, int viewDistance) implements CloTrackingView { + int minX() { + return this.center.getX() - this.viewDistance - 1; + } + + int minY() { + return this.center.getY() - this.viewDistance - 1; + } + + int minZ() { + return this.center.getZ() - this.viewDistance - 1; + } + + int maxX() { + return this.center.getX() + this.viewDistance + 1; + } + + int maxY() { + return this.center.getY() + this.viewDistance + 1; + } + + int maxZ() { + return this.center.getZ() + this.viewDistance + 1; + } + + @Override + public boolean contains(int x, int z, boolean searchAllChunks) { + return cc_isWithinDistanceCubeColumn(this.center.getX(), this.center.getZ(), this.viewDistance, Coords.sectionToCube(x), Coords.sectionToCube(z), searchAllChunks); + } + + @Override + public void forEach(Consumer pAction) { + for(int x = this.minX(); x <= this.maxX(); ++x) { + for(int z = this.minZ(); z <= this.maxZ(); ++z) { + if (this.cc_contains(x, center.getY(), z)) { + for (int dx = 0; dx < CubicConstants.DIAMETER_IN_SECTIONS; dx++) { + for (int dz = 0; dz < CubicConstants.DIAMETER_IN_SECTIONS; dz++) { + pAction.accept(new ChunkPos(Coords.cubeToSection(x, dx), Coords.cubeToSection(z, dz))); + } + } + } + } + } + } + + private boolean cc_cubeIntersects(CloTrackingView.Positioned pOther) { + return this.minX() <= pOther.maxX() && this.maxX() >= pOther.minX() + && this.minY() <= pOther.maxY() && this.maxY() >= pOther.minY() + && this.minZ() <= pOther.maxZ() && this.maxZ() >= pOther.minZ(); + } + + private boolean cc_chunkIntersects(CloTrackingView.Positioned pOther) { + return this.minX() <= pOther.maxX() && this.maxX() >= pOther.minX() + && this.minZ() <= pOther.maxZ() && this.maxZ() >= pOther.minZ(); + } + + @Override public boolean cc_contains(int x, int y, int z, boolean searchAllChunks) { + return cc_isWithinDistance(this.center.getX(), this.center.getY(), this.center.getZ(), this.viewDistance, x, y, z, searchAllChunks); + } + + @Override public void cc_forEach(Consumer pAction) { + for(int x = this.minX(); x <= this.maxX(); ++x) { + for(int z = this.minZ(); z <= this.maxZ(); ++z) { + if (this.cc_contains(x, center.getY(), z)) { + for (int dx = 0; dx < CubicConstants.DIAMETER_IN_SECTIONS; dx++) { + for (int dz = 0; dz < CubicConstants.DIAMETER_IN_SECTIONS; dz++) { + pAction.accept(CloPos.chunk(Coords.cubeToSection(x, dx), Coords.cubeToSection(z, dz))); + } + } + } + for(int y = this.minY(); y <= this.maxY(); ++y) { + if (this.cc_contains(x, y, z)) { + pAction.accept(CloPos.cube(x, y, z)); + } + } + } + } + } + + private void cc_forEachCubesOnly(Consumer pAction) { + for(int x = this.minX(); x <= this.maxX(); ++x) { + for(int z = this.minZ(); z <= this.maxZ(); ++z) { + for(int y = this.minY(); y <= this.maxY(); ++y) { + if (this.cc_contains(x, y, z)) { + pAction.accept(CloPos.cube(x, y, z)); + } + } + } + } + } + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/server/level/CubicChunkHolder.java b/src/main/java/io/github/opencubicchunks/cubicchunks/server/level/CubicChunkHolder.java index c6275846..0d8f0aee 100644 --- a/src/main/java/io/github/opencubicchunks/cubicchunks/server/level/CubicChunkHolder.java +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/server/level/CubicChunkHolder.java @@ -18,6 +18,8 @@ public interface CubicChunkHolder { CompletableFuture> cc_getOrScheduleFuture(ChunkStatus status, ChunkMap map); + void cc_addSaveDependency(String source, CompletableFuture future); + @FunctionalInterface interface LevelChangeListener { void onLevelChange(CloPos cloPos, IntSupplier p_140120_, int p_140121_, IntConsumer p_140122_); diff --git a/src/main/resources/cubicchunks.mixins.core.json b/src/main/resources/cubicchunks.mixins.core.json index 7bc2f8af..a1bbead3 100644 --- a/src/main/resources/cubicchunks.mixins.core.json +++ b/src/main/resources/cubicchunks.mixins.core.json @@ -20,6 +20,7 @@ "common.server.level.MixinChunkTaskPriorityQueueSorter", "common.server.level.MixinChunkTicketTracker", "common.server.level.MixinChunkTracker", + "common.server.level.MixinCloTrackingView", "common.server.level.MixinDistanceManager", "common.server.level.MixinFixedPlayerDistanceChunkTracker", "common.server.level.MixinPlayerRespawnLogic", diff --git a/src/test/java/io/github/opencubicchunks/cubicchunks/test/server/level/TestCloTrackingView.java b/src/test/java/io/github/opencubicchunks/cubicchunks/test/server/level/TestCloTrackingView.java new file mode 100644 index 00000000..e52c7aac --- /dev/null +++ b/src/test/java/io/github/opencubicchunks/cubicchunks/test/server/level/TestCloTrackingView.java @@ -0,0 +1,112 @@ +package io.github.opencubicchunks.cubicchunks.test.server.level; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Random; +import java.util.Set; + +import io.github.opencubicchunks.cubicchunks.server.level.CloTrackingView; +import io.github.opencubicchunks.cubicchunks.testutils.BaseTest; +import io.github.opencubicchunks.cubicchunks.world.level.chunklike.CloPos; +import org.codehaus.plexus.util.CollectionUtils; +import org.junit.jupiter.api.Test; + +public class TestCloTrackingView extends BaseTest { + // TODO might be possible to test this class more thoroughly, but this should be sufficient for now - non-trivial methods have full coverage + @Test public void basicTests() { + var originPos = CloPos.cube(0, 0, 0); + var originChunk = CloPos.chunk(0, 0); + var origin = CloTrackingView.cc_of(originPos, 1); + + assertTrue(origin.cc_contains(originPos)); + assertTrue(origin.cc_contains(originChunk)); + assertTrue(origin.contains(originChunk.chunkPos())); + + var list1 = new ArrayList<>(); + var list2 = new ArrayList<>(); + + CloTrackingView.cc_difference(CloTrackingView.EMPTY, origin, list2::add, list1::add); + assertThat(list1).isEmpty(); + assertThat(list2).anyMatch(pos -> pos.equals(originPos)); + assertThat(list2).anyMatch(pos -> pos.equals(originChunk)); + + list1.clear(); + list2.clear(); + + var higherUpPos = CloPos.cube(0, 100, 0); + var higherUp = CloTrackingView.cc_of(higherUpPos, 1); + + assertFalse(higherUp.cc_contains(originPos)); + assertTrue(higherUp.cc_contains(higherUpPos)); + assertTrue(higherUp.cc_contains(originChunk)); + + CloTrackingView.cc_difference(origin, higherUp, list2::add, list1::add); + + assertThat(list1).anyMatch(pos -> pos.equals(originPos)); + assertThat(list2).anyMatch(pos -> pos.equals(higherUpPos)); + // origin chunk is still included; shouldn't be added or removed + assertThat(list1).noneMatch(pos -> pos.equals(originChunk)); + assertThat(list2).noneMatch(pos -> pos.equals(originChunk)); + + list1.clear(); + list2.clear(); + + CloTrackingView.cc_difference(higherUp, CloTrackingView.EMPTY, list2::add, list1::add); + assertThat(list2).isEmpty(); + assertThat(list1).anyMatch(pos -> pos.equals(higherUpPos)); + assertThat(list1).anyMatch(pos -> pos.equals(originChunk)); + + list1.clear(); + list2.clear(); + + var farAwayPos = CloPos.cube(-100, -100, -100); + var farAway = CloTrackingView.cc_of(farAwayPos, 1); + + assertFalse(farAway.cc_contains(originPos)); + assertFalse(farAway.cc_contains(originChunk)); + assertTrue(farAway.cc_contains(farAwayPos)); + assertTrue(farAway.cc_contains(farAwayPos.correspondingChunkCloPos())); + + farAway.cc_forEach(list1::add); + assertThat(list1).anyMatch(pos -> pos.equals(farAwayPos)); + assertThat(list1).anyMatch(pos -> pos.equals(farAwayPos.correspondingChunkCloPos())); + + farAway.forEach(list2::add); + assertThat(list2).anyMatch(pos -> pos.equals(farAwayPos.correspondingChunkPos())); + } + + private void checkDifference(CloTrackingView before, CloTrackingView after, Set beforePos, Set afterPos) { + var removed = new ArrayList(); + var added = new ArrayList(); + CloTrackingView.cc_difference(before, after, added::add, removed::add); + var eRemoved = CollectionUtils.subtract(beforePos, afterPos); + var eAdded = CollectionUtils.subtract(afterPos, beforePos); + assertThat(removed).allMatch(eRemoved::contains); + assertThat(eRemoved).allMatch(removed::contains); + assertThat(added).allMatch(eAdded::contains); + assertThat(eAdded).allMatch(added::contains); + } + + @Test public void testDifferenceRandomized() { + var random = new Random(120829); + + for (int i = 0; i < 100; i++) { + var pos1 = CloPos.cube(random.nextInt(10000)-5000, random.nextInt(10000)-5000, random.nextInt(10000)-5000); + var pos2 = CloPos.cube(pos1.getX() + random.nextInt(20)-10, pos1.getY() + random.nextInt(20)-10, pos1.getZ() + random.nextInt(20)-10); + int r1 = random.nextInt(0, 10); + int r2 = random.nextInt(0, 10); + var track1 = CloTrackingView.cc_of(pos1, r1); + var track2 = CloTrackingView.cc_of(pos2, r2); + var list = new ArrayList(); + track1.cc_forEach(list::add); + var set1 = Set.copyOf(list); + list.clear(); + track2.cc_forEach(list::add); + var set2 = Set.copyOf(list); + checkDifference(track1, track2, set1, set2); + } + } +} diff --git a/src/test/java/io/github/opencubicchunks/cubicchunks/testutils/BaseTest.java b/src/test/java/io/github/opencubicchunks/cubicchunks/testutils/BaseTest.java index bc206dfe..c5c18592 100644 --- a/src/test/java/io/github/opencubicchunks/cubicchunks/testutils/BaseTest.java +++ b/src/test/java/io/github/opencubicchunks/cubicchunks/testutils/BaseTest.java @@ -4,8 +4,10 @@ import net.minecraft.server.Bootstrap; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; import org.mockito.Mockito; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class BaseTest { @BeforeAll public static void setup() {