/*
* This file is part of LanternServer, licensed under the MIT License (MIT).
*
* Copyright (c) LanternPowered <https://www.lanternpowered.org>
* Copyright (c) SpongePowered <https://www.spongepowered.org>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the Software), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.lanternpowered.server.world;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.flowpowered.math.GenericMath;
import com.flowpowered.math.vector.Vector3d;
import com.flowpowered.math.vector.Vector3i;
import org.apache.commons.lang3.time.DurationFormatUtils;
import org.slf4j.Logger;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.scheduler.Task;
import org.spongepowered.api.world.Chunk;
import org.spongepowered.api.world.World;
import org.spongepowered.api.world.WorldBorder.ChunkPreGenerate;
import java.util.function.Consumer;
import javax.annotation.Nullable;
public final class LanternChunkPreGenerate implements ChunkPreGenerate {
private static final int TICK_INTERVAL = 10;
private static final float DEFAULT_TICK_PERCENT = 0.15f;
private World world;
private Vector3d center;
private double diameter;
@Nullable private Object plugin = null;
@Nullable private Logger logger = null;
private int tickInterval = TICK_INTERVAL;
private int chunkCount = 0;
private float tickPercent = DEFAULT_TICK_PERCENT;
public LanternChunkPreGenerate(World world, Vector3d center, double diameter) {
this.world = world;
this.center = center;
this.diameter = diameter;
}
@Override
public LanternChunkPreGenerate owner(Object plugin) {
checkNotNull(plugin, "plugin");
this.plugin = plugin;
return this;
}
@Override
public LanternChunkPreGenerate logger(@Nullable Logger logger) {
this.logger = logger;
return this;
}
@Override
public LanternChunkPreGenerate tickInterval(int tickInterval) {
checkArgument(tickInterval > 0, "tickInterval must be greater than zero");
this.tickInterval = tickInterval;
return this;
}
@Override
public LanternChunkPreGenerate chunksPerTick(int chunkCount) {
this.chunkCount = chunkCount;
return this;
}
@Override
public LanternChunkPreGenerate tickPercentLimit(float tickPercent) {
checkArgument(tickPercent <= 1, "tickPercent must be smaller or equal to 1");
this.tickPercent = tickPercent;
return this;
}
@Override
public Task start() {
checkNotNull(this.plugin, "owner not set");
checkArgument(this.chunkCount > 0 || this.tickPercent > 0, "Must use at least one of \"chunks per tick\" or \"tick percent limit\"");
return Task.builder().name(toString())
.execute(new ChunkPreGenerator(this.world, this.center, this.diameter, this.chunkCount, this.tickPercent, this.logger))
.intervalTicks(this.tickInterval).submit(this.plugin);
}
@Override
public LanternChunkPreGenerate reset() {
this.plugin = null;
this.logger = null;
this.tickInterval = 0;
this.chunkCount = 0;
this.tickPercent = DEFAULT_TICK_PERCENT;
return this;
}
@Override
public ChunkPreGenerate from(Task value) {
final Consumer<Task> c0 = value.getConsumer();
if (c0 instanceof ChunkPreGenerator) {
final ChunkPreGenerator c1 = (ChunkPreGenerator) c0;
this.center = c1.center;
this.diameter = c1.diameter;
this.chunkCount = c1.chunkCount;
this.tickPercent = c1.tickPercent;
this.world = c1.world;
this.tickInterval = (int) value.getInterval();
this.plugin = value.getOwner();
this.logger = c1.logger;
}
return this;
}
@Override
public String toString() {
return "SpongeChunkPreGen{" +
"center=" + this.center +
", diameter=" + this.diameter +
", plugin=" + this.plugin +
", world=" + this.world +
", tickInterval=" + this.tickInterval +
", chunkCount=" + this.chunkCount +
", tickPercent=" + this.tickPercent +
'}';
}
private static class ChunkPreGenerator implements Consumer<Task> {
private static final Vector3i[] OFFSETS = {
Vector3i.UNIT_X,
Vector3i.UNIT_Z,
Vector3i.UNIT_X.negate(),
Vector3i.UNIT_Z.negate()
};
private static final String TIME_FORMAT = "s's 'S'ms'";
private final World world;
private final int chunkRadius;
private final int chunkCount;
private final float tickPercent;
private final long tickTimeLimit;
private final double diameter;
@Nullable private final Logger logger;
private Vector3i currentPosition;
private final Vector3d center;
private int currentLayerIndex;
private int currentLayerSize;
private int currentIndexInLayer;
private int totalCount;
private long totalTime;
public ChunkPreGenerator(World world, Vector3d center, double diameter, int chunkCount, float tickPercent, @Nullable Logger logger) {
this.world = world;
this.diameter = diameter;
this.chunkRadius = GenericMath.floor(diameter / 32);
this.chunkCount = chunkCount;
this.tickPercent = tickPercent;
this.logger = logger;
this.tickTimeLimit = Math.round(Sponge.getScheduler().getPreferredTickInterval() * tickPercent);
this.currentPosition = Sponge.getServer().getChunkLayout().toChunk(center.toInt()).get();
this.currentLayerIndex = 0;
this.center = center;
this.currentLayerSize = 0;
this.currentIndexInLayer = 0;
this.totalCount = 0;
this.totalTime = 0;
}
@Override
public void accept(Task task) {
final long startTime = System.currentTimeMillis();
int count = 0;
do {
this.world.loadChunk(nextChunkPosition(), true).ifPresent(Chunk::unloadChunk);
} while (hasNextChunkPosition() && checkChunkCount(++count) && checkTickTime(System.currentTimeMillis() - startTime));
if (this.logger != null) {
this.totalCount += count;
final long deltaTime = System.currentTimeMillis() - startTime;
this.totalTime += deltaTime;
this.logger.info("Generated {} chunks in {}, {}% complete", count,
DurationFormatUtils.formatDuration(deltaTime, TIME_FORMAT, false),
Math.round((float) this.totalCount / (this.chunkRadius * this.chunkRadius * 4) * 100));
}
if (!hasNextChunkPosition()) {
if (this.logger != null) {
this.logger.info("Done! Generated a total of {} chunks in {}", this.totalCount,
DurationFormatUtils.formatDuration(this.totalTime, TIME_FORMAT, false));
}
task.cancel();
}
}
private boolean hasNextChunkPosition() {
return this.currentLayerIndex <= this.chunkRadius;
}
private Vector3i nextChunkPosition() {
final Vector3i nextPosition = this.currentPosition;
if (++this.currentIndexInLayer >= this.currentLayerSize * 4) {
this.currentLayerIndex++;
this.currentLayerSize += 2;
this.currentIndexInLayer = 0;
this.currentPosition = this.currentPosition.sub(Vector3i.UNIT_Z).sub(Vector3i.UNIT_X);
}
this.currentPosition = this.currentPosition.add(OFFSETS[this.currentIndexInLayer / this.currentLayerSize]);
return nextPosition;
}
private boolean checkChunkCount(int count) {
return this.chunkCount <= 0 || count < this.chunkCount;
}
private boolean checkTickTime(long tickTime) {
return this.tickPercent <= 0 || tickTime < this.tickTimeLimit;
}
}
}