Drive idle rings from completions and preserve dimension anchors
This commit is contained in:
parent
dc2f9408a1
commit
b790ecef6b
5 changed files with 350 additions and 231 deletions
|
|
@ -3,10 +3,14 @@ package dev.quorate.tune;
|
||||||
import com.mojang.logging.LogUtils;
|
import com.mojang.logging.LogUtils;
|
||||||
|
|
||||||
import net.minecraft.core.HolderLookup;
|
import net.minecraft.core.HolderLookup;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.nbt.CompoundTag;
|
import net.minecraft.nbt.CompoundTag;
|
||||||
import net.minecraft.nbt.ListTag;
|
import net.minecraft.nbt.ListTag;
|
||||||
import net.minecraft.nbt.Tag;
|
import net.minecraft.nbt.Tag;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
import net.minecraft.world.level.saveddata.SavedData;
|
import net.minecraft.world.level.saveddata.SavedData;
|
||||||
import net.minecraft.world.level.storage.LevelResource;
|
import net.minecraft.world.level.storage.LevelResource;
|
||||||
|
|
@ -14,7 +18,7 @@ import net.neoforged.fml.ModContainer;
|
||||||
import net.neoforged.fml.config.ModConfig;
|
import net.neoforged.fml.config.ModConfig;
|
||||||
import net.neoforged.neoforge.common.ModConfigSpec;
|
import net.neoforged.neoforge.common.ModConfigSpec;
|
||||||
import net.neoforged.neoforge.common.NeoForge;
|
import net.neoforged.neoforge.common.NeoForge;
|
||||||
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
|
import net.neoforged.neoforge.event.entity.EntityLeaveLevelEvent;
|
||||||
import net.neoforged.neoforge.event.server.ServerStartedEvent;
|
import net.neoforged.neoforge.event.server.ServerStartedEvent;
|
||||||
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
|
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
|
||||||
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
||||||
|
|
@ -30,8 +34,8 @@ import org.slf4j.Logger;
|
||||||
|
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CompletionException;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
@ -66,74 +70,77 @@ public final class IdleRings {
|
||||||
else LOG.warn("Idle rings require Chunky API version 0");
|
else LOG.warn("Idle rings require Chunky API version 0");
|
||||||
});
|
});
|
||||||
NeoForge.EVENT_BUS.addListener(
|
NeoForge.EVENT_BUS.addListener(
|
||||||
(PlayerEvent.PlayerLoggedOutEvent event) -> {
|
(EntityLeaveLevelEvent event) -> {
|
||||||
if (current != null && event.getEntity() instanceof ServerPlayer player) {
|
if (current != null
|
||||||
current.visit(player);
|
&& event.getLevel() instanceof ServerLevel level
|
||||||
|
&& level.getServer() == current.server
|
||||||
|
&& event.getEntity() instanceof ServerPlayer player) {
|
||||||
|
current.visit(player, level.dimension().location().toString());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
NeoForge.EVENT_BUS.addListener(
|
NeoForge.EVENT_BUS.addListener(
|
||||||
(ServerTickEvent.Post event) -> {
|
(ServerTickEvent.Post event) -> {
|
||||||
if (current != null
|
if (current != null && current.server == event.getServer()) {
|
||||||
&& current.server == event.getServer()
|
current.run(current::tick);
|
||||||
&& event.getServer().getTickCount() % 20 == 0) current.tick();
|
}
|
||||||
});
|
});
|
||||||
NeoForge.EVENT_BUS.addListener(
|
NeoForge.EVENT_BUS.addListener(
|
||||||
(ServerStoppingEvent event) -> {
|
(ServerStoppingEvent event) -> {
|
||||||
if (current != null && current.server == event.getServer()) {
|
if (current != null && current.server == event.getServer()) {
|
||||||
event.getServer().getPlayerList().getPlayers().forEach(current::visit);
|
current.server
|
||||||
|
.getPlayerList()
|
||||||
|
.getPlayers()
|
||||||
|
.forEach(
|
||||||
|
player ->
|
||||||
|
current.visit(
|
||||||
|
player,
|
||||||
|
player.serverLevel()
|
||||||
|
.dimension()
|
||||||
|
.location()
|
||||||
|
.toString()));
|
||||||
current.close();
|
current.close();
|
||||||
current = null;
|
current = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum Phase {
|
private sealed interface State {}
|
||||||
RUNNING,
|
|
||||||
PAUSED,
|
private enum Idle implements State {
|
||||||
CANCELLED,
|
READY,
|
||||||
READY
|
YIELDED,
|
||||||
|
FAILED,
|
||||||
|
CLOSED
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class Job {
|
private record Scanning(Rings.Next next) implements State {}
|
||||||
final Rings.Next next;
|
|
||||||
final Selection selection;
|
|
||||||
GenerationTask task;
|
|
||||||
volatile Phase phase;
|
|
||||||
CompletableFuture<Void> lod;
|
|
||||||
|
|
||||||
Job(Rings.Next next, Selection selection, GenerationTask task, Phase phase) {
|
private record Terrain(Rings.Next next, GenerationTask task) implements State {}
|
||||||
this.next = next;
|
|
||||||
this.selection = selection;
|
|
||||||
this.task = task;
|
|
||||||
this.phase = phase;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean matches(Selection other) {
|
private record Paused(Rings.Next next, GenerationTask task, long until) implements State {}
|
||||||
return matches(selection, other);
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean matches(Selection selection, Selection other) {
|
private record Backfill(Rings.Next next) implements State {}
|
||||||
return selection.world().getName().equals(other.world().getName())
|
|
||||||
&& selection.shape().equals(other.shape())
|
|
||||||
&& selection.centerX() == other.centerX()
|
|
||||||
&& selection.centerZ() == other.centerZ()
|
|
||||||
&& selection.radiusX() == other.radiusX()
|
|
||||||
&& selection.radiusZ() == other.radiusZ()
|
|
||||||
&& selection.pattern().getType().equals(other.pattern().getType())
|
|
||||||
&& selection.pattern().getValue().equals(other.pattern().getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized void observe(GenerationTask other, boolean finished) {
|
private record Waiting(long until, double budget) implements State {}
|
||||||
if (phase == Phase.PAUSED && task != other && matches(other.getSelection())) {
|
|
||||||
task = other;
|
private record Checkpoint(Rings.Next next, boolean paused) {}
|
||||||
phase = Phase.RUNNING;
|
|
||||||
}
|
private record Update(
|
||||||
if (task != other || phase == Phase.CANCELLED) return;
|
GenerationTask task,
|
||||||
if (finished && !other.isCancelled()) phase = Phase.PAUSED;
|
boolean finished,
|
||||||
else if (finished && other.getChunkIterator().hasNext()) phase = Phase.CANCELLED;
|
boolean cancelled,
|
||||||
// Chunky's finish event precedes in-flight chunks; complete progress includes them.
|
boolean remaining,
|
||||||
else if (other.getProgress().isComplete()) phase = Phase.READY;
|
boolean complete) {}
|
||||||
}
|
|
||||||
|
private static boolean matches(Selection a, Selection b) {
|
||||||
|
return a.world().getName().equals(b.world().getName())
|
||||||
|
&& a.shape().equals(b.shape())
|
||||||
|
&& a.centerX() == b.centerX()
|
||||||
|
&& a.centerZ() == b.centerZ()
|
||||||
|
&& a.radiusX() == b.radiusX()
|
||||||
|
&& a.radiusZ() == b.radiusZ()
|
||||||
|
&& a.pattern().getType().equals(b.pattern().getType())
|
||||||
|
&& a.pattern().getValue().equals(b.pattern().getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class Runner implements AutoCloseable {
|
private static final class Runner implements AutoCloseable {
|
||||||
|
|
@ -144,15 +151,12 @@ public final class IdleRings {
|
||||||
final ExecutorService executor =
|
final ExecutorService executor =
|
||||||
Executors.newSingleThreadExecutor(
|
Executors.newSingleThreadExecutor(
|
||||||
Thread.ofPlatform().name("Saltatory rings").daemon(true).factory());
|
Thread.ofPlatform().name("Saltatory rings").daemon(true).factory());
|
||||||
volatile Job job;
|
State state = Idle.READY;
|
||||||
CompletableFuture<Long> usage;
|
|
||||||
boolean failed;
|
|
||||||
boolean yielded;
|
|
||||||
boolean wasOnline;
|
boolean wasOnline;
|
||||||
final Consumer<GenerationTaskUpdateEvent> update =
|
final Consumer<GenerationTaskUpdateEvent> update =
|
||||||
event -> observe(event.generationTask(), false);
|
event -> receive(event.generationTask(), false);
|
||||||
final Consumer<GenerationTaskFinishEvent> finish =
|
final Consumer<GenerationTaskFinishEvent> finish =
|
||||||
event -> observe(event.generationTask(), true);
|
event -> receive(event.generationTask(), true);
|
||||||
|
|
||||||
Runner(MinecraftServer server, Chunky chunky) {
|
Runner(MinecraftServer server, Chunky chunky) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
|
|
@ -163,136 +167,215 @@ public final class IdleRings {
|
||||||
.computeIfAbsent(
|
.computeIfAbsent(
|
||||||
new SavedData.Factory<>(Store::new, Store::load, null),
|
new SavedData.Factory<>(Store::new, Store::load, null),
|
||||||
"saltatory_rings");
|
"saltatory_rings");
|
||||||
if (store.pending != null && store.paused) {
|
if (store.checkpoint != null && store.checkpoint.paused) {
|
||||||
job = new Job(store.pending, selection(store.pending), null, Phase.PAUSED);
|
var next = store.checkpoint.next;
|
||||||
|
state = new Paused(next, null, 0);
|
||||||
}
|
}
|
||||||
chunky.getEventBus().subscribe(GenerationTaskUpdateEvent.class, update);
|
chunky.getEventBus().subscribe(GenerationTaskUpdateEvent.class, update);
|
||||||
chunky.getEventBus().subscribe(GenerationTaskFinishEvent.class, finish);
|
chunky.getEventBus().subscribe(GenerationTaskFinishEvent.class, finish);
|
||||||
}
|
}
|
||||||
|
|
||||||
void observe(GenerationTask task, boolean finished) {
|
void run(Runnable action) {
|
||||||
var active = job;
|
if (state == Idle.CLOSED || state == Idle.FAILED) return;
|
||||||
if (active != null && server.isRunning()) active.observe(task, finished);
|
|
||||||
}
|
|
||||||
|
|
||||||
void visit(ServerPlayer player) {
|
|
||||||
var level = player.serverLevel();
|
|
||||||
var world = level.dimension().location().toString();
|
|
||||||
store.rings.visit(
|
|
||||||
player.getUUID() + ":" + world,
|
|
||||||
world,
|
|
||||||
player.chunkPosition().x,
|
|
||||||
player.chunkPosition().z,
|
|
||||||
Instant.now().getEpochSecond());
|
|
||||||
store.setDirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
void tick() {
|
|
||||||
if (failed) return;
|
|
||||||
try {
|
try {
|
||||||
boolean online = server.getPlayerCount() != 0;
|
action.run();
|
||||||
if (wasOnline && !online) yielded = false;
|
} catch (IllegalStateException error) {
|
||||||
wasOnline = online;
|
fail(error);
|
||||||
if (job != null) {
|
|
||||||
if (job.phase == Phase.PAUSED) {
|
|
||||||
var world = job.selection.world();
|
|
||||||
var running = chunky.getGenerationTasks().get(world.getName());
|
|
||||||
if (running != null && job.matches(running.getSelection())) {
|
|
||||||
job.observe(running, false);
|
|
||||||
} else {
|
|
||||||
var saved = chunky.getTaskLoader().loadTask(world);
|
|
||||||
if (running != null
|
|
||||||
|| saved.isEmpty()
|
|
||||||
|| saved.get().isCancelled()
|
|
||||||
|| !job.matches(saved.get().getSelection())) {
|
|
||||||
release();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
boolean paused = job.phase == Phase.PAUSED;
|
|
||||||
if (store.paused != paused) {
|
|
||||||
store.paused = paused;
|
|
||||||
store.setDirty();
|
|
||||||
}
|
|
||||||
if (job.phase == Phase.CANCELLED) {
|
|
||||||
release();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (job.phase != Phase.READY) return;
|
|
||||||
if (job.lod == null) {
|
|
||||||
var level = server.getAllLevels().iterator();
|
|
||||||
while (level.hasNext()) {
|
|
||||||
var candidate = level.next();
|
|
||||||
if (candidate
|
|
||||||
.dimension()
|
|
||||||
.location()
|
|
||||||
.toString()
|
|
||||||
.equals(job.next.cell().world())) {
|
|
||||||
candidate.getChunkSource().save(true);
|
|
||||||
job.lod = lod.fill(candidate, job.next.cell(), executor);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new IllegalStateException("Ring dimension is not loaded");
|
|
||||||
}
|
|
||||||
if (!job.lod.isDone()) return;
|
|
||||||
job.lod.join();
|
|
||||||
store.rings.complete(job.next);
|
|
||||||
store.pending = null;
|
|
||||||
store.setDirty();
|
|
||||||
LOG.debug("Completed idle pregen batch {}", job.next.cell());
|
|
||||||
job = null;
|
|
||||||
usage = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (DISK.get() == 0 || online || yielded) return;
|
|
||||||
if (!chunky.getGenerationTasks().isEmpty()) return;
|
|
||||||
var next =
|
|
||||||
store.pending != null
|
|
||||||
? store.pending
|
|
||||||
: store.rings
|
|
||||||
.next(Instant.now().getEpochSecond() - DAYS.get() * 86400L)
|
|
||||||
.orElse(null);
|
|
||||||
if (next == null) return;
|
|
||||||
if (usage == null) {
|
|
||||||
usage =
|
|
||||||
CompletableFuture.supplyAsync(
|
|
||||||
() -> {
|
|
||||||
try {
|
|
||||||
return Rings.bytes(
|
|
||||||
server.getWorldPath(LevelResource.ROOT));
|
|
||||||
} catch (java.io.IOException error) {
|
|
||||||
throw new UncheckedIOException(error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
executor);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!usage.isDone()) return;
|
|
||||||
long bytes = usage.join();
|
|
||||||
usage = null;
|
|
||||||
if (!Rings.below(bytes, DISK.get())) return;
|
|
||||||
var selection = selection(next);
|
|
||||||
var world = chunky.getServer().getWorld(next.cell().world()).orElseThrow();
|
|
||||||
var saved = chunky.getTaskLoader().loadTask(world);
|
|
||||||
if (saved.isPresent() && !saved.get().isCancelled()) {
|
|
||||||
if (store.pending == null
|
|
||||||
|| !Job.matches(selection, saved.get().getSelection())) return;
|
|
||||||
}
|
|
||||||
var task = new GenerationTask(chunky, selection);
|
|
||||||
job = new Job(next, selection, task, Phase.RUNNING);
|
|
||||||
store.pending = next;
|
|
||||||
store.setDirty();
|
|
||||||
chunky.getGenerationTasks().put(world.getName(), task);
|
|
||||||
chunky.getScheduler().runTask(task);
|
|
||||||
} catch (CompletionException | IllegalStateException error) {
|
|
||||||
failed = true;
|
|
||||||
LOG.error("Idle pregen stopped", error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Selection selection(Rings.Next next) {
|
void receive(GenerationTask task, boolean finished) {
|
||||||
|
var message =
|
||||||
|
new Update(
|
||||||
|
task,
|
||||||
|
finished,
|
||||||
|
task.isCancelled(),
|
||||||
|
task.getChunkIterator().hasNext(),
|
||||||
|
task.getProgress().isComplete());
|
||||||
|
enqueue(() -> observe(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
void enqueue(Runnable action) {
|
||||||
|
server.execute(() -> run(action));
|
||||||
|
}
|
||||||
|
|
||||||
|
void visit(ServerPlayer player, String world) {
|
||||||
|
store.rings =
|
||||||
|
store.rings.visit(
|
||||||
|
player.getUUID() + ":" + world,
|
||||||
|
world,
|
||||||
|
player.chunkPosition().x,
|
||||||
|
player.chunkPosition().z,
|
||||||
|
Instant.now().getEpochSecond());
|
||||||
|
store.setDirty();
|
||||||
|
if (state instanceof Scanning) state = Idle.READY;
|
||||||
|
}
|
||||||
|
|
||||||
|
void tick() {
|
||||||
|
boolean online = server.getPlayerCount() != 0;
|
||||||
|
if (wasOnline && !online && (state == Idle.YIELDED || state instanceof Waiting)) {
|
||||||
|
state = Idle.READY;
|
||||||
|
}
|
||||||
|
if (online && state instanceof Scanning) state = Idle.READY;
|
||||||
|
wasOnline = online;
|
||||||
|
if (state instanceof Paused paused) {
|
||||||
|
if (server.getTickCount() >= paused.until) reconcile(paused);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state instanceof Waiting waiting) {
|
||||||
|
if (server.getTickCount() < waiting.until && DISK.get() == waiting.budget) return;
|
||||||
|
state = Idle.READY;
|
||||||
|
}
|
||||||
|
advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
void observe(Update message) {
|
||||||
|
if (state instanceof Paused paused
|
||||||
|
&& paused.next
|
||||||
|
.cell()
|
||||||
|
.world()
|
||||||
|
.equals(message.task.getSelection().world().getName())) {
|
||||||
|
if (!matches(selection(paused.next), message.task.getSelection())) {
|
||||||
|
release();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (paused.task == message.task && !message.complete) return;
|
||||||
|
state = new Terrain(paused.next, message.task);
|
||||||
|
store.checkpoint(paused.next, false);
|
||||||
|
}
|
||||||
|
if (state instanceof Terrain terrain && terrain.task == message.task) {
|
||||||
|
if (message.finished && !message.cancelled) {
|
||||||
|
state =
|
||||||
|
new Paused(
|
||||||
|
terrain.next, terrain.task, (long) server.getTickCount() + 20);
|
||||||
|
store.checkpoint(terrain.next, true);
|
||||||
|
} else if (message.finished && message.remaining) {
|
||||||
|
release();
|
||||||
|
} else if (message.complete) {
|
||||||
|
backfill(terrain.next);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (state instanceof Waiting) state = Idle.READY;
|
||||||
|
advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void reconcile(Paused paused) {
|
||||||
|
var selected = selection(paused.next);
|
||||||
|
var world = selected.world();
|
||||||
|
var running = chunky.getGenerationTasks().get(world.getName());
|
||||||
|
if (running != null
|
||||||
|
&& running != paused.task
|
||||||
|
&& matches(selected, running.getSelection())) {
|
||||||
|
state = new Terrain(paused.next, running);
|
||||||
|
store.checkpoint(paused.next, false);
|
||||||
|
if (running.getProgress().isComplete()) backfill(paused.next);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Chunky emits no event when a paused checkpoint is cancelled.
|
||||||
|
var saved = chunky.getTaskLoader().loadTask(world);
|
||||||
|
if ((running != null && running != paused.task)
|
||||||
|
|| saved.isEmpty()
|
||||||
|
|| saved.get().isCancelled()
|
||||||
|
|| !matches(selected, saved.get().getSelection())) {
|
||||||
|
release();
|
||||||
|
} else {
|
||||||
|
state = new Paused(paused.next, paused.task, (long) server.getTickCount() + 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean eligible() {
|
||||||
|
return DISK.get() != 0
|
||||||
|
&& server.getPlayerCount() == 0
|
||||||
|
&& chunky.getGenerationTasks().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean available(Rings.Next next) {
|
||||||
|
var selected = selection(next);
|
||||||
|
var saved = chunky.getTaskLoader().loadTask(selected.world());
|
||||||
|
if (saved.isPresent()
|
||||||
|
&& !saved.get().isCancelled()
|
||||||
|
&& (store.checkpoint == null
|
||||||
|
|| !matches(selected, saved.get().getSelection()))) {
|
||||||
|
state = new Waiting((long) server.getTickCount() + 20, DISK.get());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void advance() {
|
||||||
|
if (state != Idle.READY || !eligible()) return;
|
||||||
|
var next =
|
||||||
|
store.checkpoint != null
|
||||||
|
? store.checkpoint.next
|
||||||
|
: store.rings
|
||||||
|
.next(Instant.now().getEpochSecond() - DAYS.get() * 86400L)
|
||||||
|
.orElse(null);
|
||||||
|
if (next == null || !available(next)) return;
|
||||||
|
var scan = new Scanning(next);
|
||||||
|
state = scan;
|
||||||
|
var root = server.getWorldPath(LevelResource.ROOT);
|
||||||
|
CompletableFuture.supplyAsync(
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
return Rings.bytes(root);
|
||||||
|
} catch (java.io.IOException error) {
|
||||||
|
throw new UncheckedIOException(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
executor)
|
||||||
|
.whenComplete((bytes, error) -> enqueue(() -> measured(scan, bytes, error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void measured(Scanning scan, Long bytes, Throwable error) {
|
||||||
|
if (state != scan) return;
|
||||||
|
if (error != null) {
|
||||||
|
fail(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state = Idle.READY;
|
||||||
|
if (!eligible()) return;
|
||||||
|
if (!Rings.below(bytes, DISK.get())) {
|
||||||
|
// External file deletion can free space without a server event.
|
||||||
|
state = new Waiting((long) server.getTickCount() + 1200, DISK.get());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!available(scan.next)) return;
|
||||||
|
var task = new GenerationTask(chunky, selection(scan.next));
|
||||||
|
state = new Terrain(scan.next, task);
|
||||||
|
store.checkpoint(scan.next, false);
|
||||||
|
chunky.getGenerationTasks().put(task.getSelection().world().getName(), task);
|
||||||
|
chunky.getScheduler().runTask(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
void backfill(Rings.Next next) {
|
||||||
|
var key =
|
||||||
|
ResourceKey.create(
|
||||||
|
Registries.DIMENSION, ResourceLocation.parse(next.cell().world()));
|
||||||
|
var level = server.getLevel(key);
|
||||||
|
if (level == null) throw new IllegalStateException("Ring dimension is not loaded");
|
||||||
|
var backfill = new Backfill(next);
|
||||||
|
state = backfill;
|
||||||
|
store.checkpoint(next, false);
|
||||||
|
level.getChunkSource().save(true);
|
||||||
|
lod.fill(level, next.cell(), executor)
|
||||||
|
.whenComplete((ignored, error) -> enqueue(() -> filled(backfill, error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void filled(Backfill backfill, Throwable error) {
|
||||||
|
if (state != backfill) return;
|
||||||
|
if (error != null) {
|
||||||
|
fail(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
store.rings = store.rings.complete(backfill.next);
|
||||||
|
store.checkpoint(null, false);
|
||||||
|
LOG.debug("Completed idle pregen batch {}", backfill.next.cell());
|
||||||
|
state = Idle.READY;
|
||||||
|
advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
Selection selection(Rings.Next next) {
|
||||||
var world = chunky.getServer().getWorld(next.cell().world()).orElseThrow();
|
var world = chunky.getServer().getWorld(next.cell().world()).orElseThrow();
|
||||||
return Selection.builder(chunky, world)
|
return Selection.builder(chunky, world)
|
||||||
.shape("square")
|
.shape("square")
|
||||||
|
|
@ -302,13 +385,14 @@ public final class IdleRings {
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void release() {
|
void release() {
|
||||||
job = null;
|
store.checkpoint(null, false);
|
||||||
store.pending = null;
|
state = Idle.YIELDED;
|
||||||
store.paused = false;
|
}
|
||||||
store.setDirty();
|
|
||||||
yielded = true;
|
void fail(Throwable error) {
|
||||||
usage = null;
|
state = Idle.FAILED;
|
||||||
|
LOG.error("Idle pregen stopped", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double center(int cell) {
|
private static double center(int cell) {
|
||||||
|
|
@ -317,40 +401,45 @@ public final class IdleRings {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
|
state = Idle.CLOSED;
|
||||||
chunky.getEventBus().unsubscribe(GenerationTaskUpdateEvent.class, update);
|
chunky.getEventBus().unsubscribe(GenerationTaskUpdateEvent.class, update);
|
||||||
chunky.getEventBus().unsubscribe(GenerationTaskFinishEvent.class, finish);
|
chunky.getEventBus().unsubscribe(GenerationTaskFinishEvent.class, finish);
|
||||||
if (job != null) {
|
|
||||||
store.paused = job.phase == Phase.PAUSED;
|
|
||||||
store.setDirty();
|
|
||||||
}
|
|
||||||
job = null;
|
|
||||||
executor.shutdownNow();
|
executor.shutdownNow();
|
||||||
lod.close();
|
lod.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class Store extends SavedData {
|
private static final class Store extends SavedData {
|
||||||
final Rings rings = new Rings();
|
Rings rings = new Rings();
|
||||||
Rings.Next pending;
|
Checkpoint checkpoint;
|
||||||
boolean paused;
|
|
||||||
|
void checkpoint(Rings.Next next, boolean paused) {
|
||||||
|
checkpoint = next == null ? null : new Checkpoint(next, paused);
|
||||||
|
setDirty();
|
||||||
|
}
|
||||||
|
|
||||||
static Store load(CompoundTag tag, HolderLookup.Provider registries) {
|
static Store load(CompoundTag tag, HolderLookup.Provider registries) {
|
||||||
var store = new Store();
|
var store = new Store();
|
||||||
store.paused = tag.getBoolean("paused");
|
var sites = new LinkedHashMap<String, Rings.Site>();
|
||||||
for (var entry : tag.getList("sites", Tag.TAG_COMPOUND)) {
|
for (var entry : tag.getList("sites", Tag.TAG_COMPOUND)) {
|
||||||
var site = (CompoundTag) entry;
|
var site = (CompoundTag) entry;
|
||||||
store.rings.sites.put(site.getString("owner"), site(site));
|
sites.put(site.getString("owner"), site(site));
|
||||||
}
|
}
|
||||||
|
store.rings = new Rings(sites);
|
||||||
if (tag.contains("pending", Tag.TAG_COMPOUND)) {
|
if (tag.contains("pending", Tag.TAG_COMPOUND)) {
|
||||||
var data = tag.getCompound("pending");
|
var data = tag.getCompound("pending");
|
||||||
var site = site(data);
|
var site = site(data);
|
||||||
var offset = Rings.ring(site.ring()).get(site.index());
|
var offset = Rings.ring(site.ring()).get(site.index());
|
||||||
store.pending =
|
store.checkpoint =
|
||||||
new Rings.Next(
|
new Checkpoint(
|
||||||
data.getString("owner"),
|
new Rings.Next(
|
||||||
site,
|
data.getString("owner"),
|
||||||
new Rings.Cell(
|
site,
|
||||||
site.world(), site.x() + offset[0], site.z() + offset[1]));
|
new Rings.Cell(
|
||||||
|
site.world(),
|
||||||
|
site.x() + offset[0],
|
||||||
|
site.z() + offset[1])),
|
||||||
|
tag.getBoolean("paused"));
|
||||||
}
|
}
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
@ -382,10 +471,12 @@ public final class IdleRings {
|
||||||
@Override
|
@Override
|
||||||
public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
|
public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
|
||||||
var sites = new ListTag();
|
var sites = new ListTag();
|
||||||
rings.sites.forEach((owner, site) -> sites.add(tag(owner, site)));
|
rings.sites().forEach((owner, site) -> sites.add(tag(owner, site)));
|
||||||
tag.put("sites", sites);
|
tag.put("sites", sites);
|
||||||
tag.putBoolean("paused", paused);
|
tag.putBoolean("paused", checkpoint != null && checkpoint.paused);
|
||||||
if (pending != null) tag.put("pending", tag(pending.owner(), pending.site()));
|
if (checkpoint != null) {
|
||||||
|
tag.put("pending", tag(checkpoint.next.owner(), checkpoint.next.site()));
|
||||||
|
}
|
||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,20 +37,15 @@ public final class LodBatch implements AutoCloseable {
|
||||||
var target = dh;
|
var target = dh;
|
||||||
int minX = cell.x() * Rings.WIDTH;
|
int minX = cell.x() * Rings.WIDTH;
|
||||||
int minZ = cell.z() * Rings.WIDTH;
|
int minZ = cell.z() * Rings.WIDTH;
|
||||||
CompletableFuture<Void> pending = CompletableFuture.completedFuture(null);
|
var pending = new ArrayList<CompletableFuture<Void>>();
|
||||||
for (int x = Math.floorDiv(minX, 4); x <= Math.floorDiv(minX + Rings.WIDTH - 1, 4); x++) {
|
for (int x = Math.floorDiv(minX, 4); x <= Math.floorDiv(minX + Rings.WIDTH - 1, 4); x++) {
|
||||||
for (int z = Math.floorDiv(minZ, 4);
|
for (int z = Math.floorDiv(minZ, 4);
|
||||||
z <= Math.floorDiv(minZ + Rings.WIDTH - 1, 4);
|
z <= Math.floorDiv(minZ + Rings.WIDTH - 1, 4);
|
||||||
z++) {
|
z++) {
|
||||||
int tileX = x;
|
pending.add(tile(target, generator, cell, x, z, executor));
|
||||||
int tileZ = z;
|
|
||||||
pending =
|
|
||||||
pending.thenComposeAsync(
|
|
||||||
ignored -> tile(target, generator, cell, tileX, tileZ, executor),
|
|
||||||
executor);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pending;
|
return CompletableFuture.allOf(pending.toArray(CompletableFuture[]::new));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CompletableFuture<Void> tile(
|
private static CompletableFuture<Void> tile(
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public final class Rings {
|
public record Rings(Map<String, Rings.Site> sites) {
|
||||||
public static final int WIDTH = 9;
|
public static final int WIDTH = 9;
|
||||||
|
|
||||||
public record Cell(String world, int x, int z) {}
|
public record Cell(String world, int x, int z) {}
|
||||||
|
|
@ -19,15 +19,21 @@ public final class Rings {
|
||||||
|
|
||||||
public record Next(String owner, Site site, Cell cell) {}
|
public record Next(String owner, Site site, Cell cell) {}
|
||||||
|
|
||||||
public final Map<String, Site> sites = new LinkedHashMap<>();
|
public Rings {
|
||||||
private long sequence;
|
sites = Map.copyOf(sites);
|
||||||
|
}
|
||||||
|
|
||||||
public void visit(String owner, String world, int chunkX, int chunkZ, long now) {
|
public Rings() {
|
||||||
|
this(Map.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Rings visit(String owner, String world, int chunkX, int chunkZ, long now) {
|
||||||
int x = Math.floorDiv(chunkX, WIDTH);
|
int x = Math.floorDiv(chunkX, WIDTH);
|
||||||
int z = Math.floorDiv(chunkZ, WIDTH);
|
int z = Math.floorDiv(chunkZ, WIDTH);
|
||||||
var old = sites.get(owner);
|
var old = sites.get(owner);
|
||||||
boolean same = old != null && old.world.equals(world) && old.x == x && old.z == z;
|
boolean same = old != null && old.world.equals(world) && old.x == x && old.z == z;
|
||||||
sites.put(
|
var updated = new LinkedHashMap<>(sites);
|
||||||
|
updated.put(
|
||||||
owner,
|
owner,
|
||||||
new Site(
|
new Site(
|
||||||
world,
|
world,
|
||||||
|
|
@ -37,6 +43,7 @@ public final class Rings {
|
||||||
same ? old.index : 0,
|
same ? old.index : 0,
|
||||||
now,
|
now,
|
||||||
same ? old.served : 0));
|
same ? old.served : 0));
|
||||||
|
return new Rings(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<Next> next(long since) {
|
public Optional<Next> next(long since) {
|
||||||
|
|
@ -58,7 +65,7 @@ public final class Rings {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean complete(Next next) {
|
public Rings complete(Next next) {
|
||||||
var old = sites.get(next.owner);
|
var old = sites.get(next.owner);
|
||||||
var started = next.site;
|
var started = next.site;
|
||||||
if (old == null
|
if (old == null
|
||||||
|
|
@ -66,18 +73,18 @@ public final class Rings {
|
||||||
|| old.x != started.x
|
|| old.x != started.x
|
||||||
|| old.z != started.z
|
|| old.z != started.z
|
||||||
|| old.ring != started.ring
|
|| old.ring != started.ring
|
||||||
|| old.index != started.index) return false;
|
|| old.index != started.index) return this;
|
||||||
int index = old.index + 1;
|
int index = old.index + 1;
|
||||||
int radius = old.ring;
|
int radius = old.ring;
|
||||||
if (index == ring(radius).size()) {
|
if (index == ring(radius).size()) {
|
||||||
radius++;
|
radius++;
|
||||||
index = 0;
|
index = 0;
|
||||||
}
|
}
|
||||||
sequence =
|
long sequence = sites.values().stream().mapToLong(Site::served).max().orElse(0) + 1;
|
||||||
Math.max(sequence, sites.values().stream().mapToLong(Site::served).max().orElse(0));
|
var updated = new LinkedHashMap<>(sites);
|
||||||
sites.put(
|
updated.put(
|
||||||
next.owner, new Site(old.world, old.x, old.z, radius, index, old.seen, ++sequence));
|
next.owner, new Site(old.world, old.x, old.z, radius, index, old.seen, sequence));
|
||||||
return true;
|
return new Rings(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<int[]> ring(int radius) {
|
public static List<int[]> ring(int radius) {
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,9 @@ public abstract class RefineQueue implements LodWork.Queue {
|
||||||
mapper,
|
mapper,
|
||||||
reducer,
|
reducer,
|
||||||
pos -> DhSectionPos.getDetailLevel(pos) == 6,
|
pos -> DhSectionPos.getDetailLevel(pos) == 6,
|
||||||
() -> inProgressGenTasksByLodPos.isEmpty() && refine.saltatory$ready());
|
() ->
|
||||||
|
inProgressGenTasksByLodPos.keySet().stream()
|
||||||
|
.allMatch(pos -> DhSectionPos.getDetailLevel(pos) == 6)
|
||||||
|
&& refine.saltatory$ready());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package dev.quorate.tune;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
@ -32,45 +34,66 @@ final class RingsTest {
|
||||||
@Test
|
@Test
|
||||||
void locationsUseTheSameGridAcrossZero() {
|
void locationsUseTheSameGridAcrossZero() {
|
||||||
var rings = new Rings();
|
var rings = new Rings();
|
||||||
rings.visit("a", "world", -1, -10, 1);
|
rings = rings.visit("a", "world", -1, -10, 1);
|
||||||
assertEquals(new Rings.Cell("world", -1, -2), rings.next(0).orElseThrow().cell());
|
assertEquals(new Rings.Cell("world", -1, -2), rings.next(0).orElseThrow().cell());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void nearbyCoveragePrecedesLargerRingsAndPlayersTakeTurns() {
|
void nearbyCoveragePrecedesLargerRingsAndPlayersTakeTurns() {
|
||||||
var rings = new Rings();
|
var rings = new Rings();
|
||||||
rings.visit("a", "world", 0, 0, 10);
|
rings = rings.visit("a", "world", 0, 0, 10);
|
||||||
rings.visit("b", "world", 90, 90, 10);
|
rings = rings.visit("b", "world", 90, 90, 10);
|
||||||
var first = rings.next(0).orElseThrow();
|
var first = rings.next(0).orElseThrow();
|
||||||
assertEquals("a", first.owner());
|
assertEquals("a", first.owner());
|
||||||
assertTrue(rings.complete(first));
|
var advanced = rings.complete(first);
|
||||||
|
assertNotSame(rings, advanced);
|
||||||
|
rings = advanced;
|
||||||
assertEquals("b", rings.next(0).orElseThrow().owner());
|
assertEquals("b", rings.next(0).orElseThrow().owner());
|
||||||
rings.complete(rings.next(0).orElseThrow());
|
rings = rings.complete(rings.next(0).orElseThrow());
|
||||||
rings.complete(rings.next(0).orElseThrow());
|
rings = rings.complete(rings.next(0).orElseThrow());
|
||||||
assertEquals("b", rings.next(0).orElseThrow().owner());
|
assertEquals("b", rings.next(0).orElseThrow().owner());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void completionRetainsActivityButDoesNotAdvanceAMovedLocation() {
|
void completionRetainsActivityButDoesNotAdvanceAMovedLocation() {
|
||||||
var rings = new Rings();
|
var rings = new Rings();
|
||||||
rings.visit("a", "world", 0, 0, 10);
|
rings = rings.visit("a", "world", 0, 0, 10);
|
||||||
var first = rings.next(0).orElseThrow();
|
var first = rings.next(0).orElseThrow();
|
||||||
rings.visit("a", "world", 1, 1, 20);
|
rings = rings.visit("a", "world", 1, 1, 20);
|
||||||
assertTrue(rings.complete(first));
|
rings = rings.complete(first);
|
||||||
assertEquals(20, rings.sites.get("a").seen());
|
assertEquals(20, rings.sites().get("a").seen());
|
||||||
var second = rings.next(0).orElseThrow();
|
var second = rings.next(0).orElseThrow();
|
||||||
rings.visit("a", "world", 100, 100, 30);
|
rings = rings.visit("a", "world", 100, 100, 30);
|
||||||
assertFalse(rings.complete(second));
|
assertSame(rings, rings.complete(second));
|
||||||
assertEquals(0, rings.sites.get("a").ring());
|
assertEquals(0, rings.sites().get("a").ring());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void oldLocationsAreNotExpanded() {
|
void oldLocationsAreNotExpanded() {
|
||||||
var rings = new Rings();
|
var rings = new Rings();
|
||||||
rings.visit("a", "world", 0, 0, 10);
|
rings = rings.visit("a", "world", 0, 0, 10);
|
||||||
assertTrue(rings.next(11).isEmpty());
|
assertTrue(rings.next(11).isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dimensionAnchors() {
|
||||||
|
var old =
|
||||||
|
new Rings()
|
||||||
|
.visit("a:overworld", "overworld", 90, 90, 10)
|
||||||
|
.visit("a:nether", "nether", -90, -90, 20);
|
||||||
|
var next = old.visit("a:overworld", "overworld", 180, 180, 30);
|
||||||
|
assertEquals(old.sites().get("a:nether"), next.sites().get("a:nether"));
|
||||||
|
assertEquals(10, old.sites().get("a:overworld").x());
|
||||||
|
assertEquals(20, next.sites().get("a:overworld").x());
|
||||||
|
for (int i = 0; i < 50; i++) {
|
||||||
|
var batch = next.next(0).orElseThrow();
|
||||||
|
next = next.complete(batch);
|
||||||
|
int overworld = next.sites().get("a:overworld").ring();
|
||||||
|
int nether = next.sites().get("a:nether").ring();
|
||||||
|
assertTrue(Math.abs(overworld - nether) <= 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void diskBudgetUsesGibAndZeroDisablesGeneration() {
|
void diskBudgetUsesGibAndZeroDisablesGeneration() {
|
||||||
assertFalse(Rings.below(0, 0));
|
assertFalse(Rings.below(0, 0));
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue