Merge pull request #15 from Trivernis/develop

Fixes and changes to the sql structure
pull/23/head v0.13-beta
Trivernis 4 years ago committed by GitHub
commit f1ad4473de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,6 +1,6 @@
# chunkmaster [![CircleCI](https://circleci.com/gh/Trivernis/spigot-chunkmaster.svg?style=svg)](https://circleci.com/gh/Trivernis/spigot-chunkmaster)
# chunkmaster
This plugin can be used to pre-generate the region of a world around the spawn chunk.
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)
and resumes when the server is empty again. The generation also auto-resumes after a server
restart. The plugin tracks the ticks per second and pauses the generation when the tps
@ -23,6 +23,11 @@ All features can be accessed with the command `/chunkmaster` or the aliases `/ch
```yaml
generation:
# If set to true the plugin ignores the vanilla world border and doesn't stop
# the chunk generation when reaching it.
# The value should be a boolean <true/false>
ignore-worldborder: false
# 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.

@ -22,7 +22,7 @@ idea {
}
group "net.trivernis"
version "0.12-beta"
version "0.13-beta"
sourceCompatibility = 1.8
@ -48,7 +48,8 @@ dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
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"
//compileOnly "org.spigotmc:spigot-api:1.14.4-R0.1-SNAPSHOT"
compileOnly "com.destroystokyo.paper:paper-api:1.14.4-R0.1-SNAPSHOT"
compile group: 'org.xerial', name: 'sqlite-jdbc', version: '3.28.0'
compile "io.papermc:paperlib:1.0.2"
compile "org.bstats:bstats-bukkit:1.5"

@ -3,20 +3,17 @@ package net.trivernis.chunkmaster
import io.papermc.lib.PaperLib
import net.trivernis.chunkmaster.commands.*
import net.trivernis.chunkmaster.lib.generation.GenerationManager
import net.trivernis.chunkmaster.lib.SqlUpdateManager
import net.trivernis.chunkmaster.lib.SqliteManager
import org.bstats.bukkit.Metrics
import org.bukkit.plugin.java.JavaPlugin
import org.bukkit.scheduler.BukkitTask
import java.lang.Exception
import java.sql.Connection
import java.sql.DriverManager
class Chunkmaster: JavaPlugin() {
lateinit var sqliteConnection: Connection
var dbname: String? = null
lateinit var sqliteManager: SqliteManager
lateinit var generationManager: GenerationManager
private lateinit var tpsTask: BukkitTask
var mspt = 50L // keep track of the milliseconds per tick
var mspt = 20 // keep track of the milliseconds per tick
private set
/**
@ -37,12 +34,18 @@ class Chunkmaster: JavaPlugin() {
server.pluginManager.registerEvents(ChunkmasterEvents(this, server), this)
tpsTask = server.scheduler.runTaskTimer(this, Runnable {
val start = System.currentTimeMillis()
server.scheduler.runTaskLater(this, Runnable {
mspt = System.currentTimeMillis() - start
}, 1)
}, 1, 300)
if (PaperLib.isPaper() && PaperLib.getMinecraftPatchVersion() >= 225) {
tpsTask = server.scheduler.runTaskTimer(this, Runnable {
mspt = 1000/server.currentTick // use papers exposed tick rather than calculating it
}, 1, 300)
} else {
tpsTask = server.scheduler.runTaskTimer(this, Runnable {
val start = System.currentTimeMillis()
server.scheduler.runTaskLater(this, Runnable {
mspt = (System.currentTimeMillis() - start).toInt()
}, 1)
}, 1, 300)
}
}
/**
@ -51,7 +54,6 @@ class Chunkmaster: JavaPlugin() {
override fun onDisable() {
logger.info("Stopping all generation tasks...")
generationManager.stopAll()
sqliteConnection.close()
}
/**
@ -66,6 +68,8 @@ class Chunkmaster: JavaPlugin() {
config.addDefault("generation.pause-on-join", true)
config.addDefault("generation.max-pending-chunks", 10)
config.addDefault("generation.max-loaded-chunks", 10)
config.addDefault("generation.ignore-worldborder", false)
config.addDefault("database.filename", "chunkmaster.db")
config.options().copyDefaults(true)
saveConfig()
}
@ -76,13 +80,8 @@ class Chunkmaster: JavaPlugin() {
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 updateManager = SqlUpdateManager(sqliteConnection, this)
updateManager.checkUpdate()
updateManager.performUpdate()
this.sqliteManager = SqliteManager( this)
sqliteManager.init()
logger.info("Database fully initialized.")
} catch(e: Exception) {
logger.warning("Failed to init database: ${e.message}")

@ -52,17 +52,4 @@ class ChunkmasterEvents(private val chunkmaster: Chunkmaster, private val server
}
}
}
/**
* Unload all chunks before a save.
*/
@EventHandler
fun onWorldSave(event: WorldSaveEvent) {
val task = chunkmaster.generationManager.tasks.find { it.generationTask.world == event.world }
if (task != null) {
if (task.generationTask is GenerationTaskPaper) {
task.generationTask.unloadAllChunks()
}
}
}
}

