Version 1.3.0 (#85)

* Feature/async chunkmaster (#81)

* Change generation to be asynchronous

At this stage it will most likely crash the server at a certain point.
Pausing and resuming isn't stable. Saving the progress isn't stable as well.
Chunks are being unloaded in the main thread by an unloader class.

* Switch to native threads

- Use thread instead of async tasks
- Store pending paper chunks in the database
- Interrupt the thread when it should be stopped

* Fix insertion of pending chunks

Fix an error that is thrown when the sql for inserting pending chunks doesn't have any chunks to insert.

* Add task states

Add states to differentiate between generating and validating tasks
as well as a field in the database to store this information.
A task will first generate a world until the required radius or the
worldborder is reached. Then it validates that each chunk has been generated.

* Add object representation of world_properties table

* Add DAO for pending_chunks table

* Add DAO for generation_tasks table

* Add state updating to periodic save

* Fix loading of world properties

* Add states to tasks and fix completion handling

* Fix progress report and spiral shape

* Modify the paper generation task so it works with spigot

This change is being made because normal chunk generation doesn't allow
chunks to be requested from a different thread. With PaperLib this issue
can be solved.

* Add workarounds for spigot problems

* Fix some blocking issues and update README

* Add locking to ChunkUnloader class

* Add total chunk count to list command (closes #79) (#82)

* Fix shape beign stuck (#83)

* Add autostart config parameter (closes #78) (#84)

* Add circleci badge to readme

* Add codefactor badge
pull/86/head v1.3.0
Trivernis 4 years ago committed by GitHub
parent 400295ac73
commit 4d4107aaf3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,4 +1,4 @@
# chunkmaster ![](https://abstruse.trivernis.net/badge/1) ![](https://img.shields.io/discord/729250668162056313)
# chunkmaster [![](https://circleci.com/gh/Trivernis/spigot-chunkmaster.svg?style=shield)](https://app.circleci.com/pipelines/github/Trivernis/spigot-chunkmaster) [![CodeFactor](https://www.codefactor.io/repository/github/trivernis/spigot-chunkmaster/badge)](https://www.codefactor.io/repository/github/trivernis/spigot-chunkmaster) [![](https://img.shields.io/discord/729250668162056313)](https://discord.gg/KZcMAgN)
This plugin can be used to pre-generate the region of a world around the spawn chunk(s).
The generation automatically pauses when a player joins the server (assuming the server was empty before)
@ -82,45 +82,40 @@ generation:
# The maximum amount of chunks that are loaded before unloading and saving them.
# Higher values mean higher generation speed but greater memory usage.
# The value should be a positive integer.
max-loaded-chunks: 10
max-loaded-chunks: 1000
# Paper Only
# The maximum amount of requested chunks with the asynchronous paper chunk
# loading method. Higher values mean faster generation but more memory usage
# (and probably bigger performance impact).
# loading method. Higher values mean faster generation but more memory usage and
# bigger performance impact. Configuring it too hight might crash the server.
# The value should be a positive integer.
max-pending-chunks: 10
# The period (in ticks) in which a generation step is run.
# Higher values mean less performance impact but slower generation.
# The value should be a positive integer.
period: 2
# The max amount of chunks that should be generated per step.
# Higher values mean higher generation speed but higher performance impact.
# The value should be a positive integer.
chunks-per-step: 4
# Paper Only
# The number of already generated chunks that will be skipped for each step.
# Notice that these still have a performance impact because the server needs to check
# if the chunk is generated.
# Higher values mean faster generation but greater performance impact.
# The value should be a positive integer.
chunk-skips-per-step: 100
max-pending-chunks: 500
# The maximum milliseconds per tick the server is allowed to have
# during the cunk generation process.
# If the mspt is greather than this, the chunk generation task pauses.
# The value should be a positive integer greater than 50.
# The value should be a positive integer greater than 50.
mspt-pause-threshold: 500
# The period in ticks for how often loaded chunks get unloaded.
# Unloading happens in the main thread and can impact the server performance.
# You can tweak this setting with the max-loaded-chunks setting to have either
# a lot of chunks unloaded at once or fewer chunks unloaded more often.
# If the maximum number of loaded chunks is reached the generation pauses until the
# unloading task runs again so keep that in mind.
# The value should be a positive integer.
unloading-period: 50
# Pauses the generation if the number of players on the server is larger or equal
# to the configured value
# Notice that playing on a server that constantly generates chunks can be
# very laggy and can cause it to crash.
# The value should be a posivitve integer > 1.
pause-on-player-count: 1
# if the generation should automatically start on server startup
# the value should be a boolean
autostart: true
```
### Spigot and Paper

@ -22,7 +22,7 @@ idea {
}
group "net.trivernis"
version "1.2.3"
version "1.3.0"
sourceCompatibility = 1.8

@ -3,13 +3,12 @@ package net.trivernis.chunkmaster
import io.papermc.lib.PaperLib
import net.trivernis.chunkmaster.commands.CommandChunkmaster
import net.trivernis.chunkmaster.lib.LanguageManager
import net.trivernis.chunkmaster.lib.SqliteManager
import net.trivernis.chunkmaster.lib.database.SqliteManager
import net.trivernis.chunkmaster.lib.generation.GenerationManager
import org.bstats.bukkit.Metrics
import org.bukkit.plugin.java.JavaPlugin
import org.bukkit.scheduler.BukkitTask
import org.dynmap.DynmapAPI
import java.util.logging.Level
class Chunkmaster: JavaPlugin() {
lateinit var sqliteManager: SqliteManager
@ -67,6 +66,7 @@ class Chunkmaster: JavaPlugin() {
override fun onDisable() {
logger.info(langManager.getLocalized("STOPPING_ALL_TASKS"))
generationManager.stopAll()
server.scheduler.cancelTasks(this)
}
/**
@ -74,14 +74,13 @@ class Chunkmaster: JavaPlugin() {
*/
private fun configure() {
dataFolder.mkdir()
config.addDefault("generation.period", 2L)
config.addDefault("generation.chunks-per-step", 2)
config.addDefault("generation.chunk-skips-per-step", 100)
config.addDefault("generation.mspt-pause-threshold", 500L)
config.addDefault("generation.pause-on-player-count", 1)
config.addDefault("generation.max-pending-chunks", 10)
config.addDefault("generation.max-loaded-chunks", 10)
config.addDefault("generation.max-pending-chunks", 500)
config.addDefault("generation.max-loaded-chunks", 1000)
config.addDefault("generation.unloading-period", 50L)
config.addDefault("generation.ignore-worldborder", false)
config.addDefault("generation.autostart", true)
config.addDefault("database.filename", "chunkmaster.db")
config.addDefault("language", "en")
config.addDefault("dynmap", true)
@ -95,7 +94,7 @@ class Chunkmaster: JavaPlugin() {
private fun initDatabase() {
logger.info(langManager.getLocalized("DB_INIT"))
try {
this.sqliteManager = SqliteManager( this)
this.sqliteManager = SqliteManager(this)
sqliteManager.init()
logger.info(langManager.getLocalized("DB_INIT_FINISHED"))
} catch(e: Exception) {

@ -1,10 +1,7 @@
package net.trivernis.chunkmaster.commands
import net.md_5.bungee.api.ChatColor
import net.md_5.bungee.api.chat.ComponentBuilder
import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.Subcommand
import net.trivernis.chunkmaster.lib.generation.TaskEntry
import org.bukkit.command.Command
import org.bukkit.command.CommandSender
@ -39,7 +36,7 @@ class CmdCancel(private val chunkmaster: Chunkmaster): Subcommand {
}
if (index != null && chunkmaster.generationManager.removeTask(index)) {
sender.sendMessage(chunkmaster.langManager.getLocalized("TASK_CANCELED", index))
sender.sendMessage(chunkmaster.langManager.getLocalized("TASK_CANCELLED", index))
true
} else {
sender.sendMessage(chunkmaster.langManager.getLocalized("TASK_NOT_FOUND", args[0]))

@ -37,12 +37,6 @@ class CmdGetCenter(private val chunkmaster: Chunkmaster): Subcommand {
args[0]
}
}
if (chunkmaster.generationManager.worldCenters.isEmpty()) {
chunkmaster.generationManager.loadWorldCenters() {
sendCenterInfo(sender, worldName)
}
return true
}
sendCenterInfo(sender, worldName)
return true
}
@ -51,15 +45,17 @@ class CmdGetCenter(private val chunkmaster: Chunkmaster): Subcommand {
* Sends the center information
*/
private fun sendCenterInfo(sender: CommandSender, worldName: String) {
var center = chunkmaster.generationManager.worldCenters[worldName]
if (center == null) {
val world = sender.server.worlds.find { it.name == worldName }
if (world == null) {
sender.sendMessage(chunkmaster.langManager.getLocalized("WORLD_NOT_FOUND", worldName))
return
chunkmaster.generationManager.worldProperties.getWorldCenter(worldName).thenAccept { worldCenter ->
var center = worldCenter
if (center == null) {
val world = sender.server.worlds.find { it.name == worldName }
if (world == null) {
sender.sendMessage(chunkmaster.langManager.getLocalized("WORLD_NOT_FOUND", worldName))
return@thenAccept
}
center = Pair(world.spawnLocation.chunk.x, world.spawnLocation.chunk.z)
}
center = Pair(world.spawnLocation.chunk.x, world.spawnLocation.chunk.z)
sender.sendMessage(chunkmaster.langManager.getLocalized("CENTER_INFO", worldName, center.first, center.second))
}
sender.sendMessage(chunkmaster.langManager.getLocalized("CENTER_INFO", worldName, center.first, center.second))
}
}

@ -2,9 +2,10 @@ package net.trivernis.chunkmaster.commands
import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.Subcommand
import net.trivernis.chunkmaster.lib.generation.TaskEntry
import net.trivernis.chunkmaster.lib.generation.taskentry.TaskEntry
import org.bukkit.command.Command
import org.bukkit.command.CommandSender
import kotlin.math.ceil
class CmdList(private val chunkmaster: Chunkmaster): Subcommand {
override val name = "list"
@ -52,7 +53,12 @@ class CmdList(private val chunkmaster: Chunkmaster): Subcommand {
" (%.1f".format(genTask.shape.progress()*100) + "%)."
else
""
val count = if (genTask.radius > 0) {
"${genTask.count} / ${ceil(genTask.shape.total()).toInt()}"
} else {
genTask.count.toString()
}
return "\n" + chunkmaster.langManager.getLocalized("TASKS_ENTRY",
task.id, genTask.world.name, genTask.count, percentage)
task.id, genTask.world.name, genTask.state.toString(), count, percentage)
}
}

@ -67,7 +67,7 @@ class CmdSetCenter(private val chunkmaster: Chunkmaster): Subcommand {
centerZ = args[2].toInt()
}
}
chunkmaster.generationManager.updateWorldCenter(world, Pair(centerX, centerZ))
chunkmaster.generationManager.worldProperties.setWorldCenter(world, Pair(centerX, centerZ))
sender.sendMessage(chunkmaster.langManager.getLocalized("CENTER_UPDATED", world, centerX, centerZ))
return true
}

@ -41,10 +41,6 @@ class CmdStats(private val chunkmaster: Chunkmaster): Subcommand {
${chunkmaster.langManager.getLocalized("STATS_ENTITY_COUNT", world.entities.size)}
${chunkmaster.langManager.getLocalized("STATS_LOADED_CHUNKS", world.loadedChunks.size)}
""".trimIndent()
val task = chunkmaster.generationManager.tasks.find { it.generationTask.world == world }
if (task != null) {
message += "\n" + chunkmaster.langManager.getLocalized("STATS_PLUGIN_LOADED_CHUNKS", task.generationTask.loadedChunksCount)
}
return message
}
@ -59,6 +55,8 @@ class CmdStats(private val chunkmaster: Chunkmaster): Subcommand {
${chunkmaster.langManager.getLocalized("STATS_PLUGIN_VERSION", chunkmaster.description.version)}
${chunkmaster.langManager.getLocalized("STATS_MEMORY", memUsed/1000000, runtime.maxMemory()/1000000, (memUsed.toFloat()/runtime.maxMemory().toFloat()) * 100)}
${chunkmaster.langManager.getLocalized("STATS_CORES", runtime.availableProcessors())}
${chunkmaster.langManager.getLocalized("STATS_PLUGIN_LOADED_CHUNKS", chunkmaster.generationManager.loadedChunkCount)}
""".trimIndent()
for (world in sender.server.worlds) {
message += "\n\n" + getWorldStatsMessage(sender, world)

@ -0,0 +1,14 @@
package net.trivernis.chunkmaster.lib.database
import net.trivernis.chunkmaster.lib.generation.ChunkCoordinates
import net.trivernis.chunkmaster.lib.generation.TaskState
data class GenerationTaskData(
val id: Int,
val world: String,
val radius: Int,
val shape: String,
val state: TaskState,
val center: ChunkCoordinates,
val last: ChunkCoordinates
)

@ -0,0 +1,103 @@
package net.trivernis.chunkmaster.lib.database
import net.trivernis.chunkmaster.lib.generation.ChunkCoordinates
import net.trivernis.chunkmaster.lib.generation.TaskState
import java.util.concurrent.CompletableFuture
class GenerationTasks(private val sqliteManager: SqliteManager) {
/**
* Returns all stored generation tasks
*/
fun getGenerationTasks(): CompletableFuture<List<GenerationTaskData>> {
val completableFuture = CompletableFuture<List<GenerationTaskData>>()
sqliteManager.executeStatement("SELECT * FROM generation_tasks", HashMap()) { res ->
val tasks = ArrayList<GenerationTaskData>()
while (res!!.next()) {
val id = res.getInt("id")
val world = res.getString("world")
val center = ChunkCoordinates(res.getInt("center_x"), res.getInt("center_z"))
val last = ChunkCoordinates(res.getInt("last_x"), res.getInt("last_z"))
val radius = res.getInt("radius")
val shape = res.getString("shape")
val state = stringToState(res.getString("state"))
val taskData = GenerationTaskData(id, world, radius, shape, state, center, last)
if (tasks.find { it.id == id } == null) {
tasks.add(taskData)
}
}
completableFuture.complete(tasks)
}
return completableFuture
}
/**
* Adds a generation task to the database
*/
fun addGenerationTask(world: String, center: ChunkCoordinates, radius: Int, shape: String): CompletableFuture<Int> {
val completableFuture = CompletableFuture<Int>()
sqliteManager.executeStatement("""
INSERT INTO generation_tasks (center_x, center_z, last_x, last_z, world, radius, shape)
values (?, ?, ?, ?, ?, ?, ?)""",
hashMapOf(
1 to center.x,
2 to center.z,
3 to center.x,
4 to center.z,
5 to world,
6 to radius,
7 to shape
)
) {
sqliteManager.executeStatement(
"""
SELECT id FROM generation_tasks ORDER BY id DESC LIMIT 1
""".trimIndent(), HashMap()
) {
it!!.next()
completableFuture.complete(it.getInt("id"))
}
}
return completableFuture
}
/**
* Deletes a generationTask from the database
*/
fun deleteGenerationTask(id: Int): CompletableFuture<Void> {
val completableFuture = CompletableFuture<Void>()
sqliteManager.executeStatement("DELETE FROM generation_tasks WHERE id = ?;", hashMapOf(1 to id)) {
completableFuture.complete(null)
}
return completableFuture
}
fun updateGenerationTask(id: Int, last: ChunkCoordinates, state: TaskState): CompletableFuture<Void> {
val completableFuture = CompletableFuture<Void>()
sqliteManager.executeStatement(
"""
UPDATE generation_tasks SET last_x = ?, last_z = ?, state = ?
WHERE id = ?
""".trimIndent(),
hashMapOf(1 to last.x, 2 to last.z, 3 to state.toString(), 4 to id)
) {
completableFuture.complete(null)
}
return completableFuture
}
/**
* Converts a string into a task state
*/
private fun stringToState(stringState: String): TaskState {
TaskState.valueOf(stringState)
return when (stringState) {
"GENERATING" -> TaskState.GENERATING
"VALIDATING" -> TaskState.VALIDATING
"PAUSING" -> TaskState.PAUSING
"CORRECTING" -> TaskState.CORRECTING
else -> TaskState.GENERATING
}
}
}

@ -0,0 +1,57 @@
package net.trivernis.chunkmaster.lib.database
import net.trivernis.chunkmaster.lib.generation.ChunkCoordinates
import java.util.concurrent.CompletableFuture
class PendingChunks(private val sqliteManager: SqliteManager) {
/**
* Returns a list of pending chunks for a taskId
*/
fun getPendingChunks(taskId: Int): CompletableFuture<List<ChunkCoordinates>> {
val completableFuture = CompletableFuture<List<ChunkCoordinates>>()
sqliteManager.executeStatement("SELECT * FROM pending_chunks WHERE task_id = ?", hashMapOf(1 to taskId)) {
val pendingChunks = ArrayList<ChunkCoordinates>()
while (it!!.next()) {
pendingChunks.add(ChunkCoordinates(it.getInt("chunk_x"), it.getInt("chunk_z")))
}
completableFuture.complete(pendingChunks)
}
return completableFuture
}
/**
* Clears all pending chunks of a task
*/
fun clearPendingChunks(taskId: Int): CompletableFuture<Void> {
val completableFuture = CompletableFuture<Void>()
sqliteManager.executeStatement("DELETE FROM pending_chunks WHERE task_id = ?", hashMapOf(1 to taskId)) {
completableFuture.complete(null)
}
return completableFuture
}
/**
* Adds pending chunks for a taskid
*/
fun addPendingChunks(taskId: Int, pendingChunks: List<ChunkCoordinates>): CompletableFuture<Void> {
val completableFuture = CompletableFuture<Void>()
if (pendingChunks.isEmpty()) {
completableFuture.complete(null)
} else {
var sql = "INSERT INTO pending_chunks (task_id, chunk_x, chunk_z) VALUES"
var index = 1
val valueMap = HashMap<Int, Any>()
for (coordinates in pendingChunks) {
sql += "(?, ?, ?),"
valueMap[index++] = taskId
valueMap[index++] = coordinates.x
valueMap[index++] = coordinates.z
}
sqliteManager.executeStatement(sql.removeSuffix(","), valueMap) {
completableFuture.complete(null)
}
}
return completableFuture
}
}

@ -1,12 +1,10 @@
package net.trivernis.chunkmaster.lib
package net.trivernis.chunkmaster.lib.database
import net.trivernis.chunkmaster.Chunkmaster
import org.apache.commons.lang.exception.ExceptionUtils
import org.sqlite.SQLiteConnection
import java.lang.Exception
import java.sql.Connection
import java.sql.DriverManager
import java.sql.PreparedStatement
import java.sql.ResultSet
class SqliteManager(private val chunkmaster: Chunkmaster) {
@ -21,7 +19,8 @@ class SqliteManager(private val chunkmaster: Chunkmaster) {
Pair("last_z", "integer NOT NULL DEFAULT 0"),
Pair("world", "text UNIQUE NOT NULL DEFAULT 'world'"),
Pair("radius", "integer DEFAULT -1"),
Pair("shape", "text NOT NULL DEFAULT 'square'")
Pair("shape", "text NOT NULL DEFAULT 'square'"),
Pair("state", "text NOT NULL DEFAULT 'GENERATING'")
)
),
Pair(
@ -31,6 +30,15 @@ class SqliteManager(private val chunkmaster: Chunkmaster) {
Pair("center_x", "integer NOT NULL DEFAULT 0"),
Pair("center_z", "integer NOT NULL DEFAULT 0")
)
),
Pair(
"pending_chunks",
listOf(
Pair("id", "integer PRIMARY KEY AUTOINCREMENT"),
Pair("task_id", "integer NOT NULL"),
Pair("chunk_x", "integer NOT NULL"),
Pair("chunk_z", "integer NOT NULL")
)
)
)
private val needUpdate = HashSet<Pair<String, Pair<String, String>>>()
@ -38,6 +46,10 @@ class SqliteManager(private val chunkmaster: Chunkmaster) {
private var connection: Connection? = null
private var activeTasks = 0
val worldProperties = WorldProperties(this)
val pendingChunks = PendingChunks(this)
val generationTasks = GenerationTasks(this)
/**
* Returns the connection to the database
*/
@ -92,17 +104,18 @@ class SqliteManager(private val chunkmaster: Chunkmaster) {
/**
* Executes a sql statement on the database.
*/
fun executeStatement(sql: String, values: HashMap<Int, Any>, callback: ((ResultSet) -> Unit)?) {
fun executeStatement(sql: String, values: HashMap<Int, Any>, callback: ((ResultSet?) -> Unit)?) {
val connection = getConnection()
activeTasks++
if (connection != null) {
try {
//println("'$sql' with values $values")
val statement = connection.prepareStatement(sql)
for (parameterValue in values) {
statement.setObject(parameterValue.key, parameterValue.value)
}
statement.execute()
val res = statement.resultSet
val res: ResultSet? = statement.resultSet
if (callback != null) {
callback(res)
}

@ -0,0 +1,82 @@
package net.trivernis.chunkmaster.lib.database
import net.trivernis.chunkmaster.lib.generation.ChunkCoordinates
import java.util.concurrent.CompletableFuture
class WorldProperties(private val sqliteManager: SqliteManager) {
private val properties = HashMap<String, Pair<Int, Int>>()
/**
* Returns the world center for one world
*/
fun getWorldCenter(worldName: String): CompletableFuture<Pair<Int, Int>?> {
val completableFuture = CompletableFuture<Pair<Int, Int>?>()
if (properties[worldName] != null) {
completableFuture.complete(properties[worldName])
} else {
sqliteManager.executeStatement("SELECT * FROM world_properties WHERE name = ?", hashMapOf(1 to worldName)) {
if (it != null && it.next()) {
completableFuture.complete(Pair(it.getInt("center_x"), it.getInt("center_z")))
} else {
completableFuture.complete(null)
}
}
}
return completableFuture
}
/**
* Updates the center of a world
*/
fun setWorldCenter(worldName: String, center: Pair<Int, Int>): CompletableFuture<Void> {
val completableFuture = CompletableFuture<Void>()
getWorldCenter(worldName).thenAccept {
if (it != null) {
updateWorldProperties(worldName, center).thenAccept {completableFuture.complete(null) }
} else {
insertWorldProperties(worldName, center).thenAccept { completableFuture.complete(null) }
}
}
return completableFuture
}
/**
* Updates an entry in the world properties
*/
private fun updateWorldProperties(worldName: String, center: Pair<Int, Int>): CompletableFuture<Void> {
val completableFuture = CompletableFuture<Void>()
sqliteManager.executeStatement("UPDATE world_properties SET center_x = ?, center_z = ? WHERE name = ?",
hashMapOf(
1 to center.first,
2 to center.second,
3 to worldName
)
) {
properties[worldName] = center
completableFuture.complete(null)
}
return completableFuture
}
/**
* Inserts into the world properties
*/
private fun insertWorldProperties(worldName: String, center: Pair<Int, Int>): CompletableFuture<Void> {
val completableFuture = CompletableFuture<Void>()
sqliteManager.executeStatement("INSERT INTO world_properties (name, center_x, center_z) VALUES (?, ?, ?)",
hashMapOf(
1 to worldName,
2 to center.first,
3 to center.second
)
) {
properties[worldName] = center
completableFuture.complete(null)
}
return completableFuture
}
}

@ -7,4 +7,8 @@ class ChunkCoordinates(val x: Int, val z: Int) {
fun getCenterLocation(world: World): Location {
return Location(world, ((x*16) + 8).toDouble(), 1.0, ((z*16) + 8).toDouble())
}
override fun toString(): String {
return "($x, $z)"
}
}

@ -0,0 +1,60 @@
package net.trivernis.chunkmaster.lib.generation
import net.trivernis.chunkmaster.Chunkmaster
import org.bukkit.Chunk
import java.lang.Exception
import java.util.*
import java.util.concurrent.*
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.collections.HashSet
class ChunkUnloader(private val plugin: Chunkmaster): Runnable {
private val maxLoadedChunks = plugin.config.getInt("generation.max-loaded-chunks")
private val lock = ReentrantReadWriteLock()
private var unloadingQueue = Vector<Chunk>(maxLoadedChunks)
val isFull: Boolean
get() {
return pendingSize == maxLoadedChunks
}
val pendingSize: Int
get() {
lock.readLock().lock()
val size = unloadingQueue.size
lock.readLock().unlock()
return size
}
/**
* Unloads all chunks in the unloading queue with each run
*/
override fun run() {
lock.writeLock().lock()
try {
val chunkToUnload = unloadingQueue.toHashSet()
for (chunk in chunkToUnload) {
try {
chunk.unload(true)
} catch (e: Exception) {
plugin.logger.severe(e.toString())
}
}
unloadingQueue.clear()
} finally {
lock.writeLock().unlock()
}
}
/**
* Adds a chunk to unload to the queue
*/
fun add(chunk: Chunk) {
lock.writeLock().lockInterruptibly()
try {
unloadingQueue.add(chunk)
} finally {
lock.writeLock().unlock()
}
}
}

@ -0,0 +1,145 @@
package net.trivernis.chunkmaster.lib.generation
import io.papermc.lib.PaperLib
import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.shapes.Shape
import org.bukkit.World
import java.util.concurrent.*
class DefaultGenerationTask(
private val plugin: Chunkmaster,
unloader: ChunkUnloader,
world: World,
startChunk: ChunkCoordinates,
override val radius: Int = -1,
shape: Shape,
missingChunks: HashSet<ChunkCoordinates>,
state: TaskState
) : GenerationTask(plugin, world, unloader, startChunk, shape, missingChunks, state) {
private val maxPendingChunks = plugin.config.getInt("generation.max-pending-chunks")
val pendingChunks = ArrayBlockingQueue<PendingChunkEntry>(maxPendingChunks)
override var count = 0
override var endReached: Boolean = false
init {
updateGenerationAreaMarker()
count = shape.count
}
/**
* Runs the generation task. Every Iteration the next chunks will be generated if
* they haven't been generated already
* After a configured number of chunks chunks have been generated, they will all be unloaded and saved.
*/
override fun generate() {
generateMissing()
seekGenerated()
generateUntilBorder()
}
/**
* Validates that all chunks have been generated or generates missing ones
*/
override fun validate() {
this.shape.reset()
val missedChunks = HashSet<ChunkCoordinates>()
while (!cancelRun && !borderReached()) {
val chunkCoordinates = nextChunkCoordinates
triggerDynmapRender(chunkCoordinates)
if (!PaperLib.isChunkGenerated(world, chunkCoordinates.x, chunkCoordinates.z)) {
missedChunks.add(chunkCoordinates)
}
}
this.missingChunks.addAll(missedChunks)
}
/**
* Generates chunks that are missing
*/
override fun generateMissing() {
val missing = this.missingChunks.toHashSet()
this.count = 0
while (missing.size > 0 && !cancelRun) {
if (plugin.mspt < msptThreshold && !unloader.isFull) {
val chunk = missing.first()
missing.remove(chunk)
this.requestGeneration(chunk)
this.count++
} else {
Thread.sleep(50L)
}
}
if (!cancelRun) {
this.joinPending()
}
}
/**
* Seeks until it encounters a chunk that hasn't been generated yet
*/
private fun seekGenerated() {
do {
lastChunkCoords = nextChunkCoordinates
count = shape.count
} while (PaperLib.isChunkGenerated(world, lastChunkCoords.x, lastChunkCoords.z) && !borderReached())
}
/**
* Generates the world until it encounters the worlds border
*/
private fun generateUntilBorder() {
var chunkCoordinates: ChunkCoordinates
while (!cancelRun && !borderReached()) {
if (plugin.mspt < msptThreshold && !unloader.isFull) {
chunkCoordinates = nextChunkCoordinates
requestGeneration(chunkCoordinates)
lastChunkCoords = chunkCoordinates
count = shape.count
} else {
Thread.sleep(50L)
}
}
if (!cancelRun) {
joinPending()
}
}
private fun joinPending() {
while (!this.pendingChunks.isEmpty()) {
Thread.sleep(msptThreshold)
}
}
/**
* Request the generation of a chunk
*/
private fun requestGeneration(chunkCoordinates: ChunkCoordinates) {
if (!PaperLib.isChunkGenerated(world, chunkCoordinates.x, chunkCoordinates.z) || PaperLib.isSpigot()) {
val pendingChunkEntry = PendingChunkEntry(
chunkCoordinates,
PaperLib.getChunkAtAsync(world, chunkCoordinates.x, chunkCoordinates.z, true)
)
this.pendingChunks.put(pendingChunkEntry)
pendingChunkEntry.chunk.thenAccept {
this.unloader.add(it)
this.pendingChunks.remove(pendingChunkEntry)
}
}
}
/**
* Cancels the generation task.
* This unloads all chunks that were generated but not unloaded yet.
*/
override fun cancel() {
this.cancelRun = true
this.pendingChunks.forEach { it.chunk.cancel(false) }
updateGenerationAreaMarker(true)
}
}

@ -1,25 +1,47 @@
package net.trivernis.chunkmaster.lib.generation
import io.papermc.lib.PaperLib
import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.generation.taskentry.PausedTaskEntry
import net.trivernis.chunkmaster.lib.generation.taskentry.RunningTaskEntry
import net.trivernis.chunkmaster.lib.generation.taskentry.TaskEntry
import net.trivernis.chunkmaster.lib.shapes.Circle
import net.trivernis.chunkmaster.lib.shapes.Spiral
import org.bukkit.Server
import org.bukkit.World
import java.util.concurrent.CompletableFuture
class GenerationManager(private val chunkmaster: Chunkmaster, private val server: Server) {
val tasks: HashSet<RunningTaskEntry> = HashSet()
val pausedTasks: HashSet<PausedTaskEntry> = HashSet()
val worldCenters: HashMap<String, Pair<Int, Int>> = HashMap()
val worldProperties = chunkmaster.sqliteManager.worldProperties
private val pendingChunksTable = chunkmaster.sqliteManager.pendingChunks
private val generationTasks = chunkmaster.sqliteManager.generationTasks
private val unloadingPeriod: Long
get() {
return chunkmaster.config.getLong("generation.unloading-period")
}
private val pauseOnPlayerCount: Int
get () {
return chunkmaster.config.getInt("generation.pause-on-player-count")
}
private val autostart: Boolean
get () {
return chunkmaster.config.getBoolean("generation.autostart")
}
val loadedChunkCount: Int
get() {
return unloader.pendingSize
}
private val unloader = ChunkUnloader(chunkmaster)
val allTasks: HashSet<TaskEntry>
get() {
if (this.tasks.isEmpty() && this.pausedTasks.isEmpty()) {
if (this.worldCenters.isEmpty()) {
this.loadWorldCenters()
}
this.startAll()
if (server.onlinePlayers.size >= chunkmaster.config.getInt("generation.pause-on-player-count")) {
if (server.onlinePlayers.size >= pauseOnPlayerCount) {
this.pauseAll()
}
}
@ -36,41 +58,17 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
*/
fun addTask(world: World, radius: Int = -1, shape: String = "square"): Int {
val foundTask = allTasks.find { it.generationTask.world == world }
if (foundTask == null) {
val centerChunk = if (worldCenters[world.name] == null) {
val center = worldProperties.getWorldCenter(world.name).join()
val centerChunk = if (center == null) {
ChunkCoordinates(world.spawnLocation.chunk.x, world.spawnLocation.chunk.z)
} else {
val center = worldCenters[world.name]!!
ChunkCoordinates(center.first, center.second)
}
val generationTask = createGenerationTask(world, centerChunk, centerChunk, radius, shape)
chunkmaster.sqliteManager.executeStatement(
"""
INSERT INTO generation_tasks (center_x, center_z, last_x, last_z, world, radius, shape)
values (?, ?, ?, ?, ?, ?, ?)
""",
HashMap(
mapOf(
1 to centerChunk.x,
2 to centerChunk.z,
3 to centerChunk.x,
4 to centerChunk.z,
5 to world.name,
6 to radius,
7 to shape
)
),
null
)
var id = 0
chunkmaster.sqliteManager.executeStatement("""
SELECT id FROM generation_tasks ORDER BY id DESC LIMIT 1
""".trimIndent(), HashMap()) {
it.next()
id = it.getInt("id")
}
val generationTask = createGenerationTask(world, centerChunk, centerChunk, radius, shape, null)
val id = generationTasks.addGenerationTask(world.name, centerChunk, radius, shape).join()
generationTask.onEndReached {
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("TASK_FINISHED", id, it.count))
@ -78,13 +76,19 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
}
if (!paused) {
val task = server.scheduler.runTaskTimer(
chunkmaster, generationTask, 200, // 10 sec delay
chunkmaster.config.getLong("generation.period")
val taskEntry = RunningTaskEntry(
id,
generationTask
)
tasks.add(RunningTaskEntry(id, task, generationTask))
taskEntry.start()
tasks.add(taskEntry)
} else {
pausedTasks.add(PausedTaskEntry(id, generationTask))
pausedTasks.add(
PausedTaskEntry(
id,
generationTask
)
)
}
return id
@ -102,17 +106,18 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
last: ChunkCoordinates,
id: Int,
radius: Int = -1,
delay: Long = 200L,
shape: String = "square"
shape: String = "square",
pendingChunks: List<ChunkCoordinates>?
) {
if (!paused) {
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("RESUME_FOR_WORLD", world.name))
val generationTask = createGenerationTask(world, center, last, radius, shape)
val task = server.scheduler.runTaskTimer(
chunkmaster, generationTask, delay,
chunkmaster.config.getLong("generation.period")
val generationTask = createGenerationTask(world, center, last, radius, shape, pendingChunks)
val taskEntry = RunningTaskEntry(
id,
generationTask
)
tasks.add(RunningTaskEntry(id, task, generationTask))
taskEntry.start()
tasks.add(taskEntry)
generationTask.onEndReached {
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("TASK_FINISHED", id, generationTask.count))
removeTask(id)
@ -129,22 +134,23 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
} else {
this.tasks.find { it.id == id }
}
if (taskEntry != null) {
taskEntry.cancel()
chunkmaster.sqliteManager.executeStatement("""
DELETE FROM generation_tasks WHERE id = ?;
""".trimIndent(), HashMap(mapOf(1 to taskEntry.id)),
null
)
try {
if (taskEntry != null) {
if (taskEntry.generationTask.isRunning && taskEntry is RunningTaskEntry) {
taskEntry.cancel(chunkmaster.config.getLong("mspt-pause-threshold"))
}
generationTasks.deleteGenerationTask(id)
pendingChunksTable.clearPendingChunks(id)
if (taskEntry is RunningTaskEntry) {
if (taskEntry.task.isCancelled) {
if (taskEntry is RunningTaskEntry) {
tasks.remove(taskEntry)
} else if (taskEntry is PausedTaskEntry) {
pausedTasks.remove(taskEntry)
}
} else if (taskEntry is PausedTaskEntry) {
pausedTasks.remove(taskEntry)
return true
}
return true
} catch (e: Exception) {
chunkmaster.logger.severe(e.toString())
}
return false
}
@ -159,12 +165,15 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
saveProgress() // save progress every 30 seconds
}, 600, 600)
server.scheduler.runTaskLater(chunkmaster, Runnable {
this.loadWorldCenters()
this.startAll()
if (!server.onlinePlayers.isEmpty()) {
if (server.onlinePlayers.count() >= pauseOnPlayerCount || !autostart) {
if (!autostart) {
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("NO_AUTOSTART"))
}
this.pauseAll()
}
}, 20)
server.scheduler.runTaskTimer(chunkmaster, unloader, unloadingPeriod, unloadingPeriod)
}
/**
@ -173,40 +182,36 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
fun stopAll() {
val removalSet = HashSet<RunningTaskEntry>()
for (task in tasks) {
val lastChunk = task.generationTask.lastChunkCoords
val id = task.id
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("SAVING_TASK_PROGRESS", task.id))
saveProgressToDatabase(lastChunk, id)
task.task.cancel()
task.generationTask.cancel()
if (task.task.isCancelled) {
removalSet.add(task)
saveProgressToDatabase(task.generationTask, id).join()
if (!task.cancel(chunkmaster.config.getLong("mspt-pause-threshold"))) {
chunkmaster.logger.warning(chunkmaster.langManager.getLocalized("CANCEL_FAIL", task.id))
}
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("TASK_CANCELED", task.id))
removalSet.add(task)
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("TASK_CANCELLED", task.id))
}
tasks.removeAll(removalSet)
if (unloader.pendingSize > 0) {
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("SAVING_CHUNKS", unloader.pendingSize))
unloader.run()
}
}
/**
* Starts all generation tasks.
*/
fun startAll() {
chunkmaster.sqliteManager.executeStatement("SELECT * FROM generation_tasks", HashMap()) { res ->
var count = 0
while (res.next()) {
count++
try {
val id = res.getInt("id")
val world = server.getWorld(res.getString("world"))
val center = ChunkCoordinates(res.getInt("center_x"), res.getInt("center_z"))
val last = ChunkCoordinates(res.getInt("last_x"), res.getInt("last_z"))
val radius = res.getInt("radius")
val shape = res.getString("shape")
if (this.tasks.find { it.id == id } == null) {
resumeTask(world!!, center, last, id, radius, 200L + count, shape)
generationTasks.getGenerationTasks().thenAccept { tasks ->
for (task in tasks) {
val world = server.getWorld(task.world)
if (world != null) {
pendingChunksTable.getPendingChunks(task.id).thenAccept {
resumeTask(world, task.center, task.last, task.id, task.radius, task.shape, it)
}
} catch (error: NullPointerException) {
chunkmaster.logger.severe(chunkmaster.langManager.getLocalized("TASK_LOAD_FAILED", res.getInt("id")))
} else {
chunkmaster.logger.severe(chunkmaster.langManager.getLocalized("TASK_LOAD_FAILED", task.id))
}
}
}
@ -222,7 +227,12 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
fun pauseAll() {
paused = true
for (task in tasks) {
pausedTasks.add(PausedTaskEntry(task.id, task.generationTask))
pausedTasks.add(
PausedTaskEntry(
task.id,
task.generationTask
)
)
}
stopAll()
}
@ -237,98 +247,97 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
}
/**
* Overload that doesn't need an argument
*/
private fun loadWorldCenters() {
loadWorldCenters(null)
}
/**
* Loads the world centers from the database
* Saves the task progress
*/
fun loadWorldCenters(cb: (() -> Unit)?) {
chunkmaster.sqliteManager.executeStatement("SELECT * FROM world_properties", HashMap()) {
while (it.next()) {
worldCenters[it.getString("name")] = Pair(it.getInt("center_x"), it.getInt("center_z"))
private fun saveProgress() {
for (task in tasks) {
try {
if (task.generationTask.state == TaskState.CORRECTING) {
reportCorrectionProgress(task)
} else {
reportGenerationProgress(task)
}
saveProgressToDatabase(task.generationTask, task.id)
} catch (error: Exception) {
chunkmaster.logger.warning(chunkmaster.langManager.getLocalized("TASK_SAVE_FAILED", error.toString()))
error.printStackTrace()
}
cb?.invoke()
}
}
/**
* Updates the center of a world
* Reports the progress for correcting tasks
*/
fun updateWorldCenter(worldName: String, center: Pair<Int, Int>) {
chunkmaster.sqliteManager.executeStatement("SELECT * FROM world_properties WHERE name = ?", HashMap(mapOf(1 to worldName))) {
if (it.next()) {
chunkmaster.sqliteManager.executeStatement("UPDATE world_properties SET center_x = ?, center_z = ? WHERE name = ?", HashMap(
mapOf(
1 to center.first,
2 to center.second,
3 to worldName
)
), null)
} else {
chunkmaster.sqliteManager.executeStatement("INSERT INTO world_properties (name, center_x, center_z) VALUES (?, ?, ?)", HashMap(
mapOf(
1 to worldName,
2 to center.first,
3 to center.second
)
), null)
}
private fun reportCorrectionProgress(task: RunningTaskEntry) {
val genTask = task.generationTask
val progress = if (genTask.missingChunks.size > 0) {
"(${(genTask.count / genTask.missingChunks.size) * 100}%)"
} else {
""
}
worldCenters[worldName] = center
chunkmaster.logger.info(
chunkmaster.langManager.getLocalized(
"TASK_PERIODIC_REPORT_CORRECTING",
task.id,
genTask.world.name,
genTask.count,
progress
)
)
}
/**
* Saves the task progress
* Reports the progress of the chunk generation
*/
private fun saveProgress() {
for (task in tasks) {
try {
val genTask = task.generationTask
val (speed, chunkSpeed) = task.generationSpeed
val percentage = if (genTask.radius > 0) "(${"%.2f".format(genTask.shape.progress() * 100)}%)" else ""
val eta = if (genTask.radius > 0 && speed!! > 0) {
val etaSeconds = (genTask.shape.progress())/speed
val hours: Int = (etaSeconds/3600).toInt()
val minutes: Int = ((etaSeconds % 3600) / 60).toInt()
val seconds: Int = (etaSeconds % 60).toInt()
", ETA: %d:%02d:%02d".format(hours, minutes, seconds)
} else {
""
}
chunkmaster.logger.info(chunkmaster.langManager.getLocalized(
"TASK_PERIODIC_REPORT",
task.id,
genTask.world.name,
genTask.count,
percentage,
eta,
chunkSpeed!!,
genTask.lastChunkCoords.x,
genTask.lastChunkCoords.z))
saveProgressToDatabase(genTask.lastChunkCoords, task.id)
genTask.updateLastChunkMarker()
} catch (error: Exception) {
chunkmaster.logger.warning(chunkmaster.langManager.getLocalized("TASK_SAVE_FAILED", error.toString()))
}
private fun reportGenerationProgress(task: RunningTaskEntry) {
val genTask = task.generationTask
val (speed, chunkSpeed) = task.generationSpeed
val percentage = if (genTask.radius > 0) "(${"%.2f".format(genTask.shape.progress() * 100)}%)" else ""
val eta = if (genTask.radius > 0 && speed!! > 0) {
val remaining = 1 - genTask.shape.progress()
val etaSeconds = remaining / speed
val hours: Int = (etaSeconds / 3600).toInt()
val minutes: Int = ((etaSeconds % 3600) / 60).toInt()
val seconds: Int = (etaSeconds % 60).toInt()
", ETA: %dh %dmin %ds".format(hours, minutes, seconds)
} else {
""
}
chunkmaster.logger.info(
chunkmaster.langManager.getLocalized(
"TASK_PERIODIC_REPORT",
task.id,
genTask.world.name,
genTask.state.toString(),
genTask.count,
percentage,
eta,
chunkSpeed!!,
genTask.lastChunkCoords.x,
genTask.lastChunkCoords.z
)
)
}
/**
* Saves the generation progress to the database
*/
private fun saveProgressToDatabase(lastChunk: ChunkCoordinates, id: Int) {
chunkmaster.sqliteManager.executeStatement(
"""
UPDATE generation_tasks SET last_x = ?, last_z = ?
WHERE id = ?
""".trimIndent(),
HashMap(mapOf(1 to lastChunk.x, 2 to lastChunk.z, 3 to id)),
null
)
private fun saveProgressToDatabase(generationTask: GenerationTask, id: Int): CompletableFuture<Void> {
val completableFuture = CompletableFuture<Void>()
generationTasks.updateGenerationTask(id, generationTask.lastChunkCoords, generationTask.state).thenAccept {
pendingChunksTable.clearPendingChunks(id).thenAccept {
if (generationTask is DefaultGenerationTask) {
if (generationTask.pendingChunks.size > 0) {
pendingChunksTable.addPendingChunks(id, generationTask.pendingChunks.map { it.coordinates })
}
}
pendingChunksTable.addPendingChunks(id, generationTask.missingChunks.toList()).thenAccept {
completableFuture.complete(null)
}
}
}
return completableFuture
}
/**
@ -340,7 +349,8 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
center: ChunkCoordinates,
start: ChunkCoordinates,
radius: Int,
shapeName: String
shapeName: String,
pendingChunks: List<ChunkCoordinates>?
): GenerationTask {
val shape = when (shapeName) {
"circle" -> Circle(Pair(center.x, center.z), Pair(start.x, start.z), radius)
@ -348,10 +358,14 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
else -> Spiral(Pair(center.x, center.z), Pair(start.x, start.z), radius)
}
return if (PaperLib.isPaper()) {
GenerationTaskPaper(chunkmaster, world, start, radius, shape)
} else {
GenerationTaskSpigot(chunkmaster, world, start, radius, shape)
}
return DefaultGenerationTask(
chunkmaster,
unloader,
world,
start,
radius,
shape, pendingChunks?.toHashSet() ?: HashSet(),
TaskState.GENERATING
)
}
}

@ -3,37 +3,33 @@ package net.trivernis.chunkmaster.lib.generation
import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.dynmap.*
import net.trivernis.chunkmaster.lib.shapes.Shape
import org.bukkit.Chunk
import org.bukkit.World
import java.lang.Exception
import java.util.concurrent.Semaphore
import kotlin.math.ceil
/**
* Interface for generation tasks.
*/
abstract class GenerationTask(
private val plugin: Chunkmaster,
val world: World,
protected val unloader: ChunkUnloader,
startChunk: ChunkCoordinates,
val shape: Shape
val shape: Shape,
val missingChunks: HashSet<ChunkCoordinates>,
var state: TaskState
) :
Runnable {
abstract val radius: Int
abstract val world: World
abstract var count: Int
abstract var endReached: Boolean
var isRunning: Boolean = false
val loadedChunksCount: Int
get() {
return loadedChunks.size
}
protected val loadedChunks: HashSet<Chunk> = HashSet()
var lastChunkCoords = ChunkCoordinates(startChunk.x, startChunk.z)
protected set
protected val chunkSkips = plugin.config.getInt("generation.chunk-skips-per-step")
protected val msptThreshold = plugin.config.getLong("generation.mspt-pause-threshold")
protected val maxLoadedChunks = plugin.config.getInt("generation.max-loaded-chunks")
protected val chunksPerStep = plugin.config.getInt("generation.chunks-per-step")
protected var cancelRun: Boolean = false
private var endReachedCallback: ((GenerationTask) -> Unit)? = null
@ -45,16 +41,49 @@ abstract class GenerationTask(
null
}
private val markerAreaStyle = MarkerStyle(null, LineStyle(2, 1.0, 0x0022FF), FillStyle(.0, 0))
private val markerAreaId = "chunkmaster_genarea"
private val markerAreaName = "Chunkmaster Generation Area"
private val markerLastStyle = MarkerStyle(null, LineStyle(2, 1.0, 0x0077FF), FillStyle(.5, 0x0077FF))
private val markerLastId = "chunkmaster_lastchunk"
private val markerLastName = "Chunkmaster Last Chunk"
private val markerAreaId = "chunkmaster_genarea_${world.name}"
private val markerAreaName = "Chunkmaster Generation Area (${ceil(shape.total()).toInt()} chunks)"
private val ignoreWorldborder = plugin.config.getBoolean("generation.ignore-worldborder")
abstract override fun run()
abstract fun generate()
abstract fun validate()
abstract fun generateMissing()
abstract fun cancel()
override fun run() {
isRunning = true
try {
when (state) {
TaskState.GENERATING -> {
this.generate()
if (!cancelRun) {
this.state = TaskState.VALIDATING
this.validate()
}
if (!cancelRun) {
this.state = TaskState.CORRECTING
this.generateMissing()
}
}
TaskState.VALIDATING -> {
this.validate()
if (!cancelRun) {
this.state = TaskState.CORRECTING
this.generateMissing()
}
}
TaskState.CORRECTING -> {
this.generateMissing()
}
else -> { }
}
if (!cancelRun && this.borderReached()) {
this.setEndReached()
}
} catch (e: InterruptedException){}
isRunning = false
}
val nextChunkCoordinates: ChunkCoordinates
get() {
val nextChunkCoords = shape.next()
@ -69,26 +98,6 @@ abstract class GenerationTask(
|| shape.endReached()
}
/**
* Unloads all chunks that have been loaded
*/
protected fun unloadLoadedChunks() {
for (chunk in loadedChunks) {
if (chunk.isLoaded) {
try {
chunk.unload(true)
} catch (e: Exception) {
plugin.logger.severe(e.toString())
}
}
if (dynmapIntegration) {
dynmap?.triggerRenderOfVolume(chunk.getBlock(0, 0, 0).location, chunk.getBlock(15, 255, 15).location)
}
}
loadedChunks.clear()
}
/**
* Updates the dynmap marker for the generation radius
*/
@ -105,19 +114,11 @@ abstract class GenerationTask(
}
}
/**
* Updates the dynmap marker for the generation radius
*/
fun updateLastChunkMarker(clear: Boolean = false) {
if (clear) {
markerSet?.deleteAreaMarker(markerLastId)
} else if (dynmapIntegration) {
markerSet?.creUpdateAreMarker(
markerLastId,
markerLastName,
this.lastChunkCoords.getCenterLocation(world).chunk.getBlock(0, 0, 0).location,
this.lastChunkCoords.getCenterLocation(world).chunk.getBlock(15, 0, 15).location,
markerLastStyle
protected fun triggerDynmapRender(chunkCoordinates: ChunkCoordinates) {
if (dynmapIntegration) {
dynmap?.triggerRenderOfVolume(
world.getBlockAt(chunkCoordinates.x * 16, 0, chunkCoordinates.z * 16).location,
world.getBlockAt((chunkCoordinates.x * 16) + 16, 255, (chunkCoordinates.z * 16) + 16).location
)
}
}
@ -128,21 +129,8 @@ abstract class GenerationTask(
private fun setEndReached() {
endReached = true
count = shape.count
endReachedCallback?.invoke(this)
updateGenerationAreaMarker(true)
updateLastChunkMarker(true)
}
/**
* Performs a check if the border has been reached
*/
protected fun borderReachedCheck(): Boolean {
val done = borderReached()
if (done) {
unloadLoadedChunks()
setEndReached()
}
return done
endReachedCallback?.invoke(this)
}
/**

@ -1,121 +0,0 @@
package net.trivernis.chunkmaster.lib.generation
import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.shapes.Shape
import org.bukkit.Chunk
import org.bukkit.World
import java.lang.Exception
import java.util.concurrent.CompletableFuture
class GenerationTaskPaper(
private val plugin: Chunkmaster,
override val world: World,
startChunk: ChunkCoordinates,
override val radius: Int = -1,
shape: Shape
) : GenerationTask(plugin, startChunk, shape) {
private val maxPendingChunks = plugin.config.getInt("generation.max-pending-chunks")
private val pendingChunks = HashSet<CompletableFuture<Chunk>>()
override var count = 0
override var endReached: Boolean = false
init {
updateGenerationAreaMarker()
count = shape.count
}
/**
* Runs the generation task. Every Iteration the next chunks will be generated if
* they haven't been generated already
* After a configured number of chunks chunks have been generated, they will all be unloaded and saved.
*/
override fun run() {
if (plugin.mspt < msptThreshold) {
if (loadedChunks.size > maxLoadedChunks) {
unloadLoadedChunks()
} else if (pendingChunks.size < maxPendingChunks) {
if (borderReachedCheck()) return
var chunk = nextChunkCoordinates
for (i in 0 until chunkSkips) {
if (world.isChunkGenerated(chunk.x, chunk.z)) {
chunk = nextChunkCoordinates
} else {
break
}
}
if (!world.isChunkGenerated(chunk.x, chunk.z)) {
for (i in 0 until chunksPerStep) {
if (borderReached()) break
if (!world.isChunkGenerated(chunk.x, chunk.z)) {
pendingChunks.add(world.getChunkAtAsync(chunk.x, chunk.z, true))
}
chunk = nextChunkCoordinates
}
if (!world.isChunkGenerated(chunk.x, chunk.z)) {
pendingChunks.add(world.getChunkAtAsync(chunk.x, chunk.z, true))
}
}
lastChunkCoords = chunk
count = shape.count
}
}
checkChunksLoaded()
}
/**
* Cancels the generation task.
* This unloads all chunks that were generated but not unloaded yet.
*/
override fun cancel() {
updateGenerationAreaMarker(true)
updateLastChunkMarker(true)
unloadAllChunks()
}
/**
* Cancels all pending chunks and unloads all loaded chunks.
*/
private fun unloadAllChunks() {
for (pendingChunk in pendingChunks) {
if (pendingChunk.isDone) {
loadedChunks.add(pendingChunk.get())
} else {
pendingChunk.cancel(true)
}
}
pendingChunks.clear()
if (loadedChunks.isNotEmpty()) {
lastChunkCoords = ChunkCoordinates(loadedChunks.last().x, loadedChunks.last().z)
}
for (chunk in loadedChunks) {
if (chunk.isLoaded) {
try {
chunk.unload(true);
} catch (e: Exception){
plugin.logger.severe(e.toString())
}
}
}
}
/**
* Checks if some chunks have been loaded and adds them to the loaded chunk set.
*/
private fun checkChunksLoaded() {
val completedEntries = HashSet<CompletableFuture<Chunk>>()
for (pendingChunk in pendingChunks) {
if (pendingChunk.isDone) {
completedEntries.add(pendingChunk)
loadedChunks.add(pendingChunk.get())
} else if (pendingChunk.isCompletedExceptionally || pendingChunk.isCancelled) {
completedEntries.add(pendingChunk)
}
}
pendingChunks.removeAll(completedEntries)
}
}

@ -1,72 +0,0 @@
package net.trivernis.chunkmaster.lib.generation
import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.shapes.Shape
import org.bukkit.World
import java.lang.Exception
class GenerationTaskSpigot(
private val plugin: Chunkmaster,
override val world: World,
startChunk: ChunkCoordinates,
override val radius: Int = -1,
shape: Shape
) : GenerationTask(plugin, startChunk, shape) {
override var count = 0
override var endReached: Boolean = false
init {
updateGenerationAreaMarker()
count = shape.count
}
/**
* Runs the generation task. Every Iteration the next chunks will be generated if
* they haven't been generated already
* After a configured number of chunks chunks have been generated, they will all be unloaded and saved.
*/
override fun run() {
if (plugin.mspt < msptThreshold) {
if (loadedChunks.size > maxLoadedChunks) {
unloadLoadedChunks()
} else {
if (borderReachedCheck()) return
var chunk = nextChunkCoordinates
for (i in 0 until chunksPerStep) {
if (borderReached()) break
val chunkInstance = world.getChunkAt(chunk.x, chunk.z)
chunkInstance.load(true)
loadedChunks.add(chunkInstance)
chunk = nextChunkCoordinates
}
val chunkInstance = world.getChunkAt(chunk.x, chunk.z)
chunkInstance.load(true)
loadedChunks.add(chunkInstance)
lastChunkCoords = chunk
count = shape.count
}
}
}
/**
* Cancels the generation task.
* This unloads all chunks that were generated but not unloaded yet.
*/
override fun cancel() {
for (chunk in loadedChunks) {
if (chunk.isLoaded) {
try {
chunk.unload(true)
} catch (e: Exception) {
plugin.logger.severe(e.toString())
}
}
}
updateGenerationAreaMarker(true)
updateLastChunkMarker(true)
}
}

@ -1,10 +0,0 @@
package net.trivernis.chunkmaster.lib.generation
class PausedTaskEntry(
override val id: Int,
override val generationTask: GenerationTask
) : TaskEntry {
override fun cancel() {
generationTask.cancel()
}
}

@ -0,0 +1,10 @@
package net.trivernis.chunkmaster.lib.generation
import net.trivernis.chunkmaster.lib.generation.ChunkCoordinates
import org.bukkit.Chunk
import java.util.concurrent.CompletableFuture
class PendingChunkEntry(val coordinates: ChunkCoordinates, val chunk: CompletableFuture<Chunk>) {
val isDone: Boolean
get() = chunk.isDone
}

@ -1,13 +0,0 @@
package net.trivernis.chunkmaster.lib.generation
import org.bukkit.scheduler.BukkitTask
/**
* Generic task entry
*/
interface TaskEntry {
val id: Int
val generationTask: GenerationTask
fun cancel()
}

@ -0,0 +1,24 @@
package net.trivernis.chunkmaster.lib.generation
enum class TaskState {
GENERATING {
override fun toString(): String {
return "GENERATING"
}
},
VALIDATING {
override fun toString(): String {
return "VALIDATING"
}
},
CORRECTING {
override fun toString(): String {
return "CORRECTING"
}
},
PAUSING {
override fun toString(): String {
return "PAUSING"
}
},
}

@ -0,0 +1,9 @@
package net.trivernis.chunkmaster.lib.generation.taskentry
import net.trivernis.chunkmaster.lib.generation.GenerationTask
class PausedTaskEntry(
override val id: Int,
override val generationTask: GenerationTask
) : TaskEntry {
}

@ -1,15 +1,16 @@
package net.trivernis.chunkmaster.lib.generation
package net.trivernis.chunkmaster.lib.generation.taskentry
import org.bukkit.scheduler.BukkitTask
import io.papermc.lib.PaperLib
import net.trivernis.chunkmaster.lib.generation.GenerationTask
class RunningTaskEntry(
override val id: Int,
val task: BukkitTask,
override val generationTask: GenerationTask
) : TaskEntry {
private var lastProgress: Pair<Long, Double>? = null
private var lastChunkCount: Pair<Long, Int>? = null
private var thread = Thread(generationTask)
/**
* Returns the generation Speed
@ -20,13 +21,13 @@ class RunningTaskEntry(
var chunkGenerationSpeed: Double? = null
if (lastProgress != null) {
val progressDiff = generationTask.shape.progress() - lastProgress!!.second
val timeDiff = (System.currentTimeMillis() - lastProgress!!.first).toDouble()/1000
generationSpeed = progressDiff/timeDiff
val timeDiff = (System.currentTimeMillis() - lastProgress!!.first).toDouble() / 1000
generationSpeed = progressDiff / timeDiff
}
if (lastChunkCount != null) {
val chunkDiff = generationTask.count - lastChunkCount!!.second
val timeDiff = (System.currentTimeMillis() - lastChunkCount!!.first).toDouble()/1000
chunkGenerationSpeed = chunkDiff/timeDiff
val timeDiff = (System.currentTimeMillis() - lastChunkCount!!.first).toDouble() / 1000
chunkGenerationSpeed = chunkDiff / timeDiff
}
lastProgress = Pair(System.currentTimeMillis(), generationTask.shape.progress())
lastChunkCount = Pair(System.currentTimeMillis(), generationTask.count)
@ -38,9 +39,32 @@ class RunningTaskEntry(
lastChunkCount = Pair(System.currentTimeMillis(), generationTask.count)
}
fun start() {
thread.start()
}
fun cancel(timeout: Long): Boolean {
if (generationTask.isRunning) {
generationTask.cancel()
thread.interrupt()
}
return try {
joinThread(timeout)
} catch (e: InterruptedException) {
true
}
}
private fun joinThread(timeout: Long): Boolean {
var threadStopped = false
override fun cancel() {
task.cancel()
generationTask.cancel()
for (i in 0..100) {
if (!thread.isAlive || !generationTask.isRunning) {
threadStopped = true
break
}
Thread.sleep(timeout / 100)
}
return threadStopped
}
}

@ -0,0 +1,11 @@
package net.trivernis.chunkmaster.lib.generation.taskentry
import net.trivernis.chunkmaster.lib.generation.GenerationTask
/**
* Generic task entry
*/
interface TaskEntry {
val id: Int
val generationTask: GenerationTask
}

@ -10,7 +10,7 @@ import kotlin.math.pow
import kotlin.math.sqrt
import kotlin.system.exitProcess
class Circle(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(center, start, radius) {
class Circle(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int) : Shape(center, start, radius) {
private var r = 0
private var coords = Stack<Pair<Int, Int>>()
private var previousCoords = HashSet<Pair<Int, Int>>()
@ -20,9 +20,13 @@ class Circle(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(
return radius > 0 && coords.isEmpty() && r >= radius
}
override fun total(): Double {
return (PI * radius.toFloat().pow(2))
}
override fun progress(): Double {
// TODO: Radius inner progress
return (count/(PI* radius.toFloat().pow(2))).coerceAtMost(100.0)
return (count / (PI * radius.toFloat().pow(2))).coerceAtMost(1.0)
}
override fun currentRadius(): Int {
@ -36,7 +40,7 @@ class Circle(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(
override fun getShapeEdgeLocations(): List<Pair<Int, Int>> {
val locations = this.getCircleCoordinates(this.radius)
locations.add(locations.first())
return locations.map{ Pair(it.first + center.first, it.second + center.second) }
return locations.map { Pair(it.first + center.first, it.second + center.second) }
}
/**
@ -48,7 +52,7 @@ class Circle(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(
}
if (count == 0 && currentPos != center) {
val tmpCircle = Circle(center, center, radius)
while (tmpCircle.next() != currentPos);
while (tmpCircle.next() != currentPos && !tmpCircle.endReached());
this.count = tmpCircle.count
this.r = tmpCircle.r
}
@ -59,7 +63,7 @@ class Circle(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(
if (coords.isEmpty()) {
r++
val tmpCoords = HashSet<Pair<Int, Int>>()
tmpCoords.addAll(getCircleCoordinates((r*2)-1).map { Pair(it.first / 2, it.second / 2) })
tmpCoords.addAll(getCircleCoordinates((r * 2) - 1).map { Pair(it.first / 2, it.second / 2) })
tmpCoords.addAll(getCircleCoordinates(r))
tmpCoords.removeAll(previousCoords)
previousCoords.clear()
@ -77,16 +81,18 @@ class Circle(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(
* Some coordinates might already be present in the list
* @param r - the radius
*/
private fun getCircleCoordinates(r: Int): ArrayList<Pair<Int, Int>> {
val coords = ArrayList<Pair<Int, Int>>()
private fun getCircleCoordinates(r: Int): Vector<Pair<Int, Int>> {
val coords = Vector<Pair<Int, Int>>()
val segCoords = getSegment(r)
coords.addAll(segCoords.reversed())
for (step in 1..7) {
val tmpSeg = ArrayList<Pair<Int, Int>>()
val tmpSeg = Vector<Pair<Int, Int>>()
for (pos in segCoords) {
val coord = when (step) {
1 -> Pair(pos.first, -pos.second)
2 ->Pair(pos.second, -pos.first)
2 -> Pair(pos.second, -pos.first)
3 -> Pair(-pos.second, -pos.first)
4 -> Pair(-pos.first, -pos.second)
5 -> Pair(-pos.first, pos.second)
@ -128,4 +134,11 @@ class Circle(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(
}
return coords
}
}
override fun reset() {
this.r = 0
this.currentPos = center
this.previousCoords.clear()
this.count = 0
}
}

@ -25,6 +25,11 @@ abstract class Shape(protected val center: Pair<Int, Int>, start: Pair<Int, Int>
*/
abstract fun progress(): Double
/**
* The total number of chunks to generate
*/
abstract fun total(): Double
/**
* Returns the current radius
*/
@ -34,4 +39,9 @@ abstract class Shape(protected val center: Pair<Int, Int>, start: Pair<Int, Int>
* returns a poly marker for the shape
*/
abstract fun getShapeEdgeLocations(): List<Pair<Int, Int>>
/**
* Resets the shape to its center start position
*/
abstract fun reset()
}

@ -10,11 +10,18 @@ class Spiral(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(
override fun endReached(): Boolean {
val distances = getDistances(center, currentPos)
return radius > 0 && (distances.first > radius || distances.second > radius)
return radius > 0 && ((direction == 3
&& abs(distances.first) == abs(distances.second)
&& abs(distances.first) == radius)
|| (distances.first > radius || distances.second > radius))
}
override fun total(): Double {
return (radius * 2).toDouble().pow(2)
}
override fun progress(): Double {
return (count / (radius * 2).toDouble().pow(2)).coerceAtMost(100.0)
return (count / (radius * 2).toDouble().pow(2)).coerceAtMost(1.0)
}
override fun currentRadius(): Int {
@ -26,10 +33,13 @@ class Spiral(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(
* Returns the next value in the spiral
*/
override fun next(): Pair<Int, Int> {
if (endReached()) {
return currentPos
}
if (count == 0 && currentPos != center) {
// simulate the spiral to get the correct direction and count
val simSpiral = Spiral(center, center, radius)
while (simSpiral.next() != currentPos);
while (simSpiral.next() != currentPos && !simSpiral.endReached());
direction = simSpiral.direction
count = simSpiral.count
}
@ -86,4 +96,13 @@ class Spiral(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(
private fun getDistances(pos1: Pair<Int, Int>, pos2: Pair<Int, Int>): Pair<Int, Int> {
return Pair(pos2.first - pos1.first, pos2.second - pos1.second)
}
/**
* Resets the shape to its starting parameters
*/
override fun reset() {
this.currentPos = center
this.count = 0
this.direction = 0
}
}

@ -1,11 +1,12 @@
RESUME_FOR_WORLD = Resuming chunk generation task for world '%s'...
TASK_FINISHED = Task #%d finished after %d chunks.
TASK_CANCELED = Canceled task #%s.
TASK_CANCELLED = Cancelled task #%s.
TASK_LOAD_FAILED = §cFailed to load task #%d.
TASK_LOAD_SUCCESS = %d saved tasks loaded.
TASK_NOT_FOUND = §cTask %s not found!
CREATE_DELAYED_LOAD = Creating task to load chunk generation Tasks later...
TASK_PERIODIC_REPORT = Task #%d running for '%s'. Progress: %d chunks %s %s, Speed: %.1f ch/s, Last Chunk: %d, %d
TASK_PERIODIC_REPORT = Task #%d running for '%s'. State: %s. Progress: %d chunks %s %s, Speed: %.1f ch/s, Last Chunk: %d, %d
TASK_PERIODIC_REPORT_CORRECTING = Task #%d generating missing chunks for '%s'. Progress: %d chunks %s
TASK_SAVE_FAILED = §cException when saving tasks: %s
WORLD_NAME_REQUIRED = §cYou need to provide a world name!
@ -18,7 +19,7 @@ TASK_ID_REQUIRED = §cYou need to provide a task id!
INVALID_ARGUMENT = §cInvalid argument at %s: %s!
PAUSED_TASKS_HEADER = Currently Paused Generation Tasks
TASKS_ENTRY = - §9#%d§r - §2%s§r - §2%d chunks %s§r
TASKS_ENTRY = - §9#%d§r - §2%s§r - §2%s§r - §2%s chunks %s§r
RUNNING_TASKS_HEADER = Currently Running Generation Tasks
NO_GENERATION_TASKS = There are no generation tasks.
@ -72,4 +73,8 @@ STATS_CORES = - Cores: §2%d§r
STATS_WORLD_NAME = §l%s§r
STATS_ENTITY_COUNT = - §2%d§r Entities
STATS_LOADED_CHUNKS = - §2%d§r Loaded Chunks
STATS_PLUGIN_LOADED_CHUNKS = - §2%d§r Chunks Loaded by Chunkmaster
STATS_PLUGIN_LOADED_CHUNKS = - §2%d§r Chunks Loaded by Chunkmaster
SAVING_CHUNKS = Saving %d loaded chunks...
CANCEL_FAIL = Failed to cancel task #%d in the given timeout!
NO_AUTOSTART = Autostart set to §2false§r. Pausing...

@ -1,11 +1,12 @@
RESUME_FOR_WORLD = Setze das Chunk-Generieren für Welt '%s' fort...
TASK_FINISHED = Aufgabe #%d wurde nach %d chunks beendet.
TASK_CANCELED = Aufgabe #%s wurde abgebrochen.
TASK_CANCELLED = Aufgabe #%s wurde abgebrochen.
TASK_LOAD_FAILED = §cAufgabe #%d konnte nicht geladen werden.
TASK_LOAD_SUCCESS = %d gespeicherte Aufgaben wurden geladen.
TASK_NOT_FOUND = §cAufgabe %s konnte nicht gefunden werden!
CREATE_DELAYED_LOAD = Erstelle einen Bukkit-Task zum verzögerten Laden von Aufgaben...
TASK_PERIODIC_REPORT = Aufgabe #%d für Welt '%s'. Fortschritt: %d chunks %s %s, Geschwindigkeit: %.1f ch/s, Letzer Chunk: %d, %d
TASK_PERIODIC_REPORT = Aufgabe #%d für Welt '%s'. Status: %s. Fortschritt: %d chunks %s %s, Geschwindigkeit: %.1f ch/s, Letzer Chunk: %d, %d
TASK_PERIODIC_REPORT_CORRECTING = Aufgabe #%d generiert fehlende Chunks für Welt '%s'. Fortschritt: %d chunks %s
TASK_SAVE_FAILED = §cFehler beim Speichern der Aufgaben: %s
WORLD_NAME_REQUIRED = §cDu musst einen Weltennamen angeben!
@ -72,4 +73,8 @@ STATS_CORES = - Kerne: §2%d§r
STATS_WORLD_NAME = §l%s§r
STATS_ENTITY_COUNT = - §2%d§r Entities
STATS_LOADED_CHUNKS = - §2%d§r Geladene Chunks
STATS_PLUGIN_LOADED_CHUNKS = - §2%d§r von Chunkmaster geladene Chunks
STATS_PLUGIN_LOADED_CHUNKS = - §2%d§r von Chunkmaster geladene Chunks
SAVING_CHUNKS = Speichere %d geladene Chunks...
CANCEL_FAIL = Konnte Aufgabe #%d nicht im angegebenen Timeout stoppen!
NO_AUTOSTART = Autostart ist auf §2false§r gesetzt. Pausiere...

@ -1,11 +1,11 @@
RESUME_FOR_WORLD = Resuming chunk generation task for world '%s'...
TASK_FINISHED = Task #%d finished after %d chunks.
TASK_CANCELED = Canceled task #%s.
TASK_CANCELLED = Cancelled task #%s.
TASK_LOAD_FAILED = §cFailed to load task #%d.
TASK_LOAD_SUCCESS = %d saved tasks loaded.
TASK_NOT_FOUND = §cTask %s not found!
CREATE_DELAYED_LOAD = Creating task to load chunk generation Tasks later...
TASK_PERIODIC_REPORT = Task #%d running for '%s'. Progress: %d chunks %s %s, Speed: %.1f ch/s, Last Chunk: %d, %d
TASK_PERIODIC_REPORT = Task #%d running for '%s'. State: %s. Progress: %d chunks %s %s, Speed: %.1f ch/s, Last Chunk: %d, %d
TASK_SAVE_FAILED = §cException when saving tasks: %s
WORLD_NAME_REQUIRED = §cYou need to provide a world name!
@ -18,7 +18,6 @@ TASK_ID_REQUIRED = §cYou need to provide a task id!
INVALID_ARGUMENT = §cInvalid argument at %s: %s!
PAUSED_TASKS_HEADER = Currently Paused Generation Tasks
TASKS_ENTRY = - §9#%d§r - §2%s§r - §2%d chunks %s§r
RUNNING_TASKS_HEADER = Currently Running Generation Tasks
NO_GENERATION_TASKS = There are no generation tasks.
@ -72,4 +71,8 @@ STATS_CORES = - Cores: §2%d§r
STATS_WORLD_NAME = §l%s§r
STATS_ENTITY_COUNT = - §2%d§r Entities
STATS_LOADED_CHUNKS = - §2%d§r Loaded Chunks
STATS_PLUGIN_LOADED_CHUNKS = - §2%d§r Chunks Loaded by Chunkmaster
STATS_PLUGIN_LOADED_CHUNKS = - §2%d§r Chunks Loaded by Chunkmaster
SAVING_CHUNKS = Saving %d loaded chunks...
CANCEL_FAIL = Failed to cancel task #%d in the given timeout!
NO_AUTOSTART = Autostart set to §2false§r. Pausing...

@ -1,6 +1,6 @@
main: net.trivernis.chunkmaster.Chunkmaster
name: Chunkmaster
version: '1.2.3'
version: '1.3.0'
description: Automated world pregeneration.
author: Trivernis
website: trivernis.net

Loading…
Cancel
Save