diff --git a/build.gradle b/build.gradle index fc7f334..55728e3 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,9 @@ repositories { maven { url 'https://papermc.io/repo/repository/maven-public/' } + maven { + url 'https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc' + } } dependencies { @@ -32,6 +35,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" testCompile group: 'junit', name: 'junit', version: '4.12' compileOnly "org.spigotmc:spigot-api:1.14.4-R0.1-SNAPSHOT" + compile group: 'org.xerial', name: 'sqlite-jdbc', version: '3.28.0' } jar { diff --git a/src/main/kotlin/net/trivernis/chunkmaster/Chunkmaster.kt b/src/main/kotlin/net/trivernis/chunkmaster/Chunkmaster.kt index ed0b0cf..1679bcf 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/Chunkmaster.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/Chunkmaster.kt @@ -1,18 +1,63 @@ package net.trivernis.chunkmaster +import net.trivernis.chunkmaster.commands.CommandGenerate +import net.trivernis.chunkmaster.lib.GenerationManager import net.trivernis.chunkmaster.lib.Spiral import org.bukkit.plugin.java.JavaPlugin +import java.lang.Exception +import java.sql.Connection +import java.sql.DriverManager +import kotlin.math.log class Chunkmaster: JavaPlugin() { + lateinit var sqliteConnection: Connection + var dbname: String? = null + lateinit var generationManager: GenerationManager + override fun onEnable() { configure() + initDatabase() + generationManager = GenerationManager(this, server) + generationManager.init() + getCommand("generate")?.setExecutor(CommandGenerate(this)) + server.pluginManager.registerEvents(ChunkmasterEvents(this, server), this) } override fun onDisable() { - + logger.info("Stopping all generation tasks...") + generationManager.stopAll() + sqliteConnection.close() } private fun configure() { + dataFolder.mkdir() config.options().copyDefaults(true) } + + /** + * Initializes the database + */ + private fun initDatabase() { + logger.info("Initializing Database...") + try { + Class.forName("org.sqlite.JDBC") + sqliteConnection = DriverManager.getConnection("jdbc:sqlite:${dataFolder.absolutePath}/chunkmaster.db") + logger.info("Database connection established.") + val createTableStatement = sqliteConnection.prepareStatement(""" + CREATE TABLE IF NOT EXISTS generation_tasks ( + id integer PRIMARY KEY AUTOINCREMENT, + center_x integer NOT NULL DEFAULT 0, + center_z integer NOT NULL DEFAULT 0, + last_x integer NOT NULL DEFAULT 0, + last_z integer NOT NULL DEFAULT 0, + world text UNIQUE NOT NULL DEFAULT 'world' + ); + """.trimIndent()) + createTableStatement.execute() + createTableStatement.close() + logger.info("Database tables created.") + } catch(e: Exception) { + logger.warning("Failed to init database: ${e.message}") + } + } } \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/ChunkmasterEvents.kt b/src/main/kotlin/net/trivernis/chunkmaster/ChunkmasterEvents.kt new file mode 100644 index 0000000..bdae7a0 --- /dev/null +++ b/src/main/kotlin/net/trivernis/chunkmaster/ChunkmasterEvents.kt @@ -0,0 +1,29 @@ +package net.trivernis.chunkmaster + +import org.bukkit.Server +import org.bukkit.event.Listener +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.event.player.PlayerQuitEvent + +class ChunkmasterEvents(private val chunkmaster: Chunkmaster, private val server: Server): Listener { + + /** + * Autostart generation tasks + */ + fun onPlayerQuit(event: PlayerQuitEvent) { + if (server.onlinePlayers.size == 1 && server.onlinePlayers.contains(event.player)) { + chunkmaster.generationManager.startAll() + chunkmaster.logger.info("Server is empty. Starting chunk generation tasks.") + } + } + + /** + * Autostop generation tasks + */ + fun onPlayerJoin(event: PlayerJoinEvent) { + if (server.onlinePlayers.size == 1 || server.onlinePlayers.isEmpty()) { + chunkmaster.generationManager.stopAll() + chunkmaster.logger.info("Stopping generation tasks because of player join.") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/commands/CommandGenerate.kt b/src/main/kotlin/net/trivernis/chunkmaster/commands/CommandGenerate.kt new file mode 100644 index 0000000..eabbad5 --- /dev/null +++ b/src/main/kotlin/net/trivernis/chunkmaster/commands/CommandGenerate.kt @@ -0,0 +1,40 @@ +package net.trivernis.chunkmaster.commands + +import net.trivernis.chunkmaster.Chunkmaster +import org.bukkit.command.Command +import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + +class CommandGenerate(private val chunkmaster: Chunkmaster): CommandExecutor { + /** + * Start world generation task on commmand + */ + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + if (sender is Player) { + return if (command.testPermission(sender)) { + chunkmaster.generationManager.addTask(sender.world) + sender.sendMessage("Added generation task for world \"${sender.world.name}\"") + true + } else { + sender.sendMessage("You do not have permission.") + true + } + } else { + return if (args.size == 1) { + val world = sender.server.getWorld(args[0]) + if (world != null) { + chunkmaster.generationManager.addTask(world) + true + } else { + sender.sendMessage("World \"${args[0]}\" not found") + false + } + } else { + sender.sendMessage("You need to specify a world") + false + } + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/GenerationManager.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/GenerationManager.kt index 99c863f..cb39191 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/lib/GenerationManager.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/GenerationManager.kt @@ -1,5 +1,127 @@ package net.trivernis.chunkmaster.lib -class GenerationManager { - //TODO: Implement +import javafx.concurrent.Task +import net.trivernis.chunkmaster.Chunkmaster +import org.bukkit.Chunk +import org.bukkit.Server +import org.bukkit.World +import org.bukkit.scheduler.BukkitTask +import java.lang.Exception +import java.lang.NullPointerException + +class GenerationManager(private val chunkmaster: Chunkmaster, private val server: Server) { + + private val tasks: HashSet = HashSet() + + /** + * Adds a generation task + */ + fun addTask(world: World): Int { + val centerChunk = world.getChunkAt(world.spawnLocation) + val generationTask = GenerationTask(chunkmaster, world, centerChunk, centerChunk) + val task = server.scheduler.runTaskTimer(chunkmaster, generationTask, 10, 2) + val insertStatement = chunkmaster.sqliteConnection.prepareStatement(""" + INSERT INTO generation_tasks (center_x, center_z, last_x, last_z, world) + values (?, ?, ?, ?, ?) + """) + insertStatement.setInt(1, centerChunk.x) + insertStatement.setInt(2, centerChunk.z) + insertStatement.setInt(3, centerChunk.x) + insertStatement.setInt(4, centerChunk.z) + insertStatement.setString(5, world.name) + insertStatement.execute() + val getIdStatement = chunkmaster.sqliteConnection.prepareStatement(""" + SELECT id FROM generation_tasks ORDER BY id DESC LIMIT 1 + """.trimIndent()) + getIdStatement.execute() + val result = getIdStatement.resultSet + result.next() + val id: Int = result.getInt("id") + tasks.add(TaskEntry(id, task, generationTask)) + insertStatement.close() + getIdStatement.close() + return id + } + + /** + * Resumes a generation task + */ + fun resumeTask(world: World, center: Chunk, last: Chunk, id: Int) { + chunkmaster.logger.info("Resuming chunk generation task for world \"${world.name}\"") + val generationTask = GenerationTask(chunkmaster, world, center, last) + val task = server.scheduler.runTaskTimer(chunkmaster, generationTask, 10, 2) + tasks.add(TaskEntry(id, task, generationTask)) + } + + /** + * Init + * Loads tasks from the database and resumes them + */ + fun init() { + chunkmaster.logger.info("Creating task to load chunk generation Tasks later...") + server.scheduler.runTaskLater(chunkmaster, Runnable { + startAll() // run startAll after 10 seconds + }, 200) + } + + /** + * Stops all generation tasks + */ + fun stopAll() { + saveProgress() + for (task in tasks) { + task.generationTask.cancel() + task.task.cancel() + } + } + + /** + * Starts all generation tasks. + */ + fun startAll() { + chunkmaster.logger.info("Loading saved chunk generation tasks...") + val savedTasksStatement = chunkmaster.sqliteConnection.prepareStatement("SELECT * FROM generation_tasks") + savedTasksStatement.execute() + val res = savedTasksStatement.resultSet + while (res.next()) { + try { + val id = res.getInt("id") + val world = server.getWorld(res.getString("world")) + val center = world!!.getChunkAt(res.getInt("center_x"), res.getInt("center_z")) + val last = world.getChunkAt(res.getInt("last_x"), res.getInt("last_z")) + resumeTask(world, center, last, id) + } catch (error: NullPointerException) { + server.consoleSender.sendMessage("Failed to load Task ${res.getInt("id")}.") + } + } + savedTasksStatement.close() + server.scheduler.runTaskTimer(chunkmaster, Runnable { + saveProgress() // save progress every 30 seconds + }, 600, 600) + chunkmaster.logger.info("${tasks.size} saved tasks loaded.") + } + + /** + * Saves the task progress + */ + private fun saveProgress() { + for (task in tasks) { + try { + val genTask = task.generationTask + server.consoleSender.sendMessage("Task #${task.id} running for \"${genTask.world.name}\"." + + "Progress ${task.generationTask.count} chunks. Last Chunk: ${genTask.lastChunk.x}, ${genTask.lastChunk.z}") + val updateStatement = chunkmaster.sqliteConnection.prepareStatement(""" + UPDATE generation_tasks SET last_x = ?, last_z = ? + WHERE id = ? + """.trimIndent()) + updateStatement.setInt(1, genTask.lastChunk.x) + updateStatement.setInt(2, genTask.lastChunk.z) + updateStatement.setInt(3, task.id) + updateStatement.execute() + updateStatement.close() + } catch (error: Exception) { + server.consoleSender.sendMessage("Exception when saving task progress ${error.message}") + } + } + } } \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/GenerationTask.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/GenerationTask.kt new file mode 100644 index 0000000..a271503 --- /dev/null +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/GenerationTask.kt @@ -0,0 +1,45 @@ +package net.trivernis.chunkmaster.lib + +import net.trivernis.chunkmaster.Chunkmaster +import org.bukkit.Chunk +import org.bukkit.World +import org.bukkit.scheduler.BukkitRunnable +import org.bukkit.scheduler.BukkitTask + +class GenerationTask(private val plugin: Chunkmaster, val world: World, + private val centerChunk: Chunk, val startChunk: Chunk): Runnable { + private val spiral: Spiral = Spiral(Pair(centerChunk.x, centerChunk.z), Pair(startChunk.x, startChunk.z)) + private val loadedChunks: HashSet = HashSet() + var count = 0 + get() = field + var lastChunk: Chunk = startChunk + get() = field + + override fun run() { + if (loadedChunks.size > 10) { + for (chunk in loadedChunks) { + if (chunk.isLoaded) { + chunk.unload(true) + } + } + } else { + val nextChunkCoords = spiral.next() + val chunk = world.getChunkAt(nextChunkCoords.first, nextChunkCoords.second) + + if (!world.isChunkGenerated(chunk.x, chunk.z)) { + chunk.load(true) + loadedChunks.add(chunk) + } + lastChunk = chunk + count++ + } + } + + fun cancel() { + for (chunk in loadedChunks) { + if (chunk.isLoaded) { + chunk.unload(true) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/Spiral.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/Spiral.kt index 4484896..0aa147e 100644 --- a/src/main/kotlin/net/trivernis/chunkmaster/lib/Spiral.kt +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/Spiral.kt @@ -11,6 +11,17 @@ class Spiral(private val center: Pair, private val start: Pair { + if (count == 0 && currentPos != center) { + val distances = getDistances(center, currentPos) + + if (distances.second < distances.first && distances.first > 0) { + direction = 1 + } else if (distances.first > distances.second && distances.second < 0) { + direction = 2 + } else if (distances.second > distances.first && distances.first < 0) { + direction = 3 + } + } if (count == 1) { // because of the center behaviour count ++ return currentPos diff --git a/src/main/kotlin/net/trivernis/chunkmaster/lib/TaskEntry.kt b/src/main/kotlin/net/trivernis/chunkmaster/lib/TaskEntry.kt new file mode 100644 index 0000000..4307646 --- /dev/null +++ b/src/main/kotlin/net/trivernis/chunkmaster/lib/TaskEntry.kt @@ -0,0 +1,6 @@ +package net.trivernis.chunkmaster.lib + +import org.bukkit.scheduler.BukkitTask + +data class TaskEntry(val id: Int, val task: BukkitTask, val generationTask: GenerationTask) { +} \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 9c5d9dc..e6779cd 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -4,9 +4,18 @@ version: '1.0 SNAPSHOT' description: Chunk commands plugin. author: Trivernis website: trivernis.net +api-version: '1.14' commands: - + generate: + description: Generates chunks starting from the world spawnpoint + permission: chumkmaster.generate + usage: /generate [world] permissions: + cunkmaster.generate: + description: Allows generate command + default: op chunkmaster.*: description: Wildcard permission - default: op \ No newline at end of file + default: op + children: + - chunkmaster.generate \ No newline at end of file