@ -1,79 +0,0 @@
package net.trivernis.chunkmaster.lib
import net.trivernis.chunkmaster.Chunkmaster
import java.lang.Exception
import java.sql.Connection
class SqlUpdateManager(private val connnection: Connection, private val chunkmaster: Chunkmaster) {
private val tables = listOf(
Pair(
"generation_tasks",
listOf(
Pair("id", "integer PRIMARY KEY AUTOINCREMENT"),
Pair("center_x", "integer NOT NULL DEFAULT 0"),
Pair("center_z", "integer NOT NULL DEFAULT 0"),
Pair("last_x", "integer NOT NULL DEFAULT 0"),
Pair("last_z", "integer NOT NULL DEFAULT 0"),
Pair("world", "text UNIQUE NOT NULL DEFAULT 'world'"),
Pair("stop_after", "integer DEFAULT -1")
)
)
)
private val needUpdate = HashSet<Pair<String, Pair<String, String>>>()
private val needCreation = HashSet<String>()
/**
* Checks which tables need an update or creation.
*/
fun checkUpdate() {
val meta = connnection.metaData
for (table in tables) {
val resTables = meta.getTables(null, null, table.first, null)
if (resTables.next()) { // table exists
for (column in table.second) {
val resColumn = meta.getColumns(null, null, table.first, column.first)
if (!resColumn.next()) {
needUpdate.add(Pair(table.first, column))
}
}
} else {
needCreation.add(table.first)
}
}
}
/**
* Creates or updates tables that needed an update.
*/
fun performUpdate() {
for (table in needCreation) {
try {
var tableDef = "CREATE TABLE IF NOT EXISTS $table ("
for (column in tables.find{it.first == table}!!.second) {
tableDef += "${column.first} ${column.second},"
}
tableDef = tableDef.substringBeforeLast(",") + ");"
chunkmaster.logger.info("Creating table $table with definition $tableDef")
val stmt = connnection.prepareStatement(tableDef)
stmt.execute()
stmt.close()
} catch (err: Exception) {
chunkmaster.logger.severe("Error creating table $table.")
}
}
for (table in needUpdate) {
val updateSql = "ALTER TABLE ${table.first} ADD COLUMN ${table.second.first} ${table.second.second}"
try {
val stmt = connnection.prepareStatement(updateSql)
stmt.execute()
stmt.close()
chunkmaster.logger.info("Updated table ${table.first} with sql $updateSql")
} catch (e: Exception) {
chunkmaster.logger.severe("Failed to update table ${table.first} with sql $updateSql")
}
}
}
}

