org.lanternpowered.server.world.pregen.LanternChunkPreGenerateTask.java Source code

Java tutorial

Introduction

Here is the source code for org.lanternpowered.server.world.pregen.LanternChunkPreGenerateTask.java

Source

/*
 * 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.pregen;

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 com.google.common.base.Throwables;
import org.apache.commons.lang3.time.DurationFormatUtils;
import org.lanternpowered.server.data.io.ChunkIOService;
import org.lanternpowered.server.game.Lantern;
import org.lanternpowered.server.world.chunk.LanternChunkLayout;
import org.slf4j.Logger;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.event.EventListener;
import org.spongepowered.api.event.SpongeEventFactory;
import org.spongepowered.api.event.cause.Cause;
import org.spongepowered.api.event.cause.NamedCause;
import org.spongepowered.api.event.world.ChunkPreGenerationEvent;
import org.spongepowered.api.scheduler.Task;
import org.spongepowered.api.world.ChunkPreGenerate;
import org.spongepowered.api.world.World;
import org.spongepowered.api.world.WorldBorder;
import org.spongepowered.api.world.storage.WorldProperties;

import java.io.IOException;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;

import javax.annotation.Nullable;

public class LanternChunkPreGenerateTask implements ChunkPreGenerate, Consumer<Task> {

    private static final int DEFAULT_TICK_INTERVAL = 4;
    private static final float DEFAULT_TICK_PERCENT = 0.8f;

    private static final Vector3i[] OFFSETS = { Vector3i.UNIT_Z.negate().mul(2), Vector3i.UNIT_X.mul(2),
            Vector3i.UNIT_Z.mul(2), Vector3i.UNIT_X.negate().mul(2) };

    private final World world;
    private final int chunkRadius;
    private final int chunkCount;
    private final float tickPercent;
    private final long tickTimeLimit;
    private final Cause cause;
    private final int totalChunksToGenerate;
    private final Task spongeTask;
    private final int tickInterval;
    private final Object plugin;

    // If null, no listeners have been assigned, so they don't need to be registered or unregistered.
    @Nullable
    private final EventListener<ChunkPreGenerationEvent> eventListener;

    private Vector3i currentPosition;
    private int currentGenCount;
    private int currentLayer;
    private int currentIndex;
    private int nextJump;

    private int chunksSkipped = 0;
    private int chunksGenerated = 0;

    // Used for wall clock times.
    private long generationStartTime = 0;
    private long generationEndTime = 0;
    private boolean isCancelled = false;

    private LanternChunkPreGenerateTask(Object plugin, World world, Vector3d center, double diameter,
            int chunkCount, float tickPercent, int tickInterval, Cause cause,
            List<Consumer<ChunkPreGenerationEvent>> eventListeners) {
        final int preferredTickInterval = Lantern.getScheduler().getPreferredTickInterval();

        this.plugin = plugin;
        this.world = world;

        this.chunkRadius = GenericMath.floor(diameter / 32);
        this.chunkCount = chunkCount;
        this.tickPercent = tickPercent;
        this.tickTimeLimit = Math.round(preferredTickInterval * tickPercent);
        this.cause = cause;
        this.tickInterval = tickInterval;
        final Optional<Vector3i> currentPosition = LanternChunkLayout.INSTANCE.toChunk(center.toInt());
        if (currentPosition.isPresent()) {
            this.currentPosition = currentPosition.get();
        } else {
            throw new IllegalArgumentException("Center is not a valid chunk coordinate");
        }
        this.currentGenCount = 4;
        this.currentLayer = 0;
        this.currentIndex = 0;
        this.nextJump = 0;

        this.totalChunksToGenerate = (int) Math.pow(this.chunkRadius * 2 + 1, 2);

        this.spongeTask = Lantern.getScheduler().createTaskBuilder().intervalTicks(preferredTickInterval)
                .execute(this).submit(plugin);

        if (!eventListeners.isEmpty()) {
            this.eventListener = new LanternChunkPreGenerateListener(this.spongeTask.getUniqueId(), eventListeners);
            Sponge.getEventManager().registerListener(plugin, ChunkPreGenerationEvent.class, this.eventListener);
        } else {
            this.eventListener = null;
        }
    }

    Task getSpongeTask() {
        return this.spongeTask;
    }

    @Override
    public WorldProperties getWorldProperties() {
        return this.world.getProperties();
    }

    @Override
    public int getTotalGeneratedChunks() {
        return this.chunksGenerated;
    }

    @Override
    public int getTotalSkippedChunks() {
        return this.chunksSkipped;
    }

    @Override
    public int getTargetTotalChunks() {
        return this.totalChunksToGenerate;
    }

    @Override
    public Duration getTotalTime() {
        return Duration.of(
                (isCancelled() ? this.generationEndTime : System.currentTimeMillis()) - this.generationStartTime,
                ChronoUnit.MILLIS);
    }

    @Override
    public boolean isCancelled() {
        if (this.isCancelled) {
            return true;
        }

        // It's possible we haven't cancelled the task here, so we just make sure of it, and perform
        // some cleanup.
        if (!Lantern.getScheduler().getTaskById(this.spongeTask.getUniqueId()).isPresent()) {
            cancel();
        }

        return this.isCancelled;
    }

    @Override
    public void cancel() {
        if (!this.isCancelled) {
            if (this.eventListener != null) {
                Sponge.getEventManager().unregisterListeners(this.eventListener);
            }
            this.spongeTask.cancel();
            this.isCancelled = true;
        }
    }

    @Override
    public void accept(Task task) {
        final long stepStartTime = System.currentTimeMillis();
        if (this.generationStartTime == 0) {
            this.generationStartTime = stepStartTime;
        }

        // Create and fire event.
        final ChunkPreGenerationEvent.Pre preEvent = SpongeEventFactory.createChunkPreGenerationEventPre(this.cause,
                this, this.world, false);

        if (Sponge.getEventManager().post(preEvent)) {
            // Cancelled event = cancelled task.
            cancelTask(task);
            return;
        }

        if (preEvent.getSkipStep()) {
            // Skip the step, but don't cancel the task.
            return;
        }

        // Count how many chunks are generated during the tick
        int count = 0;
        int skipped = 0;
        do {
            final Vector3i position = nextChunkPosition();
            final Vector3i pos1 = position.sub(Vector3i.UNIT_X);
            final Vector3i pos2 = position.sub(Vector3i.UNIT_Z);
            final Vector3i pos3 = pos2.sub(Vector3i.UNIT_X);

            // We can only skip generation if all chunks are loaded.
            if (!areAllChunksLoaded(position, pos1, pos2, pos3)) {

                // At least one chunk isn't generated, so to populate, we need to load them all.
                this.world.loadChunk(position, true);
                this.world.loadChunk(pos1, true);
                this.world.loadChunk(pos2, true);
                this.world.loadChunk(pos3, true);

                count += this.currentGenCount;
            } else {

                // Skipped them, log this.
                skipped += this.currentGenCount;
            }
        } while (hasNextChunkPosition() && checkChunkCount(count)
                && checkTickTime(System.currentTimeMillis() - stepStartTime));

        this.chunksGenerated += count;
        this.chunksSkipped += skipped;

        final long deltaTime = System.currentTimeMillis() - stepStartTime;
        this.generationEndTime = System.currentTimeMillis();

        // Create and fire event.
        if (Sponge.getEventManager().post(SpongeEventFactory.createChunkPreGenerationEventPost(this.cause, this,
                this.world, Duration.ofMillis(deltaTime), count, skipped))) {
            cancelTask(task);
            return;
        }

        if (!hasNextChunkPosition()) {
            // Generation has completed.
            Sponge.getEventManager()
                    .post(SpongeEventFactory.createChunkPreGenerationEventComplete(this.cause, this, this.world));
            this.isCancelled = true;
            unregisterListener();
            task.cancel();
        }
    }

    private boolean areAllChunksLoaded(Vector3i chunk1, Vector3i chunk2, Vector3i chunk3, Vector3i chunk4) {
        // In order to be able to check whether a chunk exists, we could use standard Sponge API methods. However,
        // because they set up an async method which we need to get sync anyway, we just bypass it.
        // This results in a extremely noticeable speed improvement.
        final ChunkIOService service = (ChunkIOService) this.world.getWorldStorage();
        try {
            return service.exists(chunk1) && service.exists(chunk2) && service.exists(chunk3)
                    && service.exists(chunk4);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void unregisterListener() {
        if (this.eventListener != null) {
            Sponge.getEventManager().unregisterListeners(this.eventListener);
        }
    }

    private void cancelTask(Task task) {
        // Don't fire multiple instances.
        if (Lantern.getScheduler().getTaskById(task.getUniqueId()).isPresent()) {
            Sponge.getEventManager()
                    .post(SpongeEventFactory.createChunkPreGenerationEventCancelled(this.cause, this, this.world));
            task.cancel();
        }

        this.isCancelled = true;
        unregisterListener();
    }

    private boolean hasNextChunkPosition() {
        return this.currentLayer <= this.chunkRadius;
    }

    private Vector3i nextChunkPosition() {
        final Vector3i nextPosition = this.currentPosition;
        final int currentLayerIndex;
        if (this.currentIndex >= this.nextJump) {
            // Reached end of layer, jump to the next so we can keep spiralling
            this.currentPosition = this.currentPosition.sub(Vector3i.UNIT_X).sub(Vector3i.UNIT_Z);
            this.currentLayer++;
            // Each the jump increment increases by 4 at each new layer
            this.nextJump += this.currentLayer * 4;
            currentLayerIndex = 1;
        } else {
            // Get the current index since the last jump
            currentLayerIndex = this.currentIndex - (this.nextJump - this.currentLayer * 4);
            // Move to next position in layer, by following a square
            this.currentPosition = this.currentPosition.add(OFFSETS[currentLayerIndex / this.currentLayer]);
        }
        // If we're at the corner it's 3, else 2 for an edge
        this.currentGenCount = currentLayerIndex % this.currentLayer == 0 ? 3 : 2;
        this.currentIndex++;
        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;
    }

    public static class Builder implements ChunkPreGenerate.Builder {

        private static final String TIME_FORMAT = "s's 'S'ms'";

        private final World world;
        private final Vector3d center;
        private final double diameter;
        private final List<Consumer<ChunkPreGenerationEvent>> eventListeners = new ArrayList<>();

        @Nullable
        private Object plugin;
        private int tickInterval = DEFAULT_TICK_INTERVAL;
        private float tickPercent = DEFAULT_TICK_PERCENT;
        private int chunksPerTick = 0;

        public Builder(World world, Vector3d center, double diameter) {
            this.world = world;
            this.center = center;
            this.diameter = diameter;
        }

        public Builder(World world, WorldBorder worldBorder) {
            this(world, worldBorder.getCenter(), worldBorder.getNewDiameter());
        }

        @Override
        public ChunkPreGenerate.Builder owner(Object plugin) {
            checkNotNull(plugin, "plugin cannot be null");
            this.plugin = plugin;
            return this;
        }

        @Override
        public ChunkPreGenerate.Builder logger(@Nullable Logger logger) {
            if (logger != null) {
                this.addListener(event -> {
                    if (event instanceof ChunkPreGenerationEvent.Post) {
                        ChunkPreGenerationEvent.Post post = (ChunkPreGenerationEvent.Post) event;
                        logger.info("Generated {} chunks in {}, {}% complete", post.getChunksGeneratedThisStep(),
                                DurationFormatUtils.formatDuration(post.getTimeTakenForStep().toMillis(),
                                        TIME_FORMAT, false),
                                GenericMath.floor((post.getChunkPreGenerate().getTotalGeneratedChunks()
                                        + post.getChunkPreGenerate().getTotalSkippedChunks())
                                        / post.getChunkPreGenerate().getTargetTotalChunks() * 100));
                    } else if (event instanceof ChunkPreGenerationEvent.Complete) {
                        logger.info("Done! Generated a total of {} chunks in {}",
                                event.getChunkPreGenerate().getTargetTotalChunks(),
                                DurationFormatUtils.formatDuration(
                                        event.getChunkPreGenerate().getTotalTime().toMillis(), TIME_FORMAT, false));
                    }
                });
            }

            return this;
        }

        @Override
        public ChunkPreGenerate.Builder tickInterval(int tickInterval) {
            checkArgument(tickInterval > 0, "tickInterval must be greater than zero");
            this.tickInterval = tickInterval;
            return this;
        }

        @Override
        public ChunkPreGenerate.Builder chunksPerTick(int chunkCount) {
            this.chunksPerTick = chunkCount;
            return this;
        }

        @Override
        public ChunkPreGenerate.Builder tickPercentLimit(float tickPercent) {
            checkArgument(tickPercent <= 1, "tickPercent must be smaller or equal to 1");
            checkArgument(tickPercent > 0, "tickPercent must be greater than 0");
            this.tickPercent = tickPercent;
            return this;
        }

        @Override
        public ChunkPreGenerate.Builder addListener(Consumer<ChunkPreGenerationEvent> listener) {
            checkNotNull(listener, "listener cannot be null");
            this.eventListeners.add(listener);
            return this;
        }

        @Override
        public ChunkPreGenerate start() {
            checkNotNull(plugin, "owner cannot be null");
            checkArgument(this.chunksPerTick > 0 || this.tickPercent > 0,
                    "Must use at least one of \"chunks per tick\" or \"tick percent limit\"");
            return new LanternChunkPreGenerateTask(this.plugin, this.world, this.center, this.diameter,
                    this.chunksPerTick, this.tickPercent, this.tickInterval,
                    Cause.of(NamedCause.owner(this.plugin)), this.eventListeners);
        }

        @Override
        public ChunkPreGenerate.Builder from(ChunkPreGenerate value) {
            if (!(value instanceof LanternChunkPreGenerateTask)) {
                throw new IllegalArgumentException("Not a Sponge chunk pre-gen task");
            }

            final LanternChunkPreGenerateTask other = (LanternChunkPreGenerateTask) value;
            // Bypass null check
            this.plugin = other.plugin;
            return tickInterval(other.tickInterval).chunksPerTick(other.chunkCount)
                    .tickPercentLimit(other.tickPercent);
        }

        @Override
        public ChunkPreGenerate.Builder reset() {
            this.plugin = null;
            this.tickInterval = 0;
            this.chunksPerTick = 0;
            this.tickPercent = DEFAULT_TICK_PERCENT;
            this.eventListeners.clear();
            return this;
        }
    }
}