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 net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
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.neoforge.common.ModConfigSpec;
|
||||
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.ServerStoppingEvent;
|
||||
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
||||
|
|
@ -30,8 +34,8 @@ import org.slf4j.Logger;
|
|||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.Consumer;
|
||||
|
|
@ -66,74 +70,77 @@ public final class IdleRings {
|
|||
else LOG.warn("Idle rings require Chunky API version 0");
|
||||
});
|
||||
NeoForge.EVENT_BUS.addListener(
|
||||
(PlayerEvent.PlayerLoggedOutEvent event) -> {
|
||||
if (current != null && event.getEntity() instanceof ServerPlayer player) {
|
||||
current.visit(player);
|
||||
(EntityLeaveLevelEvent event) -> {
|
||||
if (current != null
|
||||
&& 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(
|
||||
(ServerTickEvent.Post event) -> {
|
||||
if (current != null
|
||||
&& current.server == event.getServer()
|
||||
&& event.getServer().getTickCount() % 20 == 0) current.tick();
|
||||
if (current != null && current.server == event.getServer()) {
|
||||
current.run(current::tick);
|
||||
}
|
||||
});
|
||||
NeoForge.EVENT_BUS.addListener(
|
||||
(ServerStoppingEvent event) -> {
|
||||
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 = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private enum Phase {
|
||||
RUNNING,
|
||||
PAUSED,
|
||||
CANCELLED,
|
||||
READY
|
||||
private sealed interface State {}
|
||||
|
||||
private enum Idle implements State {
|
||||
READY,
|
||||
YIELDED,
|
||||
FAILED,
|
||||
CLOSED
|
||||
}
|
||||
|
||||
private static final class Job {
|
||||
final Rings.Next next;
|
||||
final Selection selection;
|
||||
GenerationTask task;
|
||||
volatile Phase phase;
|
||||
CompletableFuture<Void> lod;
|
||||
private record Scanning(Rings.Next next) implements State {}
|
||||
|
||||
Job(Rings.Next next, Selection selection, GenerationTask task, Phase phase) {
|
||||
this.next = next;
|
||||
this.selection = selection;
|
||||
this.task = task;
|
||||
this.phase = phase;
|
||||
}
|
||||
private record Terrain(Rings.Next next, GenerationTask task) implements State {}
|
||||
|
||||
boolean matches(Selection other) {
|
||||
return matches(selection, other);
|
||||
}
|
||||
private record Paused(Rings.Next next, GenerationTask task, long until) implements State {}
|
||||
|
||||
static boolean matches(Selection selection, Selection other) {
|
||||
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());
|
||||
}
|
||||
private record Backfill(Rings.Next next) implements State {}
|
||||
|
||||
synchronized void observe(GenerationTask other, boolean finished) {
|
||||
if (phase == Phase.PAUSED && task != other && matches(other.getSelection())) {
|
||||
task = other;
|
||||
phase = Phase.RUNNING;
|
||||
}
|
||||
if (task != other || phase == Phase.CANCELLED) return;
|
||||
if (finished && !other.isCancelled()) phase = Phase.PAUSED;
|
||||
else if (finished && other.getChunkIterator().hasNext()) phase = Phase.CANCELLED;
|
||||
// Chunky's finish event precedes in-flight chunks; complete progress includes them.
|
||||
else if (other.getProgress().isComplete()) phase = Phase.READY;
|
||||
}
|
||||
private record Waiting(long until, double budget) implements State {}
|
||||
|
||||
private record Checkpoint(Rings.Next next, boolean paused) {}
|
||||
|
||||
private record Update(
|
||||
GenerationTask task,
|
||||
boolean finished,
|
||||
boolean cancelled,
|
||||
boolean remaining,
|
||||
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 {
|
||||
|
|
@ -144,15 +151,12 @@ public final class IdleRings {
|
|||
final ExecutorService executor =
|
||||
Executors.newSingleThreadExecutor(
|
||||
Thread.ofPlatform().name("Saltatory rings").daemon(true).factory());
|
||||
volatile Job job;
|
||||
CompletableFuture<Long> usage;
|
||||
boolean failed;
|
||||
boolean yielded;
|
||||
State state = Idle.READY;
|
||||
boolean wasOnline;
|
||||
final Consumer<GenerationTaskUpdateEvent> update =
|
||||
event -> observe(event.generationTask(), false);
|
||||
event -> receive(event.generationTask(), false);
|
||||
final Consumer<GenerationTaskFinishEvent> finish =
|
||||
event -> observe(event.generationTask(), true);
|
||||
event -> receive(event.generationTask(), true);
|
||||
|
||||
Runner(MinecraftServer server, Chunky chunky) {
|
||||
this.server = server;
|
||||
|
|
@ -163,21 +167,40 @@ public final class IdleRings {
|
|||
.computeIfAbsent(
|
||||
new SavedData.Factory<>(Store::new, Store::load, null),
|
||||
"saltatory_rings");
|
||||
if (store.pending != null && store.paused) {
|
||||
job = new Job(store.pending, selection(store.pending), null, Phase.PAUSED);
|
||||
if (store.checkpoint != null && store.checkpoint.paused) {
|
||||
var next = store.checkpoint.next;
|
||||
state = new Paused(next, null, 0);
|
||||
}
|
||||
chunky.getEventBus().subscribe(GenerationTaskUpdateEvent.class, update);
|
||||
chunky.getEventBus().subscribe(GenerationTaskFinishEvent.class, finish);
|
||||
}
|
||||
|
||||
void observe(GenerationTask task, boolean finished) {
|
||||
var active = job;
|
||||
if (active != null && server.isRunning()) active.observe(task, finished);
|
||||
void run(Runnable action) {
|
||||
if (state == Idle.CLOSED || state == Idle.FAILED) return;
|
||||
try {
|
||||
action.run();
|
||||
} catch (IllegalStateException error) {
|
||||
fail(error);
|
||||
}
|
||||
}
|
||||
|
||||
void visit(ServerPlayer player) {
|
||||
var level = player.serverLevel();
|
||||
var world = level.dimension().location().toString();
|
||||
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,
|
||||
|
|
@ -185,114 +208,174 @@ public final class IdleRings {
|
|||
player.chunkPosition().z,
|
||||
Instant.now().getEpochSecond());
|
||||
store.setDirty();
|
||||
if (state instanceof Scanning) state = Idle.READY;
|
||||
}
|
||||
|
||||
void tick() {
|
||||
if (failed) return;
|
||||
try {
|
||||
boolean online = server.getPlayerCount() != 0;
|
||||
if (wasOnline && !online) yielded = false;
|
||||
if (wasOnline && !online && (state == Idle.YIELDED || state instanceof Waiting)) {
|
||||
state = Idle.READY;
|
||||
}
|
||||
if (online && state instanceof Scanning) state = Idle.READY;
|
||||
wasOnline = online;
|
||||
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);
|
||||
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 {
|
||||
var saved = chunky.getTaskLoader().loadTask(world);
|
||||
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()
|
||||
|| !job.matches(saved.get().getSelection())) {
|
||||
|| !matches(selected, saved.get().getSelection())) {
|
||||
release();
|
||||
return;
|
||||
} 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 paused = job.phase == Phase.PAUSED;
|
||||
if (store.paused != paused) {
|
||||
store.paused = paused;
|
||||
store.setDirty();
|
||||
|
||||
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;
|
||||
}
|
||||
if (job.phase == Phase.CANCELLED) {
|
||||
release();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
|
||||
void advance() {
|
||||
if (state != Idle.READY || !eligible()) return;
|
||||
var next =
|
||||
store.pending != null
|
||||
? store.pending
|
||||
store.checkpoint != null
|
||||
? store.checkpoint.next
|
||||
: store.rings
|
||||
.next(Instant.now().getEpochSecond() - DAYS.get() * 86400L)
|
||||
.orElse(null);
|
||||
if (next == null) return;
|
||||
if (usage == null) {
|
||||
usage =
|
||||
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(
|
||||
server.getWorldPath(LevelResource.ROOT));
|
||||
return Rings.bytes(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);
|
||||
}
|
||||
executor)
|
||||
.whenComplete((bytes, error) -> enqueue(() -> measured(scan, bytes, error)));
|
||||
}
|
||||
|
||||
private Selection selection(Rings.Next next) {
|
||||
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();
|
||||
return Selection.builder(chunky, world)
|
||||
.shape("square")
|
||||
|
|
@ -302,13 +385,14 @@ public final class IdleRings {
|
|||
.build();
|
||||
}
|
||||
|
||||
private void release() {
|
||||
job = null;
|
||||
store.pending = null;
|
||||
store.paused = false;
|
||||
store.setDirty();
|
||||
yielded = true;
|
||||
usage = null;
|
||||
void release() {
|
||||
store.checkpoint(null, false);
|
||||
state = Idle.YIELDED;
|
||||
}
|
||||
|
||||
void fail(Throwable error) {
|
||||
state = Idle.FAILED;
|
||||
LOG.error("Idle pregen stopped", error);
|
||||
}
|
||||
|
||||
private static double center(int cell) {
|
||||
|
|
@ -317,40 +401,45 @@ public final class IdleRings {
|
|||
|
||||
@Override
|
||||
public void close() {
|
||||
state = Idle.CLOSED;
|
||||
chunky.getEventBus().unsubscribe(GenerationTaskUpdateEvent.class, update);
|
||||
chunky.getEventBus().unsubscribe(GenerationTaskFinishEvent.class, finish);
|
||||
if (job != null) {
|
||||
store.paused = job.phase == Phase.PAUSED;
|
||||
store.setDirty();
|
||||
}
|
||||
job = null;
|
||||
executor.shutdownNow();
|
||||
lod.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Store extends SavedData {
|
||||
final Rings rings = new Rings();
|
||||
Rings.Next pending;
|
||||
boolean paused;
|
||||
Rings rings = new Rings();
|
||||
Checkpoint checkpoint;
|
||||
|
||||
void checkpoint(Rings.Next next, boolean paused) {
|
||||
checkpoint = next == null ? null : new Checkpoint(next, paused);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
static Store load(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
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)) {
|
||||
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)) {
|
||||
var data = tag.getCompound("pending");
|
||||
var site = site(data);
|
||||
var offset = Rings.ring(site.ring()).get(site.index());
|
||||
store.pending =
|
||||
store.checkpoint =
|
||||
new Checkpoint(
|
||||
new Rings.Next(
|
||||
data.getString("owner"),
|
||||
site,
|
||||
new Rings.Cell(
|
||||
site.world(), site.x() + offset[0], site.z() + offset[1]));
|
||||
site.world(),
|
||||
site.x() + offset[0],
|
||||
site.z() + offset[1])),
|
||||
tag.getBoolean("paused"));
|
||||
}
|
||||
return store;
|
||||
}
|
||||
|
|
@ -382,10 +471,12 @@ public final class IdleRings {
|
|||
@Override
|
||||
public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
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.putBoolean("paused", paused);
|
||||
if (pending != null) tag.put("pending", tag(pending.owner(), pending.site()));
|
||||
tag.putBoolean("paused", checkpoint != null && checkpoint.paused);
|
||||
if (checkpoint != null) {
|
||||
tag.put("pending", tag(checkpoint.next.owner(), checkpoint.next.site()));
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,20 +37,15 @@ public final class LodBatch implements AutoCloseable {
|
|||
var target = dh;
|
||||
int minX = cell.x() * 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 z = Math.floorDiv(minZ, 4);
|
||||
z <= Math.floorDiv(minZ + Rings.WIDTH - 1, 4);
|
||||
z++) {
|
||||
int tileX = x;
|
||||
int tileZ = z;
|
||||
pending =
|
||||
pending.thenComposeAsync(
|
||||
ignored -> tile(target, generator, cell, tileX, tileZ, executor),
|
||||
executor);
|
||||
pending.add(tile(target, generator, cell, x, z, executor));
|
||||
}
|
||||
}
|
||||
return pending;
|
||||
return CompletableFuture.allOf(pending.toArray(CompletableFuture[]::new));
|
||||
}
|
||||
|
||||
private static CompletableFuture<Void> tile(
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class Rings {
|
||||
public record Rings(Map<String, Rings.Site> sites) {
|
||||
public static final int WIDTH = 9;
|
||||
|
||||
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 final Map<String, Site> sites = new LinkedHashMap<>();
|
||||
private long sequence;
|
||||
public Rings {
|
||||
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 z = Math.floorDiv(chunkZ, WIDTH);
|
||||
var old = sites.get(owner);
|
||||
boolean same = old != null && old.world.equals(world) && old.x == x && old.z == z;
|
||||
sites.put(
|
||||
var updated = new LinkedHashMap<>(sites);
|
||||
updated.put(
|
||||
owner,
|
||||
new Site(
|
||||
world,
|
||||
|
|
@ -37,6 +43,7 @@ public final class Rings {
|
|||
same ? old.index : 0,
|
||||
now,
|
||||
same ? old.served : 0));
|
||||
return new Rings(updated);
|
||||
}
|
||||
|
||||
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 started = next.site;
|
||||
if (old == null
|
||||
|
|
@ -66,18 +73,18 @@ public final class Rings {
|
|||
|| old.x != started.x
|
||||
|| old.z != started.z
|
||||
|| old.ring != started.ring
|
||||
|| old.index != started.index) return false;
|
||||
|| old.index != started.index) return this;
|
||||
int index = old.index + 1;
|
||||
int radius = old.ring;
|
||||
if (index == ring(radius).size()) {
|
||||
radius++;
|
||||
index = 0;
|
||||
}
|
||||
sequence =
|
||||
Math.max(sequence, sites.values().stream().mapToLong(Site::served).max().orElse(0));
|
||||
sites.put(
|
||||
next.owner, new Site(old.world, old.x, old.z, radius, index, old.seen, ++sequence));
|
||||
return true;
|
||||
long sequence = sites.values().stream().mapToLong(Site::served).max().orElse(0) + 1;
|
||||
var updated = new LinkedHashMap<>(sites);
|
||||
updated.put(
|
||||
next.owner, new Site(old.world, old.x, old.z, radius, index, old.seen, sequence));
|
||||
return new Rings(updated);
|
||||
}
|
||||
|
||||
public static List<int[]> ring(int radius) {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@ public abstract class RefineQueue implements LodWork.Queue {
|
|||
mapper,
|
||||
reducer,
|
||||
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.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 org.junit.jupiter.api.Test;
|
||||
|
|
@ -32,45 +34,66 @@ final class RingsTest {
|
|||
@Test
|
||||
void locationsUseTheSameGridAcrossZero() {
|
||||
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());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nearbyCoveragePrecedesLargerRingsAndPlayersTakeTurns() {
|
||||
var rings = new Rings();
|
||||
rings.visit("a", "world", 0, 0, 10);
|
||||
rings.visit("b", "world", 90, 90, 10);
|
||||
rings = rings.visit("a", "world", 0, 0, 10);
|
||||
rings = rings.visit("b", "world", 90, 90, 10);
|
||||
var first = rings.next(0).orElseThrow();
|
||||
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());
|
||||
rings.complete(rings.next(0).orElseThrow());
|
||||
rings.complete(rings.next(0).orElseThrow());
|
||||
rings = rings.complete(rings.next(0).orElseThrow());
|
||||
rings = rings.complete(rings.next(0).orElseThrow());
|
||||
assertEquals("b", rings.next(0).orElseThrow().owner());
|
||||
}
|
||||
|
||||
@Test
|
||||
void completionRetainsActivityButDoesNotAdvanceAMovedLocation() {
|
||||
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();
|
||||
rings.visit("a", "world", 1, 1, 20);
|
||||
assertTrue(rings.complete(first));
|
||||
assertEquals(20, rings.sites.get("a").seen());
|
||||
rings = rings.visit("a", "world", 1, 1, 20);
|
||||
rings = rings.complete(first);
|
||||
assertEquals(20, rings.sites().get("a").seen());
|
||||
var second = rings.next(0).orElseThrow();
|
||||
rings.visit("a", "world", 100, 100, 30);
|
||||
assertFalse(rings.complete(second));
|
||||
assertEquals(0, rings.sites.get("a").ring());
|
||||
rings = rings.visit("a", "world", 100, 100, 30);
|
||||
assertSame(rings, rings.complete(second));
|
||||
assertEquals(0, rings.sites().get("a").ring());
|
||||
}
|
||||
|
||||
@Test
|
||||
void oldLocationsAreNotExpanded() {
|
||||
var rings = new Rings();
|
||||
rings.visit("a", "world", 0, 0, 10);
|
||||
rings = rings.visit("a", "world", 0, 0, 10);
|
||||
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
|
||||
void diskBudgetUsesGibAndZeroDisablesGeneration() {
|
||||
assertFalse(Rings.below(0, 0));
|
||||
|
|
|
|||
Loading…
Reference in a new issue