@ -0,0 +1,137 @@
package net.trivernis.chunkmaster.lib
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) {
private val tables = listOf(
Pair(
"generation_tasks",
listOf(
Pair("id", "integer PRIMARY KEY AUTOINCREMENT"),
Pair("center_x", "integer NOT NULL DEFAULT 0"),
Pair("center_z", "integer NOT NULL DEFAULT 0"),
Pair("last_x", "integer NOT NULL DEFAULT 0"),
Pair("last_z", "integer NOT NULL DEFAULT 0"),
Pair("world", "text UNIQUE NOT NULL DEFAULT 'world'"),
Pair("stop_after", "integer DEFAULT -1")
)
)
)
private val needUpdate = HashSet<Pair<String, Pair<String, String>>>()
private val needCreation = HashSet<String>()
/**
* Returns the connection to the database
*/
fun getConnection(): Connection? {
try {
Class.forName("org.sqlite.JDBC")
return DriverManager.getConnection("jdbc:sqlite:${chunkmaster.dataFolder.absolutePath}/" +
"${chunkmaster.config.getString("database.filename")}")
} catch (e: Exception) {
chunkmaster.logger.severe("Could not get database connection.")
chunkmaster.logger.severe(e.message)
}
return null
}
/**
* Checks for and performs an update
*/
fun init() {
this.checkUpdate()
this.performUpdate()
}
/**
* Checks which tables need an update or creation.
*/
private fun checkUpdate() {
val meta = getConnection()!!.metaData
for (table in tables) {
val resTables = meta.getTables(null, null, table.first, null)
if (resTables.next()) { // table exists
for (column in table.second) {
val resColumn = meta.getColumns(null, null, table.first, column.first)
if (!resColumn.next()) {
needUpdate.add(Pair(table.first, column))
}
resColumn.close()
}
} else {
needCreation.add(table.first)
}
resTables.close()
}
}
/**
* Executes a sql statement on the database.
*/
fun executeStatement(sql: String, values: HashMap<Int, Any>, callback: ((ResultSet) -> Unit)?) {
val connection = getConnection()
if (connection != null) {
try {
val statement = connection.prepareStatement(sql)
for (parameterValue in values) {
statement.setObject(parameterValue.key, parameterValue.value)
}
statement.execute()
val res = statement.resultSet
if (callback != null) {
callback(res)
}
statement.close()
} catch (e: Exception) {
chunkmaster.logger.severe("An error occured on sql $sql. ${e.message}")
chunkmaster.logger.info(ExceptionUtils.getStackTrace(e))
} finally {
connection.close()
}
} else {
chunkmaster.logger.severe("Could not execute sql $sql. No database connection established.")
}
}
/**
* Creates or updates tables that needed an update.
*/
private fun performUpdate() {
for (table in needCreation) {
try {
var tableDef = "CREATE TABLE IF NOT EXISTS $table ("
for (column in tables.find{it.first == table}!!.second) {
tableDef += "${column.first} ${column.second},"
}
tableDef = tableDef.substringBeforeLast(",") + ");"
chunkmaster.logger.info("Creating table $table with definition $tableDef")
executeStatement(tableDef, HashMap(), null)
} catch (e: Exception) {
chunkmaster.logger.severe("Error creating table $table.")
chunkmaster.logger.severe(e.message)
chunkmaster.logger.info(ExceptionUtils.getStackTrace(e))
}
}
for (table in needUpdate) {
val updateSql = "ALTER TABLE ${table.first} ADD COLUMN ${table.second.first} ${table.second.second}"
try {
executeStatement(updateSql, HashMap(), null)
chunkmaster.logger.info("Updated table ${table.first} with sql $updateSql")
} catch (e: Exception) {
chunkmaster.logger.severe("Failed to update table ${table.first} with sql $updateSql")
chunkmaster.logger.severe(e.message)
chunkmaster.logger.info(ExceptionUtils.getStackTrace(e))
}
}
}
}

