Skip to content

Instantly share code, notes, and snippets.

@jpenilla
Last active January 13, 2024 03:49
Show Gist options
  • Save jpenilla/5890fe2065bb9b199f6049962dbd2c00 to your computer and use it in GitHub Desktop.
Save jpenilla/5890fe2065bb9b199f6049962dbd2c00 to your computer and use it in GitHub Desktop.
// licensed CC0 (except for iterator class, and Bukkit is GPL, so, IANAL, do what you want)
package xyz.jpenilla.gist;
import it.unimi.dsi.fastutil.ints.IntIntPair;
import java.util.Iterator;
import java.util.function.Consumer;
import net.kyori.adventure.key.Key;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.world.WorldLoadEvent;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitTask;
// Example of how to load a large amount of chunks quickly/efficiently, by keeping the chunk system saturated with up to MAXIMUM_OUTSTANDING_REQUESTS active chunk requests at a time
public final class SpiralOutChunksPlugin extends JavaPlugin implements Listener {
@Override
public void onEnable() {
this.getServer().getPluginManager().registerEvents(this, this);
}
@EventHandler
public void worldLoad(final WorldLoadEvent event) {
if (event.getWorld().key().equals(Key.key("overworld"))) {
this.getServer().getScheduler().runTaskTimer(this, new ChunkTask(this, event.getWorld(), 0, 0, 50), 0L, 1L);
}
}
private static final class ChunkTask implements Consumer<BukkitTask> {
private static final int MAXIMUM_OUTSTANDING_REQUESTS = 1024;
private final JavaPlugin plugin;
private final World world;
private final SpiralIterator<IntIntPair> iterator;
private long submitted = 0;
private long executed = 0;
private ChunkTask(final JavaPlugin plugin, final World world, final int centerChunkX, final int centerChunkZ, final int chunksRadius) {
this.plugin = plugin;
this.world = world;
// If you're loading a chunk radius that spans many regions, consider implementing something to work on X regions at a time,
// as a chunk spiral will become less efficient as the region radius goes past 0.
this.iterator = SpiralIterator.create(IntIntPair::of, centerChunkX, centerChunkZ, chunksRadius);
}
@Override
public void accept(final BukkitTask task) {
while ((this.submitted - this.executed) < MAXIMUM_OUTSTANDING_REQUESTS && this.iterator.hasNext()) {
final IntIntPair coord = this.iterator.next();
this.submitted++;
this.world.getChunkAtAsync(coord.firstInt(), coord.secondInt(), false).whenComplete((chunk, thr) -> {
if (chunk != null) {
// this.plugin.getSLF4JLogger().info("Processing chunk at {}...", pos(this.world, coord));
// doStuff(chunk);
// this.plugin.getSLF4JLogger().info("Done processing chunk at {}.", pos(this.world, coord));
} else if (thr != null) {
this.plugin.getSLF4JLogger().warn("Exception loading chunk at {}", pos(this.world, coord), thr);
} else {
// this.plugin.getSLF4JLogger().info("Chunk not generated at {}", pos(this.world, coord));
}
this.executed++;
});
}
if (!this.iterator.hasNext()) {
if (this.submitted - this.executed == 0) {
this.plugin.getSLF4JLogger().info("Task done for world {}.", this.world.key());
task.cancel();
return;
}
// Every five seconds print a message when we are done submitting but are waiting on completion
if (this.plugin.getServer().getCurrentTick() % (20 * 5) == 0) {
this.plugin.getSLF4JLogger().info("Done submitting tasks for world {}, waiting on completion...", this.world.key());
}
} else if (this.plugin.getServer().getCurrentTick() % 20 == 0) {
this.plugin.getSLF4JLogger().info("Processed {}/{} chunks for world {}", this.executed, this.iterator.totalSteps(), this.world.key());
}
}
private static String pos(final World world, final IntIntPair pos) {
return world.key().asString() + '[' + pos.firstInt() + ", " + pos.secondInt() + ']';
}
}
// Copied from squaremap, see the license there https://github.com/jpenilla/squaremap
public static final class SpiralIterator<T> implements Iterator<T> {
private final long totalSteps;
private final CoordinateMapper<T> coordinateMapper;
private int x;
private int z;
private long legAxis;
private long stepCount;
private long stepLeg;
private long layer;
private Direction direction = Direction.RIGHT;
private SpiralIterator(final CoordinateMapper<T> coordinateMapper, final int x, final int z, final int radius) {
this.coordinateMapper = coordinateMapper;
this.x = x;
this.z = z;
this.totalSteps = (radius * 2L + 1) * (radius * 2L + 1);
}
@Override
public boolean hasNext() {
return this.stepCount < this.totalSteps;
}
@Override
public T next() {
final T t = this.coordinateMapper.create(this.x, this.z);
if (!this.hasNext()) {
return t;
}
switch (this.direction) {
case DOWN -> this.z++;
case LEFT -> this.x--;
case UP -> this.z--;
case RIGHT -> this.x++;
}
this.stepCount++;
this.stepLeg++;
if (this.stepLeg > this.layer) {
this.direction = this.direction.next();
this.stepLeg = 0;
this.legAxis++;
if (this.legAxis > 1) {
this.legAxis = 0;
this.layer++;
}
}
return t;
}
public long totalSteps() {
return this.totalSteps;
}
public static <T> SpiralIterator<T> create(final CoordinateMapper<T> coordinateMapper, final int x, final int z, final int radius) {
return new SpiralIterator<>(coordinateMapper, x, z, radius);
}
@FunctionalInterface
public interface CoordinateMapper<T> {
T create(int x, int z);
}
private enum Direction {
RIGHT,
DOWN,
LEFT,
UP;
private static final Direction[] VALUES = values();
public Direction next() {
return VALUES[(this.ordinal() + 1) % VALUES.length];
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment