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
pull/82/head
Trivernis 4 years ago committed by GitHub
parent 1ebf3c96a8
commit acf302e8c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -82,32 +82,14 @@ generation:
# The maximum amount of chunks that are loaded before unloading and saving them. # The maximum amount of chunks that are loaded before unloading and saving them.
# Higher values mean higher generation speed but greater memory usage. # Higher values mean higher generation speed but greater memory usage.
# The value should be a positive integer. # The value should be a positive integer.
max-loaded-chunks: 10 max-loaded-chunks: 1000
# Paper Only # Paper Only
# The maximum amount of requested chunks with the asynchronous paper chunk # The maximum amount of requested chunks with the asynchronous paper chunk
# loading method. Higher values mean faster generation but more memory usage # loading method. Higher values mean faster generation but more memory usage and
# (and probably bigger performance impact). # bigger performance impact. Configuring it too hight might crash the server.
# The value should be a positive integer. # The value should be a positive integer.
max-pending-chunks: 10 max-pending-chunks: 500
# 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
# The maximum milliseconds per tick the server is allowed to have # The maximum milliseconds per tick the server is allowed to have
# during the cunk generation process. # during the cunk generation process.
@ -115,6 +97,15 @@ generation:
# The value should be a positive integer greater than 50. # The value should be a positive integer greater than 50.
mspt-pause-threshold: 500 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 # Pauses the generation if the number of players on the server is larger or equal
# to the configured value # to the configured value
# Notice that playing on a server that constantly generates chunks can be # Notice that playing on a server that constantly generates chunks can be

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