@ -2,11 +2,8 @@ package net.trivernis.chunkmaster.lib.generation
import io.papermc.lib.PaperLib
import net.trivernis.chunkmaster.Chunkmaster
import org.bukkit.Chunk
import org.bukkit.Server
import org.bukkit.World
import java.lang.Exception
import java.lang.NullPointerException
class GenerationManager(private val chunkmaster: Chunkmaster, private val server: Server) {
@ -31,35 +28,37 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
val centerChunk = ChunkCoordinates(world.spawnLocation.chunk.x, world.spawnLocation.chunk.z)
val generationTask = createGenerationTask(world, centerChunk, centerChunk, stopAfter)
val insertStatement = chunkmaster.sqliteConnection.prepareStatement(
chunkmaster.sqliteManager.executeStatement(
"""
INSERT INTO generation_tasks (center_x, center_z, last_x, last_z, world, stop_after)
values (?, ?, ?, ?, ?, ?)
"""
INSERT INTO generation_tasks (center_x, center_z, last_x, last_z, world, stop_after)
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 stopAfter
)
),
null
)
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.setInt(6, stopAfter)
insertStatement.execute()
val getIdStatement = chunkmaster.sqliteConnection.prepareStatement(
var id = 0
chunkmaster.sqliteManager.executeStatement(
"""
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")
insertStatement.close()
getIdStatement.close()
""".trimIndent(),
HashMap()
) {
it.next()
id = it.getInt("id")
}
generationTask.onEndReached {
chunkmaster.logger.info("Task #${id} finished after ${generationTask.count} chunks.")
chunkmaster.logger.info("Task #${id} finished after ${it.count} chunks.")
removeTask(id)
}
@ -115,14 +114,13 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
}
if (taskEntry != null) {
taskEntry.cancel()
val deleteTask = chunkmaster.sqliteConnection.prepareStatement(
chunkmaster.sqliteManager.executeStatement(
"""
DELETE FROM generation_tasks WHERE id = ?;
""".trimIndent()
""".trimIndent(),
HashMap(mapOf(1 to taskEntry.id)),
null
)
deleteTask.setInt(1, taskEntry.id)
deleteTask.execute()
deleteTask.close()
if (taskEntry is RunningTaskEntry) {
if (taskEntry.task.isCancelled) {
@ -173,24 +171,24 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
* Starts all generation tasks.
*/
fun startAll() {
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 = ChunkCoordinates(res.getInt("center_x"), res.getInt("center_z"))
val last = ChunkCoordinates(res.getInt("last_x"), res.getInt("last_z"))
val stopAfter = res.getInt("stop_after")
if (this.tasks.find { it.id == id } == null) {
resumeTask(world!!, center, last, id, stopAfter)
chunkmaster.sqliteManager.executeStatement("SELECT * FROM generation_tasks", HashMap()) {
val res = it
while (res.next()) {
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 stopAfter = res.getInt("stop_after")
if (this.tasks.find { it.id == id } == null) {
resumeTask(world!!, center, last, id, stopAfter)
}
} catch (error: NullPointerException) {
chunkmaster.logger.severe("Failed to load Task ${res.getInt("id")}.")
}
} catch (error: NullPointerException) {
chunkmaster.logger.severe("Failed to load Task ${res.getInt("id")}.")
}
}
savedTasksStatement.close()
if (tasks.isNotEmpty()) {
chunkmaster.logger.info("${tasks.size} saved tasks loaded.")
}
@ -226,22 +224,21 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
chunkmaster.logger.info(
"""Task #${task.id} running for "${genTask.world.name}".
|Progress ${task.generationTask.count} chunks
|${if (task.generationTask.stopAfter > 0) "(${"%.2f".format((task.generationTask.count.toDouble() /
task.generationTask.stopAfter.toDouble()) * 100)}%)" else ""}.
|${if (task.generationTask.stopAfter > 0) "(${"%.2f".format(
(task.generationTask.count.toDouble() /
task.generationTask.stopAfter.toDouble()) * 100
)}%)" else ""}.
| Speed: ${"%.1f".format(task.generationSpeed)} chunks/sec,
|Last Chunk: ${genTask.lastChunk.x}, ${genTask.lastChunk.z}""".trimMargin("|").replace('\n', ' ')
)
val updateStatement = chunkmaster.sqliteConnection.prepareStatement(
chunkmaster.sqliteManager.executeStatement(
"""
UPDATE generation_tasks SET last_x = ?, last_z = ?
WHERE id = ?
""".trimIndent()
""".trimIndent(),
HashMap(mapOf(1 to genTask.lastChunk.x, 2 to genTask.lastChunk.z, 3 to task.id)),
null
)
updateStatement.setInt(1, genTask.lastChunk.x)
updateStatement.setInt(2, genTask.lastChunk.z)
updateStatement.setInt(3, task.id)
updateStatement.execute()
updateStatement.close()
} catch (error: Exception) {
chunkmaster.logger.warning("Exception when saving task progress ${error.message}")
}

