mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +00:00
Optimize world generation
- Limit terrain cleanup to affected columns and batch cave boundary reads. - Reuse offline generation workers and native air states. - Gather history writes and defer unpublished append synchronization until atomic publication. - Cover terrain parity, worker lifecycle, history durability, and publication failures.
This commit is contained in:
1 parent
7986698c1c
commit
38cf4ac7c7
25 files changed
+1362
-226
No files matched your search
+2
-1
@@ -37,6 +37,7 @@ final class HeadlessNativePlatform implements IrisPlatform, PlatformRegistries,
|
|||||||
private final PlatformBiomeWriter biomeWriter;
|
private final PlatformBiomeWriter biomeWriter;
|
||||||
private final PlatformGenerationRegistry generationRegistry;
|
private final PlatformGenerationRegistry generationRegistry;
|
||||||
private final NativeBlockResolver blocks = new NativeBlockResolver(this);
|
private final NativeBlockResolver blocks = new NativeBlockResolver(this);
|
||||||
|
private final NativeBlockState air = blocks.getAir();
|
||||||
private final ConcurrentHashMap<String, NativeBlockState> states = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, NativeBlockState> states = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
HeadlessNativePlatform(File root, Supplier<HolderLookup.Provider> registries) {
|
HeadlessNativePlatform(File root, Supplier<HolderLookup.Provider> registries) {
|
||||||
@@ -101,7 +102,7 @@ final class HeadlessNativePlatform implements IrisPlatform, PlatformRegistries,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NativeBlockState air() {
|
public NativeBlockState air() {
|
||||||
return blocks.getAir();
|
return air;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+16
@@ -22,16 +22,26 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeStateMerger;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
import net.minecraft.core.HolderLookup;
|
import net.minecraft.core.HolderLookup;
|
||||||
import net.minecraft.core.RegistryAccess;
|
import net.minecraft.core.RegistryAccess;
|
||||||
import net.minecraft.data.registries.VanillaRegistries;
|
import net.minecraft.data.registries.VanillaRegistries;
|
||||||
import net.minecraft.server.packs.resources.ResourceManager;
|
import net.minecraft.server.packs.resources.ResourceManager;
|
||||||
|
|
||||||
final class HeadlessNativeRuntime implements RealPackProbeSupport.RuntimeBindings {
|
final class HeadlessNativeRuntime implements RealPackProbeSupport.RuntimeBindings {
|
||||||
|
private final Path stagingRoot;
|
||||||
private NativeRegionTerrainWriter writer;
|
private NativeRegionTerrainWriter writer;
|
||||||
private HeadlessNativeRegistries nativeRegistries;
|
private HeadlessNativeRegistries nativeRegistries;
|
||||||
private HolderLookup.Provider registries;
|
private HolderLookup.Provider registries;
|
||||||
|
|
||||||
|
HeadlessNativeRuntime() {
|
||||||
|
stagingRoot = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
HeadlessNativeRuntime(Path stagingRoot) throws IOException {
|
||||||
|
this.stagingRoot = stagingRoot.toRealPath();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IrisPlatform create(File root) {
|
public IrisPlatform create(File root) {
|
||||||
HeadlessNativeBootstrap.initialize();
|
HeadlessNativeBootstrap.initialize();
|
||||||
@@ -68,6 +78,12 @@ final class HeadlessNativeRuntime implements RealPackProbeSupport.RuntimeBinding
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public GenerationHistory createHistory(RealPackProbeSupport.HistoryRequest request) throws IOException {
|
public GenerationHistory createHistory(RealPackProbeSupport.HistoryRequest request) throws IOException {
|
||||||
|
if (stagingRoot != null) {
|
||||||
|
if (!request.worldRoot().getParent().toRealPath().startsWith(stagingRoot)) {
|
||||||
|
throw new IOException("Unpublished native generation must remain inside its private staging directory");
|
||||||
|
}
|
||||||
|
return HeadlessGenerationHistorySession.createUnpublished(request, new DataFixerV263());
|
||||||
|
}
|
||||||
return HeadlessGenerationHistorySession.create(request, new DataFixerV263());
|
return HeadlessGenerationHistorySession.create(request, new DataFixerV263());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -45,14 +45,15 @@ final class HeadlessRegionTerrain {
|
|||||||
|
|
||||||
static final class Session {
|
static final class Session {
|
||||||
private final Map<Long, HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord>> chunks = new HashMap<>();
|
private final Map<Long, HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord>> chunks = new HashMap<>();
|
||||||
private final RegionGenerationWindow.Policy workerPolicy;
|
private final RegionGenerationWindow workers;
|
||||||
private HeadlessTerrainContext context;
|
private HeadlessTerrainContext context;
|
||||||
|
|
||||||
Session(RegionGenerationWindow.Policy workerPolicy) {
|
Session(RegionGenerationWindow workers) {
|
||||||
this.workerPolicy = Objects.requireNonNull(workerPolicy, "workerPolicy");
|
this.workers = Objects.requireNonNull(workers, "workers");
|
||||||
}
|
}
|
||||||
|
|
||||||
Result generate(Request request, Consumer<String> progress) throws Exception {
|
Result generate(Request request, Consumer<String> progress) throws Exception {
|
||||||
|
workers.requireOpen();
|
||||||
if (context != null && context != request.context()) {
|
if (context != null && context != request.context()) {
|
||||||
throw new IllegalArgumentException("Rolling regions require one native terrain context");
|
throw new IllegalArgumentException("Rolling regions require one native terrain context");
|
||||||
}
|
}
|
||||||
@@ -85,7 +86,7 @@ final class HeadlessRegionTerrain {
|
|||||||
}
|
}
|
||||||
long started = System.nanoTime();
|
long started = System.nanoTime();
|
||||||
AtomicInteger completed = new AtomicInteger();
|
AtomicInteger completed = new AtomicInteger();
|
||||||
RegionGenerationWindow.process(new RegionGenerationWindow.Request<>(eligible.size(), request.parallelism(),
|
workers.process(new RegionGenerationWindow.Request<>(eligible.size(), request.parallelism(),
|
||||||
index -> apply(request.context(), eligible.get(index), status, chunks),
|
index -> apply(request.context(), eligible.get(index), status, chunks),
|
||||||
(index, result) -> {
|
(index, result) -> {
|
||||||
if (status == ChunkStatus.STRUCTURE_STARTS) {
|
if (status == ChunkStatus.STRUCTURE_STARTS) {
|
||||||
@@ -96,7 +97,7 @@ final class HeadlessRegionTerrain {
|
|||||||
progress.accept("stage=" + status.getName() + " chunks=" + count + "/" + eligible.size()
|
progress.accept("stage=" + status.getName() + " chunks=" + count + "/" + eligible.size()
|
||||||
+ " seconds=" + (System.nanoTime() - started) / 1_000_000_000.0);
|
+ " seconds=" + (System.nanoTime() - started) / 1_000_000_000.0);
|
||||||
}
|
}
|
||||||
}), workerPolicy);
|
}));
|
||||||
failReported();
|
failReported();
|
||||||
}
|
}
|
||||||
List<ProtoChunk> ordered = new ArrayList<>(plan.size());
|
List<ProtoChunk> ordered = new ArrayList<>(plan.size());
|
||||||
|
|||||||
+13
-7
@@ -82,7 +82,7 @@ public final class OfflineRegionGenerator {
|
|||||||
Path capturedConfiguration = staged.resolve("spigot.yml");
|
Path capturedConfiguration = staged.resolve("spigot.yml");
|
||||||
Files.write(capturedConfiguration, configurationBytes, StandardOpenOption.CREATE_NEW);
|
Files.write(capturedConfiguration, configurationBytes, StandardOpenOption.CREATE_NEW);
|
||||||
String configurationHash = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(configurationBytes));
|
String configurationHash = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(configurationBytes));
|
||||||
HeadlessNativeRuntime runtime = new HeadlessNativeRuntime();
|
HeadlessNativeRuntime runtime = new HeadlessNativeRuntime(staged);
|
||||||
long started = System.nanoTime();
|
long started = System.nanoTime();
|
||||||
long writes = 0;
|
long writes = 0;
|
||||||
long terrainWrites = 0;
|
long terrainWrites = 0;
|
||||||
@@ -102,12 +102,14 @@ public final class OfflineRegionGenerator {
|
|||||||
: hydrology.preparePregeneration(configuration.hydrologyArea());
|
: hydrology.preparePregeneration(configuration.hydrologyArea());
|
||||||
HeadlessTerrainContext context = HeadlessTerrainContext.create(new HeadlessTerrainContext.Options(
|
HeadlessTerrainContext context = HeadlessTerrainContext.create(new HeadlessTerrainContext.Options(
|
||||||
session.engine(), runtime, staged.resolve("templates"), configuration.levelKey(),
|
session.engine(), runtime, staged.resolve("templates"), configuration.levelKey(),
|
||||||
configuration.noiseSettings(), spigot))) {
|
configuration.noiseSettings(), spigot));
|
||||||
HeadlessRegionTerrain.Session rolling = new HeadlessRegionTerrain.Session(RegionGenerationWindow.Policy.STANDALONE);
|
RegionGenerationWindow workers = new RegionGenerationWindow(configuration.parallelism(),
|
||||||
|
RegionGenerationWindow.Policy.STANDALONE)) {
|
||||||
|
HeadlessRegionTerrain.Session rolling = new HeadlessRegionTerrain.Session(workers);
|
||||||
Path regionDirectory = session.engine().getTarget().getWorld().worldFolder().toPath().resolve("region");
|
Path regionDirectory = session.engine().getTarget().getWorld().worldFolder().toPath().resolve("region");
|
||||||
Files.createDirectory(regionDirectory);
|
Files.createDirectory(regionDirectory);
|
||||||
OfflineRegionWriter writer = new OfflineRegionWriter(new OfflineRegionWriter.Options(
|
OfflineRegionWriter writer = new OfflineRegionWriter(new OfflineRegionWriter.Options(
|
||||||
context, regionDirectory, configuration.parallelism(), RegionGenerationWindow.Policy.STANDALONE));
|
context, regionDirectory, workers));
|
||||||
for (int row = 0; row < configuration.regionsZ(); row++) {
|
for (int row = 0; row < configuration.regionsZ(); row++) {
|
||||||
for (int column = 0; column < configuration.regionsX(); column++) {
|
for (int column = 0; column < configuration.regionsX(); column++) {
|
||||||
int regionX = row % 2 == 0 ? column : configuration.regionsX() - column - 1;
|
int regionX = row % 2 == 0 ? column : configuration.regionsX() - column - 1;
|
||||||
@@ -142,9 +144,7 @@ public final class OfflineRegionGenerator {
|
|||||||
wallSeconds, generationSeconds, serializationSeconds);
|
wallSeconds, generationSeconds, serializationSeconds);
|
||||||
Files.writeString(staged.resolve("checkpoint.properties"), report, StandardOpenOption.CREATE_NEW);
|
Files.writeString(staged.resolve("checkpoint.properties"), report, StandardOpenOption.CREATE_NEW);
|
||||||
long publicationStarted = System.nanoTime();
|
long publicationStarted = System.nanoTime();
|
||||||
forceTree(staged);
|
publish(staged, configuration.output().resolve("native-terrain-checkpoint"));
|
||||||
Files.move(staged, configuration.output().resolve("native-terrain-checkpoint"), StandardCopyOption.ATOMIC_MOVE);
|
|
||||||
force(configuration.output());
|
|
||||||
System.out.println(report);
|
System.out.println(report);
|
||||||
System.out.println("publicationSeconds=" + (System.nanoTime() - publicationStarted) / 1_000_000_000.0);
|
System.out.println("publicationSeconds=" + (System.nanoTime() - publicationStarted) / 1_000_000_000.0);
|
||||||
System.out.println("totalSeconds=" + (System.nanoTime() - started) / 1_000_000_000.0);
|
System.out.println("totalSeconds=" + (System.nanoTime() - started) / 1_000_000_000.0);
|
||||||
@@ -207,6 +207,12 @@ public final class OfflineRegionGenerator {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void publish(Path staged, Path destination) throws IOException {
|
||||||
|
forceTree(staged);
|
||||||
|
Files.move(staged, destination, StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
force(destination.getParent());
|
||||||
|
}
|
||||||
|
|
||||||
private static void force(Path path) throws IOException {
|
private static void force(Path path) throws IOException {
|
||||||
try (FileChannel channel = FileChannel.open(path, Files.isDirectory(path)
|
try (FileChannel channel = FileChannel.open(path, Files.isDirectory(path)
|
||||||
? StandardOpenOption.READ : StandardOpenOption.WRITE)) {
|
? StandardOpenOption.READ : StandardOpenOption.WRITE)) {
|
||||||
|
|||||||
+6
-8
@@ -38,6 +38,7 @@ final class OfflineRegionWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Result write(HeadlessRegionTerrain.Result generated) throws Exception {
|
Result write(HeadlessRegionTerrain.Result generated) throws Exception {
|
||||||
|
options.workers().requireOpen();
|
||||||
try (OpenRegions regions = new OpenRegions(options)) {
|
try (OpenRegions regions = new OpenRegions(options)) {
|
||||||
List<ChunkWrite> pending = new ArrayList<>(generated.updated().size());
|
List<ChunkWrite> pending = new ArrayList<>(generated.updated().size());
|
||||||
for (ProtoChunk chunk : generated.updated()) {
|
for (ProtoChunk chunk : generated.updated()) {
|
||||||
@@ -62,8 +63,8 @@ final class OfflineRegionWriter {
|
|||||||
return new Result(0, 0);
|
return new Result(0, 0);
|
||||||
}
|
}
|
||||||
int[] counts = new int[2];
|
int[] counts = new int[2];
|
||||||
RegionGenerationWindow.process(new RegionGenerationWindow.Request<>(pending.size(),
|
options.workers().process(new RegionGenerationWindow.Request<>(pending.size(),
|
||||||
Math.min(options.parallelism(), MAXIMUM_PREPARATION_WORKERS),
|
Math.min(options.workers().parallelism(), MAXIMUM_PREPARATION_WORKERS),
|
||||||
index -> prepare(pending.get(index)),
|
index -> prepare(pending.get(index)),
|
||||||
(index, prepared) -> {
|
(index, prepared) -> {
|
||||||
ChunkWrite chunk = prepared.chunk();
|
ChunkWrite chunk = prepared.chunk();
|
||||||
@@ -73,7 +74,7 @@ final class OfflineRegionWriter {
|
|||||||
if (chunk.chunk().getPersistedStatus().isOrAfter(ChunkStatus.TERRAIN)) {
|
if (chunk.chunk().getPersistedStatus().isOrAfter(ChunkStatus.TERRAIN)) {
|
||||||
counts[1]++;
|
counts[1]++;
|
||||||
}
|
}
|
||||||
}), options.policy());
|
}));
|
||||||
return new Result(counts[0], counts[1]);
|
return new Result(counts[0], counts[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,14 +101,11 @@ final class OfflineRegionWriter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
record Options(HeadlessTerrainContext context, Path directory, int parallelism, RegionGenerationWindow.Policy policy) {
|
record Options(HeadlessTerrainContext context, Path directory, RegionGenerationWindow workers) {
|
||||||
Options {
|
Options {
|
||||||
Objects.requireNonNull(context, "context");
|
Objects.requireNonNull(context, "context");
|
||||||
Objects.requireNonNull(directory, "directory");
|
Objects.requireNonNull(directory, "directory");
|
||||||
Objects.requireNonNull(policy, "policy");
|
Objects.requireNonNull(workers, "workers");
|
||||||
if (parallelism < 1 || parallelism > 32) {
|
|
||||||
throw new IllegalArgumentException("Offline writer parallelism must be 1..32");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+44
-8
@@ -11,20 +11,55 @@ import java.util.concurrent.Future;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
final class RegionGenerationWindow {
|
final class RegionGenerationWindow implements AutoCloseable {
|
||||||
private static final int UNRESPONSIVE_WORKER_EXIT = 70;
|
private static final int UNRESPONSIVE_WORKER_EXIT = 70;
|
||||||
|
|
||||||
private RegionGenerationWindow() {
|
private final int parallelism;
|
||||||
|
private final Policy policy;
|
||||||
|
private final ExecutorService workers;
|
||||||
|
private boolean closed;
|
||||||
|
private boolean processing;
|
||||||
|
|
||||||
|
RegionGenerationWindow(int parallelism, Policy policy) {
|
||||||
|
if (parallelism < 1 || parallelism > 32) {
|
||||||
|
throw new IllegalArgumentException("Generation workers must be 1..32");
|
||||||
|
}
|
||||||
|
this.parallelism = parallelism;
|
||||||
|
this.policy = Objects.requireNonNull(policy, "policy");
|
||||||
|
workers = Executors.newFixedThreadPool(parallelism);
|
||||||
}
|
}
|
||||||
|
|
||||||
static <T> void process(Request<T> request) throws Exception {
|
@Override
|
||||||
process(request, Policy.EMBEDDED);
|
public synchronized void close() {
|
||||||
|
if (processing) {
|
||||||
|
throw new IllegalStateException("Cannot close generation workers from an active batch");
|
||||||
|
}
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closed = true;
|
||||||
|
workers.shutdown();
|
||||||
|
drain(workers, policy, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
static <T> void process(Request<T> request, Policy policy) throws Exception {
|
int parallelism() {
|
||||||
ExecutorService workers = Executors.newFixedThreadPool(request.parallelism());
|
return parallelism;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized void requireOpen() {
|
||||||
|
if (closed) {
|
||||||
|
throw new IllegalStateException("Generation workers are closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized <T> void process(Request<T> request) throws Exception {
|
||||||
|
requireOpen();
|
||||||
|
if (processing || request.parallelism() > parallelism) {
|
||||||
|
throw new IllegalArgumentException("Generation batch requires idle workers within the session limit");
|
||||||
|
}
|
||||||
ExecutorCompletionService<Completed<T>> completions = new ExecutorCompletionService<>(workers);
|
ExecutorCompletionService<Completed<T>> completions = new ExecutorCompletionService<>(workers);
|
||||||
Set<Future<Completed<T>>> active = new HashSet<>(request.parallelism());
|
Set<Future<Completed<T>>> active = new HashSet<>(request.parallelism());
|
||||||
|
processing = true;
|
||||||
try {
|
try {
|
||||||
int submitted = 0;
|
int submitted = 0;
|
||||||
for (; submitted < Math.min(request.count(), request.parallelism()); submitted++) {
|
for (; submitted < Math.min(request.count(), request.parallelism()); submitted++) {
|
||||||
@@ -43,15 +78,16 @@ final class RegionGenerationWindow {
|
|||||||
request.sink().accept(result.index(), result.value());
|
request.sink().accept(result.index(), result.value());
|
||||||
}
|
}
|
||||||
} catch (Exception | Error failure) {
|
} catch (Exception | Error failure) {
|
||||||
|
closed = true;
|
||||||
for (Future<Completed<T>> future : active) {
|
for (Future<Completed<T>> future : active) {
|
||||||
future.cancel(true);
|
future.cancel(true);
|
||||||
}
|
}
|
||||||
workers.shutdownNow();
|
workers.shutdownNow();
|
||||||
drain(workers, policy, failure);
|
drain(workers, policy, failure);
|
||||||
throw failure;
|
throw failure;
|
||||||
|
} finally {
|
||||||
|
processing = false;
|
||||||
}
|
}
|
||||||
workers.shutdown();
|
|
||||||
drain(workers, policy, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void drain(ExecutorService workers, Policy policy, Throwable failure) {
|
private static void drain(ExecutorService workers, Policy policy, Throwable failure) {
|
||||||
|
|||||||
+12
-10
@@ -55,15 +55,17 @@ final class DetachedNativeTerrain {
|
|||||||
request.level(), request.level().palettedContainerFactory(), null)));
|
request.level(), request.level().palettedContainerFactory(), null)));
|
||||||
}
|
}
|
||||||
long started = System.nanoTime();
|
long started = System.nanoTime();
|
||||||
for (ChunkStatus status : ChunkStatus.getStatusList()) {
|
try (RegionGenerationWindow workers = new RegionGenerationWindow(request.parallelism(), RegionGenerationWindow.Policy.EMBEDDED)) {
|
||||||
if (status == ChunkStatus.EMPTY) {
|
for (ChunkStatus status : ChunkStatus.getStatusList()) {
|
||||||
continue;
|
if (status == ChunkStatus.EMPTY) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (status.isAfter(ChunkStatus.TERRAIN)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ChunkStep step = ChunkPyramid.GENERATION_PYRAMID.getStepTo(status);
|
||||||
|
executeStage(new Stage(context, step, plan, holders, request.parallelism()), workers, progress);
|
||||||
}
|
}
|
||||||
if (status.isAfter(ChunkStatus.TERRAIN)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
ChunkStep step = ChunkPyramid.GENERATION_PYRAMID.getStepTo(status);
|
|
||||||
executeStage(new Stage(context, step, plan, holders, request.parallelism()), progress);
|
|
||||||
}
|
}
|
||||||
long serializationStarted = System.nanoTime();
|
long serializationStarted = System.nanoTime();
|
||||||
write(request.level(), output, plan, holders);
|
write(request.level(), output, plan, holders);
|
||||||
@@ -72,7 +74,7 @@ final class DetachedNativeTerrain {
|
|||||||
return new Result(request.width() * request.width(), plan.size(), seconds);
|
return new Result(request.width() * request.width(), plan.size(), seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void executeStage(Stage stage, Consumer<String> progress) throws Exception {
|
private static void executeStage(Stage stage, RegionGenerationWindow workers, Consumer<String> progress) throws Exception {
|
||||||
long started = System.nanoTime();
|
long started = System.nanoTime();
|
||||||
List<PlannedChunk> eligible = new ArrayList<>();
|
List<PlannedChunk> eligible = new ArrayList<>();
|
||||||
for (PlannedChunk planned : stage.plan()) {
|
for (PlannedChunk planned : stage.plan()) {
|
||||||
@@ -82,7 +84,7 @@ final class DetachedNativeTerrain {
|
|||||||
}
|
}
|
||||||
AtomicInteger completed = new AtomicInteger();
|
AtomicInteger completed = new AtomicInteger();
|
||||||
AtomicInteger targetCompleted = new AtomicInteger();
|
AtomicInteger targetCompleted = new AtomicInteger();
|
||||||
RegionGenerationWindow.process(new RegionGenerationWindow.Request<>(eligible.size(), stage.parallelism(),
|
workers.process(new RegionGenerationWindow.Request<>(eligible.size(), stage.parallelism(),
|
||||||
index -> {
|
index -> {
|
||||||
PlannedChunk planned = eligible.get(index);
|
PlannedChunk planned = eligible.get(index);
|
||||||
apply(stage, planned);
|
apply(stage, planned);
|
||||||
|
|||||||
+23
@@ -5,14 +5,22 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockResolver;
|
|||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
import net.minecraft.core.registries.BuiltInRegistries;
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
import net.minecraft.world.level.block.Block;
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
import net.minecraft.world.level.block.state.BlockState;
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
|
||||||
public final class HeadlessNativeStateTest {
|
public final class HeadlessNativeStateTest {
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
@@ -20,6 +28,21 @@ public final class HeadlessNativeStateTest {
|
|||||||
HeadlessNativeBootstrap.initialize();
|
HeadlessNativeBootstrap.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void platformAirResolvesOnceAndRetainsCanonicalNativeState() {
|
||||||
|
BlockState air = Blocks.AIR.defaultBlockState();
|
||||||
|
try (MockedStatic<ModdedBlockState> states = mockStatic(ModdedBlockState.class, CALLS_REAL_METHODS)) {
|
||||||
|
HeadlessNativePlatform platform = new HeadlessNativePlatform(new File("unused-native-platform"),
|
||||||
|
HeadlessNativeTestRegistries::get);
|
||||||
|
NativeBlockState wrapped = platform.air();
|
||||||
|
for (int iteration = 0; iteration < 1024; iteration++) {
|
||||||
|
assertSame(wrapped, platform.air());
|
||||||
|
assertSame(air, ((ModdedBlockState) platform.air()).handle());
|
||||||
|
}
|
||||||
|
states.verify(() -> ModdedBlockState.of(air, null), times(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void nativeMaterialsDistinguishDecorationsFluidsGlassAndBlockEntities() {
|
public void nativeMaterialsDistinguishDecorationsFluidsGlassAndBlockEntities() {
|
||||||
assertFalse(NativeBlockResolver.strictParse("minecraft:short_grass").isSolid());
|
assertFalse(NativeBlockResolver.strictParse("minecraft:short_grass").isSolid());
|
||||||
|
|||||||
+13
-7
@@ -56,8 +56,9 @@ public final class HeadlessRegionTerrainTest {
|
|||||||
configuration, "probe", Level.OVERWORLD, root.resolve("configuration")));
|
configuration, "probe", Level.OVERWORLD, root.resolve("configuration")));
|
||||||
try (HeadlessTerrainContext context = HeadlessTerrainContext.create(new HeadlessTerrainContext.Options(
|
try (HeadlessTerrainContext context = HeadlessTerrainContext.create(new HeadlessTerrainContext.Options(
|
||||||
session.engine(), runtime, root.resolve("templates"), Level.OVERWORLD,
|
session.engine(), runtime, root.resolve("templates"), Level.OVERWORLD,
|
||||||
NoiseGeneratorSettings.OVERWORLD, spigot))) {
|
NoiseGeneratorSettings.OVERWORLD, spigot));
|
||||||
HeadlessRegionTerrain.Session rolling = new HeadlessRegionTerrain.Session(RegionGenerationWindow.Policy.EMBEDDED);
|
RegionGenerationWindow workers = new RegionGenerationWindow(4, RegionGenerationWindow.Policy.EMBEDDED)) {
|
||||||
|
HeadlessRegionTerrain.Session rolling = new HeadlessRegionTerrain.Session(workers);
|
||||||
HeadlessRegionTerrain.Result result = rolling.generate(
|
HeadlessRegionTerrain.Result result = rolling.generate(
|
||||||
new HeadlessRegionTerrain.Request(context, -3, 2, 1, 4), System.out::println);
|
new HeadlessRegionTerrain.Request(context, -3, 2, 1, 4), System.out::println);
|
||||||
OfflineRegionGenerator.Configuration export = new OfflineRegionGenerator.Configuration(
|
OfflineRegionGenerator.Configuration export = new OfflineRegionGenerator.Configuration(
|
||||||
@@ -100,14 +101,14 @@ public final class HeadlessRegionTerrainTest {
|
|||||||
assertEquals(ChunkStatus.TERRAIN, retained.getPersistedStatus());
|
assertEquals(ChunkStatus.TERRAIN, retained.getPersistedStatus());
|
||||||
Path regionDirectory = Files.createDirectory(root.resolve("region"));
|
Path regionDirectory = Files.createDirectory(root.resolve("region"));
|
||||||
OfflineRegionWriter writer = new OfflineRegionWriter(new OfflineRegionWriter.Options(
|
OfflineRegionWriter writer = new OfflineRegionWriter(new OfflineRegionWriter.Options(
|
||||||
context, regionDirectory, 4, RegionGenerationWindow.Policy.EMBEDDED));
|
context, regionDirectory, workers));
|
||||||
OfflineRegionWriter.Result firstWrite = writer.write(result);
|
OfflineRegionWriter.Result firstWrite = writer.write(result);
|
||||||
assertEquals(result.updated().size(), firstWrite.chunks());
|
assertEquals(result.updated().size(), firstWrite.chunks());
|
||||||
assertEquals(tag, read(regionDirectory, target.getPos()));
|
assertEquals(tag, read(regionDirectory, target.getPos()));
|
||||||
OfflineRegionWriter.Result duplicateWrite = writer.write(result);
|
OfflineRegionWriter.Result duplicateWrite = writer.write(result);
|
||||||
assertEquals(0, duplicateWrite.chunks());
|
assertEquals(0, duplicateWrite.chunks());
|
||||||
OfflineRegionWriter reopened = new OfflineRegionWriter(new OfflineRegionWriter.Options(
|
OfflineRegionWriter reopened = new OfflineRegionWriter(new OfflineRegionWriter.Options(
|
||||||
context, regionDirectory, 4, RegionGenerationWindow.Policy.EMBEDDED));
|
context, regionDirectory, workers));
|
||||||
assertEquals(0, reopened.write(result).chunks());
|
assertEquals(0, reopened.write(result).chunks());
|
||||||
target.setPersistedStatus(ChunkStatus.STRUCTURE_STARTS);
|
target.setPersistedStatus(ChunkStatus.STRUCTURE_STARTS);
|
||||||
try {
|
try {
|
||||||
@@ -138,7 +139,7 @@ public final class HeadlessRegionTerrainTest {
|
|||||||
assertEquals(0, writer.write(back).chunks());
|
assertEquals(0, writer.write(back).chunks());
|
||||||
Path oversizedDirectory = Files.createDirectory(root.resolve("oversized"));
|
Path oversizedDirectory = Files.createDirectory(root.resolve("oversized"));
|
||||||
OfflineRegionWriter oversizedWriter = new OfflineRegionWriter(new OfflineRegionWriter.Options(
|
OfflineRegionWriter oversizedWriter = new OfflineRegionWriter(new OfflineRegionWriter.Options(
|
||||||
context, oversizedDirectory, 4, RegionGenerationWindow.Policy.EMBEDDED));
|
context, oversizedDirectory, workers));
|
||||||
NamespacedKey payloadKey = new NamespacedKey("iris", "serialization_test_payload");
|
NamespacedKey payloadKey = new NamespacedKey("iris", "serialization_test_payload");
|
||||||
byte[] payload = new byte[2 * 1024 * 1024];
|
byte[] payload = new byte[2 * 1024 * 1024];
|
||||||
new Random(69420L).nextBytes(payload);
|
new Random(69420L).nextBytes(payload);
|
||||||
@@ -151,14 +152,19 @@ public final class HeadlessRegionTerrainTest {
|
|||||||
regenerated.persistentDataContainer.remove(payloadKey);
|
regenerated.persistentDataContainer.remove(payloadKey);
|
||||||
Path failedDirectory = Files.createDirectory(root.resolve("failed"));
|
Path failedDirectory = Files.createDirectory(root.resolve("failed"));
|
||||||
OfflineRegionWriter failedWriter = new OfflineRegionWriter(new OfflineRegionWriter.Options(
|
OfflineRegionWriter failedWriter = new OfflineRegionWriter(new OfflineRegionWriter.Options(
|
||||||
context, failedDirectory, 4, RegionGenerationWindow.Policy.EMBEDDED));
|
context, failedDirectory, workers));
|
||||||
regenerated.setPersistedStatus(ChunkStatus.FULL);
|
regenerated.setPersistedStatus(ChunkStatus.FULL);
|
||||||
try {
|
try {
|
||||||
assertThrows(Exception.class, () -> failedWriter.write(singleton));
|
assertThrows(Exception.class, () -> failedWriter.write(singleton));
|
||||||
} finally {
|
} finally {
|
||||||
regenerated.setPersistedStatus(ChunkStatus.TERRAIN);
|
regenerated.setPersistedStatus(ChunkStatus.TERRAIN);
|
||||||
}
|
}
|
||||||
assertEquals(1, failedWriter.write(singleton).chunks());
|
assertThrows(IllegalStateException.class, () -> failedWriter.write(singleton));
|
||||||
|
try (RegionGenerationWindow replacement = new RegionGenerationWindow(4, RegionGenerationWindow.Policy.EMBEDDED)) {
|
||||||
|
OfflineRegionWriter retried = new OfflineRegionWriter(new OfflineRegionWriter.Options(
|
||||||
|
context, failedDirectory, replacement));
|
||||||
|
assertEquals(1, retried.write(singleton).chunks());
|
||||||
|
}
|
||||||
assertEquals("minecraft:terrain", read(failedDirectory, regenerated.getPos()).getStringOr("Status", ""));
|
assertEquals("minecraft:terrain", read(failedDirectory, regenerated.getPos()).getStringOr("Status", ""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.AdditionalAnswers.delegatesTo;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
|
||||||
|
public final class OfflineRegionPublicationTest {
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporary = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unpublishedRuntimeRejectsWorldRootsOutsideItsStagingDirectory() throws Exception {
|
||||||
|
Path staging = temporary.newFolder("staging").toPath();
|
||||||
|
Path outside = temporary.getRoot().toPath().resolve("outside");
|
||||||
|
try (HeadlessNativeRuntime runtime = new HeadlessNativeRuntime(staging)) {
|
||||||
|
assertThrows(IOException.class, () -> runtime.createHistory(
|
||||||
|
new RealPackProbeSupport.HistoryRequest(outside, null, null, 1L)));
|
||||||
|
}
|
||||||
|
assertFalse(Files.exists(outside));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void publishesOnlyAfterEveryFileAndDirectoryIsForced() throws Exception {
|
||||||
|
for (boolean fail : List.of(true, false)) {
|
||||||
|
Path output = temporary.newFolder().toPath();
|
||||||
|
Path staged = Files.createDirectory(output.resolve("incomplete"));
|
||||||
|
Path nested = Files.createDirectory(staged.resolve("world"));
|
||||||
|
Path data = Files.writeString(nested.resolve("data"), "checkpoint");
|
||||||
|
Path target = output.resolve("native-terrain-checkpoint");
|
||||||
|
List<Path> forced = new ArrayList<>();
|
||||||
|
try (MockedStatic<FileChannel> ignored = mockStatic(FileChannel.class, invocation -> {
|
||||||
|
if (invocation.getMethod().getParameterCount() != 2) {
|
||||||
|
return invocation.callRealMethod();
|
||||||
|
}
|
||||||
|
FileChannel actual = (FileChannel) invocation.callRealMethod();
|
||||||
|
Path path = invocation.getArgument(0);
|
||||||
|
FileChannel channel = mock(FileChannel.class, delegatesTo(actual));
|
||||||
|
doAnswer(force -> {
|
||||||
|
if (path.equals(output)) {
|
||||||
|
assertTrue(Files.isDirectory(target));
|
||||||
|
assertFalse(Files.exists(staged));
|
||||||
|
} else {
|
||||||
|
assertFalse(Files.exists(target));
|
||||||
|
}
|
||||||
|
if (fail && path.equals(data)) {
|
||||||
|
throw new IOException("Expected publication sync failure");
|
||||||
|
}
|
||||||
|
actual.force(true);
|
||||||
|
forced.add(path);
|
||||||
|
return null;
|
||||||
|
}).when(channel).force(true);
|
||||||
|
return channel;
|
||||||
|
})) {
|
||||||
|
if (fail) {
|
||||||
|
assertThrows(IOException.class, () -> OfflineRegionGenerator.publish(staged, target));
|
||||||
|
assertTrue(Files.isRegularFile(data));
|
||||||
|
assertFalse(Files.exists(target));
|
||||||
|
} else {
|
||||||
|
OfflineRegionGenerator.publish(staged, target);
|
||||||
|
assertEquals(List.of(data, nested, staged, output), forced);
|
||||||
|
assertEquals("checkpoint", Files.readString(target.resolve("world/data")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+141
-62
@@ -9,6 +9,8 @@ import java.nio.file.Path;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
@@ -20,27 +22,96 @@ import static org.junit.Assert.assertThrows;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public final class RegionGenerationWindowTest {
|
public final class RegionGenerationWindowTest {
|
||||||
|
@Test
|
||||||
|
public void retainsWorkersAndThreadLocalScratchAcrossBoundedBatches() throws Exception {
|
||||||
|
Set<Thread> firstWorkers = ConcurrentHashMap.newKeySet();
|
||||||
|
AtomicInteger allocations = new AtomicInteger();
|
||||||
|
ThreadLocal<Integer> scratch = ThreadLocal.withInitial(allocations::incrementAndGet);
|
||||||
|
CountDownLatch bothStarted = new CountDownLatch(2);
|
||||||
|
AtomicInteger active = new AtomicInteger();
|
||||||
|
AtomicInteger maximum = new AtomicInteger();
|
||||||
|
RegionGenerationWindow workers = new RegionGenerationWindow(2, RegionGenerationWindow.Policy.EMBEDDED);
|
||||||
|
try (workers) {
|
||||||
|
workers.process(new RegionGenerationWindow.Request<>(2, 2, index -> {
|
||||||
|
firstWorkers.add(Thread.currentThread());
|
||||||
|
scratch.get();
|
||||||
|
bothStarted.countDown();
|
||||||
|
assertTrue(bothStarted.await(5, TimeUnit.SECONDS));
|
||||||
|
return index;
|
||||||
|
}, (index, value) -> { }));
|
||||||
|
assertEquals(2, allocations.get());
|
||||||
|
workers.process(new RegionGenerationWindow.Request<>(16, 1, index -> {
|
||||||
|
maximum.accumulateAndGet(active.incrementAndGet(), Math::max);
|
||||||
|
try {
|
||||||
|
assertTrue(firstWorkers.contains(Thread.currentThread()));
|
||||||
|
return scratch.get();
|
||||||
|
} finally {
|
||||||
|
active.decrementAndGet();
|
||||||
|
}
|
||||||
|
}, (index, value) -> assertTrue(value > 0)));
|
||||||
|
assertEquals(2, allocations.get());
|
||||||
|
assertEquals(1, maximum.get());
|
||||||
|
}
|
||||||
|
for (Thread worker : firstWorkers) {
|
||||||
|
worker.join(1000L);
|
||||||
|
assertFalse(worker.isAlive());
|
||||||
|
}
|
||||||
|
workers.close();
|
||||||
|
assertThrows(IllegalStateException.class, () -> workers.process(
|
||||||
|
new RegionGenerationWindow.Request<>(1, 1, index -> index, (index, value) -> { })));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout = 5000)
|
||||||
|
public void sinkFailureDrainsWorkersAndPermanentlyClosesSession() {
|
||||||
|
CountDownLatch entered = new CountDownLatch(1);
|
||||||
|
AtomicInteger exited = new AtomicInteger();
|
||||||
|
try (RegionGenerationWindow workers = new RegionGenerationWindow(2, RegionGenerationWindow.Policy.EMBEDDED)) {
|
||||||
|
assertThrows(IllegalStateException.class, () -> workers.process(
|
||||||
|
new RegionGenerationWindow.Request<>(2, 2, index -> {
|
||||||
|
if (index == 0) {
|
||||||
|
assertTrue(entered.await(2, TimeUnit.SECONDS));
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
entered.countDown();
|
||||||
|
try {
|
||||||
|
new CountDownLatch(1).await();
|
||||||
|
} finally {
|
||||||
|
exited.incrementAndGet();
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}, (index, value) -> {
|
||||||
|
throw new IllegalStateException("Expected sink failure");
|
||||||
|
})));
|
||||||
|
assertEquals(1, exited.get());
|
||||||
|
assertThrows(IllegalStateException.class, workers::requireOpen);
|
||||||
|
assertThrows(IllegalStateException.class, () -> workers.process(
|
||||||
|
new RegionGenerationWindow.Request<>(1, 1, index -> index, (index, value) -> { })));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void replenishesWorkersWhileEarlierChunksRemainBlocked() throws Exception {
|
public void replenishesWorkersWhileEarlierChunksRemainBlocked() throws Exception {
|
||||||
CountDownLatch replacementStarted = new CountDownLatch(1);
|
CountDownLatch replacementStarted = new CountDownLatch(1);
|
||||||
AtomicInteger active = new AtomicInteger();
|
AtomicInteger active = new AtomicInteger();
|
||||||
AtomicInteger maximum = new AtomicInteger();
|
AtomicInteger maximum = new AtomicInteger();
|
||||||
List<Integer> completed = new ArrayList<>();
|
List<Integer> completed = new ArrayList<>();
|
||||||
RegionGenerationWindow.process(new RegionGenerationWindow.Request<>(16, 2, index -> {
|
try (RegionGenerationWindow workers = new RegionGenerationWindow(2, RegionGenerationWindow.Policy.EMBEDDED)) {
|
||||||
int running = active.incrementAndGet();
|
workers.process(new RegionGenerationWindow.Request<>(16, 2, index -> {
|
||||||
maximum.accumulateAndGet(running, Math::max);
|
int running = active.incrementAndGet();
|
||||||
try {
|
maximum.accumulateAndGet(running, Math::max);
|
||||||
if (index == 0 && !replacementStarted.await(5, TimeUnit.SECONDS)) {
|
try {
|
||||||
throw new IllegalStateException("A completed worker was not replenished");
|
if (index == 0 && !replacementStarted.await(5, TimeUnit.SECONDS)) {
|
||||||
|
throw new IllegalStateException("A completed worker was not replenished");
|
||||||
|
}
|
||||||
|
if (index == 2) {
|
||||||
|
replacementStarted.countDown();
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
} finally {
|
||||||
|
active.decrementAndGet();
|
||||||
}
|
}
|
||||||
if (index == 2) {
|
}, (index, value) -> completed.add(value)));
|
||||||
replacementStarted.countDown();
|
}
|
||||||
}
|
|
||||||
return index;
|
|
||||||
} finally {
|
|
||||||
active.decrementAndGet();
|
|
||||||
}
|
|
||||||
}, (index, value) -> completed.add(value)));
|
|
||||||
assertEquals(16, completed.size());
|
assertEquals(16, completed.size());
|
||||||
assertTrue(completed.indexOf(1) < completed.indexOf(0));
|
assertTrue(completed.indexOf(1) < completed.indexOf(0));
|
||||||
assertTrue(maximum.get() <= 2);
|
assertTrue(maximum.get() <= 2);
|
||||||
@@ -53,18 +124,20 @@ public final class RegionGenerationWindowTest {
|
|||||||
AtomicInteger active = new AtomicInteger();
|
AtomicInteger active = new AtomicInteger();
|
||||||
AtomicInteger maximum = new AtomicInteger();
|
AtomicInteger maximum = new AtomicInteger();
|
||||||
AtomicInteger completed = new AtomicInteger();
|
AtomicInteger completed = new AtomicInteger();
|
||||||
RegionGenerationWindow.process(new RegionGenerationWindow.Request<>(97, 32, index -> {
|
try (RegionGenerationWindow workers = new RegionGenerationWindow(32, RegionGenerationWindow.Policy.EMBEDDED)) {
|
||||||
maximum.accumulateAndGet(active.incrementAndGet(), Math::max);
|
workers.process(new RegionGenerationWindow.Request<>(97, 32, index -> {
|
||||||
try {
|
maximum.accumulateAndGet(active.incrementAndGet(), Math::max);
|
||||||
if (index < 32) {
|
try {
|
||||||
initialWorkers.countDown();
|
if (index < 32) {
|
||||||
assertTrue(initialWorkers.await(5, TimeUnit.SECONDS));
|
initialWorkers.countDown();
|
||||||
|
assertTrue(initialWorkers.await(5, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
} finally {
|
||||||
|
active.decrementAndGet();
|
||||||
}
|
}
|
||||||
return index;
|
}, (index, value) -> completed.incrementAndGet()));
|
||||||
} finally {
|
}
|
||||||
active.decrementAndGet();
|
|
||||||
}
|
|
||||||
}, (index, value) -> completed.incrementAndGet()));
|
|
||||||
assertEquals(32, maximum.get());
|
assertEquals(32, maximum.get());
|
||||||
assertEquals(97, completed.get());
|
assertEquals(97, completed.get());
|
||||||
assertEquals(0, active.get());
|
assertEquals(0, active.get());
|
||||||
@@ -76,21 +149,23 @@ public final class RegionGenerationWindowTest {
|
|||||||
public void interruptsAndDrainsOtherWorkersAfterFailure() {
|
public void interruptsAndDrainsOtherWorkersAfterFailure() {
|
||||||
CountDownLatch blocked = new CountDownLatch(1);
|
CountDownLatch blocked = new CountDownLatch(1);
|
||||||
AtomicInteger interrupted = new AtomicInteger();
|
AtomicInteger interrupted = new AtomicInteger();
|
||||||
assertThrows(Exception.class, () -> RegionGenerationWindow.process(
|
try (RegionGenerationWindow workers = new RegionGenerationWindow(2, RegionGenerationWindow.Policy.EMBEDDED)) {
|
||||||
new RegionGenerationWindow.Request<>(2, 2, index -> {
|
assertThrows(Exception.class, () -> workers.process(
|
||||||
if (index == 0) {
|
new RegionGenerationWindow.Request<>(2, 2, index -> {
|
||||||
assertTrue(blocked.await(5, TimeUnit.SECONDS));
|
if (index == 0) {
|
||||||
throw new IllegalStateException("Expected fixture failure");
|
assertTrue(blocked.await(5, TimeUnit.SECONDS));
|
||||||
}
|
throw new IllegalStateException("Expected fixture failure");
|
||||||
blocked.countDown();
|
}
|
||||||
try {
|
blocked.countDown();
|
||||||
new CountDownLatch(1).await();
|
try {
|
||||||
} catch (InterruptedException failure) {
|
new CountDownLatch(1).await();
|
||||||
interrupted.incrementAndGet();
|
} catch (InterruptedException failure) {
|
||||||
Thread.currentThread().interrupt();
|
interrupted.incrementAndGet();
|
||||||
}
|
Thread.currentThread().interrupt();
|
||||||
return index;
|
}
|
||||||
}, (index, value) -> { })));
|
return index;
|
||||||
|
}, (index, value) -> { })));
|
||||||
|
}
|
||||||
assertEquals(1, interrupted.get());
|
assertEquals(1, interrupted.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,16 +173,18 @@ public final class RegionGenerationWindowTest {
|
|||||||
public void drainsStartedWorkBeforeReportingFailure() {
|
public void drainsStartedWorkBeforeReportingFailure() {
|
||||||
CountDownLatch secondStarted = new CountDownLatch(1);
|
CountDownLatch secondStarted = new CountDownLatch(1);
|
||||||
AtomicInteger finished = new AtomicInteger();
|
AtomicInteger finished = new AtomicInteger();
|
||||||
assertThrows(Exception.class, () -> RegionGenerationWindow.process(
|
try (RegionGenerationWindow workers = new RegionGenerationWindow(2, RegionGenerationWindow.Policy.EMBEDDED)) {
|
||||||
new RegionGenerationWindow.Request<>(4, 2, index -> {
|
assertThrows(Exception.class, () -> workers.process(
|
||||||
if (index == 0) {
|
new RegionGenerationWindow.Request<>(4, 2, index -> {
|
||||||
assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
|
if (index == 0) {
|
||||||
throw new IllegalStateException("Expected fixture failure");
|
assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
|
||||||
}
|
throw new IllegalStateException("Expected fixture failure");
|
||||||
secondStarted.countDown();
|
}
|
||||||
finished.incrementAndGet();
|
secondStarted.countDown();
|
||||||
return index;
|
finished.incrementAndGet();
|
||||||
}, (index, value) -> { })));
|
return index;
|
||||||
|
}, (index, value) -> { })));
|
||||||
|
}
|
||||||
assertTrue(finished.get() >= 1);
|
assertTrue(finished.get() >= 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,15 +193,17 @@ public final class RegionGenerationWindowTest {
|
|||||||
AtomicInteger completed = new AtomicInteger();
|
AtomicInteger completed = new AtomicInteger();
|
||||||
RegionGenerationWindow.Policy policy = new RegionGenerationWindow.Policy(
|
RegionGenerationWindow.Policy policy = new RegionGenerationWindow.Policy(
|
||||||
Duration.ofMillis(50), Duration.ofSeconds(1), false);
|
Duration.ofMillis(50), Duration.ofSeconds(1), false);
|
||||||
assertThrows(TimeoutException.class, () -> RegionGenerationWindow.process(
|
try (RegionGenerationWindow workers = new RegionGenerationWindow(1, policy)) {
|
||||||
new RegionGenerationWindow.Request<>(1, 1, index -> {
|
assertThrows(TimeoutException.class, () -> workers.process(
|
||||||
try {
|
new RegionGenerationWindow.Request<>(1, 1, index -> {
|
||||||
new CountDownLatch(1).await();
|
try {
|
||||||
} finally {
|
new CountDownLatch(1).await();
|
||||||
completed.incrementAndGet();
|
} finally {
|
||||||
}
|
completed.incrementAndGet();
|
||||||
return index;
|
}
|
||||||
}, (index, value) -> { }), policy));
|
return index;
|
||||||
|
}, (index, value) -> { })));
|
||||||
|
}
|
||||||
assertEquals(1, completed.get());
|
assertEquals(1, completed.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,8 +239,8 @@ public final class RegionGenerationWindowTest {
|
|||||||
CountDownLatch never = new CountDownLatch(1);
|
CountDownLatch never = new CountDownLatch(1);
|
||||||
RegionGenerationWindow.Policy policy = new RegionGenerationWindow.Policy(
|
RegionGenerationWindow.Policy policy = new RegionGenerationWindow.Policy(
|
||||||
Duration.ofSeconds(1), Duration.ofMillis(25), true);
|
Duration.ofSeconds(1), Duration.ofMillis(25), true);
|
||||||
try {
|
try (RegionGenerationWindow workers = new RegionGenerationWindow(2, policy)) {
|
||||||
RegionGenerationWindow.process(new RegionGenerationWindow.Request<>(2, 2, index -> {
|
workers.process(new RegionGenerationWindow.Request<>(2, 2, index -> {
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
if (!entered.await(1, TimeUnit.SECONDS)) {
|
if (!entered.await(1, TimeUnit.SECONDS)) {
|
||||||
throw new IllegalStateException("Worker did not start");
|
throw new IllegalStateException("Worker did not start");
|
||||||
@@ -177,7 +256,7 @@ public final class RegionGenerationWindowTest {
|
|||||||
}
|
}
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
}, (index, value) -> { }), policy);
|
}, (index, value) -> { }));
|
||||||
} finally {
|
} finally {
|
||||||
Files.writeString(Path.of(arguments[0]), "closed");
|
Files.writeString(Path.of(arguments[0]), "closed");
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -125,8 +125,8 @@ public final class RegionNativeFixture extends JavaPlugin {
|
|||||||
int chunks = request.width() * request.width();
|
int chunks = request.width() * request.width();
|
||||||
getLogger().info("NATIVE_LOAD_STARTED chunks=" + chunks + " chunkX=" + request.chunkX()
|
getLogger().info("NATIVE_LOAD_STARTED chunks=" + chunks + " chunkX=" + request.chunkX()
|
||||||
+ " chunkZ=" + request.chunkZ() + " workers=" + request.parallelism());
|
+ " chunkZ=" + request.chunkZ() + " workers=" + request.parallelism());
|
||||||
try {
|
try (RegionGenerationWindow workers = new RegionGenerationWindow(request.parallelism(), RegionGenerationWindow.Policy.EMBEDDED)) {
|
||||||
RegionGenerationWindow.process(new RegionGenerationWindow.Request<>(chunks, request.parallelism(),
|
workers.process(new RegionGenerationWindow.Request<>(chunks, request.parallelism(),
|
||||||
index -> {
|
index -> {
|
||||||
int x = request.chunkX() + index % request.width();
|
int x = request.chunkX() + index % request.width();
|
||||||
int z = request.chunkZ() + index / request.width();
|
int z = request.chunkZ() + index / request.width();
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
|||||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
||||||
import art.arcane.iris.world.storage.matter.PreObjectMatterCell;
|
import art.arcane.iris.world.storage.matter.PreObjectMatterCell;
|
||||||
import art.arcane.volmlib.util.function.Consumer4;
|
import art.arcane.volmlib.util.function.Consumer4;
|
||||||
|
import art.arcane.volmlib.util.hunk.bits.DataContainer;
|
||||||
|
import art.arcane.volmlib.util.hunk.storage.PaletteOrHunk;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
import art.arcane.volmlib.util.matter.Matter;
|
import art.arcane.volmlib.util.matter.Matter;
|
||||||
@@ -11,6 +13,7 @@ import art.arcane.volmlib.util.matter.MatterCavern;
|
|||||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -67,6 +70,82 @@ public final class TerrainMatterView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static MatterCavern[] getComposedFace(MantleChunk<Matter> chunk, Face face, int height) {
|
||||||
|
Objects.requireNonNull(face, "Terrain face");
|
||||||
|
if (height < 0) {
|
||||||
|
throw new IllegalArgumentException("Terrain face height must be nonnegative");
|
||||||
|
}
|
||||||
|
MatterCavern[] result = new MatterCavern[Math.multiplyExact(height, 16)];
|
||||||
|
if (chunk == null || height == 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
PreObjectMatterCell[] journal = new PreObjectMatterCell[256];
|
||||||
|
HydrologyCaveCell[] hydrology = new HydrologyCaveCell[256];
|
||||||
|
MatterCavern[] caverns = new MatterCavern[256];
|
||||||
|
synchronized (chunk) {
|
||||||
|
for (int baseY = 0; baseY < height; baseY += 16) {
|
||||||
|
int section = baseY >> 4;
|
||||||
|
if (!chunk.exists(section)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Matter matter = chunk.get(section);
|
||||||
|
if (matter == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
copyFace(matter.getSlice(PreObjectMatterCell.class), face, journal);
|
||||||
|
copyFace(matter.getSlice(HydrologyCaveCell.class), face, hydrology);
|
||||||
|
copyFace(matter.getSlice(MatterCavern.class), face, caverns);
|
||||||
|
int count = Math.min(16, height - baseY) * 16;
|
||||||
|
for (int index = 0; index < count; index++) {
|
||||||
|
PreObjectMatterCell cell = journal[index];
|
||||||
|
HydrologyCaveCell effectiveHydrology = cell != null && cell.hydrologyCaptured()
|
||||||
|
? cell.hydrology() : hydrology[index];
|
||||||
|
result[baseY * 16 + index] = effectiveHydrology != null
|
||||||
|
? effectiveHydrology.asCavern()
|
||||||
|
: cell != null && cell.cavernCaptured() ? cell.cavern() : caverns[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <T> void copyFace(MatterSlice<T> slice, Face face, T[] destination) {
|
||||||
|
if (slice == null) {
|
||||||
|
Arrays.fill(destination, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (slice instanceof PaletteOrHunk<?> storage && storage.isPalette()
|
||||||
|
&& slice.getWidth() == 16 && slice.getHeight() == 16 && slice.getDepth() == 16) {
|
||||||
|
DataContainer<T> palette = (DataContainer<T>) storage.palette();
|
||||||
|
palette.copyTo(face.positions, destination);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int index = 0; index < destination.length; index++) {
|
||||||
|
int position = face.positions[index];
|
||||||
|
destination[index] = slice.get(position & 15, (position >> 4) & 15, position >> 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Face {
|
||||||
|
WEST(true, 0),
|
||||||
|
EAST(true, 15),
|
||||||
|
NORTH(false, 0),
|
||||||
|
SOUTH(false, 15);
|
||||||
|
|
||||||
|
private final int[] positions = new int[256];
|
||||||
|
|
||||||
|
Face(boolean fixedX, int coordinate) {
|
||||||
|
for (int y = 0; y < 16; y++) {
|
||||||
|
for (int offset = 0; offset < 16; offset++) {
|
||||||
|
int x = fixedX ? coordinate : offset;
|
||||||
|
int z = fixedX ? offset : coordinate;
|
||||||
|
positions[y * 16 + offset] = (z << 8) | (y << 4) | x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static <T> void iterate(MantleChunk<Matter> chunk, Class<T> type,
|
public static <T> void iterate(MantleChunk<Matter> chunk, Class<T> type,
|
||||||
Consumer4<Integer, Integer, Integer, T> consumer) {
|
Consumer4<Integer, Integer, Integer, T> consumer) {
|
||||||
Objects.requireNonNull(chunk, "Terrain mantle chunk");
|
Objects.requireNonNull(chunk, "Terrain mantle chunk");
|
||||||
|
|||||||
@@ -532,44 +532,46 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int height = maxY + 1;
|
||||||
|
MatterCavern[] ownWest = west == null ? null : TerrainMatterView.getComposedFace(mc, TerrainMatterView.Face.WEST, height);
|
||||||
|
MatterCavern[] ownEast = east == null ? null : TerrainMatterView.getComposedFace(mc, TerrainMatterView.Face.EAST, height);
|
||||||
|
MatterCavern[] ownNorth = north == null ? null : TerrainMatterView.getComposedFace(mc, TerrainMatterView.Face.NORTH, height);
|
||||||
|
MatterCavern[] ownSouth = south == null ? null : TerrainMatterView.getComposedFace(mc, TerrainMatterView.Face.SOUTH, height);
|
||||||
|
MatterCavern[] fromWest = west == null ? null : TerrainMatterView.getComposedFace(west, TerrainMatterView.Face.EAST, height);
|
||||||
|
MatterCavern[] fromEast = east == null ? null : TerrainMatterView.getComposedFace(east, TerrainMatterView.Face.WEST, height);
|
||||||
|
MatterCavern[] fromNorth = north == null ? null : TerrainMatterView.getComposedFace(north, TerrainMatterView.Face.SOUTH, height);
|
||||||
|
MatterCavern[] fromSouth = south == null ? null : TerrainMatterView.getComposedFace(south, TerrainMatterView.Face.NORTH, height);
|
||||||
for (int yy = 1; yy <= maxY; yy++) {
|
for (int yy = 1; yy <= maxY; yy++) {
|
||||||
for (int offset = 0; offset < 16; offset++) {
|
for (int offset = 0; offset < 16; offset++) {
|
||||||
|
int index = yy * 16 + offset;
|
||||||
if (west != null) {
|
if (west != null) {
|
||||||
tryAddBoundaryWall(mc, west, walls, boundaryMasks, 0, yy, offset, 15, offset);
|
tryAddBoundaryWall(ownWest[index], fromWest[index], walls, boundaryMasks, 0, yy, offset);
|
||||||
}
|
}
|
||||||
if (east != null) {
|
if (east != null) {
|
||||||
tryAddBoundaryWall(mc, east, walls, boundaryMasks, 15, yy, offset, 0, offset);
|
tryAddBoundaryWall(ownEast[index], fromEast[index], walls, boundaryMasks, 15, yy, offset);
|
||||||
}
|
}
|
||||||
if (north != null) {
|
if (north != null) {
|
||||||
tryAddBoundaryWall(mc, north, walls, boundaryMasks, offset, yy, 0, offset, 15);
|
tryAddBoundaryWall(ownNorth[index], fromNorth[index], walls, boundaryMasks, offset, yy, 0);
|
||||||
}
|
}
|
||||||
if (south != null) {
|
if (south != null) {
|
||||||
tryAddBoundaryWall(mc, south, walls, boundaryMasks, offset, yy, 15, offset, 0);
|
tryAddBoundaryWall(ownSouth[index], fromSouth[index], walls, boundaryMasks, offset, yy, 15);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void tryAddBoundaryWall(
|
private void tryAddBoundaryWall(
|
||||||
MantleChunk<Matter> mc,
|
MatterCavern current,
|
||||||
MantleChunk<Matter> neighborChunk,
|
MatterCavern neighbor,
|
||||||
CarveWallBuffer walls,
|
CarveWallBuffer walls,
|
||||||
CarveColumnMask[] boundaryMasks,
|
CarveColumnMask[] boundaryMasks,
|
||||||
int localX,
|
int localX,
|
||||||
int yy,
|
int yy,
|
||||||
int localZ,
|
int localZ
|
||||||
int neighborX,
|
|
||||||
int neighborZ
|
|
||||||
) {
|
) {
|
||||||
if (composedCavernAt(mc, localX, yy, localZ) != null) {
|
if (current != null || neighbor == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
MatterCavern neighbor = composedCavernAt(neighborChunk, neighborX, yy, neighborZ);
|
|
||||||
if (neighbor == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
walls.put(localX, yy, localZ, neighbor);
|
walls.put(localX, yy, localZ, neighbor);
|
||||||
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
|
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
|
||||||
boundaryMasks[columnIndex].add(yy);
|
boundaryMasks[columnIndex].add(yy);
|
||||||
|
|||||||
@@ -30,11 +30,9 @@ import art.arcane.iris.generation.concurrent.BurstExecutor;
|
|||||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||||
|
|
||||||
public class IrisPerfectionModifier extends EngineAssignedModifier<NativeBlockState> {
|
public class IrisPerfectionModifier extends EngineAssignedModifier<NativeBlockState> {
|
||||||
private static final BoundBlockState AIR = BoundBlockState.of("AIR");
|
private static final BoundBlockState AIR = BoundBlockState.of("AIR");
|
||||||
@@ -86,84 +84,67 @@ public class IrisPerfectionModifier extends EngineAssignedModifier<NativeBlockSt
|
|||||||
if (getDimension().isHideOresForHiddenOre()) {
|
if (getDimension().isHideOresForHiddenOre()) {
|
||||||
hideOres(output, multicore);
|
hideOres(output, multicore);
|
||||||
}
|
}
|
||||||
AtomicBoolean changed = new AtomicBoolean(true);
|
|
||||||
BurstExecutor burst = burst().burst(multicore);
|
BurstExecutor burst = burst().burst(multicore);
|
||||||
while (changed.get()) {
|
for (int index = 0; index < 16; index++) {
|
||||||
changed.set(false);
|
int columnX = index;
|
||||||
for (int i = 0; i < 16; i++) {
|
burst.queue(() -> perfectSlice(output, columnX));
|
||||||
int finalI = i;
|
|
||||||
burst.queue(() -> {
|
|
||||||
List<Integer> surfaces = new ArrayList<>();
|
|
||||||
List<Integer> ceilings = new ArrayList<>();
|
|
||||||
for (int j = 0; j < 16; j++) {
|
|
||||||
surfaces.clear();
|
|
||||||
ceilings.clear();
|
|
||||||
int top = getHeight(output, finalI, j);
|
|
||||||
boolean inside = true;
|
|
||||||
surfaces.add(top);
|
|
||||||
|
|
||||||
for (int k = top; k >= 0; k--) {
|
|
||||||
NativeBlockState b = output.get(finalI, k, j);
|
|
||||||
if (IrisSpeleothems.isSpike(b)) {
|
|
||||||
b = normalizeSpike(b, output, finalI, j, k, AIR.get(), WATER.get());
|
|
||||||
}
|
|
||||||
boolean now = b != null && !(B.isAir(b) || B.isFluid(b));
|
|
||||||
|
|
||||||
if (now != inside) {
|
|
||||||
inside = now;
|
|
||||||
|
|
||||||
if (inside) {
|
|
||||||
surfaces.add(k);
|
|
||||||
} else {
|
|
||||||
ceilings.add(k + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int k : surfaces) {
|
|
||||||
NativeBlockState tip = output.get(finalI, k, j);
|
|
||||||
|
|
||||||
if (tip == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean remove = false;
|
|
||||||
boolean remove2 = false;
|
|
||||||
|
|
||||||
if (B.isDecorant(tip)) {
|
|
||||||
NativeBlockState bel = output.get(finalI, k - 1, j);
|
|
||||||
|
|
||||||
if (bel == null) {
|
|
||||||
remove = true;
|
|
||||||
} else if (!B.canPlaceOnto(tip, bel)) {
|
|
||||||
remove = true;
|
|
||||||
} else if (IrisProceduralBlocks.hasProperty(bel, "half")) {
|
|
||||||
NativeBlockState bb = output.get(finalI, k - 2, j);
|
|
||||||
if (bb == null || !B.canPlaceOnto(bel, bb)) {
|
|
||||||
remove = true;
|
|
||||||
remove2 = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remove) {
|
|
||||||
changed.set(true);
|
|
||||||
output.set(finalI, k, j, AIR.get());
|
|
||||||
|
|
||||||
if (remove2) {
|
|
||||||
output.set(finalI, k - 1, j, AIR.get());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
burst.complete();
|
|
||||||
}
|
}
|
||||||
|
burst.complete();
|
||||||
|
|
||||||
getEngine().getMetrics().getPerfection().put(p.getMilliseconds());
|
getEngine().getMetrics().getPerfection().put(p.getMilliseconds());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void perfectSlice(Hunk<NativeBlockState> output, int x) {
|
||||||
|
IntArrayList surfaces = new IntArrayList();
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
boolean changed;
|
||||||
|
do {
|
||||||
|
surfaces.clear();
|
||||||
|
int top = getHeight(output, x, z);
|
||||||
|
boolean inside = true;
|
||||||
|
surfaces.add(top);
|
||||||
|
for (int y = top; y >= 0; y--) {
|
||||||
|
NativeBlockState block = output.get(x, y, z);
|
||||||
|
if (IrisSpeleothems.isSpike(block)) {
|
||||||
|
block = normalizeSpike(block, output, x, z, y, AIR.get(), WATER.get());
|
||||||
|
}
|
||||||
|
boolean now = block != null && !(B.isAir(block) || B.isFluid(block));
|
||||||
|
if (now != inside) {
|
||||||
|
inside = now;
|
||||||
|
if (inside) {
|
||||||
|
surfaces.add(y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changed = false;
|
||||||
|
for (int index = 0; index < surfaces.size(); index++) {
|
||||||
|
int y = surfaces.getInt(index);
|
||||||
|
NativeBlockState tip = output.get(x, y, z);
|
||||||
|
if (!B.isDecorant(tip)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
NativeBlockState below = output.get(x, y - 1, z);
|
||||||
|
boolean remove = below == null || !B.canPlaceOnto(tip, below);
|
||||||
|
boolean removeBelow = false;
|
||||||
|
if (!remove && IrisProceduralBlocks.hasProperty(below, "half")) {
|
||||||
|
NativeBlockState support = output.get(x, y - 2, z);
|
||||||
|
if (support == null || !B.canPlaceOnto(below, support)) {
|
||||||
|
remove = true;
|
||||||
|
removeBelow = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (remove) {
|
||||||
|
changed = true;
|
||||||
|
output.set(x, y, z, AIR.get());
|
||||||
|
if (removeBelow) {
|
||||||
|
output.set(x, y - 1, z, AIR.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (changed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static NativeBlockState normalizeSpike(NativeBlockState state, Hunk<NativeBlockState> output,
|
static NativeBlockState normalizeSpike(NativeBlockState state, Hunk<NativeBlockState> output,
|
||||||
int x, int z, int y, NativeBlockState air, NativeBlockState water) {
|
int x, int z, int y, NativeBlockState air, NativeBlockState water) {
|
||||||
if (IrisSpeleothems.isSupported(state, output, x, z, y)) {
|
if (IrisSpeleothems.isSupported(state, output, x, z, y)) {
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ public final class GenerationHistory {
|
|||||||
GenerationHistoryStore store,
|
GenerationHistoryStore store,
|
||||||
ChunkGenerationOwnership ownership,
|
ChunkGenerationOwnership ownership,
|
||||||
GenerationKernelRegistry kernels,
|
GenerationKernelRegistry kernels,
|
||||||
Optional<FreshPublication> publication
|
Optional<FreshPublication> publication,
|
||||||
|
boolean unpublished
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
this.paths = Objects.requireNonNull(paths, "paths");
|
this.paths = Objects.requireNonNull(paths, "paths");
|
||||||
this.packs = Objects.requireNonNull(packs, "packs");
|
this.packs = Objects.requireNonNull(packs, "packs");
|
||||||
@@ -62,8 +63,8 @@ public final class GenerationHistory {
|
|||||||
this.kernels = Objects.requireNonNull(kernels, "kernels");
|
this.kernels = Objects.requireNonNull(kernels, "kernels");
|
||||||
this.boundaries = new GenerationBoundaryStore(paths.dimensionRoot());
|
this.boundaries = new GenerationBoundaryStore(paths.dimensionRoot());
|
||||||
this.terrainSignatures = new TerrainBoundarySignatureStore(paths.dimensionRoot());
|
this.terrainSignatures = new TerrainBoundarySignatureStore(paths.dimensionRoot());
|
||||||
this.semantics = GenerationSemanticIndex.loadRequired(paths.dimensionRoot());
|
this.semantics = GenerationSemanticIndex.loadRequired(paths.dimensionRoot(), unpublished);
|
||||||
this.savedBiomes = SavedBiomeStore.open(paths.dimensionRoot());
|
this.savedBiomes = SavedBiomeStore.open(paths.dimensionRoot(), unpublished);
|
||||||
this.admission = new GenerationAdmission(paths.dimensionRoot());
|
this.admission = new GenerationAdmission(paths.dimensionRoot());
|
||||||
this.boundaryCache = boundedCache(MAXIMUM_CACHED_BOUNDARIES);
|
this.boundaryCache = boundedCache(MAXIMUM_CACHED_BOUNDARIES);
|
||||||
this.terrainSignatureCache = boundedCache(MAXIMUM_CACHED_TERRAIN_SIGNATURES);
|
this.terrainSignatureCache = boundedCache(MAXIMUM_CACHED_TERRAIN_SIGNATURES);
|
||||||
@@ -108,6 +109,25 @@ public final class GenerationHistory {
|
|||||||
GenerationKernelRegistry.Version version,
|
GenerationKernelRegistry.Version version,
|
||||||
GenerationKernelRegistry kernels
|
GenerationKernelRegistry kernels
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
|
return create(new FreshCreation(dimensionRoot, packSource, packFingerprint, worldSeed, dimensionContract, registryContract),
|
||||||
|
version, kernels, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GenerationHistory createUnpublished(FreshCreation creation) throws IOException {
|
||||||
|
Objects.requireNonNull(creation, "creation");
|
||||||
|
Files.createDirectory(creation.dimensionRoot());
|
||||||
|
GenerationKernelRegistry kernels = GenerationKernelRegistry.standard();
|
||||||
|
return create(creation, kernels.current(), kernels, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenerationHistory create(FreshCreation creation, GenerationKernelRegistry.Version version,
|
||||||
|
GenerationKernelRegistry kernels, boolean unpublished) throws IOException {
|
||||||
|
Path dimensionRoot = creation.dimensionRoot();
|
||||||
|
Path packSource = creation.packSource();
|
||||||
|
String packFingerprint = creation.packFingerprint();
|
||||||
|
long worldSeed = creation.worldSeed();
|
||||||
|
GenerationEpoch.DimensionContract dimensionContract = creation.dimensionContract();
|
||||||
|
GenerationRegistryContract registryContract = creation.registryContract();
|
||||||
GenerationHistoryPaths paths = GenerationHistoryPaths.forDimension(dimensionRoot);
|
GenerationHistoryPaths paths = GenerationHistoryPaths.forDimension(dimensionRoot);
|
||||||
requireSafeStatePaths(paths);
|
requireSafeStatePaths(paths);
|
||||||
if (Files.exists(paths.manifest(), LinkOption.NOFOLLOW_LINKS)
|
if (Files.exists(paths.manifest(), LinkOption.NOFOLLOW_LINKS)
|
||||||
@@ -141,7 +161,7 @@ public final class GenerationHistory {
|
|||||||
GenerationSemanticIndex.initialize(paths.dimensionRoot());
|
GenerationSemanticIndex.initialize(paths.dimensionRoot());
|
||||||
GenerationHistoryStore store = GenerationHistoryStore.initialize(paths.generationRoot(), epoch);
|
GenerationHistoryStore store = GenerationHistoryStore.initialize(paths.generationRoot(), epoch);
|
||||||
return new GenerationHistory(paths, packs, store, ownership, kernels,
|
return new GenerationHistory(paths, packs, store, ownership, kernels,
|
||||||
Optional.of(new FreshPublication(publishedPack, epoch)));
|
Optional.of(new FreshPublication(publishedPack, epoch)), unpublished);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static GenerationHistory open(Path dimensionRoot) throws IOException {
|
public static GenerationHistory open(Path dimensionRoot) throws IOException {
|
||||||
@@ -183,7 +203,7 @@ public final class GenerationHistory {
|
|||||||
GenerationPackRepository packs = new GenerationPackRepository(paths.dimensionRoot());
|
GenerationPackRepository packs = new GenerationPackRepository(paths.dimensionRoot());
|
||||||
GenerationHistoryStore store = GenerationHistoryStore.open(paths.generationRoot());
|
GenerationHistoryStore store = GenerationHistoryStore.open(paths.generationRoot());
|
||||||
ChunkGenerationOwnership ownership = ChunkGenerationOwnership.load(paths.ownershipRoot());
|
ChunkGenerationOwnership ownership = ChunkGenerationOwnership.load(paths.ownershipRoot());
|
||||||
return new GenerationHistory(paths, packs, store, ownership, kernels, Optional.empty());
|
return new GenerationHistory(paths, packs, store, ownership, kernels, Optional.empty(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Optional<GenerationHistory> openIfPresent(Path dimensionRoot) throws IOException {
|
public static Optional<GenerationHistory> openIfPresent(Path dimensionRoot) throws IOException {
|
||||||
@@ -1344,4 +1364,15 @@ public final class GenerationHistory {
|
|||||||
public interface BoundarySignatureCapture {
|
public interface BoundarySignatureCapture {
|
||||||
TerrainBoundarySignatureStore.SignatureSampler capture(GenerationBoundary boundary) throws IOException;
|
TerrainBoundarySignatureStore.SignatureSampler capture(GenerationBoundary boundary) throws IOException;
|
||||||
}
|
}
|
||||||
|
public record FreshCreation(Path dimensionRoot, Path packSource, String packFingerprint, long worldSeed,
|
||||||
|
GenerationEpoch.DimensionContract dimensionContract, GenerationRegistryContract registryContract) {
|
||||||
|
public FreshCreation {
|
||||||
|
Objects.requireNonNull(dimensionRoot, "dimensionRoot");
|
||||||
|
Objects.requireNonNull(packSource, "packSource");
|
||||||
|
Objects.requireNonNull(packFingerprint, "packFingerprint");
|
||||||
|
Objects.requireNonNull(dimensionContract, "dimensionContract");
|
||||||
|
Objects.requireNonNull(registryContract, "registryContract");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -92,6 +92,7 @@ public final class GenerationSemanticIndex {
|
|||||||
private final ShardPublisher publisher;
|
private final ShardPublisher publisher;
|
||||||
private final PointerPublisher pointerPublisher;
|
private final PointerPublisher pointerPublisher;
|
||||||
private final CatalogPublisher catalogPublisher;
|
private final CatalogPublisher catalogPublisher;
|
||||||
|
private final boolean unpublished;
|
||||||
private long regionDecodeCount;
|
private long regionDecodeCount;
|
||||||
private volatile IOException journalFailure;
|
private volatile IOException journalFailure;
|
||||||
private long appendingRegionKey;
|
private long appendingRegionKey;
|
||||||
@@ -101,7 +102,8 @@ public final class GenerationSemanticIndex {
|
|||||||
Path dimensionRoot,
|
Path dimensionRoot,
|
||||||
ShardPublisher publisher,
|
ShardPublisher publisher,
|
||||||
PointerPublisher pointerPublisher,
|
PointerPublisher pointerPublisher,
|
||||||
CatalogPublisher catalogPublisher
|
CatalogPublisher catalogPublisher,
|
||||||
|
boolean unpublished
|
||||||
) {
|
) {
|
||||||
this.dimensionRoot = dimensionRoot.toAbsolutePath().normalize();
|
this.dimensionRoot = dimensionRoot.toAbsolutePath().normalize();
|
||||||
directory = this.dimensionRoot.resolve("iris").resolve("generation").resolve("semantics");
|
directory = this.dimensionRoot.resolve("iris").resolve("generation").resolve("semantics");
|
||||||
@@ -115,6 +117,7 @@ public final class GenerationSemanticIndex {
|
|||||||
this.publisher = publisher;
|
this.publisher = publisher;
|
||||||
this.pointerPublisher = pointerPublisher;
|
this.pointerPublisher = pointerPublisher;
|
||||||
this.catalogPublisher = catalogPublisher;
|
this.catalogPublisher = catalogPublisher;
|
||||||
|
this.unpublished = unpublished;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static GenerationSemanticIndex load(Path dimensionRoot) throws IOException {
|
public static GenerationSemanticIndex load(Path dimensionRoot) throws IOException {
|
||||||
@@ -126,7 +129,8 @@ public final class GenerationSemanticIndex {
|
|||||||
Objects.requireNonNull(dimensionRoot, "dimensionRoot"),
|
Objects.requireNonNull(dimensionRoot, "dimensionRoot"),
|
||||||
RegionShard::publish,
|
RegionShard::publish,
|
||||||
ShardPointer::publish,
|
ShardPointer::publish,
|
||||||
Catalog::publish
|
Catalog::publish,
|
||||||
|
false
|
||||||
);
|
);
|
||||||
index.ensureStorageDirectory();
|
index.ensureStorageDirectory();
|
||||||
Path catalog = index.directory.resolve(CATALOG_FILE_NAME);
|
Path catalog = index.directory.resolve(CATALOG_FILE_NAME);
|
||||||
@@ -138,7 +142,13 @@ public final class GenerationSemanticIndex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static GenerationSemanticIndex loadRequired(Path dimensionRoot) throws IOException {
|
public static GenerationSemanticIndex loadRequired(Path dimensionRoot) throws IOException {
|
||||||
GenerationSemanticIndex index = load(dimensionRoot);
|
return loadRequired(dimensionRoot, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static GenerationSemanticIndex loadRequired(Path dimensionRoot, boolean unpublished) throws IOException {
|
||||||
|
GenerationSemanticIndex index = new GenerationSemanticIndex(Objects.requireNonNull(dimensionRoot, "dimensionRoot"),
|
||||||
|
RegionShard::publish, ShardPointer::publish, Catalog::publish, unpublished);
|
||||||
|
index.loadShards();
|
||||||
Path catalog = index.directory.resolve(CATALOG_FILE_NAME);
|
Path catalog = index.directory.resolve(CATALOG_FILE_NAME);
|
||||||
if (!Files.isRegularFile(catalog, LinkOption.NOFOLLOW_LINKS)) {
|
if (!Files.isRegularFile(catalog, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
throw new IOException("Generation semantic shard catalog is missing: " + catalog);
|
throw new IOException("Generation semantic shard catalog is missing: " + catalog);
|
||||||
@@ -167,7 +177,8 @@ public final class GenerationSemanticIndex {
|
|||||||
Objects.requireNonNull(dimensionRoot, "dimensionRoot"),
|
Objects.requireNonNull(dimensionRoot, "dimensionRoot"),
|
||||||
Objects.requireNonNull(publisher, "publisher"),
|
Objects.requireNonNull(publisher, "publisher"),
|
||||||
Objects.requireNonNull(pointerPublisher, "pointerPublisher"),
|
Objects.requireNonNull(pointerPublisher, "pointerPublisher"),
|
||||||
Objects.requireNonNull(catalogPublisher, "catalogPublisher")
|
Objects.requireNonNull(catalogPublisher, "catalogPublisher"),
|
||||||
|
false
|
||||||
);
|
);
|
||||||
index.loadShards();
|
index.loadShards();
|
||||||
return index;
|
return index;
|
||||||
@@ -254,7 +265,7 @@ public final class GenerationSemanticIndex {
|
|||||||
appendingBase = baseRegion;
|
appendingBase = baseRegion;
|
||||||
publicationLock.unlock();
|
publicationLock.unlock();
|
||||||
try {
|
try {
|
||||||
SemanticJournal.append(directory, regionX, regionZ, updates);
|
SemanticJournal.append(directory, regionX, regionZ, updates, unpublished);
|
||||||
} catch (JournalAppendFailure failure) {
|
} catch (JournalAppendFailure failure) {
|
||||||
journalFailure = failure;
|
journalFailure = failure;
|
||||||
throw failure;
|
throw failure;
|
||||||
@@ -1928,7 +1939,8 @@ public final class GenerationSemanticIndex {
|
|||||||
Path directory,
|
Path directory,
|
||||||
int regionX,
|
int regionX,
|
||||||
int regionZ,
|
int regionZ,
|
||||||
List<ChunkGenerationSemantics> claims
|
List<ChunkGenerationSemantics> claims,
|
||||||
|
boolean unpublished
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
Path file = directory.resolve(journalFileName(regionX, regionZ));
|
Path file = directory.resolve(journalFileName(regionX, regionZ));
|
||||||
boolean created = !Files.exists(file, LinkOption.NOFOLLOW_LINKS);
|
boolean created = !Files.exists(file, LinkOption.NOFOLLOW_LINKS);
|
||||||
@@ -1961,14 +1973,18 @@ public final class GenerationSemanticIndex {
|
|||||||
channel.write(entry);
|
channel.write(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Durability.force(channel);
|
if (!unpublished) {
|
||||||
|
Durability.force(channel);
|
||||||
|
}
|
||||||
if (created) {
|
if (created) {
|
||||||
RegionShard.forceDirectory(directory);
|
RegionShard.forceDirectory(directory);
|
||||||
}
|
}
|
||||||
} catch (IOException failure) {
|
} catch (IOException failure) {
|
||||||
try {
|
try {
|
||||||
channel.truncate(size);
|
channel.truncate(size);
|
||||||
Durability.force(channel);
|
if (!unpublished) {
|
||||||
|
Durability.force(channel);
|
||||||
|
}
|
||||||
if (created) {
|
if (created) {
|
||||||
RegionShard.forceDirectory(directory);
|
RegionShard.forceDirectory(directory);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,14 +49,16 @@ public final class SavedBiomeStore {
|
|||||||
|
|
||||||
private final Path dimensionRoot;
|
private final Path dimensionRoot;
|
||||||
private final Path directory;
|
private final Path directory;
|
||||||
|
private final boolean unpublished;
|
||||||
private final WriteStripe[] regionLocks = new WriteStripe[64];
|
private final WriteStripe[] regionLocks = new WriteStripe[64];
|
||||||
private final LinkedHashMap<Long, RegionIndex> regions = new LinkedHashMap<>(64, 0.75F, true);
|
private final LinkedHashMap<Long, RegionIndex> regions = new LinkedHashMap<>(64, 0.75F, true);
|
||||||
private final LinkedHashMap<Long, Optional<SavedBiomeChunk>> chunks = new LinkedHashMap<>(128, 0.75F, true);
|
private final LinkedHashMap<Long, Optional<SavedBiomeChunk>> chunks = new LinkedHashMap<>(128, 0.75F, true);
|
||||||
private long cachedBytes;
|
private long cachedBytes;
|
||||||
private volatile IOException writeFailure;
|
private volatile IOException writeFailure;
|
||||||
|
|
||||||
private SavedBiomeStore(Path dimensionRoot) throws IOException {
|
private SavedBiomeStore(Path dimensionRoot, boolean unpublished) throws IOException {
|
||||||
this.dimensionRoot = Objects.requireNonNull(dimensionRoot, "dimensionRoot").toAbsolutePath().normalize();
|
this.dimensionRoot = Objects.requireNonNull(dimensionRoot, "dimensionRoot").toAbsolutePath().normalize();
|
||||||
|
this.unpublished = unpublished;
|
||||||
directory = this.dimensionRoot.resolve("iris/generation/biomes");
|
directory = this.dimensionRoot.resolve("iris/generation/biomes");
|
||||||
requireSafeAncestors();
|
requireSafeAncestors();
|
||||||
for (int index = 0; index < regionLocks.length; index++) {
|
for (int index = 0; index < regionLocks.length; index++) {
|
||||||
@@ -65,7 +67,11 @@ public final class SavedBiomeStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static SavedBiomeStore open(Path dimensionRoot) throws IOException {
|
public static SavedBiomeStore open(Path dimensionRoot) throws IOException {
|
||||||
return new SavedBiomeStore(dimensionRoot);
|
return open(dimensionRoot, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static SavedBiomeStore open(Path dimensionRoot, boolean unpublished) throws IOException {
|
||||||
|
return new SavedBiomeStore(dimensionRoot, unpublished);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Path storageDirectory() {
|
public Path storageDirectory() {
|
||||||
@@ -195,11 +201,14 @@ public final class SavedBiomeStore {
|
|||||||
RegionIndex region = region(first.chunkX(), first.chunkZ());
|
RegionIndex region = region(first.chunkX(), first.chunkZ());
|
||||||
ensureRegionFile(region);
|
ensureRegionFile(region);
|
||||||
long offset = 0L;
|
long offset = 0L;
|
||||||
boolean durable = false;
|
boolean appendCompleted = false;
|
||||||
try (RandomAccessFile output = new RandomAccessFile(region.path.toFile(), "rw")) {
|
try (RandomAccessFile output = new RandomAccessFile(region.path.toFile(), "rw")) {
|
||||||
offset = output.length();
|
offset = output.length();
|
||||||
long length = 0L;
|
long length = 0L;
|
||||||
|
ByteBuffer[] records = new ByteBuffer[claimed.size()];
|
||||||
|
int recordIndex = 0;
|
||||||
for (PendingWrite pending : claimed.values()) {
|
for (PendingWrite pending : claimed.values()) {
|
||||||
|
records[recordIndex++] = ByteBuffer.wrap(pending.record);
|
||||||
length += pending.record.length;
|
length += pending.record.length;
|
||||||
}
|
}
|
||||||
if (offset + length > MAXIMUM_REGION_BYTES) {
|
if (offset + length > MAXIMUM_REGION_BYTES) {
|
||||||
@@ -207,15 +216,24 @@ public final class SavedBiomeStore {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
output.seek(offset);
|
output.seek(offset);
|
||||||
for (PendingWrite pending : claimed.values()) {
|
FileChannel channel = output.getChannel();
|
||||||
output.write(pending.record);
|
int firstBuffer = 0;
|
||||||
|
while (firstBuffer < records.length) {
|
||||||
|
channel.write(records, firstBuffer, records.length - firstBuffer);
|
||||||
|
while (firstBuffer < records.length && !records[firstBuffer].hasRemaining()) {
|
||||||
|
firstBuffer++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
output.getChannel().force(true);
|
if (!unpublished) {
|
||||||
durable = true;
|
output.getChannel().force(true);
|
||||||
|
}
|
||||||
|
appendCompleted = true;
|
||||||
} catch (IOException failure) {
|
} catch (IOException failure) {
|
||||||
try {
|
try {
|
||||||
output.setLength(offset);
|
output.setLength(offset);
|
||||||
output.getChannel().force(true);
|
if (!unpublished) {
|
||||||
|
output.getChannel().force(true);
|
||||||
|
}
|
||||||
} catch (IOException rollback) {
|
} catch (IOException rollback) {
|
||||||
failure.addSuppressed(rollback);
|
failure.addSuppressed(rollback);
|
||||||
writeFailure = failure;
|
writeFailure = failure;
|
||||||
@@ -223,7 +241,7 @@ public final class SavedBiomeStore {
|
|||||||
throw failure;
|
throw failure;
|
||||||
}
|
}
|
||||||
} catch (IOException failure) {
|
} catch (IOException failure) {
|
||||||
if (durable) {
|
if (appendCompleted) {
|
||||||
writeFailure = failure;
|
writeFailure = failure;
|
||||||
}
|
}
|
||||||
throw failure;
|
throw failure;
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package art.arcane.iris.generation.mantle;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
|
||||||
|
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
||||||
|
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
|
||||||
|
import art.arcane.iris.world.storage.matter.PreObjectMatterCell;
|
||||||
|
import art.arcane.volmlib.util.hunk.bits.DataContainer;
|
||||||
|
import art.arcane.volmlib.util.hunk.storage.PaletteOrHunk;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||||
|
import art.arcane.volmlib.util.matter.IrisMatter;
|
||||||
|
import art.arcane.volmlib.util.matter.Matter;
|
||||||
|
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.spy;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class TerrainComposedFaceTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void registerMatter() {
|
||||||
|
IrisMatterSupport.ensureRegistered();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void allFacesMatchScalarCompositionAcrossSectionsAndCapturedNulls() {
|
||||||
|
Matter[] sections = {new IrisMatter(16, 16, 16), null, new IrisMatter(16, 16, 16)};
|
||||||
|
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||||
|
when(chunk.exists(anyInt())).thenAnswer(call -> (int) call.getArgument(0) != 1);
|
||||||
|
when(chunk.get(anyInt())).thenAnswer(call -> {
|
||||||
|
assertTrue(Thread.holdsLock(chunk));
|
||||||
|
return sections[(int) call.getArgument(0)];
|
||||||
|
});
|
||||||
|
MatterCavern natural = new MatterCavern(true, "natural", (byte) 0);
|
||||||
|
MatterCavern original = new MatterCavern(false, "original", (byte) 2);
|
||||||
|
HydrologyCaveCell[] hydrology = new HydrologyCaveCell[HydrologyCaveAction.values().length + 1];
|
||||||
|
for (int index = 1; index < hydrology.length; index++) {
|
||||||
|
hydrology[index] = new HydrologyCaveCell(HydrologyCaveAction.values()[index - 1], "river", "flooded");
|
||||||
|
}
|
||||||
|
PreObjectMatterCell[] journal = {null, PreObjectMatterCell.string(null),
|
||||||
|
PreObjectMatterCell.cavern(null), PreObjectMatterCell.cavern(original),
|
||||||
|
PreObjectMatterCell.hydrology(null), PreObjectMatterCell.hydrology(hydrology[1]),
|
||||||
|
PreObjectMatterCell.cavern(null).captureHydrology(null),
|
||||||
|
PreObjectMatterCell.cavern(original).captureHydrology(null)};
|
||||||
|
for (Matter matter : sections) {
|
||||||
|
if (matter == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
for (int y = 0; y < 16; y++) {
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
int index = x + y * 17 + z * 31;
|
||||||
|
matter.slice(MatterCavern.class).set(x, y, z, index % 3 == 0 ? null : natural);
|
||||||
|
matter.slice(HydrologyCaveCell.class).set(x, y, z, hydrology[index % hydrology.length]);
|
||||||
|
matter.slice(PreObjectMatterCell.class).set(x, y, z, journal[index % journal.length]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (TerrainMatterView.Face face : TerrainMatterView.Face.values()) {
|
||||||
|
MatterCavern[] copied = TerrainMatterView.getComposedFace(chunk, face, 43);
|
||||||
|
assertEquals(43 * 16, copied.length);
|
||||||
|
for (int y = 0; y < 43; y++) {
|
||||||
|
for (int offset = 0; offset < 16; offset++) {
|
||||||
|
int x = face == TerrainMatterView.Face.WEST ? 0 : face == TerrainMatterView.Face.EAST ? 15 : offset;
|
||||||
|
int z = face == TerrainMatterView.Face.NORTH ? 0 : face == TerrainMatterView.Face.SOUTH ? 15 : offset;
|
||||||
|
assertSame(TerrainMatterView.getComposedCavern(chunk, x, y, z), copied[y * 16 + offset]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void paletteFacesUseOneBulkReadAndDoNotLeakAcrossAbsentSlices() {
|
||||||
|
Matter first = new IrisMatter(16, 16, 16);
|
||||||
|
Matter second = new IrisMatter(16, 16, 16);
|
||||||
|
MatterCavern cavern = new MatterCavern(true, "cave", (byte) 0);
|
||||||
|
PaletteOrHunk<MatterCavern> slice = (PaletteOrHunk<MatterCavern>) first.<MatterCavern>slice(MatterCavern.class);
|
||||||
|
slice.setRaw(15, 4, 7, cavern);
|
||||||
|
DataContainer<MatterCavern> palette = spy(slice.palette());
|
||||||
|
slice.setPalette(palette);
|
||||||
|
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||||
|
when(chunk.exists(anyInt())).thenReturn(true);
|
||||||
|
when(chunk.get(0)).thenReturn(first);
|
||||||
|
when(chunk.get(1)).thenReturn(second);
|
||||||
|
doAnswer(call -> {
|
||||||
|
assertTrue(Thread.holdsLock(chunk));
|
||||||
|
return call.callRealMethod();
|
||||||
|
}).when(palette).copyTo(any(int[].class), any(MatterCavern[].class));
|
||||||
|
MatterCavern[] copied = TerrainMatterView.getComposedFace(chunk, TerrainMatterView.Face.EAST, 32);
|
||||||
|
assertSame(cavern, copied[4 * 16 + 7]);
|
||||||
|
for (int index = 16 * 16; index < copied.length; index++) {
|
||||||
|
assertNull(copied[index]);
|
||||||
|
}
|
||||||
|
verify(palette, times(1)).copyTo(any(int[].class), any(MatterCavern[].class));
|
||||||
|
verify(palette, never()).get(anyInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void nonstandardPaletteDimensionsRetainScalarCoordinatesAndBounds() {
|
||||||
|
for (int[] dimensions : new int[][]{{8, 16, 16}, {16, 8, 16}, {16, 16, 8}}) {
|
||||||
|
Matter matter = new IrisMatter(dimensions[0], dimensions[1], dimensions[2]);
|
||||||
|
MatterCavern cavern = new MatterCavern(true, "small", (byte) 0);
|
||||||
|
for (int x = 0; x < dimensions[0]; x++) {
|
||||||
|
for (int y = 0; y < dimensions[1]; y++) {
|
||||||
|
for (int z = 0; z < dimensions[2]; z++) {
|
||||||
|
if ((x + y + z) % 3 == 0) {
|
||||||
|
matter.slice(MatterCavern.class).set(x, y, z, cavern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||||
|
when(chunk.exists(0)).thenReturn(true);
|
||||||
|
when(chunk.get(0)).thenReturn(matter);
|
||||||
|
for (TerrainMatterView.Face face : TerrainMatterView.Face.values()) {
|
||||||
|
MatterCavern[] copied = TerrainMatterView.getComposedFace(chunk, face, 16);
|
||||||
|
for (int y = 0; y < 16; y++) {
|
||||||
|
for (int offset = 0; offset < 16; offset++) {
|
||||||
|
int x = face == TerrainMatterView.Face.WEST ? 0 : face == TerrainMatterView.Face.EAST ? 15 : offset;
|
||||||
|
int z = face == TerrainMatterView.Face.NORTH ? 0 : face == TerrainMatterView.Face.SOUTH ? 15 : offset;
|
||||||
|
assertSame(TerrainMatterView.getComposedCavern(chunk, x, y, z), copied[y * 16 + offset]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void absentFacesAndInvalidHeightsAreBounded() {
|
||||||
|
assertEquals(0, TerrainMatterView.getComposedFace(null, TerrainMatterView.Face.WEST, 0).length);
|
||||||
|
for (MatterCavern cavern : TerrainMatterView.getComposedFace(null, TerrainMatterView.Face.WEST, 17)) {
|
||||||
|
assertNull(cavern);
|
||||||
|
}
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> TerrainMatterView.getComposedFace(null, TerrainMatterView.Face.WEST, -1));
|
||||||
|
assertThrows(ArithmeticException.class, () -> TerrainMatterView.getComposedFace(null, TerrainMatterView.Face.WEST, Integer.MAX_VALUE));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package art.arcane.iris.generation.stage;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.mantle.TerrainMatterView;
|
||||||
|
import art.arcane.iris.world.IrisWorld;
|
||||||
|
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
|
||||||
|
import art.arcane.iris.world.storage.matter.PreObjectMatterCell;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
|
||||||
|
import art.arcane.volmlib.util.matter.IrisMatter;
|
||||||
|
import art.arcane.volmlib.util.matter.Matter;
|
||||||
|
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
|
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class IrisCarveBoundaryFacesTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void registerMatter() {
|
||||||
|
IrisMatterSupport.ensureRegistered();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void borderSnapshotsPreserveCornerOrderAndNeverNestChunkLocks() throws Exception {
|
||||||
|
MantleChunk<Matter>[] chunks = new MantleChunk[5];
|
||||||
|
Matter[][] matter = new Matter[5][2];
|
||||||
|
MatterCavern[] values = new MatterCavern[5];
|
||||||
|
for (int side = 0; side < chunks.length; side++) {
|
||||||
|
int selected = side;
|
||||||
|
chunks[side] = mock(MantleChunk.class);
|
||||||
|
matter[side][0] = new IrisMatter(16, 16, 16);
|
||||||
|
matter[side][1] = new IrisMatter(16, 16, 16);
|
||||||
|
values[side] = new MatterCavern(true, "side-" + side, (byte) 0);
|
||||||
|
when(chunks[side].exists(anyInt())).thenAnswer(call -> {
|
||||||
|
assertTrue(Thread.holdsLock(chunks[selected]));
|
||||||
|
for (int other = 0; other < chunks.length; other++) {
|
||||||
|
if (other != selected && chunks[other] != null) {
|
||||||
|
assertFalse(Thread.holdsLock(chunks[other]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
when(chunks[side].get(anyInt())).thenAnswer(call -> matter[selected][(int) call.getArgument(0)]);
|
||||||
|
if (side == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int y = 1; y < 32; y++) {
|
||||||
|
for (int offset = 0; offset < 16; offset++) {
|
||||||
|
int x = side == 1 ? 15 : side == 2 ? 0 : offset;
|
||||||
|
int z = side == 3 ? 15 : side == 4 ? 0 : offset;
|
||||||
|
matter[side][y >> 4].slice(MatterCavern.class).set(x, y & 15, z, values[side]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matter[0][0].slice(MatterCavern.class).set(0, 7, 0, values[0]);
|
||||||
|
matter[0][0].slice(MatterCavern.class).set(0, 5, 0, values[0]);
|
||||||
|
matter[0][0].slice(PreObjectMatterCell.class).set(0, 5, 0, PreObjectMatterCell.cavern(null));
|
||||||
|
Mantle<Matter> mantle = mock(Mantle.class, RETURNS_DEEP_STUBS);
|
||||||
|
TectonicPlate<Matter> plate = mock(TectonicPlate.class);
|
||||||
|
when(mantle.getLoadedRegions().get(anyLong())).thenReturn(plate);
|
||||||
|
when(plate.get(31, 0)).thenReturn(chunks[1]);
|
||||||
|
when(plate.get(1, 0)).thenReturn(chunks[2]);
|
||||||
|
when(plate.get(0, 31)).thenReturn(chunks[3]);
|
||||||
|
when(plate.get(0, 1)).thenReturn(chunks[4]);
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
when(engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(0).maxHeight(48).build());
|
||||||
|
IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS);
|
||||||
|
doReturn(engine).when(modifier).getEngine();
|
||||||
|
CarveWallBuffer walls = new CarveWallBuffer(64);
|
||||||
|
CarveColumnMask[] masks = new CarveColumnMask[256];
|
||||||
|
Arrays.setAll(masks, index -> new CarveColumnMask());
|
||||||
|
int[] surfaces = new int[256];
|
||||||
|
Arrays.fill(surfaces, 30);
|
||||||
|
Method method = IrisCarveModifier.class.getDeclaredMethod("addCrossChunkBoundaryWalls",
|
||||||
|
Mantle.class, MantleChunk.class, CarveWallBuffer.class, CarveColumnMask[].class,
|
||||||
|
int.class, int.class, int[].class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
method.invoke(modifier, mantle, chunks[0], walls, masks, 0, 0, surfaces);
|
||||||
|
CarveWallBuffer expectedWalls = new CarveWallBuffer(64);
|
||||||
|
for (int y = 1; y < 32; y++) {
|
||||||
|
for (int offset = 0; offset < 16; offset++) {
|
||||||
|
addScalarWall(expectedWalls, chunks[0], chunks[1], 0, y, offset, 15, offset);
|
||||||
|
addScalarWall(expectedWalls, chunks[0], chunks[2], 15, y, offset, 0, offset);
|
||||||
|
addScalarWall(expectedWalls, chunks[0], chunks[3], offset, y, 0, offset, 15);
|
||||||
|
addScalarWall(expectedWalls, chunks[0], chunks[4], offset, y, 15, offset, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int y = 1; y < 32; y++) {
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
MatterCavern expected = expectedWalls.get(x, y, z);
|
||||||
|
assertSame("voxel " + x + "," + y + "," + z, expected, walls.get(x, y, z));
|
||||||
|
assertTrue(masks[x * 16 + z].contains(y) == (expected != null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addScalarWall(CarveWallBuffer walls, MantleChunk<Matter> chunk,
|
||||||
|
MantleChunk<Matter> neighbor, int x, int y, int z,
|
||||||
|
int neighborX, int neighborZ) {
|
||||||
|
if (TerrainMatterView.getComposedCavern(chunk, x, y, z) != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MatterCavern value = TerrainMatterView.getComposedCavern(neighbor, neighborX, y, neighborZ);
|
||||||
|
if (value != null) {
|
||||||
|
walls.put(x, y, z, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+239
@@ -0,0 +1,239 @@
|
|||||||
|
package art.arcane.iris.generation.stage;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.block.B;
|
||||||
|
import art.arcane.iris.generation.concurrent.BurstExecutor;
|
||||||
|
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||||
|
import art.arcane.iris.generation.decoration.IrisProceduralBlocks;
|
||||||
|
import art.arcane.iris.generation.decoration.IrisSpeleothems;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.runtime.EngineMetrics;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.testsupport.PlatformBinding;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import art.arcane.volmlib.util.hunk.Hunk;
|
||||||
|
import art.arcane.volmlib.util.hunk.storage.ArrayHunk;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class IrisPerfectionModifierColumnTest {
|
||||||
|
@Rule
|
||||||
|
public final PlatformBinding platform = PlatformBinding.mockPlatform();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void randomColumnsMatchOriginalGlobalPasses() {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
Random random = new Random(82714L);
|
||||||
|
NativeBlockState[] palette = {null, fixture.air, fixture.water, fixture.stone,
|
||||||
|
fixture.decorant, fixture.half, fixture.spike(true, false), fixture.spike(false, true)};
|
||||||
|
for (int trial = 0; trial < 8; trial++) {
|
||||||
|
Hunk<NativeBlockState> source = Hunk.newArrayHunk(16, 32, 16);
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
for (int y = 0; y < 32; y++) {
|
||||||
|
source.setRaw(x, y, z, y == 0 ? fixture.stone : palette[random.nextInt(palette.length)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertParity(fixture, source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unrelatedTallColumnsAreScannedOnceDuringRepeatedCleanup() {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
CountingHunk output = new CountingHunk(768);
|
||||||
|
for (int y = 0; y < 768; y++) {
|
||||||
|
output.setRaw(1, y, 0, fixture.stone);
|
||||||
|
}
|
||||||
|
for (int y = 1; y <= 24; y++) {
|
||||||
|
output.setRaw(0, y, 0, fixture.decorant);
|
||||||
|
}
|
||||||
|
fixture.modifier.onModify(0, 0, output, false, null);
|
||||||
|
assertTrue("clean column must not be scanned for each removed decorant", output.cleanColumnReads < 780);
|
||||||
|
for (int y = 1; y <= 24; y++) {
|
||||||
|
assertTrue(output.getRaw(0, y, 0).isAir());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void pairedHalvesMergedSpikesAndWaterloggedSupportsMatchOriginal() {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
Hunk<NativeBlockState> source = Hunk.newArrayHunk(16, 16, 16);
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
source.setRaw(x, 1, 0, x % 2 == 0 ? fixture.stone : fixture.water);
|
||||||
|
source.setRaw(x, 8, 0, x % 3 == 0 ? fixture.stone : fixture.decorant);
|
||||||
|
for (int y = 2; y <= 4; y++) {
|
||||||
|
source.setRaw(x, y, 0, fixture.spike(true, x % 2 == 0));
|
||||||
|
}
|
||||||
|
for (int y = 5; y <= 7; y++) {
|
||||||
|
source.setRaw(x, y, 0, fixture.spike(false, x % 2 != 0));
|
||||||
|
}
|
||||||
|
source.setRaw(x, 10, 0, fixture.half);
|
||||||
|
source.setRaw(x, 11, 0, fixture.decorant);
|
||||||
|
}
|
||||||
|
assertParity(fixture, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertParity(Fixture fixture, Hunk<NativeBlockState> source) {
|
||||||
|
Hunk<NativeBlockState> expected = Hunk.newArrayHunk(16, source.getHeight(), 16);
|
||||||
|
Hunk<NativeBlockState> actual = Hunk.newArrayHunk(16, source.getHeight(), 16);
|
||||||
|
source.iterateSync((x, y, z, state) -> {
|
||||||
|
expected.setRaw(x, y, z, state);
|
||||||
|
actual.setRaw(x, y, z, state);
|
||||||
|
});
|
||||||
|
original(expected, fixture.air, fixture.water);
|
||||||
|
fixture.modifier.onModify(0, 0, actual, false, null);
|
||||||
|
expected.iterateSync((x, y, z, state) -> assertEquals("voxel " + x + "," + y + "," + z,
|
||||||
|
state == null ? null : state.key(), actual.getRaw(x, y, z) == null ? null : actual.getRaw(x, y, z).key()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void original(Hunk<NativeBlockState> output, NativeBlockState air, NativeBlockState water) {
|
||||||
|
boolean changed;
|
||||||
|
do {
|
||||||
|
changed = false;
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
List<Integer> surfaces = new ArrayList<>();
|
||||||
|
int top = 0;
|
||||||
|
for (int y = output.getHeight() - 1; y >= 0; y--) {
|
||||||
|
NativeBlockState block = output.get(x, y, z);
|
||||||
|
if (block != null && !B.isAir(block) && !B.isFluid(block)) {
|
||||||
|
top = y;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean inside = true;
|
||||||
|
surfaces.add(top);
|
||||||
|
for (int y = top; y >= 0; y--) {
|
||||||
|
NativeBlockState block = output.get(x, y, z);
|
||||||
|
if (IrisSpeleothems.isSpike(block)) {
|
||||||
|
block = IrisPerfectionModifier.normalizeSpike(block, output, x, z, y, air, water);
|
||||||
|
}
|
||||||
|
boolean now = block != null && !(B.isAir(block) || B.isFluid(block));
|
||||||
|
if (now != inside) {
|
||||||
|
inside = now;
|
||||||
|
if (inside) {
|
||||||
|
surfaces.add(y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int y : surfaces) {
|
||||||
|
NativeBlockState tip = output.get(x, y, z);
|
||||||
|
if (tip == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
boolean remove = false;
|
||||||
|
boolean removeBelow = false;
|
||||||
|
if (B.isDecorant(tip)) {
|
||||||
|
NativeBlockState below = output.get(x, y - 1, z);
|
||||||
|
if (below == null || !B.canPlaceOnto(tip, below)) {
|
||||||
|
remove = true;
|
||||||
|
} else if (IrisProceduralBlocks.hasProperty(below, "half")) {
|
||||||
|
NativeBlockState support = output.get(x, y - 2, z);
|
||||||
|
if (support == null || !B.canPlaceOnto(below, support)) {
|
||||||
|
remove = true;
|
||||||
|
removeBelow = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (remove) {
|
||||||
|
changed = true;
|
||||||
|
output.set(x, y, z, air);
|
||||||
|
if (removeBelow) {
|
||||||
|
output.set(x, y - 1, z, air);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class Fixture {
|
||||||
|
private final Map<String, NativeBlockState> states = new HashMap<>();
|
||||||
|
private final NativeBlockState air = state("minecraft:air");
|
||||||
|
private final NativeBlockState water = state("minecraft:water");
|
||||||
|
private final NativeBlockState stone = state("minecraft:stone");
|
||||||
|
private final NativeBlockState decorant = state("minecraft:poppy");
|
||||||
|
private final NativeBlockState half = state("minecraft:tall_grass[half=lower]");
|
||||||
|
private final IrisPerfectionModifier modifier;
|
||||||
|
|
||||||
|
private Fixture() {
|
||||||
|
when(platform.registries().block("AIR")).thenReturn(air);
|
||||||
|
when(platform.registries().block("WATER")).thenReturn(water);
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
when(engine.getDimension()).thenReturn(new IrisDimension());
|
||||||
|
when(engine.getMetrics()).thenReturn(mock(EngineMetrics.class, RETURNS_DEEP_STUBS));
|
||||||
|
MultiBurst pool = mock(MultiBurst.class);
|
||||||
|
when(engine.burst()).thenReturn(pool);
|
||||||
|
when(pool.burst(false)).thenAnswer(call -> {
|
||||||
|
BurstExecutor burst = new BurstExecutor(mock(ExecutorService.class), 16);
|
||||||
|
burst.setMulticore(false);
|
||||||
|
return burst;
|
||||||
|
});
|
||||||
|
modifier = new IrisPerfectionModifier(engine);
|
||||||
|
}
|
||||||
|
|
||||||
|
private NativeBlockState spike(boolean upward, boolean waterlogged) {
|
||||||
|
return state("minecraft:pointed_dripstone[thickness=tip,vertical_direction="
|
||||||
|
+ (upward ? "up" : "down") + ",waterlogged=" + waterlogged + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
private NativeBlockState state(String key) {
|
||||||
|
NativeBlockState existing = states.get(key);
|
||||||
|
if (existing != null) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
NativeBlockState block = mock(NativeBlockState.class);
|
||||||
|
states.put(key, block);
|
||||||
|
when(block.key()).thenReturn(key);
|
||||||
|
when(block.isAir()).thenReturn(key.equals("minecraft:air"));
|
||||||
|
when(block.isFluid()).thenReturn(key.equals("minecraft:water"));
|
||||||
|
when(block.isWater()).thenReturn(key.equals("minecraft:water"));
|
||||||
|
when(block.isWaterLogged()).thenReturn(key.contains("waterlogged=true"));
|
||||||
|
when(block.isDecorant()).thenReturn(key.equals("minecraft:poppy") || key.startsWith("minecraft:tall_grass"));
|
||||||
|
when(block.canPlaceOnto(any())).thenAnswer(call -> "minecraft:tall_grass[half=lower]".equals(call.<NativeBlockState>getArgument(0).key()));
|
||||||
|
BlockData handle = mock(BlockData.class);
|
||||||
|
when(block.nativeHandle()).thenReturn(handle);
|
||||||
|
when(handle.isFaceSturdy(any(), any())).thenReturn(key.equals("minecraft:stone"));
|
||||||
|
when(block.withProperty(anyString(), anyString())).thenAnswer(call -> {
|
||||||
|
String property = call.getArgument(0);
|
||||||
|
String value = call.getArgument(1);
|
||||||
|
return state(key.replaceAll(property + "=[^,\\]]+", property + "=" + value));
|
||||||
|
});
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CountingHunk extends ArrayHunk<NativeBlockState> {
|
||||||
|
private int cleanColumnReads;
|
||||||
|
|
||||||
|
private CountingHunk(int height) {
|
||||||
|
super(16, height, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NativeBlockState getRaw(int x, int y, int z) {
|
||||||
|
if (x == 1 && z == 0) {
|
||||||
|
cleanColumnReads++;
|
||||||
|
}
|
||||||
|
return super.getRaw(x, y, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
package art.arcane.iris.world.history;
|
||||||
|
|
||||||
|
import art.arcane.iris.world.storage.Durability;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedConstruction;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
|
import java.nio.file.FileAlreadyExistsException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.AdditionalAnswers.delegatesTo;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockConstruction;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.withSettings;
|
||||||
|
|
||||||
|
public final class GenerationUnpublishedHistoryTest extends GenerationHistorySupport {
|
||||||
|
@Test
|
||||||
|
public void unpublishedHistoryRequiresNewRootAndReopeningRestoresImmediateBiomeWrites() throws Exception {
|
||||||
|
Path root = temporaryFolder.getRoot().toPath().resolve("unpublished");
|
||||||
|
Path pack = createPack("unpublished-pack", "alpha");
|
||||||
|
GenerationHistory.FreshCreation creation = new GenerationHistory.FreshCreation(root, pack, fingerprint(pack),
|
||||||
|
42L, contract(), GenerationRegistryContract.empty());
|
||||||
|
GenerationHistory history = GenerationHistory.createUnpublished(creation);
|
||||||
|
assertThrows(FileAlreadyExistsException.class, () -> GenerationHistory.createUnpublished(creation));
|
||||||
|
history.savedBiomes().claimAndPersist(biomes(0));
|
||||||
|
assertBiomeAppendForces(history.savedBiomes(), root, 1, 0);
|
||||||
|
GenerationHistory reopened = GenerationHistory.open(root);
|
||||||
|
assertEquals(biomes(1), reopened.savedBiomes().get(1, 0).orElseThrow());
|
||||||
|
assertBiomeAppendForces(reopened.savedBiomes(), root, 2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void semanticAppendDeferralDoesNotChangeReplayOrNormalOpenDurability() throws Exception {
|
||||||
|
Path root = temporaryFolder.newFolder("semantics").toPath();
|
||||||
|
GenerationSemanticIndex.initialize(root);
|
||||||
|
GenerationSemanticIndex staged = GenerationSemanticIndex.loadRequired(root, true);
|
||||||
|
staged.claimAndPersist(semantics(0));
|
||||||
|
try (MockedStatic<Durability> forces = mockStatic(Durability.class, CALLS_REAL_METHODS)) {
|
||||||
|
assertTrue(staged.claimAndPersist(semantics(1)));
|
||||||
|
forces.verify(() -> Durability.force(any(FileChannel.class)), times(0));
|
||||||
|
}
|
||||||
|
GenerationSemanticIndex reopened = GenerationSemanticIndex.loadRequired(root);
|
||||||
|
assertEquals(semantics(1), reopened.get(1, 0).orElseThrow());
|
||||||
|
try (MockedStatic<Durability> forces = mockStatic(Durability.class, CALLS_REAL_METHODS)) {
|
||||||
|
assertTrue(reopened.claimAndPersist(semantics(2)));
|
||||||
|
forces.verify(() -> Durability.force(any(FileChannel.class)), times(1));
|
||||||
|
}
|
||||||
|
reopened.compactJournals();
|
||||||
|
assertEquals(3, GenerationSemanticIndex.loadRequired(root).recordCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unpublishedBiomeWriteFailureStillRollsBackAndDoesNotPublishClaim() throws Exception {
|
||||||
|
Path root = temporaryFolder.newFolder("failed-biomes").toPath();
|
||||||
|
SavedBiomeStore store = SavedBiomeStore.open(root, true);
|
||||||
|
store.claimAndPersist(biomes(0));
|
||||||
|
Path region = region(root);
|
||||||
|
byte[] before = Files.readAllBytes(region);
|
||||||
|
try (RandomAccessFile actual = new RandomAccessFile(region.toFile(), "rw")) {
|
||||||
|
FileChannel channel = mock(FileChannel.class, delegatesTo(actual.getChannel()));
|
||||||
|
doThrow(new IOException("Expected staged write failure")).when(channel).write(any(ByteBuffer[].class), anyInt(), anyInt());
|
||||||
|
try (MockedConstruction<RandomAccessFile> ignored = mockConstruction(RandomAccessFile.class,
|
||||||
|
withSettings().defaultAnswer(delegatesTo(actual)),
|
||||||
|
(file, context) -> doReturn(channel).when(file).getChannel())) {
|
||||||
|
assertThrows(IOException.class, () -> store.claimAndPersist(biomes(1)));
|
||||||
|
}
|
||||||
|
verify(channel, times(0)).force(true);
|
||||||
|
}
|
||||||
|
assertArrayEquals(before, Files.readAllBytes(region));
|
||||||
|
assertTrue(store.get(1, 0).isEmpty());
|
||||||
|
assertFalse(SavedBiomeStore.open(root).get(1, 0).isPresent());
|
||||||
|
assertTrue(store.claimAndPersist(biomes(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertBiomeAppendForces(SavedBiomeStore store, Path root, int x, int count) throws Exception {
|
||||||
|
try (RandomAccessFile actual = new RandomAccessFile(region(root).toFile(), "rw")) {
|
||||||
|
FileChannel channel = mock(FileChannel.class, delegatesTo(actual.getChannel()));
|
||||||
|
try (MockedConstruction<RandomAccessFile> ignored = mockConstruction(RandomAccessFile.class,
|
||||||
|
withSettings().defaultAnswer(delegatesTo(actual)),
|
||||||
|
(file, context) -> doReturn(channel).when(file).getChannel())) {
|
||||||
|
assertTrue(store.claimAndPersist(biomes(x)));
|
||||||
|
}
|
||||||
|
verify(channel, times(count)).force(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path region(Path root) {
|
||||||
|
return root.resolve("iris/generation/biomes/r.0.0.ibio");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ChunkGenerationSemantics semantics(int x) {
|
||||||
|
return ChunkGenerationSemantics.builder(x, 0, 1L).addSurfaceBiome("biome/main").seal().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SavedBiomeChunk biomes(int x) {
|
||||||
|
SavedBiomeChunk.Cell cell = new SavedBiomeChunk.Cell(1L, "biome/main", "region/main");
|
||||||
|
SavedBiomeChunk.Column column = new SavedBiomeChunk.Column(cell, cell,
|
||||||
|
List.of(new SavedBiomeChunk.Span(-64, 320, cell)));
|
||||||
|
SavedBiomeChunk.Builder builder = SavedBiomeChunk.builder(new SavedBiomeChunk.Header(x, 0, 1L, -64, 384));
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
for (int localX = 0; localX < 16; localX++) {
|
||||||
|
builder.column(localX, z, column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import java.io.RandomAccessFile;
|
|||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.nio.channels.FileChannel;
|
import java.nio.channels.FileChannel;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
@@ -35,6 +36,8 @@ import static org.junit.Assert.assertSame;
|
|||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.AdditionalAnswers.delegatesTo;
|
import static org.mockito.AdditionalAnswers.delegatesTo;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.doReturn;
|
import static org.mockito.Mockito.doReturn;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
@@ -45,6 +48,74 @@ public class SavedBiomeStoreBatchTest {
|
|||||||
@Rule
|
@Rule
|
||||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void gatheringWritesHandleZeroAndPartialProgressAcrossQueuedFrames() throws Exception {
|
||||||
|
Path root = temporaryFolder.newFolder().toPath();
|
||||||
|
SavedBiomeStore store = SavedBiomeStore.open(root);
|
||||||
|
store.claimAndPersist(chunk(0, 3L));
|
||||||
|
BatchControl control = new BatchControl();
|
||||||
|
control.maximumWriteBytes = 11;
|
||||||
|
control.zeroFirstWrite = true;
|
||||||
|
ExecutorService workers = Executors.newFixedThreadPool(4);
|
||||||
|
List<SavedBiomeChunk> claims = List.of(chunk(1, 3L), chunk(2, 3L), chunk(3, 3L));
|
||||||
|
try {
|
||||||
|
List<Future<ClaimResult>> results = enqueue(store, root, control, workers, claims);
|
||||||
|
assertTrue(control.forcing.await(5, TimeUnit.SECONDS));
|
||||||
|
assertEquals(3, control.maximumGathered.get());
|
||||||
|
assertTrue(control.writes.get() > claims.size());
|
||||||
|
for (SavedBiomeChunk claim : claims) {
|
||||||
|
assertTrue(store.cached(claim.chunkX(), claim.chunkZ()).isEmpty());
|
||||||
|
}
|
||||||
|
control.release.countDown();
|
||||||
|
for (Future<ClaimResult> result : results) {
|
||||||
|
ClaimResult completed = result.get(5, TimeUnit.SECONDS);
|
||||||
|
assertTrue(completed.persisted());
|
||||||
|
assertNull(completed.failure());
|
||||||
|
}
|
||||||
|
assertEquals(1, control.forces.get());
|
||||||
|
SavedBiomeStore reopened = SavedBiomeStore.open(root);
|
||||||
|
for (SavedBiomeChunk claim : claims) {
|
||||||
|
assertEquals(claim, reopened.get(claim.chunkX(), claim.chunkZ()).orElseThrow());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
control.release.countDown();
|
||||||
|
workers.shutdownNow();
|
||||||
|
assertTrue(workers.awaitTermination(10, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void partialGatherFailureRollsBackWholeBatchBeforePublication() throws Exception {
|
||||||
|
Path root = temporaryFolder.newFolder().toPath();
|
||||||
|
SavedBiomeStore store = SavedBiomeStore.open(root);
|
||||||
|
store.claimAndPersist(chunk(0, 3L));
|
||||||
|
Path region = root.resolve("iris/generation/biomes/r.0.0.ibio");
|
||||||
|
byte[] original = Files.readAllBytes(region);
|
||||||
|
BatchControl control = new BatchControl();
|
||||||
|
control.maximumWriteBytes = 11;
|
||||||
|
control.writeFailure = new IOException("Partial gathered append failed");
|
||||||
|
control.release.countDown();
|
||||||
|
ExecutorService workers = Executors.newFixedThreadPool(4);
|
||||||
|
List<SavedBiomeChunk> claims = List.of(chunk(1, 3L), chunk(2, 3L), chunk(3, 3L));
|
||||||
|
try {
|
||||||
|
for (Future<ClaimResult> result : enqueue(store, root, control, workers, claims)) {
|
||||||
|
ClaimResult completed = result.get(5, TimeUnit.SECONDS);
|
||||||
|
assertFalse(completed.persisted());
|
||||||
|
assertSame(control.writeFailure, completed.failure());
|
||||||
|
}
|
||||||
|
assertEquals(2, control.writes.get());
|
||||||
|
assertEquals(1, control.forces.get());
|
||||||
|
assertArrayEquals(original, Files.readAllBytes(region));
|
||||||
|
for (SavedBiomeChunk claim : claims) {
|
||||||
|
assertTrue(store.cached(claim.chunkX(), claim.chunkZ()).isEmpty());
|
||||||
|
assertTrue(store.claimAndPersist(claim));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
workers.shutdownNow();
|
||||||
|
assertTrue(workers.awaitTermination(10, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void independentOriginRegionsPersistWhileOneRegionRemainsLocked() throws Exception {
|
public void independentOriginRegionsPersistWhileOneRegionRemainsLocked() throws Exception {
|
||||||
Path root = temporaryFolder.newFolder().toPath();
|
Path root = temporaryFolder.newFolder().toPath();
|
||||||
@@ -277,6 +348,33 @@ public class SavedBiomeStoreBatchTest {
|
|||||||
try (RandomAccessFile actual = new RandomAccessFile(
|
try (RandomAccessFile actual = new RandomAccessFile(
|
||||||
root.resolve("iris/generation/biomes/r.0.0.ibio").toFile(), "rw")) {
|
root.resolve("iris/generation/biomes/r.0.0.ibio").toFile(), "rw")) {
|
||||||
FileChannel channel = mock(FileChannel.class, delegatesTo(actual.getChannel()));
|
FileChannel channel = mock(FileChannel.class, delegatesTo(actual.getChannel()));
|
||||||
|
if (control.maximumWriteBytes > 0) {
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
ByteBuffer[] records = invocation.getArgument(0);
|
||||||
|
int offset = invocation.getArgument(1);
|
||||||
|
int count = invocation.getArgument(2);
|
||||||
|
control.maximumGathered.accumulateAndGet(count, Math::max);
|
||||||
|
int write = control.writes.incrementAndGet();
|
||||||
|
if (control.zeroFirstWrite && write == 1) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
if (control.writeFailure != null && write == 2) {
|
||||||
|
throw control.writeFailure;
|
||||||
|
}
|
||||||
|
long written = 0L;
|
||||||
|
for (int index = offset; index < offset + count && written < control.maximumWriteBytes; index++) {
|
||||||
|
ByteBuffer record = records[index];
|
||||||
|
int previousLimit = record.limit();
|
||||||
|
record.limit(record.position() + Math.min(record.remaining(), control.maximumWriteBytes - (int) written));
|
||||||
|
try {
|
||||||
|
written += actual.getChannel().write(record);
|
||||||
|
} finally {
|
||||||
|
record.limit(previousLimit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return written;
|
||||||
|
}).when(channel).write(any(ByteBuffer[].class), anyInt(), anyInt());
|
||||||
|
}
|
||||||
doAnswer(invocation -> {
|
doAnswer(invocation -> {
|
||||||
int force = control.forces.incrementAndGet();
|
int force = control.forces.incrementAndGet();
|
||||||
if (force == 1) {
|
if (force == 1) {
|
||||||
@@ -331,6 +429,11 @@ public class SavedBiomeStoreBatchTest {
|
|||||||
private final CountDownLatch forcing = new CountDownLatch(1);
|
private final CountDownLatch forcing = new CountDownLatch(1);
|
||||||
private final CountDownLatch release = new CountDownLatch(1);
|
private final CountDownLatch release = new CountDownLatch(1);
|
||||||
private final AtomicInteger forces = new AtomicInteger();
|
private final AtomicInteger forces = new AtomicInteger();
|
||||||
|
private int maximumWriteBytes;
|
||||||
|
private boolean zeroFirstWrite;
|
||||||
|
private IOException writeFailure;
|
||||||
|
private final AtomicInteger writes = new AtomicInteger();
|
||||||
|
private final AtomicInteger maximumGathered = new AtomicInteger();
|
||||||
private IOException failure;
|
private IOException failure;
|
||||||
private RuntimeException uncheckedFailure;
|
private RuntimeException uncheckedFailure;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import java.io.DataOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.RandomAccessFile;
|
import java.io.RandomAccessFile;
|
||||||
import java.nio.channels.FileChannel;
|
import java.nio.channels.FileChannel;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -103,7 +104,8 @@ public class SavedBiomeStoreWriteTest {
|
|||||||
(file, context) -> doReturn(channel).when(file).getChannel())) {
|
(file, context) -> doReturn(channel).when(file).getChannel())) {
|
||||||
boolean result = store.claimAndPersist(chunk(1, 3L));
|
boolean result = store.claimAndPersist(chunk(1, 3L));
|
||||||
assertEquals(1, files.constructed().size());
|
assertEquals(1, files.constructed().size());
|
||||||
verify(files.constructed().getFirst()).write(any(byte[].class));
|
verify(channel).write(any(ByteBuffer[].class), anyInt(), anyInt());
|
||||||
|
verify(files.constructed().getFirst(), never()).write(any(byte[].class));
|
||||||
verify(files.constructed().getFirst(), never()).writeInt(anyInt());
|
verify(files.constructed().getFirst(), never()).writeInt(anyInt());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,17 @@ final class HeadlessGenerationHistorySession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static GenerationHistory create(RealPackProbeSupport.HistoryRequest request, IDataFixer fixer) throws IOException {
|
static GenerationHistory create(RealPackProbeSupport.HistoryRequest request, IDataFixer fixer) throws IOException {
|
||||||
|
GenerationHistory.FreshCreation creation = capture(request, fixer);
|
||||||
|
Files.createDirectories(creation.dimensionRoot());
|
||||||
|
return GenerationHistory.create(creation.dimensionRoot(), creation.packSource(), creation.packFingerprint(),
|
||||||
|
creation.worldSeed(), creation.dimensionContract(), creation.registryContract());
|
||||||
|
}
|
||||||
|
|
||||||
|
static GenerationHistory createUnpublished(RealPackProbeSupport.HistoryRequest request, IDataFixer fixer) throws IOException {
|
||||||
|
return GenerationHistory.createUnpublished(capture(request, fixer));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenerationHistory.FreshCreation capture(RealPackProbeSupport.HistoryRequest request, IDataFixer fixer) throws IOException {
|
||||||
IrisData data = request.data();
|
IrisData data = request.data();
|
||||||
IrisDimension dimension = request.dimension();
|
IrisDimension dimension = request.dimension();
|
||||||
PlatformRegistries registries = IrisPlatforms.get().registries();
|
PlatformRegistries registries = IrisPlatforms.get().registries();
|
||||||
@@ -34,8 +45,7 @@ final class HeadlessGenerationHistorySession {
|
|||||||
data.getDataFolder().getName(), dimension.getLoadKey(), dimension.getDimensionTypeKey());
|
data.getDataFolder().getName(), dimension.getLoadKey(), dimension.getDimensionTypeKey());
|
||||||
GenerationEpoch.DimensionContract dimensionContract = GenerationEpochContractFactory.create(
|
GenerationEpoch.DimensionContract dimensionContract = GenerationEpochContractFactory.create(
|
||||||
dimension, dimension.getLoadKey(), dimensionTypeKey);
|
dimension, dimension.getLoadKey(), dimensionTypeKey);
|
||||||
Files.createDirectories(request.worldRoot());
|
return new GenerationHistory.FreshCreation(request.worldRoot(), pack, fingerprint, request.seed(),
|
||||||
return GenerationHistory.create(request.worldRoot(), pack, fingerprint, request.seed(),
|
|
||||||
dimensionContract, registryContract);
|
dimensionContract, registryContract);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user