@ -3,13 +3,12 @@ package net.trivernis.chunkmaster
import io.papermc.lib.PaperLib import io.papermc.lib.PaperLib
import net.trivernis.chunkmaster.commands.CommandChunkmaster import net.trivernis.chunkmaster.commands.CommandChunkmaster
import net.trivernis.chunkmaster.lib.LanguageManager 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 net.trivernis.chunkmaster.lib.generation.GenerationManager
import org.bstats.bukkit.Metrics import org.bstats.bukkit.Metrics
import org.bukkit.plugin.java.JavaPlugin import org.bukkit.plugin.java.JavaPlugin
import org.bukkit.scheduler.BukkitTask import org.bukkit.scheduler.BukkitTask
import org.dynmap.DynmapAPI import org.dynmap.DynmapAPI
import java.util.logging.Level
class Chunkmaster: JavaPlugin() { class Chunkmaster: JavaPlugin() {
lateinit var sqliteManager: SqliteManager lateinit var sqliteManager: SqliteManager
@ -67,6 +66,7 @@ class Chunkmaster: JavaPlugin() {
override fun onDisable() { override fun onDisable() {
logger.info(langManager.getLocalized("STOPPING_ALL_TASKS")) logger.info(langManager.getLocalized("STOPPING_ALL_TASKS"))
generationManager.stopAll() generationManager.stopAll()
server.scheduler.cancelTasks(this)
} }
/** /**
@ -74,13 +74,11 @@ class Chunkmaster: JavaPlugin() {
*/ */
private fun configure() { private fun configure() {
dataFolder.mkdir() 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.mspt-pause-threshold", 500L)
config.addDefault("generation.pause-on-player-count", 1) config.addDefault("generation.pause-on-player-count", 1)
config.addDefault("generation.max-pending-chunks", 10) config.addDefault("generation.max-pending-chunks", 500)
config.addDefault("generation.max-loaded-chunks", 10) config.addDefault("generation.max-loaded-chunks", 1000)
config.addDefault("generation.unloading-period", 50L)
config.addDefault("generation.ignore-worldborder", false) config.addDefault("generation.ignore-worldborder", false)
config.addDefault("database.filename", "chunkmaster.db") config.addDefault("database.filename", "chunkmaster.db")
config.addDefault("language", "en") config.addDefault("language", "en")

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

@ -37,12 +37,6 @@ class CmdGetCenter(private val chunkmaster: Chunkmaster): Subcommand {
args[0] args[0]
} }
} }
if (chunkmaster.generationManager.worldCenters.isEmpty()) {
chunkmaster.generationManager.loadWorldCenters() {
sendCenterInfo(sender, worldName)
}
return true
}
sendCenterInfo(sender, worldName) sendCenterInfo(sender, worldName)
return true return true
} }
@ -51,15 +45,17 @@ class CmdGetCenter(private val chunkmaster: Chunkmaster): Subcommand {
* Sends the center information * Sends the center information
*/ */
private fun sendCenterInfo(sender: CommandSender, worldName: String) { private fun sendCenterInfo(sender: CommandSender, worldName: String) {
var center = chunkmaster.generationManager.worldCenters[worldName] chunkmaster.generationManager.worldProperties.getWorldCenter(worldName).thenAccept { worldCenter ->
var center = worldCenter
if (center == null) { if (center == null) {
val world = sender.server.worlds.find { it.name == worldName } val world = sender.server.worlds.find { it.name == worldName }
if (world == null) { if (world == null) {
sender.sendMessage(chunkmaster.langManager.getLocalized("WORLD_NOT_FOUND", worldName)) sender.sendMessage(chunkmaster.langManager.getLocalized("WORLD_NOT_FOUND", worldName))
return 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,7 +2,7 @@ package net.trivernis.chunkmaster.commands
import net.trivernis.chunkmaster.Chunkmaster import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.Subcommand 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.Command
import org.bukkit.command.CommandSender import org.bukkit.command.CommandSender
@ -53,6 +53,6 @@ class CmdList(private val chunkmaster: Chunkmaster): Subcommand {
else else
"" ""
return "\n" + chunkmaster.langManager.getLocalized("TASKS_ENTRY", return "\n" + chunkmaster.langManager.getLocalized("TASKS_ENTRY",
task.id, genTask.world.name, genTask.count, percentage) task.id, genTask.world.name, genTask.state.toString(), genTask.count, percentage)
} }
} }

@ -67,7 +67,7 @@ class CmdSetCenter(private val chunkmaster: Chunkmaster): Subcommand {
centerZ = args[2].toInt() 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)) sender.sendMessage(chunkmaster.langManager.getLocalized("CENTER_UPDATED", world, centerX, centerZ))
return true 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_ENTITY_COUNT", world.entities.size)}
${chunkmaster.langManager.getLocalized("STATS_LOADED_CHUNKS", world.loadedChunks.size)} ${chunkmaster.langManager.getLocalized("STATS_LOADED_CHUNKS", world.loadedChunks.size)}
""".trimIndent() """.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 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_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_MEMORY", memUsed/1000000, runtime.maxMemory()/1000000, (memUsed.toFloat()/runtime.maxMemory().toFloat()) * 100)}
${chunkmaster.langManager.getLocalized("STATS_CORES", runtime.availableProcessors())} ${chunkmaster.langManager.getLocalized("STATS_CORES", runtime.availableProcessors())}
${chunkmaster.langManager.getLocalized("STATS_PLUGIN_LOADED_CHUNKS", chunkmaster.generationManager.loadedChunkCount)}
""".trimIndent() """.trimIndent()
for (world in sender.server.worlds) { for (world in sender.server.worlds) {
message += "\n\n" + getWorldStatsMessage(sender, world) 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 net.trivernis.chunkmaster.Chunkmaster
import org.apache.commons.lang.exception.ExceptionUtils import org.apache.commons.lang.exception.ExceptionUtils
import org.sqlite.SQLiteConnection
import java.lang.Exception import java.lang.Exception
import java.sql.Connection import java.sql.Connection
import java.sql.DriverManager import java.sql.DriverManager
import java.sql.PreparedStatement
import java.sql.ResultSet import java.sql.ResultSet
class SqliteManager(private val chunkmaster: Chunkmaster) { 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("last_z", "integer NOT NULL DEFAULT 0"),
Pair("world", "text UNIQUE NOT NULL DEFAULT 'world'"), Pair("world", "text UNIQUE NOT NULL DEFAULT 'world'"),
Pair("radius", "integer DEFAULT -1"), 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( Pair(
@ -31,6 +30,15 @@ class SqliteManager(private val chunkmaster: Chunkmaster) {
Pair("center_x", "integer NOT NULL DEFAULT 0"), Pair("center_x", "integer NOT NULL DEFAULT 0"),
Pair("center_z", "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>>>() 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 connection: Connection? = null
private var activeTasks = 0 private var activeTasks = 0
val worldProperties = WorldProperties(this)
val pendingChunks = PendingChunks(this)
val generationTasks = GenerationTasks(this)
/** /**
* Returns the connection to the database * Returns the connection to the database
*/ */
@ -92,17 +104,18 @@ class SqliteManager(private val chunkmaster: Chunkmaster) {
/** /**
* Executes a sql statement on the database. * 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() val connection = getConnection()
activeTasks++ activeTasks++
if (connection != null) { if (connection != null) {
try { try {
//println("'$sql' with values $values")
val statement = connection.prepareStatement(sql) val statement = connection.prepareStatement(sql)
for (parameterValue in values) { for (parameterValue in values) {
statement.setObject(parameterValue.key, parameterValue.value) statement.setObject(parameterValue.key, parameterValue.value)
} }
statement.execute() statement.execute()
val res = statement.resultSet val res: ResultSet? = statement.resultSet
if (callback != null) { if (callback != null) {
callback(res) 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 { fun getCenterLocation(world: World): Location {
return Location(world, ((x*16) + 8).toDouble(), 1.0, ((z*16) + 8).toDouble()) 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))
}
/**
* 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,23 +1,34 @@
package net.trivernis.chunkmaster.lib.generation package net.trivernis.chunkmaster.lib.generation
import io.papermc.lib.PaperLib
import net.trivernis.chunkmaster.Chunkmaster 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.Circle
import net.trivernis.chunkmaster.lib.shapes.Spiral import net.trivernis.chunkmaster.lib.shapes.Spiral
import org.bukkit.Server import org.bukkit.Server
import org.bukkit.World import org.bukkit.World
import java.util.concurrent.CompletableFuture
class GenerationManager(private val chunkmaster: Chunkmaster, private val server: Server) { class GenerationManager(private val chunkmaster: Chunkmaster, private val server: Server) {
val tasks: HashSet<RunningTaskEntry> = HashSet() val tasks: HashSet<RunningTaskEntry> = HashSet()
val pausedTasks: HashSet<PausedTaskEntry> = 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 = chunkmaster.config.getLong("generation.unloading-period")
val loadedChunkCount: Int
get() {
return unloader.pendingSize
}
private val unloader = ChunkUnloader(chunkmaster)
val allTasks: HashSet<TaskEntry> val allTasks: HashSet<TaskEntry>
get() { get() {
if (this.tasks.isEmpty() && this.pausedTasks.isEmpty()) { if (this.tasks.isEmpty() && this.pausedTasks.isEmpty()) {
if (this.worldCenters.isEmpty()) {
this.loadWorldCenters()
}
this.startAll() this.startAll()
if (server.onlinePlayers.size >= chunkmaster.config.getInt("generation.pause-on-player-count")) { if (server.onlinePlayers.size >= chunkmaster.config.getInt("generation.pause-on-player-count")) {
this.pauseAll() this.pauseAll()
@ -36,41 +47,17 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
*/ */
fun addTask(world: World, radius: Int = -1, shape: String = "square"): Int { fun addTask(world: World, radius: Int = -1, shape: String = "square"): Int {
val foundTask = allTasks.find { it.generationTask.world == world } val foundTask = allTasks.find { it.generationTask.world == world }
if (foundTask == null) { 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) ChunkCoordinates(world.spawnLocation.chunk.x, world.spawnLocation.chunk.z)
} else { } else {
val center = worldCenters[world.name]!!
ChunkCoordinates(center.first, center.second) ChunkCoordinates(center.first, center.second)
} }
val generationTask = createGenerationTask(world, centerChunk, centerChunk, radius, shape) val generationTask = createGenerationTask(world, centerChunk, centerChunk, radius, shape, null)
val id = generationTasks.addGenerationTask(world.name, centerChunk, radius, shape).join()
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")
}
generationTask.onEndReached { generationTask.onEndReached {
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("TASK_FINISHED", id, it.count)) chunkmaster.logger.info(chunkmaster.langManager.getLocalized("TASK_FINISHED", id, it.count))
@ -78,13 +65,19 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
} }
if (!paused) { if (!paused) {
val task = server.scheduler.runTaskTimer( val taskEntry = RunningTaskEntry(
chunkmaster, generationTask, 200, // 10 sec delay id,
chunkmaster.config.getLong("generation.period") generationTask
) )
tasks.add(RunningTaskEntry(id, task, generationTask)) taskEntry.start()
tasks.add(taskEntry)
} else { } else {
pausedTasks.add(PausedTaskEntry(id, generationTask)) pausedTasks.add(
PausedTaskEntry(
id,
generationTask
)
)
} }
return id return id
@ -102,17 +95,18 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
last: ChunkCoordinates, last: ChunkCoordinates,
id: Int, id: Int,
radius: Int = -1, radius: Int = -1,
delay: Long = 200L, shape: String = "square",
shape: String = "square" pendingChunks: List<ChunkCoordinates>?
) { ) {
if (!paused) { if (!paused) {
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("RESUME_FOR_WORLD", world.name)) chunkmaster.logger.info(chunkmaster.langManager.getLocalized("RESUME_FOR_WORLD", world.name))
val generationTask = createGenerationTask(world, center, last, radius, shape) val generationTask = createGenerationTask(world, center, last, radius, shape, pendingChunks)
val task = server.scheduler.runTaskTimer( val taskEntry = RunningTaskEntry(
chunkmaster, generationTask, delay, id,
chunkmaster.config.getLong("generation.period") generationTask
) )
tasks.add(RunningTaskEntry(id, task, generationTask)) taskEntry.start()
tasks.add(taskEntry)
generationTask.onEndReached { generationTask.onEndReached {
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("TASK_FINISHED", id, generationTask.count)) chunkmaster.logger.info(chunkmaster.langManager.getLocalized("TASK_FINISHED", id, generationTask.count))
removeTask(id) removeTask(id)
@ -129,23 +123,24 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
} else { } else {
this.tasks.find { it.id == id } this.tasks.find { it.id == id }
} }
try {
if (taskEntry != null) { if (taskEntry != null) {
taskEntry.cancel() if (taskEntry.generationTask.isRunning && taskEntry is RunningTaskEntry) {
chunkmaster.sqliteManager.executeStatement(""" taskEntry.cancel(chunkmaster.config.getLong("mspt-pause-threshold"))
DELETE FROM generation_tasks WHERE id = ?; }
""".trimIndent(), HashMap(mapOf(1 to taskEntry.id)), generationTasks.deleteGenerationTask(id)
null pendingChunksTable.clearPendingChunks(id)
)
if (taskEntry is RunningTaskEntry) { if (taskEntry is RunningTaskEntry) {
if (taskEntry.task.isCancelled) {
tasks.remove(taskEntry) tasks.remove(taskEntry)
}
} else if (taskEntry is PausedTaskEntry) { } else if (taskEntry is PausedTaskEntry) {
pausedTasks.remove(taskEntry) pausedTasks.remove(taskEntry)
} }
return true return true
} }
} catch (e: Exception) {
chunkmaster.logger.severe(e.toString())
}
return false return false
} }
@ -159,12 +154,12 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
saveProgress() // save progress every 30 seconds saveProgress() // save progress every 30 seconds
}, 600, 600) }, 600, 600)
server.scheduler.runTaskLater(chunkmaster, Runnable { server.scheduler.runTaskLater(chunkmaster, Runnable {
this.loadWorldCenters()
this.startAll() this.startAll()
if (!server.onlinePlayers.isEmpty()) { if (!server.onlinePlayers.isEmpty()) {
this.pauseAll() this.pauseAll()
} }
}, 20) }, 20)
server.scheduler.runTaskTimer(chunkmaster, unloader, unloadingPeriod, unloadingPeriod)
} }
/** /**
@ -173,40 +168,36 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
fun stopAll() { fun stopAll() {
val removalSet = HashSet<RunningTaskEntry>() val removalSet = HashSet<RunningTaskEntry>()
for (task in tasks) { for (task in tasks) {
val lastChunk = task.generationTask.lastChunkCoords
val id = task.id val id = task.id
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("SAVING_TASK_PROGRESS", task.id)) chunkmaster.logger.info(chunkmaster.langManager.getLocalized("SAVING_TASK_PROGRESS", task.id))
saveProgressToDatabase(lastChunk, id) saveProgressToDatabase(task.generationTask, id).join()
task.task.cancel() if (!task.cancel(chunkmaster.config.getLong("mspt-pause-threshold"))) {
task.generationTask.cancel() chunkmaster.logger.warning(chunkmaster.langManager.getLocalized("CANCEL_FAIL", task.id))
if (task.task.isCancelled) {
removalSet.add(task)
} }
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) tasks.removeAll(removalSet)
if (unloader.pendingSize > 0) {
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("SAVING_CHUNKS", unloader.pendingSize))
unloader.run()
}
} }
/** /**
* Starts all generation tasks. * Starts all generation tasks.
*/ */
fun startAll() { fun startAll() {
chunkmaster.sqliteManager.executeStatement("SELECT * FROM generation_tasks", HashMap()) { res -> generationTasks.getGenerationTasks().thenAccept { tasks ->
var count = 0 for (task in tasks) {
while (res.next()) { val world = server.getWorld(task.world)
count++ if (world != null) {
try { pendingChunksTable.getPendingChunks(task.id).thenAccept {
val id = res.getInt("id") resumeTask(world, task.center, task.last, task.id, task.radius, task.shape, it)
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)
} }
} catch (error: NullPointerException) { } else {
chunkmaster.logger.severe(chunkmaster.langManager.getLocalized("TASK_LOAD_FAILED", res.getInt("id"))) chunkmaster.logger.severe(chunkmaster.langManager.getLocalized("TASK_LOAD_FAILED", task.id))
} }
} }
} }
@ -222,7 +213,12 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
fun pauseAll() { fun pauseAll() {
paused = true paused = true
for (task in tasks) { for (task in tasks) {
pausedTasks.add(PausedTaskEntry(task.id, task.generationTask)) pausedTasks.add(
PausedTaskEntry(
task.id,
task.generationTask
)
)
} }
stopAll() stopAll()
} }
@ -237,98 +233,97 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
} }
/** /**
* Overload that doesn't need an argument * Saves the task progress
*/ */
private fun loadWorldCenters() { private fun saveProgress() {
loadWorldCenters(null) for (task in tasks) {
try {
if (task.generationTask.state == TaskState.CORRECTING) {
reportCorrectionProgress(task)
} else {
reportGenerationProgress(task)
} }
saveProgressToDatabase(task.generationTask, task.id)
/** } catch (error: Exception) {
* Loads the world centers from the database chunkmaster.logger.warning(chunkmaster.langManager.getLocalized("TASK_SAVE_FAILED", error.toString()))
*/ error.printStackTrace()
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"))
} }
cb?.invoke()
} }
} }
/** /**
* Updates the center of a world * Reports the progress for correcting tasks
*/ */
fun updateWorldCenter(worldName: String, center: Pair<Int, Int>) { private fun reportCorrectionProgress(task: RunningTaskEntry) {
chunkmaster.sqliteManager.executeStatement("SELECT * FROM world_properties WHERE name = ?", HashMap(mapOf(1 to worldName))) { val genTask = task.generationTask
if (it.next()) { val progress = if (genTask.missingChunks.size > 0) {
chunkmaster.sqliteManager.executeStatement("UPDATE world_properties SET center_x = ?, center_z = ? WHERE name = ?", HashMap( "(${(genTask.count / genTask.missingChunks.size) * 100}%)"
mapOf(
1 to center.first,
2 to center.second,
3 to worldName
)
), null)
} else { } 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)
}
} }
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() { private fun reportGenerationProgress(task: RunningTaskEntry) {
for (task in tasks) {
try {
val genTask = task.generationTask val genTask = task.generationTask
val (speed, chunkSpeed) = task.generationSpeed val (speed, chunkSpeed) = task.generationSpeed
val percentage = if (genTask.radius > 0) "(${"%.2f".format(genTask.shape.progress() * 100)}%)" else "" val percentage = if (genTask.radius > 0) "(${"%.2f".format(genTask.shape.progress() * 100)}%)" else ""
val eta = if (genTask.radius > 0 && speed!! > 0) { val eta = if (genTask.radius > 0 && speed!! > 0) {
val etaSeconds = (genTask.shape.progress())/speed val remaining = 1 - genTask.shape.progress()
val etaSeconds = remaining / speed
val hours: Int = (etaSeconds / 3600).toInt() val hours: Int = (etaSeconds / 3600).toInt()
val minutes: Int = ((etaSeconds % 3600) / 60).toInt() val minutes: Int = ((etaSeconds % 3600) / 60).toInt()
val seconds: Int = (etaSeconds % 60).toInt() val seconds: Int = (etaSeconds % 60).toInt()
", ETA: %d:%02d:%02d".format(hours, minutes, seconds) ", ETA: %dh %dmin %ds".format(hours, minutes, seconds)
} else { } else {
"" ""
} }
chunkmaster.logger.info(chunkmaster.langManager.getLocalized( chunkmaster.logger.info(
chunkmaster.langManager.getLocalized(
"TASK_PERIODIC_REPORT", "TASK_PERIODIC_REPORT",
task.id, task.id,
genTask.world.name, genTask.world.name,
genTask.state.toString(),
genTask.count, genTask.count,
percentage, percentage,
eta, eta,
chunkSpeed!!, chunkSpeed!!,
genTask.lastChunkCoords.x, genTask.lastChunkCoords.x,
genTask.lastChunkCoords.z)) genTask.lastChunkCoords.z
saveProgressToDatabase(genTask.lastChunkCoords, task.id) )
genTask.updateLastChunkMarker() )
} catch (error: Exception) {
chunkmaster.logger.warning(chunkmaster.langManager.getLocalized("TASK_SAVE_FAILED", error.toString()))
}
}
} }
/** /**
* Saves the generation progress to the database * Saves the generation progress to the database
*/ */
private fun saveProgressToDatabase(lastChunk: ChunkCoordinates, id: Int) { private fun saveProgressToDatabase(generationTask: GenerationTask, id: Int): CompletableFuture<Void> {
chunkmaster.sqliteManager.executeStatement( val completableFuture = CompletableFuture<Void>()
""" generationTasks.updateGenerationTask(id, generationTask.lastChunkCoords, generationTask.state).thenAccept {
UPDATE generation_tasks SET last_x = ?, last_z = ? pendingChunksTable.clearPendingChunks(id).thenAccept {
WHERE id = ? if (generationTask is DefaultGenerationTask) {
""".trimIndent(), if (generationTask.pendingChunks.size > 0) {
HashMap(mapOf(1 to lastChunk.x, 2 to lastChunk.z, 3 to id)), pendingChunksTable.addPendingChunks(id, generationTask.pendingChunks.map { it.coordinates })
null }
) }
pendingChunksTable.addPendingChunks(id, generationTask.missingChunks.toList()).thenAccept {
completableFuture.complete(null)
}
}
}
return completableFuture
} }
/** /**
@ -340,7 +335,8 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
center: ChunkCoordinates, center: ChunkCoordinates,
start: ChunkCoordinates, start: ChunkCoordinates,
radius: Int, radius: Int,
shapeName: String shapeName: String,
pendingChunks: List<ChunkCoordinates>?
): GenerationTask { ): GenerationTask {
val shape = when (shapeName) { val shape = when (shapeName) {
"circle" -> Circle(Pair(center.x, center.z), Pair(start.x, start.z), radius) "circle" -> Circle(Pair(center.x, center.z), Pair(start.x, start.z), radius)
@ -348,10 +344,14 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
else -> Spiral(Pair(center.x, center.z), Pair(start.x, start.z), radius) else -> Spiral(Pair(center.x, center.z), Pair(start.x, start.z), radius)
} }
return if (PaperLib.isPaper()) { return DefaultGenerationTask(
GenerationTaskPaper(chunkmaster, world, start, radius, shape) chunkmaster,
} else { unloader,
GenerationTaskSpigot(chunkmaster, world, start, radius, shape) 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.Chunkmaster
import net.trivernis.chunkmaster.lib.dynmap.* import net.trivernis.chunkmaster.lib.dynmap.*
import net.trivernis.chunkmaster.lib.shapes.Shape import net.trivernis.chunkmaster.lib.shapes.Shape
import org.bukkit.Chunk
import org.bukkit.World import org.bukkit.World
import java.lang.Exception import java.util.concurrent.Semaphore
import kotlin.math.ceil
/** /**
* Interface for generation tasks. * Interface for generation tasks.
*/ */
abstract class GenerationTask( abstract class GenerationTask(
private val plugin: Chunkmaster, private val plugin: Chunkmaster,
val world: World,
protected val unloader: ChunkUnloader,
startChunk: ChunkCoordinates, startChunk: ChunkCoordinates,
val shape: Shape val shape: Shape,
val missingChunks: HashSet<ChunkCoordinates>,
var state: TaskState
) : ) :
Runnable { Runnable {
abstract val radius: Int abstract val radius: Int
abstract val world: World
abstract var count: Int abstract var count: Int
abstract var endReached: Boolean 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) var lastChunkCoords = ChunkCoordinates(startChunk.x, startChunk.z)
protected set 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 msptThreshold = plugin.config.getLong("generation.mspt-pause-threshold")
protected val maxLoadedChunks = plugin.config.getInt("generation.max-loaded-chunks") protected var cancelRun: Boolean = false
protected val chunksPerStep = plugin.config.getInt("generation.chunks-per-step")
private var endReachedCallback: ((GenerationTask) -> Unit)? = null private var endReachedCallback: ((GenerationTask) -> Unit)? = null
@ -45,16 +41,49 @@ abstract class GenerationTask(
null null
} }
private val markerAreaStyle = MarkerStyle(null, LineStyle(2, 1.0, 0x0022FF), FillStyle(.0, 0)) private val markerAreaStyle = MarkerStyle(null, LineStyle(2, 1.0, 0x0022FF), FillStyle(.0, 0))
private val markerAreaId = "chunkmaster_genarea" private val markerAreaId = "chunkmaster_genarea_${world.name}"
private val markerAreaName = "Chunkmaster Generation Area" private val markerAreaName = "Chunkmaster Generation Area (${ceil(shape.total()).toInt()} chunks)"
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 ignoreWorldborder = plugin.config.getBoolean("generation.ignore-worldborder") 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() 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 val nextChunkCoordinates: ChunkCoordinates
get() { get() {
val nextChunkCoords = shape.next() val nextChunkCoords = shape.next()
@ -69,26 +98,6 @@ abstract class GenerationTask(
|| shape.endReached() || 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 * Updates the dynmap marker for the generation radius
*/ */
@ -105,19 +114,11 @@ abstract class GenerationTask(
} }
} }
/** protected fun triggerDynmapRender(chunkCoordinates: ChunkCoordinates) {
* Updates the dynmap marker for the generation radius if (dynmapIntegration) {
*/ dynmap?.triggerRenderOfVolume(
fun updateLastChunkMarker(clear: Boolean = false) { world.getBlockAt(chunkCoordinates.x * 16, 0, chunkCoordinates.z * 16).location,
if (clear) { world.getBlockAt((chunkCoordinates.x * 16) + 16, 255, (chunkCoordinates.z * 16) + 16).location
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
) )
} }
} }
@ -128,21 +129,8 @@ abstract class GenerationTask(
private fun setEndReached() { private fun setEndReached() {
endReached = true endReached = true
count = shape.count count = shape.count
endReachedCallback?.invoke(this)
updateGenerationAreaMarker(true) updateGenerationAreaMarker(true)
updateLastChunkMarker(true) endReachedCallback?.invoke(this)
}
/**
* Performs a check if the border has been reached
*/
protected fun borderReachedCheck(): Boolean {
val done = borderReached()
if (done) {
unloadLoadedChunks()
setEndReached()
}
return done
} }
/** /**

@ -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( class RunningTaskEntry(
override val id: Int, override val id: Int,
val task: BukkitTask,
override val generationTask: GenerationTask override val generationTask: GenerationTask
) : TaskEntry { ) : TaskEntry {
private var lastProgress: Pair<Long, Double>? = null private var lastProgress: Pair<Long, Double>? = null
private var lastChunkCount: Pair<Long, Int>? = null private var lastChunkCount: Pair<Long, Int>? = null
private var thread = Thread(generationTask)
/** /**
* Returns the generation Speed * Returns the generation Speed
@ -38,9 +39,32 @@ class RunningTaskEntry(
lastChunkCount = Pair(System.currentTimeMillis(), generationTask.count) lastChunkCount = Pair(System.currentTimeMillis(), generationTask.count)
} }
fun start() {
thread.start()
}
override fun cancel() { fun cancel(timeout: Long): Boolean {
task.cancel() if (generationTask.isRunning) {
generationTask.cancel() generationTask.cancel()
thread.interrupt()
}
return try {
joinThread(timeout)
} catch (e: InterruptedException) {
true
}
}
private fun joinThread(timeout: Long): Boolean {
var threadStopped = false
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
}

@ -20,9 +20,13 @@ class Circle(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(
return radius > 0 && coords.isEmpty() && r >= radius return radius > 0 && coords.isEmpty() && r >= radius
} }
override fun total(): Double {
return (PI * radius.toFloat().pow(2))
}
override fun progress(): Double { override fun progress(): Double {
// TODO: Radius inner progress // 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 { override fun currentRadius(): Int {
@ -77,12 +81,14 @@ class Circle(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(
* Some coordinates might already be present in the list * Some coordinates might already be present in the list
* @param r - the radius * @param r - the radius
*/ */
private fun getCircleCoordinates(r: Int): ArrayList<Pair<Int, Int>> { private fun getCircleCoordinates(r: Int): Vector<Pair<Int, Int>> {
val coords = ArrayList<Pair<Int, Int>>() val coords = Vector<Pair<Int, Int>>()
val segCoords = getSegment(r) val segCoords = getSegment(r)
coords.addAll(segCoords.reversed()) coords.addAll(segCoords.reversed())
for (step in 1..7) { for (step in 1..7) {
val tmpSeg = ArrayList<Pair<Int, Int>>() val tmpSeg = Vector<Pair<Int, Int>>()
for (pos in segCoords) { for (pos in segCoords) {
val coord = when (step) { val coord = when (step) {
1 -> Pair(pos.first, -pos.second) 1 -> Pair(pos.first, -pos.second)
@ -128,4 +134,11 @@ class Circle(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(
} }
return coords 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 abstract fun progress(): Double
/**
* The total number of chunks to generate
*/
abstract fun total(): Double
/** /**
* Returns the current radius * 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 * returns a poly marker for the shape
*/ */
abstract fun getShapeEdgeLocations(): List<Pair<Int, Int>> 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 { override fun endReached(): Boolean {
val distances = getDistances(center, currentPos) 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 { 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 { override fun currentRadius(): Int {
@ -26,6 +33,9 @@ class Spiral(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(
* Returns the next value in the spiral * Returns the next value in the spiral
*/ */
override fun next(): Pair<Int, Int> { override fun next(): Pair<Int, Int> {
if (endReached()) {
return currentPos
}
if (count == 0 && currentPos != center) { if (count == 0 && currentPos != center) {
// simulate the spiral to get the correct direction and count // simulate the spiral to get the correct direction and count
val simSpiral = Spiral(center, center, radius) val simSpiral = Spiral(center, center, radius)
@ -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> { private fun getDistances(pos1: Pair<Int, Int>, pos2: Pair<Int, Int>): Pair<Int, Int> {
return Pair(pos2.first - pos1.first, pos2.second - pos1.second) 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'... RESUME_FOR_WORLD = Resuming chunk generation task for world '%s'...
TASK_FINISHED = Task #%d finished after %d chunks. 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_FAILED = §cFailed to load task #%d.
TASK_LOAD_SUCCESS = %d saved tasks loaded. TASK_LOAD_SUCCESS = %d saved tasks loaded.
TASK_NOT_FOUND = §cTask %s not found! TASK_NOT_FOUND = §cTask %s not found!
CREATE_DELAYED_LOAD = Creating task to load chunk generation Tasks later... 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 TASK_SAVE_FAILED = §cException when saving tasks: %s
WORLD_NAME_REQUIRED = §cYou need to provide a world name! 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! INVALID_ARGUMENT = §cInvalid argument at %s: %s!
PAUSED_TASKS_HEADER = Currently Paused Generation Tasks 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 RUNNING_TASKS_HEADER = Currently Running Generation Tasks
NO_GENERATION_TASKS = There are no generation tasks. NO_GENERATION_TASKS = There are no generation tasks.
@ -73,3 +74,6 @@ STATS_WORLD_NAME = §l%s§r
STATS_ENTITY_COUNT = - §2%d§r Entities STATS_ENTITY_COUNT = - §2%d§r Entities
STATS_LOADED_CHUNKS = - §2%d§r Loaded Chunks 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!

@ -1,11 +1,12 @@
RESUME_FOR_WORLD = Setze das Chunk-Generieren für Welt '%s' fort... RESUME_FOR_WORLD = Setze das Chunk-Generieren für Welt '%s' fort...
TASK_FINISHED = Aufgabe #%d wurde nach %d chunks beendet. 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_FAILED = §cAufgabe #%d konnte nicht geladen werden.
TASK_LOAD_SUCCESS = %d gespeicherte Aufgaben wurden geladen. TASK_LOAD_SUCCESS = %d gespeicherte Aufgaben wurden geladen.
TASK_NOT_FOUND = §cAufgabe %s konnte nicht gefunden werden! TASK_NOT_FOUND = §cAufgabe %s konnte nicht gefunden werden!
CREATE_DELAYED_LOAD = Erstelle einen Bukkit-Task zum verzögerten Laden von Aufgaben... 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 TASK_SAVE_FAILED = §cFehler beim Speichern der Aufgaben: %s
WORLD_NAME_REQUIRED = §cDu musst einen Weltennamen angeben! WORLD_NAME_REQUIRED = §cDu musst einen Weltennamen angeben!
@ -73,3 +74,6 @@ STATS_WORLD_NAME = §l%s§r
STATS_ENTITY_COUNT = - §2%d§r Entities STATS_ENTITY_COUNT = - §2%d§r Entities
STATS_LOADED_CHUNKS = - §2%d§r Geladene Chunks 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!

@ -1,11 +1,11 @@
RESUME_FOR_WORLD = Resuming chunk generation task for world '%s'... RESUME_FOR_WORLD = Resuming chunk generation task for world '%s'...
TASK_FINISHED = Task #%d finished after %d chunks. 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_FAILED = §cFailed to load task #%d.
TASK_LOAD_SUCCESS = %d saved tasks loaded. TASK_LOAD_SUCCESS = %d saved tasks loaded.
TASK_NOT_FOUND = §cTask %s not found! TASK_NOT_FOUND = §cTask %s not found!
CREATE_DELAYED_LOAD = Creating task to load chunk generation Tasks later... 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 TASK_SAVE_FAILED = §cException when saving tasks: %s
WORLD_NAME_REQUIRED = §cYou need to provide a world name! 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! INVALID_ARGUMENT = §cInvalid argument at %s: %s!
PAUSED_TASKS_HEADER = Currently Paused Generation Tasks 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 RUNNING_TASKS_HEADER = Currently Running Generation Tasks
NO_GENERATION_TASKS = There are no generation tasks. NO_GENERATION_TASKS = There are no generation tasks.
@ -73,3 +72,6 @@ STATS_WORLD_NAME = §l%s§r
STATS_ENTITY_COUNT = - §2%d§r Entities STATS_ENTITY_COUNT = - §2%d§r Entities
STATS_LOADED_CHUNKS = - §2%d§r Loaded Chunks 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!

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

Loading…
Cancel
Save