@ -24,8 +24,9 @@ abstract class GenerationTask(plugin: Chunkmaster, centerChunk: ChunkCoordinates
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 val ignoreWorldborder = plugin.config.getBoolean("generation.ignore-worldborder")
protected var endReachedCallback: (() -> Unit)? = null
protected var endReachedCallback: ((GenerationTask) -> Unit)? = null
private set
abstract override fun run()
@ -52,13 +53,14 @@ abstract class GenerationTask(plugin: Chunkmaster, centerChunk: ChunkCoordinates
* Checks if the World border or the maximum chunk setting for the task is reached.
*/
protected fun borderReached(): Boolean {
return !world.worldBorder.isInside(lastChunkCoords.getCenterLocation(world)) || (stopAfter in 1..count)
return (!world.worldBorder.isInside(lastChunkCoords.getCenterLocation(world)) && !ignoreWorldborder)
|| (stopAfter in 1..count)
}
/**
* Registers end reached callback
*/
fun onEndReached(cb: () -> Unit) {
fun onEndReached(cb: (GenerationTask) -> Unit) {
endReachedCallback = cb
}
}

@ -4,7 +4,6 @@ import net.trivernis.chunkmaster.Chunkmaster
import org.bukkit.Chunk
import org.bukkit.World
import java.util.concurrent.CompletableFuture
import io.papermc.lib.PaperLib
class GenerationTaskPaper(
private val plugin: Chunkmaster, override val world: World,
@ -38,28 +37,28 @@ class GenerationTaskPaper(
} else if (pendingChunks.size < maxPendingChunks) { // if more than 10 chunks are pending, wait.
if (borderReached()) {
endReached = true
endReachedCallback?.invoke()
endReachedCallback?.invoke(this)
return
}
var chunk = nextChunkCoordinates
for (i in 1 until chunkSkips) {
if (PaperLib.isChunkGenerated(world, chunk.x, chunk.z)) {
if (world.isChunkGenerated(chunk.x, chunk.z)) {
chunk = nextChunkCoordinates
} else {
break
}
}
if (!PaperLib.isChunkGenerated(world, chunk.x, chunk.z)) {
if (!world.isChunkGenerated(chunk.x, chunk.z)) {
for (i in 0 until minOf(chunksPerStep, (stopAfter - count) - 1)) {
if (!PaperLib.isChunkGenerated(world, chunk.x, chunk.z)) {
pendingChunks.add(PaperLib.getChunkAtAsync(world, chunk.x, chunk.z, true))
if (!world.isChunkGenerated(chunk.x, chunk.z)) {
pendingChunks.add(world.getChunkAtAsync(chunk.x, chunk.z, true))
}
chunk = nextChunkCoordinates
}
if (!PaperLib.isChunkGenerated(world, chunk.x, chunk.z)) {
pendingChunks.add(PaperLib.getChunkAtAsync(world, chunk.x, chunk.z, true))
if (!world.isChunkGenerated(chunk.x, chunk.z)) {
pendingChunks.add(world.getChunkAtAsync(chunk.x, chunk.z, true))
}
}
lastChunkCoords = chunk

@ -1,7 +1,6 @@
package net.trivernis.chunkmaster.lib.generation
import net.trivernis.chunkmaster.Chunkmaster
import org.bukkit.Chunk
import org.bukkit.World
class GenerationTaskSpigot(
@ -33,7 +32,7 @@ class GenerationTaskSpigot(
} else {
if (borderReached()) {
endReached = true
endReachedCallback?.invoke()
endReachedCallback?.invoke(this)
return
}

@ -1,6 +1,6 @@
main: net.trivernis.chunkmaster.Chunkmaster
name: Chunkmaster
version: '0.12-beta'
version: '0.13-beta'
description: Chunk commands plugin.
author: Trivernis
website: trivernis.net

Loading…
Cancel
Save