diff options
author | Dico200 <dico.karssiens@gmail.com> | 2018-07-24 01:14:23 +0100 |
---|---|---|
committer | Dico200 <dico.karssiens@gmail.com> | 2018-07-24 01:14:23 +0100 |
commit | d15d1b767bc89d087fd46450cb5e62fe0c4e9e61 (patch) | |
tree | a1904036d8d46ecca2ac5f3901d89367878b2cc5 | |
parent | 42026191ec3a1f6468d8a46304d6ce5cd2d0689c (diff) |
Work on commands. Implemented helper functions, among others to handle asynchronous commands
24 files changed, 425 insertions, 103 deletions
diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..32f5e31 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=false +indent_style=space +indent_size=4 + +[{.babelrc,.stylelintrc,jest.config,.eslintrc,*.bowerrc,*.jsb3,*.jsb2,*.json}] +indent_style=space +indent_size=2 + +[{*.ddl,*.sql}] +indent_style=space +indent_size=2 + +[{*.yml,*.yaml}] +indent_style=space +indent_size=2 + diff --git a/build.gradle.kts b/build.gradle.kts index 8b65cd9..421da58 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,7 +28,7 @@ dependencies { compile("org.jetbrains.exposed:exposed:0.10.3") compile("org.jetbrains.kotlinx:kotlinx-coroutines-core:0.23.4") compile("com.zaxxer:HikariCP:3.2.0") - compile(files("../h2/bin/h2-client-1.4.197.jar")) + compile(files("../h2/bin/h2-1.4.197.jar")) val jacksonVersion = "2.9.6" compile("com.fasterxml.jackson.core:jackson-core:$jacksonVersion") @@ -44,9 +44,12 @@ dependencies { } tasks { - val jar by getting(Jar::class) + val jar by getting(Jar::class) { + group = "artifacts" + } val fatJar by creating(Jar::class) { + group = "artifacts" destinationDir = file("$rootDir/debug/plugins") baseName = "parcels2-all" from(*configurations.compile.map(::zipTree).toTypedArray()) @@ -54,6 +57,7 @@ tasks { } val shadowJar by getting(ShadowJar::class) { + group = "artifacts" destinationDir = file("$rootDir/debug/plugins") baseName = "parcels2-shaded" @@ -72,6 +76,7 @@ tasks { } val relocateSnakeyamlJar by creating(ShadowJar::class) { + group = "artifacts" destinationDir = file("$rootDir/debug/plugins") baseName = "parcels2-shaded" relocate("org.yaml", "shadow.org.yaml") diff --git a/src/main/kotlin/io/dico/parcels2/Options.kt b/src/main/kotlin/io/dico/parcels2/Options.kt index 5dcb318..d8cc9d6 100644 --- a/src/main/kotlin/io/dico/parcels2/Options.kt +++ b/src/main/kotlin/io/dico/parcels2/Options.kt @@ -5,7 +5,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore import io.dico.parcels2.storage.Storage import io.dico.parcels2.storage.StorageFactory import io.dico.parcels2.storage.yamlObjectMapper -import org.bukkit.Bukkit import org.bukkit.Bukkit.createBlockData import org.bukkit.GameMode import org.bukkit.Material diff --git a/src/main/kotlin/io/dico/parcels2/Parcel.kt b/src/main/kotlin/io/dico/parcels2/Parcel.kt index 046d6f5..ef45e08 100644 --- a/src/main/kotlin/io/dico/parcels2/Parcel.kt +++ b/src/main/kotlin/io/dico/parcels2/Parcel.kt @@ -19,6 +19,15 @@ interface ParcelData { var allowInteractInputs: Boolean var allowInteractInventory: Boolean + + fun isOwner(uuid: UUID): Boolean { + return owner?.uuid == uuid + } + + val infoString: String + get() { + TODO() + } } /** @@ -88,14 +97,14 @@ class ParcelDataHolder : ParcelData { override fun getAddedStatus(uuid: UUID): AddedStatus = added.getOrDefault(uuid, AddedStatus.DEFAULT) override fun setAddedStatus(uuid: UUID, status: AddedStatus): Boolean = status.takeIf { it != AddedStatus.DEFAULT } - ?.let { added.put(uuid, it) != it } - ?: added.remove(uuid) != null + ?.let { added.put(uuid, it) != it } + ?: added.remove(uuid) != null override fun isBanned(uuid: UUID) = getAddedStatus(uuid) == AddedStatus.BANNED override fun isAllowed(uuid: UUID) = getAddedStatus(uuid) == AddedStatus.ALLOWED override fun canBuild(player: Player) = isAllowed(player.uniqueId) - || owner?.matches(player, allowNameMatch = false) ?: false - || player.hasBuildAnywhere + || owner?.matches(player, allowNameMatch = false) ?: false + || player.hasBuildAnywhere override var allowInteractInputs = true override var allowInteractInventory = true @@ -121,7 +130,7 @@ class ParcelOwner(val uuid: UUID? = null, companion object { fun create(uuid: UUID?, name: String?): ParcelOwner? { return uuid?.let { ParcelOwner(uuid, name) } - ?: name?.let { ParcelOwner(uuid, name) } + ?: name?.let { ParcelOwner(uuid, name) } } } @@ -141,7 +150,7 @@ class ParcelOwner(val uuid: UUID? = null, fun matches(player: Player, allowNameMatch: Boolean = false): Boolean { return uuid?.let { it == player.uniqueId } ?: false - || (allowNameMatch && name?.let { it == player.name } ?: false) + || (allowNameMatch && name?.let { it == player.name } ?: false) } val onlinePlayer: Player? get() = uuid?.let { Bukkit.getPlayer(uuid) } @@ -150,5 +159,5 @@ class ParcelOwner(val uuid: UUID? = null, @Suppress("DEPRECATION") val offlinePlayer get() = (uuid?.let { Bukkit.getOfflinePlayer(it) } ?: Bukkit.getOfflinePlayer(name)) - ?.takeIf { it.isOnline() || it.hasPlayedBefore() } + ?.takeIf { it.isOnline() || it.hasPlayedBefore() } } diff --git a/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt b/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt index 376b2ab..1003075 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt @@ -53,10 +53,10 @@ class Worlds(private val plugin: ParcelsPlugin) { try { world = ParcelWorld( - worldName, - worldOptions, - worldOptions.generator.getGenerator(this, worldName), - plugin.storage) + worldName, + worldOptions, + worldOptions.generator.getGenerator(this, worldName), + plugin.storage) } catch (ex: Exception) { ex.printStackTrace() diff --git a/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt b/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt index 9e9fc10..da8577c 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt @@ -1,8 +1,8 @@ package io.dico.parcels2 -import io.dico.dicore.command.CommandBuilder import io.dico.dicore.command.EOverridePolicy import io.dico.dicore.command.ICommandDispatcher +import io.dico.parcels2.command.getParcelCommands import io.dico.parcels2.storage.Storage import io.dico.parcels2.storage.yamlObjectMapper import io.dico.parcels2.util.tryCreate @@ -14,6 +14,7 @@ import java.io.File val logger = LoggerFactory.getLogger("ParcelsPlugin") private inline val plogger get() = logger +const val debugging = true class ParcelsPlugin : JavaPlugin() { lateinit var optionsFile: File @@ -73,15 +74,9 @@ class ParcelsPlugin : JavaPlugin() { } private fun registerCommands() { - //@formatting:off - cmdDispatcher = CommandBuilder() - .group("parcel", "plot", "plots", "p") - .registerCommands(PlotCommands(this)) - .parent() - .getDispatcher() - //@formatting:on - - cmdDispatcher!!.registerToCommandMap("parcels:", EOverridePolicy.FALLBACK_ONLY) + cmdDispatcher = getParcelCommands(this).apply { + registerToCommandMap("parcels:", EOverridePolicy.FALLBACK_ONLY) + } } }
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/WorldGenerator.kt b/src/main/kotlin/io/dico/parcels2/WorldGenerator.kt index 1123955..e9fdcc1 100644 --- a/src/main/kotlin/io/dico/parcels2/WorldGenerator.kt +++ b/src/main/kotlin/io/dico/parcels2/WorldGenerator.kt @@ -179,7 +179,7 @@ class DefaultParcelGenerator(val worlds: Worlds, val name: String, private val o } override fun getBottomCoord(parcel: Parcel): Vec2i = Vec2i(sectionSize * parcel.pos.x + pathOffset + o.offsetX, - sectionSize * parcel.pos.z + pathOffset + o.offsetZ) + sectionSize * parcel.pos.z + pathOffset + o.offsetZ) override fun getHomeLocation(parcel: Parcel): Location { val bottom = getBottomCoord(parcel) diff --git a/src/main/kotlin/io/dico/parcels2/command/CommandAsync.kt b/src/main/kotlin/io/dico/parcels2/command/CommandAsync.kt new file mode 100644 index 0000000..bf38e04 --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/command/CommandAsync.kt @@ -0,0 +1,83 @@ +package io.dico.parcels2.command + +import io.dico.dicore.command.CommandException +import io.dico.dicore.command.CommandResult +import io.dico.dicore.command.EMessageType +import io.dico.dicore.command.ExecutionContext +import io.dico.dicore.command.chat.IChatController +import io.dico.parcels2.ParcelsPlugin +import io.dico.parcels2.logger +import kotlinx.coroutines.experimental.* +import org.bukkit.command.CommandSender +import kotlin.coroutines.experimental.Continuation +import kotlin.coroutines.experimental.suspendCoroutine + +/* + * Interface to implicitly access plugin by creating extension functions for it + */ +interface HasPlugin { + val plugin: ParcelsPlugin +} + +class CommandAsyncScope { + + suspend fun <T> HasPlugin.awaitSynchronousTask(delay: Int = 0, task: () -> T): T { + return suspendCoroutine { cont: Continuation<T> -> + plugin.server.scheduler.runTaskLater(plugin, l@ { + val result = try { + task() + } catch (ex: CommandException) { + cont.resumeWithException(ex) + return@l + } catch (ex: Throwable) { + cont.context.cancel(ex) + return@l + } + cont.resume(result) + }, delay.toLong()) + } + } + + fun <T> HasPlugin.synchronousTask(delay: Int = 0, task: () -> T): Deferred<T> { + return async { awaitSynchronousTask(delay, task) } + } + +} + +fun <T : Any?> HasPlugin.delegateCommandAsync(context: ExecutionContext, + block: suspend CommandAsyncScope.() -> T) { + + val job: Deferred<Any?> = async(/*context = plugin.storage.asyncDispatcher, */start = CoroutineStart.ATOMIC) { + CommandAsyncScope().block() + } + + fun Job.invokeOnCompletionSynchronously(block: (Throwable?) -> Unit) = invokeOnCompletion { + plugin.server.scheduler.runTask(plugin) { block(it) } + } + + job.invokeOnCompletionSynchronously l@{ exception: Throwable? -> + exception?.let { + context.address.chatController.handleCoroutineException(context.sender, context, it) + return@l + } + + val result = job.getCompleted() + val message = when (result) { + is String -> result + is CommandResult -> result.message + else -> null + } + + context.address.chatController.sendMessage(context.sender, EMessageType.RESULT, message) + } + +} + +fun IChatController.handleCoroutineException(sender: CommandSender, context: ExecutionContext, exception: Throwable) { + if (exception is CancellationException) { + sendMessage(sender, EMessageType.EXCEPTION, "The command was cancelled unexpectedly (see console)") + logger.warn("An asynchronously dispatched command was cancelled unexpectedly", exception) + } else { + handleException(sender, context, exception) + } +} diff --git a/src/main/kotlin/io/dico/parcels2/command/CommandRequirement.kt b/src/main/kotlin/io/dico/parcels2/command/CommandRequirement.kt new file mode 100644 index 0000000..b4eea83 --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/command/CommandRequirement.kt @@ -0,0 +1,63 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package io.dico.parcels2.command + +import io.dico.dicore.command.CommandException +import io.dico.dicore.command.Validate +import io.dico.parcels2.Parcel +import io.dico.parcels2.ParcelWorld +import io.dico.parcels2.Worlds +import io.dico.parcels2.util.hasAdminManage +import io.dico.parcels2.util.uuid +import org.bukkit.entity.Player + +/* + * Scope types for extension lambdas + */ +sealed class BaseScope + +class WorldOnlyScope(val world: ParcelWorld) : BaseScope() +class ParcelScope(val world: ParcelWorld, val parcel: Parcel) : BaseScope() + +/* + * Interface to implicitly access worlds object by creating extension functions for it + */ +interface HasWorlds { + val worlds: Worlds +} + +/* + * Functions to be used by command implementations + */ +inline fun <T> HasWorlds.requireInWorld(player: Player, + admin: Boolean = false, + block: WorldOnlyScope.() -> T): T { + return WorldOnlyScope(worlds.getWorldRequired(player, admin = admin)).block() +} + +inline fun <T> HasWorlds.requireInParcel(player: Player, + admin: Boolean = false, + own: Boolean = false, + block: ParcelScope.() -> T): T { + val parcel = worlds.getParcelRequired(player, admin = admin, own = own) + return ParcelScope(parcel.world, parcel).block() +} + +/* + * Functions for checking + */ +fun Worlds.getWorldRequired(player: Player, admin: Boolean = false): ParcelWorld { + if (admin) Validate.isTrue(player.hasAdminManage, "You must have admin rights to use that command") + return getWorld(player.world) + ?: throw CommandException("You must be in a parcel world to use that command") +} + +fun Worlds.getParcelRequired(player: Player, admin: Boolean = false, own: Boolean = false): Parcel { + val parcel = getWorldRequired(player, admin = admin).parcelAt(player) + ?: throw CommandException("You must be in a parcel to use that command") + if (own) Validate.isTrue(parcel.isOwner(player.uuid) || player.hasAdminManage, + "You must own this parcel to use that command") + return parcel +} + + diff --git a/src/main/kotlin/io/dico/parcels2/PlotCommands.kt b/src/main/kotlin/io/dico/parcels2/command/DebugCommands.kt index 0001067..24a61c5 100644 --- a/src/main/kotlin/io/dico/parcels2/PlotCommands.kt +++ b/src/main/kotlin/io/dico/parcels2/command/DebugCommands.kt @@ -1,11 +1,12 @@ -package io.dico.parcels2 +package io.dico.parcels2.command + import io.dico.dicore.command.CommandException import io.dico.dicore.command.annotation.Cmd +import io.dico.parcels2.ParcelsPlugin import org.bukkit.Bukkit import org.bukkit.entity.Player - -class PlotCommands(val plugin: ParcelsPlugin) { +class DebugCommands(val plugin: ParcelsPlugin) { @Cmd("reloadoptions") fun reloadOptions() { diff --git a/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt b/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt new file mode 100644 index 0000000..1cca5be --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt @@ -0,0 +1,68 @@ +package io.dico.parcels2.command + +import io.dico.dicore.command.CommandBuilder +import io.dico.dicore.command.CommandException +import io.dico.dicore.command.ICommandDispatcher +import io.dico.dicore.command.parameter.ArgumentBuffer +import io.dico.dicore.command.parameter.IParameter +import io.dico.dicore.command.parameter.type.ParameterType +import io.dico.parcels2.Parcel +import io.dico.parcels2.ParcelsPlugin +import io.dico.parcels2.Worlds +import io.dico.parcels2.debugging +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + +@Suppress("UsePropertyAccessSyntax") +fun getParcelCommands(plugin: ParcelsPlugin): ICommandDispatcher { + //@formatter:off + return CommandBuilder() + .addParameterType(false, ParcelParameterType(plugin.worlds)) + .group("parcel", "plot", "plots", "p") + .registerCommands(ParcelCommands(plugin)) + .putDebugCommands(plugin) + .parent() + .getDispatcher() + //@formatter:on +} + +private fun CommandBuilder.putDebugCommands(plugin: ParcelsPlugin): CommandBuilder { + if (!debugging) return this + //@formatter:off + return group("debug", "d") + .registerCommands(DebugCommands(plugin)) + .parent() + //@formatter:on +} + +private val regex = Regex.fromLiteral("((.+)->)?([0-9]+):([0-9]+)") + +private class ParcelParameterType(val worlds: Worlds) : ParameterType<Parcel, Unit>(Parcel::class.java) { + + private fun invalidInput(parameter: IParameter<*>, message: String): Nothing { + throw CommandException("invalid input for ${parameter.name}: $message") + } + + override fun parse(parameter: IParameter<Parcel>, sender: CommandSender, buffer: ArgumentBuffer): Parcel { + val matchResult = regex.matchEntire(buffer.next()) + ?: invalidInput(parameter, "must match (w->)?a:b (/${regex.pattern}/)") + + val worldName = matchResult.groupValues[2] + .takeUnless { it.isEmpty() } + ?: (sender as? Player)?.world?.name + ?: invalidInput(parameter, "console cannot omit the world name") + + val world = worlds.getWorld(worldName) + ?: invalidInput(parameter, "$worldName is not a parcel world") + + val x = matchResult.groupValues[3].toIntOrNull() + ?: invalidInput(parameter, "couldn't parse int") + + val z = matchResult.groupValues[4].toIntOrNull() + ?: invalidInput(parameter, "couldn't parse int") + + return world.parcelByID(x, z) + ?: invalidInput(parameter, "parcel id is out of range") + } + +} diff --git a/src/main/kotlin/io/dico/parcels2/command/ParcelCommands.kt b/src/main/kotlin/io/dico/parcels2/command/ParcelCommands.kt new file mode 100644 index 0000000..016ad22 --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/command/ParcelCommands.kt @@ -0,0 +1,51 @@ +package io.dico.parcels2.command + +import io.dico.dicore.command.CommandException +import io.dico.dicore.command.ExecutionContext +import io.dico.dicore.command.annotation.Cmd +import io.dico.dicore.command.annotation.Desc +import io.dico.parcels2.ParcelOwner +import io.dico.parcels2.ParcelsPlugin +import io.dico.parcels2.util.parcelLimit +import io.dico.parcels2.util.uuid +import org.bukkit.entity.Player + +class ParcelCommands(override val plugin: ParcelsPlugin) : HasWorlds, HasPlugin { + override val worlds = plugin.worlds + + private fun error(message: String): Nothing { + throw CommandException(message) + } + + @Cmd("auto") + @Desc("Finds the unclaimed parcel nearest to origin,", + "and gives it to you", + shortVersion = "sets you up with a fresh, unclaimed parcel") + fun cmdAuto(player: Player, context: ExecutionContext) = requireInWorld(player) { + delegateCommandAsync(context) { + val numOwnedParcels = plugin.storage.getNumParcels(ParcelOwner(uuid = player.uuid)).await() + + awaitSynchronousTask { + val limit = player.parcelLimit + + if (numOwnedParcels >= limit) { + error("You have enough plots for now") + } + + val parcel = world.nextEmptyParcel() + ?: error("This world is full, please ask an admin to upsize it") + parcel.owner = ParcelOwner(uuid = player.uuid) + player.teleport(world.generator.getHomeLocation(parcel)) + "Enjoy your new parcel!" + } + } + } + + @Cmd("info", aliases = ["i"]) + @Desc("Displays general information", + "about the parcel you're on", + shortVersion = "displays information about this parcel") + fun cmdInfo(player: Player) = requireInParcel(player) { parcel.infoString } + + +}
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/math/NumberExtensions.kt b/src/main/kotlin/io/dico/parcels2/math/NumberExtensions.kt index faf2939..985fd2e 100644 --- a/src/main/kotlin/io/dico/parcels2/math/NumberExtensions.kt +++ b/src/main/kotlin/io/dico/parcels2/math/NumberExtensions.kt @@ -3,7 +3,7 @@ package io.dico.parcels2.math fun Double.floor(): Int { val down = toInt() if (down.toDouble() != this && (java.lang.Double.doubleToRawLongBits(this).ushr(63).toInt()) == 1) { - return down-1 + return down - 1 } return down } diff --git a/src/main/kotlin/io/dico/parcels2/math/Vec2i.kt b/src/main/kotlin/io/dico/parcels2/math/Vec2i.kt index 70ca246..bff0326 100644 --- a/src/main/kotlin/io/dico/parcels2/math/Vec2i.kt +++ b/src/main/kotlin/io/dico/parcels2/math/Vec2i.kt @@ -1,7 +1,7 @@ package io.dico.parcels2.math data class Vec2i( - val x: Int, - val z: Int + val x: Int, + val z: Int ) diff --git a/src/main/kotlin/io/dico/parcels2/storage/Backing.kt b/src/main/kotlin/io/dico/parcels2/storage/Backing.kt index 0f8829d..252c602 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/Backing.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/Backing.kt @@ -10,6 +10,8 @@ interface Backing { val name: String + val isConnected: Boolean + suspend fun init() suspend fun shutdown() @@ -25,6 +27,8 @@ interface Backing { suspend fun getOwnedParcels(user: ParcelOwner): List<SerializableParcel> + suspend fun getNumParcels(user: ParcelOwner): Int = getOwnedParcels(user).size + suspend fun setParcelData(parcelFor: Parcel, data: ParcelData?) diff --git a/src/main/kotlin/io/dico/parcels2/storage/ExposedBacking.kt b/src/main/kotlin/io/dico/parcels2/storage/ExposedBacking.kt index e79c7e0..47efc87 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/ExposedBacking.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/ExposedBacking.kt @@ -3,6 +3,7 @@ package io.dico.parcels2.storage import com.zaxxer.hikari.HikariDataSource import io.dico.parcels2.* import io.dico.parcels2.math.Vec2i +import io.dico.parcels2.util.synchronized import io.dico.parcels2.util.toByteArray import io.dico.parcels2.util.toUUID import kotlinx.coroutines.experimental.channels.ProducerScope @@ -16,7 +17,7 @@ object WorldsT : Table("worlds") { val id = integer("id").autoIncrement().primaryKey() val name = varchar("name", 50) val uid = binary("uid", 16) - .also { uniqueIndex("index_uid", it) } + .also { uniqueIndex("index_uid", it) } } object ParcelsT : Table("parcels") { @@ -24,31 +25,31 @@ object ParcelsT : Table("parcels") { val px = integer("px") val pz = integer("pz") val world_id = integer("id") - .also { uniqueIndex("index_location", it, px, pz) } - .references(WorldsT.id) + .also { uniqueIndex("index_location", it, px, pz) } + .references(WorldsT.id) val owner_uuid = binary("owner_uuid", 16).nullable() val owner_name = varchar("owner_name", 16).nullable() } object AddedLocalT : Table("parcels_added_local") { val parcel_id = integer("parcel_id") - .references(ParcelsT.id, ReferenceOption.CASCADE) + .references(ParcelsT.id, ReferenceOption.CASCADE) val player_uuid = binary("player_uuid", 16) - .also { uniqueIndex("index_pair", parcel_id, it) } + .also { uniqueIndex("index_pair", parcel_id, it) } val allowed_flag = bool("allowed_flag") } object AddedGlobalT : Table("parcels_added_global") { val owner_uuid = binary("owner_uuid", 16) val player_uuid = binary("player_uuid", 16) - .also { uniqueIndex("index_pair", owner_uuid, it) } + .also { uniqueIndex("index_pair", owner_uuid, it) } val allowed_flag = bool("allowed_flag") } object ParcelOptionsT : Table("parcel_options") { val parcel_id = integer("parcel_id") - .also { uniqueIndex("index_parcel_id", it) } - .references(ParcelsT.id, ReferenceOption.CASCADE) + .also { uniqueIndex("index_parcel_id", it) } + .references(ParcelsT.id, ReferenceOption.CASCADE) val interact_inventory = bool("interact_inventory").default(false) val interact_inputs = bool("interact_inputs").default(false) } @@ -58,18 +59,29 @@ private class ExposedDatabaseException(message: String? = null) : Exception(mess @Suppress("NOTHING_TO_INLINE") class ExposedBacking(val dataSource: DataSource) : Backing { override val name get() = "Exposed" - lateinit var database: Database + private var database: Database? = null + private var isShutdown: Boolean = false + + override val isConnected get() = database != null override suspend fun init() { - database = Database.connect(dataSource) - transaction(database) { - create(ParcelsT, AddedLocalT) + synchronized { + if (isShutdown) throw IllegalStateException() + database = Database.connect(dataSource) + transaction(database) { + create(WorldsT, ParcelsT, AddedLocalT, ParcelOptionsT) + } } } override suspend fun shutdown() { - if (dataSource is HikariDataSource) { - dataSource.close() + synchronized { + if (isShutdown) throw IllegalStateException() + if (dataSource is HikariDataSource) { + dataSource.close() + } + database = null + isShutdown = true } } @@ -86,13 +98,13 @@ class ExposedBacking(val dataSource: DataSource) : Backing { private inline fun Transaction.getOrInitWorldId(worldUid: UUID, worldName: String): Int { val binaryUid = worldUid.toByteArray()!! return getWorldId(binaryUid) - ?: WorldsT.insertIgnore { it[uid] = binaryUid; it[name] = worldName }.get(WorldsT.id) - ?: throw ExposedDatabaseException("This should not happen - failed to insert world named $worldName and get its id") + ?: WorldsT.insertIgnore { it[uid] = binaryUid; it[name] = worldName }.get(WorldsT.id) + ?: throw ExposedDatabaseException("This should not happen - failed to insert world named $worldName and get its id") } private inline fun Transaction.getParcelId(worldId: Int, parcelX: Int, parcelZ: Int): Int? { return ParcelsT.select { (ParcelsT.world_id eq worldId) and (ParcelsT.px eq parcelX) and (ParcelsT.pz eq parcelZ) } - .firstOrNull()?.let { it[ParcelsT.id] } + .firstOrNull()?.let { it[ParcelsT.id] } } private inline fun Transaction.getParcelId(worldUid: UUID, parcelX: Int, parcelZ: Int): Int? { @@ -102,8 +114,8 @@ class ExposedBacking(val dataSource: DataSource) : Backing { private inline fun Transaction.getOrInitParcelId(worldUid: UUID, worldName: String, parcelX: Int, parcelZ: Int): Int { val worldId = getOrInitWorldId(worldUid, worldName) return getParcelId(worldId, parcelX, parcelZ) - ?: ParcelsT.insertIgnore { it[world_id] = worldId; it[px] = parcelX; it[pz] = parcelZ }.get(ParcelsT.id) - ?: throw ExposedDatabaseException("This should not happen - failed to insert parcel at $worldName($parcelX, $parcelZ)") + ?: ParcelsT.insertIgnore { it[world_id] = worldId; it[px] = parcelX; it[pz] = parcelZ }.get(ParcelsT.id) + ?: throw ExposedDatabaseException("This should not happen - failed to insert parcel at $worldName($parcelX, $parcelZ)") } private inline fun Transaction.getParcelRow(id: Int): ResultRow? { @@ -144,8 +156,8 @@ class ExposedBacking(val dataSource: DataSource) : Backing { ParcelDataHolder().apply { owner = ParcelOwner.create( - uuid = row[ParcelsT.owner_uuid]?.toUUID(), - name = row[ParcelsT.owner_name] + uuid = row[ParcelsT.owner_uuid]?.toUUID(), + name = row[ParcelsT.owner_name] ) val parcelId = row[ParcelsT.id] @@ -176,16 +188,16 @@ class ExposedBacking(val dataSource: DataSource) : Backing { } ParcelsT.select(where) - .map { parcelRow -> - val worldId = parcelRow[ParcelsT.world_id] - val worldRow = WorldsT.select({ WorldsT.id eq worldId }).firstOrNull() - ?: return@map null + .map { parcelRow -> + val worldId = parcelRow[ParcelsT.world_id] + val worldRow = WorldsT.select({ WorldsT.id eq worldId }).firstOrNull() + ?: return@map null - val world = SerializableWorld(worldRow[WorldsT.name], worldRow[WorldsT.uid].toUUID()) - SerializableParcel(world, Vec2i(parcelRow[ParcelsT.px], parcelRow[ParcelsT.pz])) - } - .filterNotNull() - .toList() + val world = SerializableWorld(worldRow[WorldsT.name], worldRow[WorldsT.uid].toUUID()) + SerializableParcel(world, Vec2i(parcelRow[ParcelsT.px], parcelRow[ParcelsT.pz])) + } + .filterNotNull() + .toList() } diff --git a/src/main/kotlin/io/dico/parcels2/storage/ExposedExtensions.kt b/src/main/kotlin/io/dico/parcels2/storage/ExposedExtensions.kt index f429d7e..d4aee11 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/ExposedExtensions.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/ExposedExtensions.kt @@ -10,15 +10,15 @@ import org.jetbrains.exposed.sql.transactions.TransactionManager * insertOrUpdate from https://github.com/JetBrains/Exposed/issues/167#issuecomment-403837917 */ inline fun <T : Table> T.insertOrUpdate(vararg onDuplicateUpdateKeys: Column<*>, body: T.(InsertStatement<Number>) -> Unit) = - InsertOrUpdate<Number>(onDuplicateUpdateKeys, this).apply { - body(this) - execute(TransactionManager.current()) - } + InsertOrUpdate<Number>(onDuplicateUpdateKeys, this).apply { + body(this) + execute(TransactionManager.current()) + } class InsertOrUpdate<Key : Any>( - private val onDuplicateUpdateKeys: Array<out Column<*>>, - table: Table, - isIgnore: Boolean = false + private val onDuplicateUpdateKeys: Array<out Column<*>>, + table: Table, + isIgnore: Boolean = false ) : InsertStatement<Key>(table, isIgnore) { override fun prepareSQL(transaction: Transaction): String { val onUpdateSQL = if (onDuplicateUpdateKeys.isNotEmpty()) { diff --git a/src/main/kotlin/io/dico/parcels2/storage/Hikari.kt b/src/main/kotlin/io/dico/parcels2/storage/Hikari.kt index 108ad92..ae16c83 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/Hikari.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/Hikari.kt @@ -31,7 +31,7 @@ fun getHikariDataSource(dialectName: String, dataSourceProperties.remove("serverName") dataSourceProperties.remove("port") dataSourceProperties.remove("databaseName") - addDataSourceProperty("url", "jdbc:h2:tcp://$address/~/${dco.database}") + addDataSourceProperty("url", "jdbc:h2:${if (address.isBlank()) "" else "tcp://$address/"}~/${dco.database}") } else { // doesn't exist on the MariaDB driver addDataSourceProperty("cachePrepStmts", "true") diff --git a/src/main/kotlin/io/dico/parcels2/storage/Jackson.kt b/src/main/kotlin/io/dico/parcels2/storage/Jackson.kt index 08ca810..97225b8 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/Jackson.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/Jackson.kt @@ -8,10 +8,11 @@ import com.fasterxml.jackson.databind.ser.BeanSerializerModifier import com.fasterxml.jackson.databind.ser.std.StdSerializer import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import com.fasterxml.jackson.module.kotlin.KotlinModule -import io.dico.parcels2.* +import io.dico.parcels2.GeneratorFactory +import io.dico.parcels2.GeneratorOptions +import io.dico.parcels2.StorageOptions import org.bukkit.Bukkit import org.bukkit.block.data.BlockData -import kotlin.reflect.KClass import kotlin.reflect.full.isSuperclassOf val yamlObjectMapper = ObjectMapper(YAMLFactory()).apply { diff --git a/src/main/kotlin/io/dico/parcels2/storage/Storage.kt b/src/main/kotlin/io/dico/parcels2/storage/Storage.kt index 67c4b05..7af6bc5 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/Storage.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/Storage.kt @@ -14,10 +14,11 @@ interface Storage { val name: String val syncDispatcher: CoroutineDispatcher val asyncDispatcher: CoroutineDispatcher + val isConnected: Boolean - fun init(): Deferred<Unit> + fun init(): Job - fun shutdown(): Deferred<Unit> + fun shutdown(): Job fun readParcelData(parcelFor: Parcel): Deferred<ParcelData?> @@ -26,16 +27,18 @@ interface Storage { fun getOwnedParcels(user: ParcelOwner): Deferred<List<SerializableParcel>> + fun getNumParcels(user: ParcelOwner): Deferred<Int> - fun setParcelData(parcelFor: Parcel, data: ParcelData?): Deferred<Unit> - fun setParcelOwner(parcelFor: Parcel, owner: ParcelOwner?): Deferred<Unit> + fun setParcelData(parcelFor: Parcel, data: ParcelData?): Job - fun setParcelPlayerState(parcelFor: Parcel, player: UUID, state: Boolean?): Deferred<Unit> + fun setParcelOwner(parcelFor: Parcel, owner: ParcelOwner?): Job - fun setParcelAllowsInteractInventory(parcel: Parcel, value: Boolean): Deferred<Unit> + fun setParcelPlayerState(parcelFor: Parcel, player: UUID, state: Boolean?): Job - fun setParcelAllowsInteractInputs(parcel: Parcel, value: Boolean): Deferred<Unit> + fun setParcelAllowsInteractInventory(parcel: Parcel, value: Boolean): Job + + fun setParcelAllowsInteractInputs(parcel: Parcel, value: Boolean): Job } @@ -44,15 +47,21 @@ class StorageWithCoroutineBacking internal constructor(val backing: Backing) : S override val syncDispatcher = Executor { it.run() }.asCoroutineDispatcher() val poolSize: Int get() = 4 override val asyncDispatcher = Executors.newFixedThreadPool(poolSize) { Thread(it, "Parcels2_StorageThread") }.asCoroutineDispatcher() + override val isConnected get() = backing.isConnected @Suppress("NOTHING_TO_INLINE") private inline fun <T> defer(noinline block: suspend CoroutineScope.() -> T): Deferred<T> { return async(context = asyncDispatcher, start = CoroutineStart.ATOMIC, block = block) } - override fun init() = defer { backing.init() } + @Suppress("NOTHING_TO_INLINE") + private inline fun job(noinline block: suspend CoroutineScope.() -> Unit): Job { + return launch(context = asyncDispatcher, start = CoroutineStart.ATOMIC, block = block) + } + + override fun init() = job { backing.init() } - override fun shutdown() = defer { backing.shutdown() } + override fun shutdown() = job { backing.shutdown() } override fun readParcelData(parcelFor: Parcel) = defer { backing.readParcelData(parcelFor) } @@ -61,16 +70,17 @@ class StorageWithCoroutineBacking internal constructor(val backing: Backing) : S with(backing) { produceParcelData(parcelsFor) } } + override fun getOwnedParcels(user: ParcelOwner) = defer { backing.getOwnedParcels(user) } - override fun setParcelData(parcelFor: Parcel, data: ParcelData?) = defer { backing.setParcelData(parcelFor, data) } + override fun getNumParcels(user: ParcelOwner) = defer { backing.getNumParcels(user) } - override fun getOwnedParcels(user: ParcelOwner) = defer { backing.getOwnedParcels(user) } + override fun setParcelData(parcelFor: Parcel, data: ParcelData?) = job { backing.setParcelData(parcelFor, data) } - override fun setParcelOwner(parcelFor: Parcel, owner: ParcelOwner?) = defer { backing.setParcelOwner(parcelFor, owner) } + override fun setParcelOwner(parcelFor: Parcel, owner: ParcelOwner?) = job { backing.setParcelOwner(parcelFor, owner) } - override fun setParcelPlayerState(parcelFor: Parcel, player: UUID, state: Boolean?) = defer { backing.setParcelPlayerState(parcelFor, player, state) } + override fun setParcelPlayerState(parcelFor: Parcel, player: UUID, state: Boolean?) = job { backing.setParcelPlayerState(parcelFor, player, state) } - override fun setParcelAllowsInteractInventory(parcel: Parcel, value: Boolean) = defer { backing.setParcelAllowsInteractInventory(parcel, value) } + override fun setParcelAllowsInteractInventory(parcel: Parcel, value: Boolean) = job { backing.setParcelAllowsInteractInventory(parcel, value) } - override fun setParcelAllowsInteractInputs(parcel: Parcel, value: Boolean) = defer { backing.setParcelAllowsInteractInputs(parcel, value) } + override fun setParcelAllowsInteractInputs(parcel: Parcel, value: Boolean) = job { backing.setParcelAllowsInteractInputs(parcel, value) } } diff --git a/src/main/kotlin/io/dico/parcels2/storage/StorageFactory.kt b/src/main/kotlin/io/dico/parcels2/storage/StorageFactory.kt index c1e158a..b0140f1 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/StorageFactory.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/StorageFactory.kt @@ -1,7 +1,6 @@ package io.dico.parcels2.storage import io.dico.parcels2.DataConnectionOptions -import net.minecraft.server.v1_13_R1.WorldType.types import kotlin.reflect.KClass interface StorageFactory { @@ -29,8 +28,8 @@ class ConnectionStorageFactory : StorageFactory { override val optionsClass = DataConnectionOptions::class private val types: Map<String, String> = mutableMapOf( - "mysql" to "com.mysql.jdbc.jdbc2.optional.MysqlDataSource", - "h2" to "org.h2.jdbcx.JdbcDataSource" + "mysql" to "com.mysql.jdbc.jdbc2.optional.MysqlDataSource", + "h2" to "org.h2.jdbcx.JdbcDataSource" ) fun register(companion: StorageFactory.StorageFactories) { diff --git a/src/main/kotlin/io/dico/parcels2/util/MiscExtensions.kt b/src/main/kotlin/io/dico/parcels2/util/MiscExtensions.kt index f70014d..952595b 100644 --- a/src/main/kotlin/io/dico/parcels2/util/MiscExtensions.kt +++ b/src/main/kotlin/io/dico/parcels2/util/MiscExtensions.kt @@ -1,9 +1,7 @@ package io.dico.parcels2.util import io.dico.parcels2.logger -import org.slf4j.Logger import java.io.File -import java.io.PrintWriter fun File.tryCreate(): Boolean { val parent = parentFile @@ -13,3 +11,7 @@ fun File.tryCreate(): Boolean { } return true } + +inline fun <R> Any.synchronized(block: () -> R): R { + return synchronized(this, block) +} diff --git a/src/main/kotlin/io/dico/parcels2/util/PlayerExtensions.kt b/src/main/kotlin/io/dico/parcels2/util/PlayerExtensions.kt index 3ef3c89..3424655 100644 --- a/src/main/kotlin/io/dico/parcels2/util/PlayerExtensions.kt +++ b/src/main/kotlin/io/dico/parcels2/util/PlayerExtensions.kt @@ -6,18 +6,19 @@ import io.dico.parcels2.logger import org.bukkit.entity.Player import org.bukkit.plugin.java.JavaPlugin -inline val Player.hasBanBypass get() = hasPermission("plots.admin.bypass.ban") -inline val Player.hasBuildAnywhere get() = hasPermission("plots.admin.bypass.build") -inline val Player.hasGamemodeBypass get() = hasPermission("plots.admin.bypass.gamemode") -inline val Player.hasAdminManage get() = hasPermission("plots.admin.manage") -inline val Player.hasPlotHomeOthers get() = hasPermission("plots.command.home.others") -inline val Player.hasRandomSpecific get() = hasPermission("plots.command.random.specific") -val Player.plotLimit: Int +inline val Player.uuid get() = uniqueId +inline val Player.hasBanBypass get() = hasPermission("parcels.admin.bypass.ban") +inline val Player.hasBuildAnywhere get() = hasPermission("parcels.admin.bypass.build") +inline val Player.hasGamemodeBypass get() = hasPermission("parcels.admin.bypass.gamemode") +inline val Player.hasAdminManage get() = hasPermission("parcels.admin.manage") +inline val Player.hasParcelHomeOthers get() = hasPermission("parcels.command.home.others") +inline val Player.hasRandomSpecific get() = hasPermission("parcels.command.random.specific") +val Player.parcelLimit: Int get() { for (info in effectivePermissions) { val perm = info.permission - if (perm.startsWith("plots.limit.")) { - val limitString = perm.substring("plots.limit.".length) + if (perm.startsWith("parcels.limit.")) { + val limitString = perm.substring("parcels.limit.".length) if (limitString == "*") { return Int.MAX_VALUE } @@ -32,7 +33,7 @@ val Player.plotLimit: Int private const val DEFAULT_LIMIT = 1 private val prefix = Formatting.translateChars('&', "&4[&c${JavaPlugin.getPlugin(ParcelsPlugin::class.java).name}&4] &a") -fun Player.sendPlotMessage(except: Boolean = false, nopermit: Boolean = false, message: String) { +fun Player.sendParcelMessage(except: Boolean = false, nopermit: Boolean = false, message: String) { if (except) { sendMessage(prefix + Formatting.YELLOW + Formatting.translateChars('&', message)) } else if (nopermit) { diff --git a/src/main/kotlin/io/dico/parcels2/util/UUIDUtil.kt b/src/main/kotlin/io/dico/parcels2/util/UUIDUtil.kt index 7cd298f..6cdbe3a 100644 --- a/src/main/kotlin/io/dico/parcels2/util/UUIDUtil.kt +++ b/src/main/kotlin/io/dico/parcels2/util/UUIDUtil.kt @@ -8,8 +8,8 @@ import java.util.* @Suppress("UsePropertyAccessSyntax") fun getPlayerName(uuid: UUID?, ifUnknown: String? = null): String { return uuid?.let { Bukkit.getOfflinePlayer(uuid)?.takeIf { it.isOnline() || it.hasPlayedBefore() }?.name } - ?: ifUnknown - ?: ":unknown_name:" + ?: ifUnknown + ?: ":unknown_name:" } @Contract("null -> null; !null -> !null", pure = true) |