diff --git a/gradle.properties b/gradle.properties index b1aea94..73569e9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ kotlin.code.style=official -PLUGIN_VERSION=1.3.4 \ No newline at end of file +PLUGIN_VERSION=1.4.0 \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdCompleted.kt b/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdCompleted.kt new file mode 100644 index 0000000..f9fde7e --- /dev/null +++ b/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdCompleted.kt @@ -0,0 +1,44 @@ +package net.trivernis.chunkmaster.commands + +import net.trivernis.chunkmaster.Chunkmaster +import net.trivernis.chunkmaster.lib.Subcommand +import org.bukkit.command.Command +import org.bukkit.command.CommandSender + +class CmdCompleted(private val plugin: Chunkmaster) : Subcommand { + override val name = "completed" + + override fun execute(sender: CommandSender, args: List): Boolean { + plugin.sqliteManager.completedGenerationTasks.getCompletedTasks().thenAccept { tasks -> + val worlds = tasks.map { it.world }.toHashSet() + var response = "\n" + plugin.langManager.getLocalized("COMPLETED_TASKS_HEADER") + "\n\n" + + for (world in worlds) { + response += plugin.langManager.getLocalized("COMPLETED_WORLD_HEADER", world) + "\n" + + for (task in tasks.filter { it.world == world }) { + response += plugin.langManager.getLocalized( + "COMPLETED_TASK_ENTRY", + task.id, + task.radius, + task.center.x, + task.center.z, + task.shape + ) + "\n" + } + response += "\n" + } + sender.sendMessage(response) + } + return true + } + + override fun onTabComplete( + sender: CommandSender, + command: Command, + alias: String, + args: List + ): MutableList { + return mutableListOf() + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdStats.kt b/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdStats.kt index 9f9adfe..e7826d5 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdStats.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/commands/CmdStats.kt @@ -41,6 +41,7 @@ class CmdStats(private val chunkmaster: Chunkmaster) : Subcommand { ${chunkmaster.langManager.getLocalized("STATS_WORLD_NAME", world.name)} ${chunkmaster.langManager.getLocalized("STATS_ENTITY_COUNT", world.entities.size)} ${chunkmaster.langManager.getLocalized("STATS_LOADED_CHUNKS", world.loadedChunks.size)} + ${chunkmaster.langManager.getLocalized("STATS_GENERATING", chunkmaster.generationManager.tasks.find { it.generationTask.world == world } != null)} """.trimIndent() return message } diff --git a/src/main/kotlin/net/trivernis/chunkmaster/commands/CommandChunkmaster.kt b/src/main/kotlin/net/trivernis/chunkmaster/commands/CommandChunkmaster.kt index b8870ea..8452673 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/commands/CommandChunkmaster.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/commands/CommandChunkmaster.kt @@ -87,5 +87,8 @@ class CommandChunkmaster(private val chunkmaster: Chunkmaster, private val serve val cmdStats = CmdStats(chunkmaster) commands[cmdStats.name] = cmdStats + + val cmdCompleted = CmdCompleted(chunkmaster) + commands[cmdCompleted.name] = cmdCompleted } } \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/database/CompletedGenerationTask.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/database/CompletedGenerationTask.kt new file mode 100644 index 0000000..d90d7d7 --- /dev/null +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/database/CompletedGenerationTask.kt @@ -0,0 +1,5 @@ +package net.trivernis.chunkmaster.lib.database + +import net.trivernis.chunkmaster.lib.generation.ChunkCoordinates + +data class CompletedGenerationTask (val id: Int, val world: String, val radius: Int, val center: ChunkCoordinates, val shape: String) \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/database/CompletedGenerationTasks.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/database/CompletedGenerationTasks.kt new file mode 100644 index 0000000..6c23aa1 --- /dev/null +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/database/CompletedGenerationTasks.kt @@ -0,0 +1,68 @@ +package net.trivernis.chunkmaster.lib.database + +import net.trivernis.chunkmaster.lib.generation.ChunkCoordinates +import java.sql.ResultSet +import java.util.concurrent.CompletableFuture + +class CompletedGenerationTasks(private val sqliteManager: SqliteManager) { + /** + * Returns the list of all completed tasks + */ + fun getCompletedTasks(): CompletableFuture> { + val completableFuture = CompletableFuture>() + + sqliteManager.executeStatement("SELECT * FROM completed_generation_tasks", HashMap()) { res -> + val tasks = ArrayList() + + while (res!!.next()) { + tasks.add(mapSqlResponseToWrapperObject(res)) + } + completableFuture.complete(tasks) + } + return completableFuture + } + + /** + * Returns a list of completed tasks for a world + */ + fun getCompletedTasksForWorld(world: String): CompletableFuture> { + val completableFuture = CompletableFuture>() + + sqliteManager.executeStatement("SELECT * FROM completed_generation_tasks WHERE world = ?", hashMapOf(1 to world)) { res -> + val tasks = ArrayList() + + while (res!!.next()) { + tasks.add(mapSqlResponseToWrapperObject(res)) + } + completableFuture.complete(tasks) + } + return completableFuture + } + + private fun mapSqlResponseToWrapperObject(res: ResultSet): CompletedGenerationTask { + val id = res.getInt("id") + val world = res.getString("world") + val center = ChunkCoordinates(res.getInt("center_x"), res.getInt("center_z")) + val radius = res.getInt("completed_radius") + val shape = res.getString("shape") + return CompletedGenerationTask(id, world, radius, center, shape) + } + + /** + * Adds a completed task + */ + fun addCompletedTask(id: Int, world: String, radius: Int, center: ChunkCoordinates, shape: String): CompletableFuture { + val completableFuture = CompletableFuture() + sqliteManager.executeStatement("INSERT INTO completed_generation_tasks (id, world, completed_radius, center_x, center_z, shape) VALUES (?, ?, ?, ?, ?, ?)", hashMapOf( + 1 to id, + 2 to world, + 3 to radius, + 4 to center.x, + 5 to center.z, + 6 to shape, + )) { + completableFuture.complete(null) + } + return completableFuture + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/database/SqliteManager.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/database/SqliteManager.kt index f2ed67c..67e1969 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/lib/database/SqliteManager.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/database/SqliteManager.kt @@ -38,6 +38,17 @@ class SqliteManager(private val chunkmaster: Chunkmaster) { Pair("chunk_x", "integer NOT NULL"), Pair("chunk_z", "integer NOT NULL") ) + ), + Pair( + "completed_generation_tasks", + listOf( + Pair("id", "integer PRIMARY KEY"), + Pair("world", "text NOT NULL"), + Pair("completed_radius", "integer NOT NULL"), + Pair("center_x", "integer NOT NULL"), + Pair("center_z", "integer NOT NULL"), + Pair("shape", "text NOT NULL") + ) ) ) private val needUpdate = HashSet>>() @@ -48,6 +59,7 @@ class SqliteManager(private val chunkmaster: Chunkmaster) { val worldProperties = WorldProperties(this) val pendingChunks = PendingChunks(this) val generationTasks = GenerationTasks(this) + val completedGenerationTasks = CompletedGenerationTasks(this) /** * Returns the connection to the database 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 2b9bc8c..33f6122 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/GenerationManager.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/GenerationManager.kt @@ -5,7 +5,7 @@ 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 net.trivernis.chunkmaster.lib.shapes.Square import org.bukkit.Server import org.bukkit.World import java.util.concurrent.CompletableFuture @@ -17,6 +17,8 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server val worldProperties = chunkmaster.sqliteManager.worldProperties private val pendingChunksTable = chunkmaster.sqliteManager.pendingChunks private val generationTasks = chunkmaster.sqliteManager.generationTasks + private val completedGenerationTasks = chunkmaster.sqliteManager.completedGenerationTasks + private val unloadingPeriod: Long get() { return chunkmaster.config.getLong("generation.unloading-period") @@ -140,6 +142,13 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server taskEntry.cancel(chunkmaster.config.getLong("mspt-pause-threshold")) } generationTasks.deleteGenerationTask(id) + completedGenerationTasks.addCompletedTask( + id, + taskEntry.generationTask.world.name, + taskEntry.generationTask.shape.currentRadius(), + taskEntry.generationTask.startChunk, + taskEntry.generationTask.shape.javaClass.simpleName + ) pendingChunksTable.clearPendingChunks(id) if (taskEntry is RunningTaskEntry) { @@ -356,8 +365,8 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server ): GenerationTask { val shape = when (shapeName) { "circle" -> Circle(Pair(center.x, center.z), Pair(start.x, start.z), radius) - "square" -> 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) + "square" -> Square(Pair(center.x, center.z), Pair(start.x, start.z), radius) + else -> Square(Pair(center.x, center.z), Pair(start.x, start.z), radius) } return DefaultGenerationTask( 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 63344a7..b661cb6 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/GenerationTask.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/generation/GenerationTask.kt @@ -10,10 +10,10 @@ import kotlin.math.ceil * Interface for generation tasks. */ abstract class GenerationTask( - private val plugin: Chunkmaster, + plugin: Chunkmaster, val world: World, protected val unloader: ChunkUnloader, - startChunk: ChunkCoordinates, + val startChunk: ChunkCoordinates, val shape: Shape, val missingChunks: HashSet, var state: TaskState diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/shapes/Spiral.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/shapes/Square.kt similarity index 93% rename from src/main/kotlin/net/trivernis/chunkmaster/lib/shapes/Spiral.kt rename to src/main/kotlin/net/trivernis/chunkmaster/lib/shapes/Square.kt index 15b5f35..20beeab 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/lib/shapes/Spiral.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/shapes/Square.kt @@ -3,7 +3,7 @@ package net.trivernis.chunkmaster.lib.shapes import kotlin.math.abs import kotlin.math.pow -class Spiral(center: Pair, start: Pair, radius: Int) : Shape(center, start, radius) { +class Square(center: Pair, start: Pair, radius: Int) : Shape(center, start, radius) { private var direction = 0 override fun endReached(): Boolean { @@ -40,7 +40,7 @@ class Spiral(center: Pair, start: Pair, radius: Int) : Shape } if (count == 0 && currentPos != center) { // simulate the spiral to get the correct direction and count - val simSpiral = Spiral(center, center, radius) + val simSpiral = Square(center, center, radius) while (simSpiral.next() != currentPos && !simSpiral.endReached()); direction = simSpiral.direction count = simSpiral.count diff --git a/src/main/resources/i18n/DEFAULT.i18n.properties b/src/main/resources/i18n/DEFAULT.i18n.properties index b8719ec..30ffa10 100644 --- a/src/main/resources/i18n/DEFAULT.i18n.properties +++ b/src/main/resources/i18n/DEFAULT.i18n.properties @@ -23,6 +23,10 @@ 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. +COMPLETED_TASKS_HEADER = §nCompleted Generation Tasks§r +COMPLETED_WORLD_HEADER = §l%s§r +COMPLETED_TASK_ENTRY = - §9#%d§r: §2%d§r chunks radius from center §2(%d, %d)§r with shape §2%s§r + PAUSE_SUCCESS = §9Paused all generation tasks. ALREADY_PAUSED = §cThe generation process is already paused! @@ -74,6 +78,7 @@ STATS_WORLD_NAME = §l%s§r STATS_ENTITY_COUNT = - §2%d§r Entities STATS_LOADED_CHUNKS = - §2%d§r Loaded Chunks STATS_PLUGIN_LOADED_CHUNKS = - §2%d§r Chunks Loaded by Chunkmaster +STATS_GENERATING = - Generating: §2%s§r SAVING_CHUNKS = Saving %d loaded chunks... CANCEL_FAIL = Failed to cancel task #%d in the given timeout! diff --git a/src/main/resources/i18n/de.i18n.properties b/src/main/resources/i18n/de.i18n.properties index 9eedf03..54e64e7 100644 --- a/src/main/resources/i18n/de.i18n.properties +++ b/src/main/resources/i18n/de.i18n.properties @@ -77,4 +77,10 @@ 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 +NO_AUTOSTART = Autostart ist auf §2false§r gesetzt. Pausiere... + +COMPLETED_TASKS_HEADER = §nAbgeschlossene Aufgaben§r +COMPLETED_WORLD_HEADER = §l%s§r +COMPLETED_TASK_ENTRY = - §9#%d§r: §2%d§r Chunks Radius von der Mitte §2(%d, %d)§r aus in der Form §2%s§r + +STATS_GENERATING = - Generiert: §2%s§r \ 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 06a257f..ac93601 100644 --- a/src/main/resources/i18n/en.i18n.properties +++ b/src/main/resources/i18n/en.i18n.properties @@ -75,4 +75,10 @@ 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 +NO_AUTOSTART = Autostart set to §2false§r. Pausing... + +COMPLETED_TASKS_HEADER = §nCompleted Generation Tasks§r +COMPLETED_WORLD_HEADER = §l%s§r +COMPLETED_TASK_ENTRY = - §9#%d§r: §2%d§r chunks radius from center §2(%d, %d)§r with shape §2%s§r + +STATS_GENERATING = - Generating: §2%s§r \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index f51b143..ae5cf85 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -23,6 +23,7 @@ commands: / setCenter [[] ]] - sets the center chunk of the world / getCenter [] - returns the center chunk of the world / stats [] - returns some chunk stats for the world or the whole server + / completed - lists all completed tasks for all worlds aliases: - chm - chunkm @@ -35,7 +36,7 @@ permissions: description: Allows the list subcommand. default: op chunkmaster.cancel: - description: Allows the remove subcommand. + description: Allows the cancel subcommand. default: op chunkmaster.pause: description: Allows the pause subcommand. @@ -55,6 +56,12 @@ permissions: chunkmaster.getcenter: description: Allows the getCenter subcommand. default: op + chunkmaster.stats: + description: Allows the stats subcommand. + deault: op + chunkmaster.completed: + description: Allows the completed subcommand. + default: op chunkmaster.chunkmaster: description: Allows Chunkmaster commands. default: op @@ -63,8 +70,14 @@ permissions: default: op children: - chunkmaster.generate - - chunkmaster.listgentasks - - chunkmaster.removegentask - - chunkmaster.pausegentasks - - chunkmaster.resumegentasks + - chunkmaster.list + - chunkmaster.cancel + - chunkmaster.pause + - chunkmaster.resume + - chunkmaster.completed + - chunkmaster.tpchunk + - chunkmaster.reload + - chunkmaster.setcenter + - chunkmaster.getcenter + - chunkmaster.stats - chunkmaster.chunkmaster \ No newline at end of file diff --git a/src/test/kotlin/net/trivernis/chunkmaster/lib/shapes/SpiralTest.kt b/src/test/kotlin/net/trivernis/chunkmaster/lib/shapes/SpiralTest.kt deleted file mode 100644 index f18c978..0000000 --- a/src/test/kotlin/net/trivernis/chunkmaster/lib/shapes/SpiralTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package net.trivernis.chunkmaster.lib.shapes - -import io.kotest.matchers.booleans.shouldBeTrue -import io.kotest.matchers.collections.shouldContainAll -import io.kotest.matchers.shouldBe -import org.junit.Test -import org.junit.jupiter.api.BeforeEach - -class SpiralTest { - - private val spiral = Spiral(center = Pair(0, 0), radius = 2, start = Pair(0, 0)) - - @BeforeEach - fun init() { - spiral.reset() - } - - @Test - fun `it generates coordinates`() { - spiral.next().shouldBe(Pair(0, 0)) - spiral.next().shouldBe(Pair(0, 1)) - spiral.next().shouldBe(Pair(1, 1)) - spiral.next().shouldBe(Pair(1, 0)) - spiral.next().shouldBe(Pair(1, -1)) - spiral.next().shouldBe(Pair(0, -1)) - spiral.next().shouldBe(Pair(-1, -1)) - spiral.next().shouldBe(Pair(-1, 0)) - spiral.next().shouldBe(Pair(-1, 1)) - spiral.next().shouldBe(Pair(-1, 2)) - spiral.next().shouldBe(Pair(0, 2)) - } - - @Test - fun `it reports when reaching the end`() { - for (i in 1..25) { - spiral.next() - } - spiral.endReached().shouldBeTrue() - } - - @Test - fun `it reports the radius`() { - for (i in 1..9) { - spiral.next() - } - spiral.currentRadius().shouldBe(1) - } - - @Test - fun `it returns the right edges`() { - spiral.getShapeEdgeLocations().shouldContainAll(listOf(Pair(2, 2), Pair(-2, 2), Pair(2, -2), Pair(-2, -2))) - } - - @Test - fun `it returns the progress`() { - spiral.progress(2).shouldBe(0) - for (i in 1..8) { - spiral.next() - } - spiral.progress(2).shouldBe(0.5) - } -} \ No newline at end of file diff --git a/src/test/kotlin/net/trivernis/chunkmaster/lib/shapes/SquareTest.kt b/src/test/kotlin/net/trivernis/chunkmaster/lib/shapes/SquareTest.kt new file mode 100644 index 0000000..f653c90 --- /dev/null +++ b/src/test/kotlin/net/trivernis/chunkmaster/lib/shapes/SquareTest.kt @@ -0,0 +1,62 @@ +package net.trivernis.chunkmaster.lib.shapes + +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.collections.shouldContainAll +import io.kotest.matchers.shouldBe +import org.junit.Test +import org.junit.jupiter.api.BeforeEach + +class SquareTest { + + private val square = Square(center = Pair(0, 0), radius = 2, start = Pair(0, 0)) + + @BeforeEach + fun init() { + square.reset() + } + + @Test + fun `it generates coordinates`() { + square.next().shouldBe(Pair(0, 0)) + square.next().shouldBe(Pair(0, 1)) + square.next().shouldBe(Pair(1, 1)) + square.next().shouldBe(Pair(1, 0)) + square.next().shouldBe(Pair(1, -1)) + square.next().shouldBe(Pair(0, -1)) + square.next().shouldBe(Pair(-1, -1)) + square.next().shouldBe(Pair(-1, 0)) + square.next().shouldBe(Pair(-1, 1)) + square.next().shouldBe(Pair(-1, 2)) + square.next().shouldBe(Pair(0, 2)) + } + + @Test + fun `it reports when reaching the end`() { + for (i in 1..25) { + square.next() + } + square.endReached().shouldBeTrue() + } + + @Test + fun `it reports the radius`() { + for (i in 1..9) { + square.next() + } + square.currentRadius().shouldBe(1) + } + + @Test + fun `it returns the right edges`() { + square.getShapeEdgeLocations().shouldContainAll(listOf(Pair(2, 2), Pair(-2, 2), Pair(2, -2), Pair(-2, -2))) + } + + @Test + fun `it returns the progress`() { + square.progress(2).shouldBe(0) + for (i in 1..8) { + square.next() + } + square.progress(2).shouldBe(0.5) + } +} \ No newline at end of file