From 4d4107aaf3f35628665175e8d1166ed04ad31cd6 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 26 Jul 2020 17:54:14 +0200 Subject: [PATCH] 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 --- README.md | 43 +-- build.gradle | 2 +- .../net/trivernis/chunkmaster/Chunkmaster.kt | 15 +- .../chunkmaster/commands/CmdCancel.kt | 5 +- .../chunkmaster/commands/CmdGetCenter.kt | 24 +- .../trivernis/chunkmaster/commands/CmdList.kt | 10 +- .../chunkmaster/commands/CmdSetCenter.kt | 2 +- .../chunkmaster/commands/CmdStats.kt | 6 +- .../lib/database/GenerationTaskData.kt | 14 + .../lib/database/GenerationTasks.kt | 103 ++++++ .../chunkmaster/lib/database/PendingChunks.kt | 57 +++ .../lib/{ => database}/SqliteManager.kt | 25 +- .../lib/database/WorldProperties.kt | 82 +++++ .../lib/generation/ChunkCoordinates.kt | 4 + .../lib/generation/ChunkUnloader.kt | 60 +++ .../lib/generation/DefaultGenerationTask.kt | 145 ++++++++ .../lib/generation/GenerationManager.kt | 344 +++++++++--------- .../lib/generation/GenerationTask.kt | 120 +++--- .../lib/generation/GenerationTaskPaper.kt | 121 ------ .../lib/generation/GenerationTaskSpigot.kt | 72 ---- .../lib/generation/PausedTaskEntry.kt | 10 - .../lib/generation/PendingChunkEntry.kt | 10 + .../chunkmaster/lib/generation/TaskEntry.kt | 13 - .../chunkmaster/lib/generation/TaskState.kt | 24 ++ .../generation/taskentry/PausedTaskEntry.kt | 9 + .../{ => taskentry}/RunningTaskEntry.kt | 44 ++- .../lib/generation/taskentry/TaskEntry.kt | 11 + .../chunkmaster/lib/shapes/Circle.kt | 33 +- .../trivernis/chunkmaster/lib/shapes/Shape.kt | 10 + .../chunkmaster/lib/shapes/Spiral.kt | 25 +- .../resources/i18n/DEFAULT.i18n.properties | 13 +- src/main/resources/i18n/de.i18n.properties | 11 +- src/main/resources/i18n/en.i18n.properties | 11 +- src/main/resources/plugin.yml | 2 +- 34 files changed, 934 insertions(+), 546 deletions(-) create mode 100644 src/main/kotlin/net/trivernis/chunkmaster/lib/database/GenerationTaskData.kt create mode 100644 src/main/kotlin/net/trivernis/chunkmaster/lib/database/GenerationTasks.kt create mode 100644 src/main/kotlin/net/trivernis/chunkmaster/lib/database/PendingChunks.kt rename src/main/kotlin/net/trivernis/chunkmaster/lib/{ => database}/SqliteManager.kt (85%) create mode 100644 src/main/kotlin/net/trivernis/chunkmaster/lib/database/WorldProperties.kt create mode 100644 src/main/kotlin/net/trivernis/chunkmaster/lib/generation/ChunkUnloader.kt create mode 100644 src/main/kotlin/net/trivernis/chunkmaster/lib/generation/DefaultGenerationTask.kt delete mode 100644 src/main/kotlin/net/trivernis/chunkmaster/lib/generation/GenerationTaskPaper.kt delete mode 100644 src/main/kotlin/net/trivernis/chunkmaster/lib/generation/GenerationTaskSpigot.kt delete mode 100644 src/main/kotlin/net/trivernis/chunkmaster/lib/generation/PausedTaskEntry.kt create mode 100644 src/main/kotlin/net/trivernis/chunkmaster/lib/generation/PendingChunkEntry.kt delete mode 100644 src/main/kotlin/net/trivernis/chunkmaster/lib/generation/TaskEntry.kt create mode 100644 src/main/kotlin/net/trivernis/chunkmaster/lib/generation/TaskState.kt create mode 100644 src/main/kotlin/net/trivernis/chunkmaster/lib/generation/taskentry/PausedTaskEntry.kt rename src/main/kotlin/net/trivernis/chunkmaster/lib/generation/{ => taskentry}/RunningTaskEntry.kt (53%) create mode 100644 src/main/kotlin/net/trivernis/chunkmaster/lib/generation/taskentry/TaskEntry.kt diff --git a/README.md b/README.md index cc2fd67..57b6fce 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/build.gradle b/build.gradle index ce371b7..eb3994f 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ idea { } group "net.trivernis" -version "1.2.3" +version "1.3.0" sourceCompatibility = 1.8 diff --git a/src/main/kotlin/net/trivernis/chunkmaster/Chunkmaster.kt b/src/main/kotlin/net/trivernis/chunkmaster/Chunkmaster.kt index ab5a687..6d74308 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/Chunkmaster.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/Chunkmaster.kt @@ -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) { diff --git a/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdCancel.kt b/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdCancel.kt index 65e6e47..ec2bb78 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdCancel.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdCancel.kt @@ -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])) diff --git a/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdGetCenter.kt b/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdGetCenter.kt index b763cd3..316d8e8 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdGetCenter.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdGetCenter.kt @@ -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)) } } \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdList.kt b/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdList.kt index 98472cb..54082ce 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdList.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdList.kt @@ -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) } } \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdSetCenter.kt b/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdSetCenter.kt index 8714303..9adee80 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdSetCenter.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdSetCenter.kt @@ -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 } diff --git a/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdStats.kt b/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdStats.kt index 4390bf9..bab4b20 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdStats.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdStats.kt @@ -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) diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/database/GenerationTaskData.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/database/GenerationTaskData.kt new file mode 100644 index 0000000..2ea835c --- /dev/null +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/database/GenerationTaskData.kt @@ -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 +) \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/database/GenerationTasks.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/database/GenerationTasks.kt new file mode 100644 index 0000000..c8f9948 --- /dev/null +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/database/GenerationTasks.kt @@ -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> { + val completableFuture = CompletableFuture>() + + sqliteManager.executeStatement("SELECT * FROM generation_tasks", HashMap()) { res -> + val tasks = ArrayList() + + 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 { + val completableFuture = CompletableFuture() + 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 { + val completableFuture = CompletableFuture() + 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 { + val completableFuture = CompletableFuture() + 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 + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/database/PendingChunks.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/database/PendingChunks.kt new file mode 100644 index 0000000..9b79dfd --- /dev/null +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/database/PendingChunks.kt @@ -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> { + val completableFuture = CompletableFuture>() + sqliteManager.executeStatement("SELECT * FROM pending_chunks WHERE task_id = ?", hashMapOf(1 to taskId)) { + val pendingChunks = ArrayList() + 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 { + val completableFuture = CompletableFuture() + 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): CompletableFuture { + val completableFuture = CompletableFuture() + 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() + + 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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/SqliteManager.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/database/SqliteManager.kt similarity index 85% rename from src/main/kotlin/net/trivernis/chunkmaster/lib/SqliteManager.kt rename to src/main/kotlin/net/trivernis/chunkmaster/lib/database/SqliteManager.kt index 47419f5..c65940a 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/lib/SqliteManager.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/database/SqliteManager.kt @@ -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>>() @@ -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, callback: ((ResultSet) -> Unit)?) { + fun executeStatement(sql: String, values: HashMap, 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) } diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/database/WorldProperties.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/database/WorldProperties.kt new file mode 100644 index 0000000..fe6dab4 --- /dev/null +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/database/WorldProperties.kt @@ -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>() + + /** + * Returns the world center for one world + */ + fun getWorldCenter(worldName: String): CompletableFuture?> { + val completableFuture = CompletableFuture?>() + + 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): CompletableFuture { + val completableFuture = CompletableFuture() + + 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): CompletableFuture { + val completableFuture = CompletableFuture() + 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): CompletableFuture { + val completableFuture = CompletableFuture() + 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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/ChunkCoordinates.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/ChunkCoordinates.kt index cbb4441..b5c7cbc 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/ChunkCoordinates.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/ChunkCoordinates.kt @@ -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)" + } } \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/ChunkUnloader.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/ChunkUnloader.kt new file mode 100644 index 0000000..50c1ada --- /dev/null +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/ChunkUnloader.kt @@ -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(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() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/DefaultGenerationTask.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/DefaultGenerationTask.kt new file mode 100644 index 0000000..8441755 --- /dev/null +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/DefaultGenerationTask.kt @@ -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, + state: TaskState +) : GenerationTask(plugin, world, unloader, startChunk, shape, missingChunks, state) { + + private val maxPendingChunks = plugin.config.getInt("generation.max-pending-chunks") + val pendingChunks = ArrayBlockingQueue(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() + + 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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/GenerationManager.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/GenerationManager.kt index 98e5e61..dbe64a6 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/GenerationManager.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/GenerationManager.kt @@ -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 = HashSet() val pausedTasks: HashSet = HashSet() - val worldCenters: HashMap> = 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 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? ) { 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() 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) { - 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 { + val completableFuture = CompletableFuture() + 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? ): 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 + ) } } \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/GenerationTask.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/GenerationTask.kt index e3e889d..efa1005 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/GenerationTask.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/GenerationTask.kt @@ -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, + 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 = 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) } /** diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/GenerationTaskPaper.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/GenerationTaskPaper.kt deleted file mode 100644 index b1cefc3..0000000 --- a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/GenerationTaskPaper.kt +++ /dev/null @@ -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>() - - 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>() - 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) - } -} \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/GenerationTaskSpigot.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/GenerationTaskSpigot.kt deleted file mode 100644 index a812d3f..0000000 --- a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/GenerationTaskSpigot.kt +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/PausedTaskEntry.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/PausedTaskEntry.kt deleted file mode 100644 index e8a752e..0000000 --- a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/PausedTaskEntry.kt +++ /dev/null @@ -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() - } -} \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/PendingChunkEntry.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/PendingChunkEntry.kt new file mode 100644 index 0000000..6aa5b64 --- /dev/null +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/PendingChunkEntry.kt @@ -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) { + val isDone: Boolean + get() = chunk.isDone +} \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/TaskEntry.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/TaskEntry.kt deleted file mode 100644 index 47eda34..0000000 --- a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/TaskEntry.kt +++ /dev/null @@ -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() -} \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/TaskState.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/TaskState.kt new file mode 100644 index 0000000..bf9117c --- /dev/null +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/TaskState.kt @@ -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" + } + }, +} \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/taskentry/PausedTaskEntry.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/taskentry/PausedTaskEntry.kt new file mode 100644 index 0000000..57f2405 --- /dev/null +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/taskentry/PausedTaskEntry.kt @@ -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 { +} \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/RunningTaskEntry.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/taskentry/RunningTaskEntry.kt similarity index 53% rename from src/main/kotlin/net/trivernis/chunkmaster/lib/generation/RunningTaskEntry.kt rename to src/main/kotlin/net/trivernis/chunkmaster/lib/generation/taskentry/RunningTaskEntry.kt index d28d8bb..0aca508 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/RunningTaskEntry.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/taskentry/RunningTaskEntry.kt @@ -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? = null private var lastChunkCount: Pair? = 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 } } \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/taskentry/TaskEntry.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/taskentry/TaskEntry.kt new file mode 100644 index 0000000..869e87b --- /dev/null +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/taskentry/TaskEntry.kt @@ -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 +} \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/shapes/Circle.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/shapes/Circle.kt index 4702635..2546b04 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/lib/shapes/Circle.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/shapes/Circle.kt @@ -10,7 +10,7 @@ import kotlin.math.pow import kotlin.math.sqrt import kotlin.system.exitProcess -class Circle(center: Pair, start: Pair, radius: Int): Shape(center, start, radius) { +class Circle(center: Pair, start: Pair, radius: Int) : Shape(center, start, radius) { private var r = 0 private var coords = Stack>() private var previousCoords = HashSet>() @@ -20,9 +20,13 @@ class Circle(center: Pair, start: Pair, 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, start: Pair, radius: Int): Shape( override fun getShapeEdgeLocations(): List> { 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, start: Pair, 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, start: Pair, radius: Int): Shape( if (coords.isEmpty()) { r++ val tmpCoords = HashSet>() - 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, start: Pair, radius: Int): Shape( * Some coordinates might already be present in the list * @param r - the radius */ - private fun getCircleCoordinates(r: Int): ArrayList> { - val coords = ArrayList>() + private fun getCircleCoordinates(r: Int): Vector> { + val coords = Vector>() val segCoords = getSegment(r) coords.addAll(segCoords.reversed()) + for (step in 1..7) { - val tmpSeg = ArrayList>() + val tmpSeg = Vector>() + 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, start: Pair, radius: Int): Shape( } return coords } -} \ No newline at end of file + + override fun reset() { + this.r = 0 + this.currentPos = center + this.previousCoords.clear() + this.count = 0 + } +} diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/shapes/Shape.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/shapes/Shape.kt index 22d3902..298b603 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/lib/shapes/Shape.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/shapes/Shape.kt @@ -25,6 +25,11 @@ abstract class Shape(protected val center: Pair, start: Pair */ 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, start: Pair * returns a poly marker for the shape */ abstract fun getShapeEdgeLocations(): List> + + /** + * Resets the shape to its center start position + */ + abstract fun reset() } \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/shapes/Spiral.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/shapes/Spiral.kt index 33b7ac3..9564ca6 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/lib/shapes/Spiral.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/shapes/Spiral.kt @@ -10,11 +10,18 @@ class Spiral(center: Pair, start: Pair, 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, start: Pair, radius: Int): Shape( * Returns the next value in the spiral */ override fun next(): Pair { + 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, start: Pair, radius: Int): Shape( private fun getDistances(pos1: Pair, pos2: Pair): Pair { 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 + } } \ No newline at end of file diff --git a/src/main/resources/i18n/DEFAULT.i18n.properties b/src/main/resources/i18n/DEFAULT.i18n.properties index 283d4c3..b8719ec 100644 --- a/src/main/resources/i18n/DEFAULT.i18n.properties +++ b/src/main/resources/i18n/DEFAULT.i18n.properties @@ -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 \ No newline at end of file +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... \ No newline at end of file diff --git a/src/main/resources/i18n/de.i18n.properties b/src/main/resources/i18n/de.i18n.properties index ece9aa2..9eedf03 100644 --- a/src/main/resources/i18n/de.i18n.properties +++ b/src/main/resources/i18n/de.i18n.properties @@ -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 \ No newline at end of file +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... \ No newline at end of file diff --git a/src/main/resources/i18n/en.i18n.properties b/src/main/resources/i18n/en.i18n.properties index 283d4c3..06a257f 100644 --- a/src/main/resources/i18n/en.i18n.properties +++ b/src/main/resources/i18n/en.i18n.properties @@ -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 \ No newline at end of file +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... \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 27e99d1..640c698 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -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