From 703e02d6b23165003835692b0213a20f0a627e9d Mon Sep 17 00:00:00 2001 From: Dico Date: Fri, 3 Aug 2018 03:25:52 +0100 Subject: Clean up code for polymorphic options serialization, fix logger configuration --- build.gradle.kts | 3 +- src/main/kotlin/io/dico/parcels2/Options.kt | 104 ------------------ .../kotlin/io/dico/parcels2/ParcelGenerator.kt | 21 +--- src/main/kotlin/io/dico/parcels2/ParcelWorld.kt | 3 +- src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt | 9 +- .../parcels2/defaultimpl/DefaultParcelGenerator.kt | 25 +---- .../parcels2/defaultimpl/ParcelProviderImpl.kt | 2 +- .../dico/parcels2/defaultimpl/ParcelWorldImpl.kt | 5 +- .../io/dico/parcels2/options/GeneratorOptions.kt | 36 ++++++ .../io/dico/parcels2/options/MigrationOptions.kt | 17 +++ .../kotlin/io/dico/parcels2/options/Options.kt | 51 +++++++++ .../io/dico/parcels2/options/OptionsMapper.kt | 66 +++++++++++ .../io/dico/parcels2/options/PolymorphicOptions.kt | 89 +++++++++++++++ .../io/dico/parcels2/options/StorageOptions.kt | 45 ++++++++ src/main/kotlin/io/dico/parcels2/storage/Hikari.kt | 2 +- .../kotlin/io/dico/parcels2/storage/Jackson.kt | 122 --------------------- .../io/dico/parcels2/storage/StorageFactory.kt | 43 -------- .../parcels2/storage/migration/MigrationFactory.kt | 5 - src/main/resources/logback.xml | 7 +- 19 files changed, 324 insertions(+), 331 deletions(-) delete mode 100644 src/main/kotlin/io/dico/parcels2/Options.kt create mode 100644 src/main/kotlin/io/dico/parcels2/options/GeneratorOptions.kt create mode 100644 src/main/kotlin/io/dico/parcels2/options/MigrationOptions.kt create mode 100644 src/main/kotlin/io/dico/parcels2/options/Options.kt create mode 100644 src/main/kotlin/io/dico/parcels2/options/OptionsMapper.kt create mode 100644 src/main/kotlin/io/dico/parcels2/options/PolymorphicOptions.kt create mode 100644 src/main/kotlin/io/dico/parcels2/options/StorageOptions.kt delete mode 100644 src/main/kotlin/io/dico/parcels2/storage/Jackson.kt delete mode 100644 src/main/kotlin/io/dico/parcels2/storage/StorageFactory.kt delete mode 100644 src/main/kotlin/io/dico/parcels2/storage/migration/MigrationFactory.kt diff --git a/build.gradle.kts b/build.gradle.kts index 11975a1..fa4eb09 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -72,7 +72,8 @@ dependencies { compile("org.jetbrains.exposed:exposed:0.10.3") { isTransitive = false } compile("joda-time:joda-time:2.10") compile("com.zaxxer:HikariCP:3.2.0") - compile("ch.qos.logback:logback-classic:1.2.3") + compile("ch.qos.logback:logback-classic:1.2.3") { isTransitive = false } + compile("ch.qos.logback:logback-core:1.2.3") { isTransitive = false } val jacksonVersion = "2.9.6" compile("com.fasterxml.jackson.core:jackson-core:$jacksonVersion") diff --git a/src/main/kotlin/io/dico/parcels2/Options.kt b/src/main/kotlin/io/dico/parcels2/Options.kt deleted file mode 100644 index aefa9e1..0000000 --- a/src/main/kotlin/io/dico/parcels2/Options.kt +++ /dev/null @@ -1,104 +0,0 @@ -package io.dico.parcels2 - -import com.fasterxml.jackson.annotation.JsonIgnore -import io.dico.parcels2.blockvisitor.TickWorktimeOptions -import io.dico.parcels2.defaultimpl.DefaultGeneratorOptions -import io.dico.parcels2.storage.Storage -import io.dico.parcels2.storage.StorageFactory -import io.dico.parcels2.storage.yamlObjectMapper -import org.bukkit.GameMode -import org.bukkit.Material -import java.io.Reader -import java.io.Writer -import java.util.EnumSet - -class Options { - var worlds: Map = hashMapOf() - private set - var storage: StorageOptions = StorageOptions("postgresql", DataConnectionOptions()) - var tickWorktime: TickWorktimeOptions = TickWorktimeOptions(20, 1) - - fun addWorld(name: String, - generatorOptions: GeneratorOptions? = null, - worldOptions: WorldOptions? = null) { - val optionsHolder = WorldOptionsHolder( - generatorOptions ?: DefaultGeneratorOptions(), - worldOptions ?: WorldOptions() - ) - - (worlds as MutableMap).put(name, optionsHolder) - } - - fun writeTo(writer: Writer) = yamlObjectMapper.writeValue(writer, this) - - fun mergeFrom(reader: Reader) = yamlObjectMapper.readerForUpdating(this).readValue(reader) - - override fun toString(): String = yamlObjectMapper.writeValueAsString(this) - -} - -class WorldOptionsHolder(var generator: GeneratorOptions = DefaultGeneratorOptions(), - var runtime: WorldOptions = WorldOptions()) - -data class WorldOptions(var gameMode: GameMode? = GameMode.CREATIVE, - var dayTime: Boolean = true, - var noWeather: Boolean = true, - var preventWeatherBlockChanges: Boolean = true, - var preventBlockSpread: Boolean = true, // TODO - var dropEntityItems: Boolean = true, - var doTileDrops: Boolean = false, - var disableExplosions: Boolean = true, - var blockPortalCreation: Boolean = true, - var blockMobSpawning: Boolean = true, - var blockedItems: Set = EnumSet.of(Material.FLINT_AND_STEEL, Material.SNOWBALL), - var axisLimit: Int = 10) { - -} - -abstract class GeneratorOptions { - - abstract fun generatorFactory(): GeneratorFactory - - fun newGenerator(worldName: String) = generatorFactory().newParcelGenerator(worldName, this) - -} - -class StorageOptions(val dialect: String, - val options: Any) { - - @get:JsonIgnore - val factory = StorageFactory.getFactory(dialect) - ?: throw IllegalArgumentException("Invalid storage dialect: $dialect") - - fun newStorageInstance(): Storage = factory.newStorageInstance(dialect, options) - -} - -data class DataConnectionOptions(val address: String = "localhost", - val database: String = "parcels", - val username: String = "root", - val password: String = "", - val poolSize: Int = 4) { - - fun splitAddressAndPort(defaultPort: Int = 3306): Pair? { - val idx = address.indexOf(":").takeUnless { it == -1 } ?: return Pair(address, defaultPort) - - val addressName = address.substring(0, idx).takeUnless { it.isBlank() } ?: return null.also { - logger.error("(Invalidly) blank address in data storage options") - } - - val port = address.substring(idx + 1).toIntOrNull() ?: return null.also { - logger.error("Invalid port number in data storage options: $it, using $defaultPort as default") - } - - return Pair(addressName, port) - } - -} - -data class DataFileOptions(val location: String = "/flatfile-storage/") - -class MigrationOptions() { - - -} diff --git a/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt b/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt index ff28537..6f504d0 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt @@ -4,6 +4,7 @@ import io.dico.parcels2.blockvisitor.RegionTraversal import io.dico.parcels2.blockvisitor.Worker import io.dico.parcels2.blockvisitor.WorktimeLimiter import io.dico.parcels2.defaultimpl.DefaultParcelGenerator +import io.dico.parcels2.options.GeneratorOptions import io.dico.parcels2.util.Vec2i import org.bukkit.Chunk import org.bukkit.Location @@ -17,26 +18,6 @@ import java.util.HashMap import java.util.Random import kotlin.reflect.KClass -object GeneratorFactories { - private val map: MutableMap = HashMap() - - fun registerFactory(generator: GeneratorFactory): Boolean = map.putIfAbsent(generator.name, generator) == null - - fun getFactory(name: String): GeneratorFactory? = map.get(name) - - init { - registerFactory(DefaultParcelGenerator.Factory) - } -} - -interface GeneratorFactory { - val name: String - - val optionsClass: KClass - - fun newParcelGenerator(worldName: String, options: GeneratorOptions): ParcelGenerator -} - abstract class ParcelGenerator : ChunkGenerator() { abstract val world: World diff --git a/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt b/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt index 16f108f..0dcdfbd 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt @@ -1,5 +1,6 @@ package io.dico.parcels2 +import io.dico.parcels2.options.RuntimeWorldOptions import io.dico.parcels2.storage.Storage import io.dico.parcels2.util.Vec2i import io.dico.parcels2.util.floor @@ -76,7 +77,7 @@ interface ParcelWorld : ParcelLocator, ParcelContainer, ParcelBlockManager { val id: ParcelWorldId val name: String val uid: UUID? - val options: WorldOptions + val options: RuntimeWorldOptions val generator: ParcelGenerator val storage: Storage val container: ParcelContainer diff --git a/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt b/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt index 6d08d28..ffa11c7 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt @@ -10,8 +10,9 @@ import io.dico.parcels2.defaultimpl.GlobalAddedDataManagerImpl import io.dico.parcels2.defaultimpl.ParcelProviderImpl import io.dico.parcels2.listener.ParcelEntityTracker import io.dico.parcels2.listener.ParcelListeners +import io.dico.parcels2.options.Options +import io.dico.parcels2.options.optionsMapper import io.dico.parcels2.storage.Storage -import io.dico.parcels2.storage.yamlObjectMapper import io.dico.parcels2.util.FunctionHelper import io.dico.parcels2.util.tryCreate import org.bukkit.Bukkit @@ -60,7 +61,7 @@ class ParcelsPlugin : JavaPlugin() { if (!loadOptions()) return false try { - storage = options.storage.newStorageInstance() + storage = options.storage.newInstance() storage.init() } catch (ex: Exception) { plogger.error("Failed to connect to database", ex) @@ -83,11 +84,11 @@ class ParcelsPlugin : JavaPlugin() { fun loadOptions(): Boolean { when { - optionsFile.exists() -> yamlObjectMapper.readerForUpdating(options).readValue(optionsFile) + optionsFile.exists() -> optionsMapper.readerForUpdating(options).readValue(optionsFile) optionsFile.tryCreate() -> { options.addWorld("parcels") try { - yamlObjectMapper.writeValue(optionsFile, options) + optionsMapper.writeValue(optionsFile, options) } catch (ex: Throwable) { optionsFile.delete() throw ex diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt index 90eb631..7dbcb78 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt @@ -4,6 +4,7 @@ import io.dico.parcels2.* import io.dico.parcels2.blockvisitor.RegionTraversal import io.dico.parcels2.blockvisitor.Worker import io.dico.parcels2.blockvisitor.WorktimeLimiter +import io.dico.parcels2.options.DefaultGeneratorOptions import io.dico.parcels2.util.* import org.bukkit.* import org.bukkit.block.Biome @@ -17,21 +18,6 @@ import java.util.Random private val airType = Bukkit.createBlockData(Material.AIR) -data class DefaultGeneratorOptions(var defaultBiome: Biome = Biome.JUNGLE, - var wallType: BlockData = Bukkit.createBlockData(Material.STONE_SLAB), - var floorType: BlockData = Bukkit.createBlockData(Material.QUARTZ_BLOCK), - var fillType: BlockData = Bukkit.createBlockData(Material.QUARTZ_BLOCK), - var pathMainType: BlockData = Bukkit.createBlockData(Material.SANDSTONE), - var pathAltType: BlockData = Bukkit.createBlockData(Material.REDSTONE_BLOCK), - var parcelSize: Int = 101, - var pathSize: Int = 9, - var floorHeight: Int = 64, - var offsetX: Int = 0, - var offsetZ: Int = 0) : GeneratorOptions() { - - override fun generatorFactory(): GeneratorFactory = DefaultParcelGenerator.Factory -} - class DefaultParcelGenerator(val name: String, private val o: DefaultGeneratorOptions) : ParcelGenerator() { private var _world: World? = null override val world: World @@ -44,15 +30,6 @@ class DefaultParcelGenerator(val name: String, private val o: DefaultGeneratorOp } private var maxHeight = 0 - - companion object Factory : GeneratorFactory { - override val name get() = "default" - override val optionsClass get() = DefaultGeneratorOptions::class - override fun newParcelGenerator(worldName: String, options: GeneratorOptions): ParcelGenerator { - return DefaultParcelGenerator(worldName, options as DefaultGeneratorOptions) - } - } - val sectionSize = o.parcelSize + o.pathSize val pathOffset = (if (o.pathSize % 2 == 0) o.pathSize + 2 else o.pathSize + 1) / 2 val makePathMain = o.pathSize > 2 diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt index 52e675d..569d18f 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt @@ -28,7 +28,7 @@ class ParcelProviderImpl(val plugin: ParcelsPlugin) : ParcelProvider { override fun getWorldGenerator(worldName: String): ParcelGenerator? { return _worlds[worldName]?.generator ?: _generators[worldName] - ?: options.worlds[worldName]?.generator?.newGenerator(worldName)?.also { _generators[worldName] = it } + ?: options.worlds[worldName]?.generator?.newInstance(worldName)?.also { _generators[worldName] = it } } override fun loadWorlds() { diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelWorldImpl.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelWorldImpl.kt index 590794d..4a168f5 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelWorldImpl.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelWorldImpl.kt @@ -4,6 +4,7 @@ package io.dico.parcels2.defaultimpl import io.dico.parcels2.* import io.dico.parcels2.blockvisitor.WorktimeLimiter +import io.dico.parcels2.options.RuntimeWorldOptions import io.dico.parcels2.storage.Storage import org.bukkit.World import java.util.UUID @@ -11,7 +12,7 @@ import java.util.UUID class ParcelWorldImpl private constructor(override val world: World, override val generator: ParcelGenerator, - override var options: WorldOptions, + override var options: RuntimeWorldOptions, override val storage: Storage, override val globalAddedData: GlobalAddedDataManager, containerFactory: ParcelContainerFactory, @@ -62,7 +63,7 @@ constructor(override val world: World, // Use this to be able to delegate blockManager and assign it to a property too, at least. operator fun invoke(world: World, generator: ParcelGenerator, - options: WorldOptions, + options: RuntimeWorldOptions, storage: Storage, globalAddedData: GlobalAddedDataManager, containerFactory: ParcelContainerFactory, diff --git a/src/main/kotlin/io/dico/parcels2/options/GeneratorOptions.kt b/src/main/kotlin/io/dico/parcels2/options/GeneratorOptions.kt new file mode 100644 index 0000000..ac4d975 --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/options/GeneratorOptions.kt @@ -0,0 +1,36 @@ +package io.dico.parcels2.options + +import io.dico.parcels2.ParcelGenerator +import io.dico.parcels2.defaultimpl.DefaultParcelGenerator +import org.bukkit.Bukkit +import org.bukkit.Material +import org.bukkit.block.Biome +import org.bukkit.block.data.BlockData +import kotlin.reflect.KClass + +object GeneratorOptionsFactories : PolymorphicOptionsFactories("name", GeneratorOptions::class, DefaultGeneratorOptionsFactory()) + +class GeneratorOptions(name: String, options: Any) : PolymorphicOptions(name, options, GeneratorOptionsFactories) { + fun newInstance(worldName: String) = factory.newInstance(key, options, worldName) +} + +private class DefaultGeneratorOptionsFactory : PolymorphicOptionsFactory { + override val supportedKeys: List = listOf("default") + override val optionsClass: KClass get() = DefaultGeneratorOptions::class + + override fun newInstance(key: String, options: Any, vararg extra: Any?): ParcelGenerator { + return DefaultParcelGenerator(extra.first() as String, options as DefaultGeneratorOptions) + } +} + +class DefaultGeneratorOptions(val defaultBiome: Biome = Biome.JUNGLE, + val wallType: BlockData = Bukkit.createBlockData(Material.STONE_SLAB), + val floorType: BlockData = Bukkit.createBlockData(Material.QUARTZ_BLOCK), + val fillType: BlockData = Bukkit.createBlockData(Material.QUARTZ_BLOCK), + val pathMainType: BlockData = Bukkit.createBlockData(Material.SANDSTONE), + val pathAltType: BlockData = Bukkit.createBlockData(Material.REDSTONE_BLOCK), + val parcelSize: Int = 101, + val pathSize: Int = 9, + val floorHeight: Int = 64, + val offsetX: Int = 0, + val offsetZ: Int = 0) \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/options/MigrationOptions.kt b/src/main/kotlin/io/dico/parcels2/options/MigrationOptions.kt new file mode 100644 index 0000000..bb4e052 --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/options/MigrationOptions.kt @@ -0,0 +1,17 @@ +package io.dico.parcels2.options + +import io.dico.parcels2.storage.migration.Migration +import kotlin.reflect.KClass + +object MigrationOptionsFactories : PolymorphicOptionsFactories("kind", MigrationOptions::class, PlotmeMigrationFactory()) + +class MigrationOptions(kind: String, options: Any) : SimplePolymorphicOptions(kind, options, MigrationOptionsFactories) + +private class PlotmeMigrationFactory : PolymorphicOptionsFactory { + override val supportedKeys = listOf("plotme-0.17") + override val optionsClass: KClass get() = TODO() + + override fun newInstance(key: String, options: Any, vararg extra: Any?): Migration { + TODO() + } +} diff --git a/src/main/kotlin/io/dico/parcels2/options/Options.kt b/src/main/kotlin/io/dico/parcels2/options/Options.kt new file mode 100644 index 0000000..fb2e4cc --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/options/Options.kt @@ -0,0 +1,51 @@ +package io.dico.parcels2.options + +import io.dico.parcels2.blockvisitor.TickWorktimeOptions +import org.bukkit.GameMode +import org.bukkit.Material +import java.io.Reader +import java.io.Writer +import java.util.EnumSet + +class Options { + var worlds: Map = hashMapOf() + private set + var storage: StorageOptions = StorageOptions("postgresql", DataConnectionOptions()) + var tickWorktime: TickWorktimeOptions = TickWorktimeOptions(20, 1) + + fun addWorld(name: String, + generatorOptions: GeneratorOptions? = null, + worldOptions: RuntimeWorldOptions? = null) { + val optionsHolder = WorldOptions( + generatorOptions ?: GeneratorOptions("default", DefaultGeneratorOptions()), + worldOptions ?: RuntimeWorldOptions() + ) + + (worlds as MutableMap).put(name, optionsHolder) + } + + fun writeTo(writer: Writer) = optionsMapper.writeValue(writer, this) + + fun mergeFrom(reader: Reader) = optionsMapper.readerForUpdating(this).readValue(reader) + + override fun toString(): String = optionsMapper.writeValueAsString(this) + +} + +class WorldOptions(val generator: GeneratorOptions, + var runtime: RuntimeWorldOptions = RuntimeWorldOptions()) + +class RuntimeWorldOptions(var gameMode: GameMode? = GameMode.CREATIVE, + var dayTime: Boolean = true, + var noWeather: Boolean = true, + var preventWeatherBlockChanges: Boolean = true, + var preventBlockSpread: Boolean = true, // TODO + var dropEntityItems: Boolean = true, + var doTileDrops: Boolean = false, + var disableExplosions: Boolean = true, + var blockPortalCreation: Boolean = true, + var blockMobSpawning: Boolean = true, + var blockedItems: Set = EnumSet.of(Material.FLINT_AND_STEEL, Material.SNOWBALL), + var axisLimit: Int = 10) + +class DataFileOptions(val location: String = "/flatfile-storage/") diff --git a/src/main/kotlin/io/dico/parcels2/options/OptionsMapper.kt b/src/main/kotlin/io/dico/parcels2/options/OptionsMapper.kt new file mode 100644 index 0000000..671a25f --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/options/OptionsMapper.kt @@ -0,0 +1,66 @@ +package io.dico.parcels2.options + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.PropertyNamingStrategy +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import com.fasterxml.jackson.module.kotlin.KotlinModule +import org.bukkit.Bukkit +import org.bukkit.block.data.BlockData + +val optionsMapper = ObjectMapper(YAMLFactory()).apply { + propertyNamingStrategy = PropertyNamingStrategy.KEBAB_CASE + + val kotlinModule = KotlinModule() + + with(kotlinModule) { + /* + setSerializerModifier(object : BeanSerializerModifier() { + @Suppress("UNCHECKED_CAST") + override fun modifySerializer(config: SerializationConfig?, beanDesc: BeanDescription, serializer: JsonSerializer<*>): JsonSerializer<*> { + + val newSerializer = if (GeneratorOptions::class.isSuperclassOf(beanDesc.beanClass.kotlin)) { + GeneratorOptionsSerializer(serializer as JsonSerializer) + } else { + serializer + } + + return super.modifySerializer(config, beanDesc, newSerializer) + } + })*/ + + addSerializer(BlockDataSerializer()) + addDeserializer(BlockData::class.java, BlockDataDeserializer()) + + GeneratorOptionsFactories.registerSerialization(this) + StorageOptionsFactories.registerSerialization(this) + MigrationOptionsFactories.registerSerialization(this) + } + + registerModule(kotlinModule) +} + +private class BlockDataSerializer : StdSerializer(BlockData::class.java) { + + override fun serialize(value: BlockData, gen: JsonGenerator, provider: SerializerProvider) { + gen.writeString(value.asString) + } + +} + +private class BlockDataDeserializer : StdDeserializer(BlockData::class.java) { + + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): BlockData? { + try { + return Bukkit.createBlockData(p.valueAsString) + } catch (ex: Exception) { + throw RuntimeException("Exception occurred at ${p.currentLocation}", ex) + } + } + +} diff --git a/src/main/kotlin/io/dico/parcels2/options/PolymorphicOptions.kt b/src/main/kotlin/io/dico/parcels2/options/PolymorphicOptions.kt new file mode 100644 index 0000000..aa60f39 --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/options/PolymorphicOptions.kt @@ -0,0 +1,89 @@ +package io.dico.parcels2.options + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import io.dico.parcels2.logger +import kotlin.reflect.KClass + +abstract class PolymorphicOptions(val key: String, + val options: Any, + factories: PolymorphicOptionsFactories) { + val factory = factories.getFactory(key)!! +} + +abstract class SimplePolymorphicOptions(key: String, options: Any, factories: PolymorphicOptionsFactories) + : PolymorphicOptions(key, options, factories) { + fun newInstance(): T = factory.newInstance(key, options) +} + +interface PolymorphicOptionsFactory { + val supportedKeys: List + val optionsClass: KClass + fun newInstance(key: String, options: Any, vararg extra: Any?): T +} + +@Suppress("UNCHECKED_CAST") +abstract class PolymorphicOptionsFactories(val serializeKeyAs: String, + rootClass: KClass>, + vararg defaultFactories: PolymorphicOptionsFactory) { + val rootClass = rootClass as KClass> + private val map: MutableMap> = linkedMapOf() + val availableKeys: Collection get() = map.keys + + fun registerFactory(factory: PolymorphicOptionsFactory) = factory.supportedKeys.forEach { map.putIfAbsent(it.toLowerCase(), factory) } + + fun getFactory(key: String): PolymorphicOptionsFactory? = map[key.toLowerCase()] + + fun registerSerialization(module: SimpleModule) { + module.addSerializer(PolymorphicOptionsSerializer(this)) + module.addDeserializer(rootClass.java, PolymorphicOptionsDeserializer(this)) + } + + init { + defaultFactories.forEach { registerFactory(it) } + } +} + + +private class PolymorphicOptionsDeserializer(val factories: PolymorphicOptionsFactories) : JsonDeserializer>() { + + override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): PolymorphicOptions { + val node = p.readValueAsTree() + val key = node.get(factories.serializeKeyAs).asText() + val factory = getFactory(key) + val optionsNode = node.get("options") + val options = p.codec.treeToValue(optionsNode, factory.optionsClass.java) + return factories.rootClass.constructors.first().call(key, options) + } + + private fun getFactory(key: String): PolymorphicOptionsFactory { + factories.getFactory(key)?.let { return it } + + logger.warn("Unknown ${factories.rootClass.simpleName} ${factories.serializeKeyAs}: $key. " + + "\nAvailable options: ${factories.availableKeys}") + + val default = factories.getFactory(factories.availableKeys.first()) + ?: throw IllegalStateException("No default ${factories.rootClass.simpleName} factory registered.") + return default + } + +} + +private class PolymorphicOptionsSerializer(val factories: PolymorphicOptionsFactories) : StdSerializer>(factories.rootClass.java) { + + override fun serialize(value: PolymorphicOptions, gen: JsonGenerator, sp: SerializerProvider?) { + with(gen) { + writeStartObject() + writeStringField(factories.serializeKeyAs, value.key) + writeFieldName("options") + writeObject(value.options) + writeEndObject() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/options/StorageOptions.kt b/src/main/kotlin/io/dico/parcels2/options/StorageOptions.kt new file mode 100644 index 0000000..639be9d --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/options/StorageOptions.kt @@ -0,0 +1,45 @@ +package io.dico.parcels2.options + +import com.zaxxer.hikari.HikariDataSource +import io.dico.parcels2.logger +import io.dico.parcels2.storage.Storage +import io.dico.parcels2.storage.StorageWithCoroutineBacking +import io.dico.parcels2.storage.exposed.ExposedBacking +import io.dico.parcels2.storage.getHikariConfig + +object StorageOptionsFactories : PolymorphicOptionsFactories("dialect", StorageOptions::class, ConnectionStorageFactory()) + +class StorageOptions(dialect: String, options: Any) : SimplePolymorphicOptions(dialect, options, StorageOptionsFactories) + +private class ConnectionStorageFactory : PolymorphicOptionsFactory { + override val optionsClass = DataConnectionOptions::class + override val supportedKeys: List = listOf("postgresql", "mariadb") + + override fun newInstance(key: String, options: Any, vararg extra: Any?): Storage { + val hikariConfig = getHikariConfig(key, options as DataConnectionOptions) + val dataSourceFactory = suspend { HikariDataSource(hikariConfig) } + return StorageWithCoroutineBacking(ExposedBacking(dataSourceFactory)) + } +} + +data class DataConnectionOptions(val address: String = "localhost", + val database: String = "parcels", + val username: String = "root", + val password: String = "", + val poolSize: Int = 4) { + + fun splitAddressAndPort(defaultPort: Int = 3306): Pair? { + val idx = address.indexOf(":").takeUnless { it == -1 } ?: return Pair(address, defaultPort) + + val addressName = address.substring(0, idx).takeUnless { it.isBlank() } ?: return null.also { + logger.error("(Invalidly) blank address in data storage options") + } + + val port = address.substring(idx + 1).toIntOrNull() ?: return null.also { + logger.error("Invalid port number in data storage options: $it, using $defaultPort as default") + } + + return Pair(addressName, port) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/storage/Hikari.kt b/src/main/kotlin/io/dico/parcels2/storage/Hikari.kt index 2dfa872..480d533 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/Hikari.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/Hikari.kt @@ -1,7 +1,7 @@ package io.dico.parcels2.storage import com.zaxxer.hikari.HikariConfig -import io.dico.parcels2.DataConnectionOptions +import io.dico.parcels2.options.DataConnectionOptions fun getHikariConfig(dialectName: String, dco: DataConnectionOptions): HikariConfig = HikariConfig().apply { diff --git a/src/main/kotlin/io/dico/parcels2/storage/Jackson.kt b/src/main/kotlin/io/dico/parcels2/storage/Jackson.kt deleted file mode 100644 index ede33ae..0000000 --- a/src/main/kotlin/io/dico/parcels2/storage/Jackson.kt +++ /dev/null @@ -1,122 +0,0 @@ -package io.dico.parcels2.storage - -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.databind.* -import com.fasterxml.jackson.databind.deser.std.StdDeserializer -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.GeneratorFactories -import io.dico.parcels2.GeneratorOptions -import io.dico.parcels2.StorageOptions -import org.bukkit.Bukkit -import org.bukkit.block.data.BlockData -import kotlin.reflect.full.isSuperclassOf - -val yamlObjectMapper = ObjectMapper(YAMLFactory()).apply { - propertyNamingStrategy = PropertyNamingStrategy.KEBAB_CASE - - val kotlinModule = KotlinModule() - - with(kotlinModule) { - setSerializerModifier(object : BeanSerializerModifier() { - @Suppress("UNCHECKED_CAST") - override fun modifySerializer(config: SerializationConfig?, beanDesc: BeanDescription, serializer: JsonSerializer<*>): JsonSerializer<*> { - - val newSerializer = if (GeneratorOptions::class.isSuperclassOf(beanDesc.beanClass.kotlin)) { - GeneratorOptionsSerializer(serializer as JsonSerializer) - } else { - serializer - } - - return super.modifySerializer(config, beanDesc, newSerializer) - } - }) - - addSerializer(BlockDataSerializer()) - addDeserializer(BlockData::class.java, BlockDataDeserializer()) - - addSerializer(StorageOptionsSerializer()) - addDeserializer(StorageOptions::class.java, StorageOptionsDeserializer()) - - addDeserializer(GeneratorOptions::class.java, GeneratorOptionsDeserializer()) - } - - registerModule(kotlinModule) -} - -private class BlockDataSerializer : StdSerializer(BlockData::class.java) { - - override fun serialize(value: BlockData, gen: JsonGenerator, provider: SerializerProvider) { - gen.writeString(value.asString) - } - -} - -private class BlockDataDeserializer : StdDeserializer(BlockData::class.java) { - - override fun deserialize(p: JsonParser, ctxt: DeserializationContext): BlockData? { - try { - return Bukkit.createBlockData(p.valueAsString) - } catch (ex: Exception) { - throw RuntimeException("Exception occurred at ${p.currentLocation}", ex) - } - } - -} - -class StorageOptionsDeserializer : JsonDeserializer() { - - override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): StorageOptions { - val node = p!!.readValueAsTree() - val dialect = node.get("dialect").asText() - val optionsNode = node.get("options") - val factory = StorageFactory.getFactory(dialect) ?: throw IllegalStateException("Unknown storage dialect: $dialect") - val options = p.codec.treeToValue(optionsNode, factory.optionsClass.java) - return StorageOptions(dialect, options) - } - -} - -class StorageOptionsSerializer : StdSerializer(StorageOptions::class.java) { - - override fun serialize(value: StorageOptions?, gen: JsonGenerator?, serializers: SerializerProvider?) { - with(gen!!) { - writeStartObject() - writeStringField("dialect", value!!.dialect) - writeFieldName("options") - writeObject(value.options) - writeEndObject() - } - } - -} - -class GeneratorOptionsDeserializer : JsonDeserializer() { - - override fun deserialize(parser: JsonParser?, ctx: DeserializationContext?): GeneratorOptions? { - val node = parser!!.readValueAsTree() - val name = node.get("name").asText() - val optionsNode = node.get("options") - val factory = GeneratorFactories.getFactory(name) ?: throw IllegalStateException("Unknown generator: $name") - - return parser.codec.treeToValue(optionsNode, factory.optionsClass.java) - } - -} - -class GeneratorOptionsSerializer(private val defaultSerializer: JsonSerializer) : JsonSerializer() { - - override fun serialize(input: GeneratorOptions?, generator: JsonGenerator?, provider: SerializerProvider?) { - with(generator!!) { - writeStartObject() - writeStringField("name", input!!.generatorFactory().name) - writeFieldName("options") - defaultSerializer.serialize(input, generator, provider) - writeEndObject() - } - } - -} \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/storage/StorageFactory.kt b/src/main/kotlin/io/dico/parcels2/storage/StorageFactory.kt deleted file mode 100644 index e798df9..0000000 --- a/src/main/kotlin/io/dico/parcels2/storage/StorageFactory.kt +++ /dev/null @@ -1,43 +0,0 @@ -package io.dico.parcels2.storage - -import com.zaxxer.hikari.HikariDataSource -import io.dico.parcels2.DataConnectionOptions -import io.dico.parcels2.storage.exposed.ExposedBacking -import kotlin.reflect.KClass - -interface StorageFactory { - companion object StorageFactories { - private val map: MutableMap = HashMap() - - fun registerFactory(dialect: String, generator: StorageFactory): Boolean = map.putIfAbsent(dialect.toLowerCase(), generator) == null - - fun getFactory(dialect: String): StorageFactory? = map[dialect.toLowerCase()] - - init { - // have to write the code like this in kotlin. - // This code is absolutely disgusting - ConnectionStorageFactory().register(this) - } - } - - val optionsClass: KClass - - fun newStorageInstance(dialect: String, options: Any): Storage - -} - -class ConnectionStorageFactory : StorageFactory { - override val optionsClass = DataConnectionOptions::class - private val types: List = listOf("postgresql", "mariadb") - - fun register(companion: StorageFactory.StorageFactories) { - types.forEach { companion.registerFactory(it, this) } - } - - override fun newStorageInstance(dialect: String, options: Any): Storage { - val hikariConfig = getHikariConfig(dialect, options as DataConnectionOptions) - val dataSourceFactory = suspend { HikariDataSource(hikariConfig) } - return StorageWithCoroutineBacking(ExposedBacking(dataSourceFactory)) - } - -} diff --git a/src/main/kotlin/io/dico/parcels2/storage/migration/MigrationFactory.kt b/src/main/kotlin/io/dico/parcels2/storage/migration/MigrationFactory.kt deleted file mode 100644 index 4fb3088..0000000 --- a/src/main/kotlin/io/dico/parcels2/storage/migration/MigrationFactory.kt +++ /dev/null @@ -1,5 +0,0 @@ -package io.dico.parcels2.storage.migration - -interface MigrationFactory { - fun getMigration() -} \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 58426c6..65186e5 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,11 +1,12 @@ - + - %d{HH:mm:ss.SSS} %magenta(%-8.-8(%thread)) %highlight(%-5level) %boldCyan(%32.-32logger{32}) - %msg + + %magenta(%-8.-8(%thread)) %highlight(%-5level) %boldCyan(%6.-32logger{32}) - %msg - + \ No newline at end of file -- cgit v1.2.3 From 3573f9ade67010e3e538151375faecbec32825c4 Mon Sep 17 00:00:00 2001 From: Dico Date: Sat, 4 Aug 2018 00:13:09 +0100 Subject: work on plotme migration --- src/main/kotlin/io/dico/parcels2/Parcel.kt | 2 +- src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt | 31 ++++-- .../io/dico/parcels2/command/CommandsDebug.kt | 9 +- .../parcels2/defaultimpl/DefaultParcelGenerator.kt | 2 +- .../parcels2/defaultimpl/ParcelProviderImpl.kt | 16 +++ .../dico/parcels2/listener/ParcelEntityTracker.kt | 2 +- .../io/dico/parcels2/listener/ParcelListeners.kt | 106 +++++++++--------- .../io/dico/parcels2/options/GeneratorOptions.kt | 2 +- .../io/dico/parcels2/options/MigrationOptions.kt | 10 +- .../kotlin/io/dico/parcels2/options/Options.kt | 11 +- .../io/dico/parcels2/options/StorageOptions.kt | 24 ++++- .../kotlin/io/dico/parcels2/storage/Backing.kt | 55 +++++++--- .../kotlin/io/dico/parcels2/storage/Storage.kt | 61 ++++------- .../storage/exposed/CoroutineTransactionManager.kt | 118 +++++++++++++++++++++ .../parcels2/storage/exposed/ExposedBacking.kt | 66 ++++++------ .../parcels2/storage/exposed/ExposedExtensions.kt | 2 + .../dico/parcels2/storage/migration/Migration.kt | 3 +- .../storage/migration/plotme/PlotmeMigration.kt | 64 ++++++----- .../storage/migration/plotme/PlotmeTables.kt | 10 +- .../kotlin/io/dico/parcels2/util/MiscExtensions.kt | 5 +- src/main/resources/logback.xml | 31 +++++- 21 files changed, 430 insertions(+), 200 deletions(-) create mode 100644 src/main/kotlin/io/dico/parcels2/storage/exposed/CoroutineTransactionManager.kt diff --git a/src/main/kotlin/io/dico/parcels2/Parcel.kt b/src/main/kotlin/io/dico/parcels2/Parcel.kt index a69116a..6505a49 100644 --- a/src/main/kotlin/io/dico/parcels2/Parcel.kt +++ b/src/main/kotlin/io/dico/parcels2/Parcel.kt @@ -46,7 +46,7 @@ interface ParcelData : AddedData { } } -class ParcelDataHolder : AddedDataHolder(), ParcelData { +class ParcelDataHolder(addedMap: MutableAddedDataMap = mutableMapOf()) : AddedDataHolder(addedMap), ParcelData { override var owner: ParcelOwner? = null override var since: DateTime? = null diff --git a/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt b/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt index ffa11c7..f0b5fbc 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt @@ -85,25 +85,32 @@ class ParcelsPlugin : JavaPlugin() { fun loadOptions(): Boolean { when { optionsFile.exists() -> optionsMapper.readerForUpdating(options).readValue(optionsFile) - optionsFile.tryCreate() -> { + else -> run { options.addWorld("parcels") - try { - optionsMapper.writeValue(optionsFile, options) - } catch (ex: Throwable) { - optionsFile.delete() - throw ex + if (saveOptions()) { + plogger.warn("Created options file with a world template. Please review it before next start.") + } else { + plogger.error("Failed to save options file ${optionsFile.canonicalPath}") } - plogger.warn("Created options file with a world template. Please review it before next start.") - return false - } - else -> { - plogger.error("Failed to save options file ${optionsFile.canonicalPath}") return false } } return true } + fun saveOptions(): Boolean { + if (optionsFile.tryCreate()) { + try { + optionsMapper.writeValue(optionsFile, options) + } catch (ex: Throwable) { + optionsFile.delete() + throw ex + } + return true + } + return false + } + override fun getDefaultWorldGenerator(worldName: String, generatorId: String?): ChunkGenerator? { return parcelProvider.getWorldGenerator(worldName) } @@ -119,6 +126,8 @@ class ParcelsPlugin : JavaPlugin() { listeners = ParcelListeners(parcelProvider, entityTracker) registrator.registerListeners(listeners!!) } + + functionHelper.scheduleRepeating(100, 5, entityTracker::tick) } } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/command/CommandsDebug.kt b/src/main/kotlin/io/dico/parcels2/command/CommandsDebug.kt index bf5a870..3c5ba41 100644 --- a/src/main/kotlin/io/dico/parcels2/command/CommandsDebug.kt +++ b/src/main/kotlin/io/dico/parcels2/command/CommandsDebug.kt @@ -33,15 +33,18 @@ class CommandsDebug(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) { fun ParcelScope.cmdMakeMess(context: ExecutionContext) { val server = plugin.server val blockDatas = arrayOf( - server.createBlockData(Material.STICKY_PISTON), + server.createBlockData(Material.BLUE_WOOL), + server.createBlockData(Material.LIME_WOOL), server.createBlockData(Material.GLASS), server.createBlockData(Material.STONE_SLAB), - server.createBlockData(Material.QUARTZ_BLOCK) + server.createBlockData(Material.STONE), + server.createBlockData(Material.QUARTZ_BLOCK), + server.createBlockData(Material.BROWN_CONCRETE) ) val random = Random() world.doBlockOperation(parcel.id, direction = RegionTraversal.UPWARD) { block -> - block.blockData = blockDatas[random.nextInt(4)] + block.blockData = blockDatas[random.nextInt(7)] }.onProgressUpdate(1000, 1000) { progress, elapsedTime -> context.sendMessage(EMessageType.INFORMATIVE, "Mess progress: %.02f%%, %.2fs elapsed" .format(progress * 100, elapsedTime / 1000.0)) diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt index 7dbcb78..3b6bfb5 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt @@ -146,7 +146,7 @@ class DefaultParcelGenerator(val name: String, private val o: DefaultGeneratorOp override fun getHomeLocation(parcel: ParcelId): Location { val bottom = getBottomBlock(parcel) - return Location(world, bottom.x.toDouble(), o.floorHeight + 1.0, bottom.z + (o.parcelSize - 1) / 2.0, -90F, 0F) + return Location(world, bottom.x.toDouble() + 0.5, o.floorHeight + 1.0, bottom.z + 0.5 + (o.parcelSize - 1) / 2.0, -90F, 0F) } override fun setOwnerBlock(parcel: ParcelId, owner: ParcelOwner?) { diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt index 569d18f..f7abccd 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt @@ -60,6 +60,21 @@ class ParcelProviderImpl(val plugin: ParcelsPlugin) : ParcelProvider { private fun loadStoredData() { plugin.functionHelper.launchLazilyOnMainThread { + val migration = plugin.options.migration + if (migration.enabled) { + migration.instance?.newInstance()?.apply { + logger.warn("Migrating database now...") + migrateTo(plugin.storage).join() + logger.warn("Migration completed") + + if (migration.disableWhenComplete) { + migration.enabled = false + plugin.saveOptions() + } + } + } + + logger.info("Loading all parcel data...") val channel = plugin.storage.readAllParcelData() do { val pair = channel.receiveOrNull() ?: break @@ -67,6 +82,7 @@ class ParcelProviderImpl(val plugin: ParcelsPlugin) : ParcelProvider { pair.second?.let { parcel.copyDataIgnoringDatabase(it) } } while (true) + logger.info("Loading data completed") _dataIsLoaded = true }.start() } diff --git a/src/main/kotlin/io/dico/parcels2/listener/ParcelEntityTracker.kt b/src/main/kotlin/io/dico/parcels2/listener/ParcelEntityTracker.kt index 285fd3e..edb48b5 100644 --- a/src/main/kotlin/io/dico/parcels2/listener/ParcelEntityTracker.kt +++ b/src/main/kotlin/io/dico/parcels2/listener/ParcelEntityTracker.kt @@ -26,7 +26,7 @@ class ParcelEntityTracker(val parcelProvider: ParcelProvider) { */ fun tick() { map.editLoop { entity, parcel -> - if (entity.isDead || entity.isOnGround) { + if (entity.isDead) { remove(); return@editLoop } if (parcel.isPresentAnd { hasBlockVisitors }) { diff --git a/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt b/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt index d34f8bf..b2637f5 100644 --- a/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt +++ b/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt @@ -83,9 +83,9 @@ class ParcelListeners(val parcelProvider: ParcelProvider, val entityTracker: Par * Prevents players from placing blocks outside of their parcels */ @field:ListenerMarker(priority = NORMAL) - val onBlockPlaceEvent = RegistratorListener l@{ event -> + val onBlockPlaceEvent = RegistratorListener l@{ event -> val (wo, ppa) = getWoAndPPa(event.block) ?: return@l - if (!event.player.hasBuildAnywhere && !ppa.isNullOr { !canBuild(event.player) }) { + if (!event.player.hasBuildAnywhere && ppa.isNullOr { !canBuild(event.player) }) { event.isCancelled = true } } @@ -184,62 +184,69 @@ class ParcelListeners(val parcelProvider: ParcelProvider, val entityTracker: Par } when (event.action) { - Action.RIGHT_CLICK_BLOCK -> when (clickedBlock.type) { - REPEATER, - COMPARATOR -> run { - if (!parcel.canBuildN(user)) { - event.isCancelled = true; return@l + Action.RIGHT_CLICK_BLOCK -> run { + when (clickedBlock.type) { + REPEATER, + COMPARATOR -> run { + if (!parcel.canBuildN(user)) { + event.isCancelled = true; return@l + } } - } - LEVER, - STONE_BUTTON, - ANVIL, - TRAPPED_CHEST, - OAK_BUTTON, BIRCH_BUTTON, SPRUCE_BUTTON, JUNGLE_BUTTON, ACACIA_BUTTON, DARK_OAK_BUTTON, - OAK_FENCE_GATE, BIRCH_FENCE_GATE, SPRUCE_FENCE_GATE, JUNGLE_FENCE_GATE, ACACIA_FENCE_GATE, DARK_OAK_FENCE_GATE, - OAK_DOOR, BIRCH_DOOR, SPRUCE_DOOR, JUNGLE_DOOR, ACACIA_DOOR, DARK_OAK_DOOR, - OAK_TRAPDOOR, BIRCH_TRAPDOOR, SPRUCE_TRAPDOOR, JUNGLE_TRAPDOOR, ACACIA_TRAPDOOR, DARK_OAK_TRAPDOOR - -> run { - if (!user.hasBuildAnywhere && !parcel.isNullOr { canBuild(user) || allowInteractInputs }) { - user.sendParcelMessage(nopermit = true, message = "You cannot use inputs in this parcel") - event.isCancelled = true; return@l + LEVER, + STONE_BUTTON, + ANVIL, + TRAPPED_CHEST, + OAK_BUTTON, BIRCH_BUTTON, SPRUCE_BUTTON, JUNGLE_BUTTON, ACACIA_BUTTON, DARK_OAK_BUTTON, + OAK_FENCE_GATE, BIRCH_FENCE_GATE, SPRUCE_FENCE_GATE, JUNGLE_FENCE_GATE, ACACIA_FENCE_GATE, DARK_OAK_FENCE_GATE, + OAK_DOOR, BIRCH_DOOR, SPRUCE_DOOR, JUNGLE_DOOR, ACACIA_DOOR, DARK_OAK_DOOR, + OAK_TRAPDOOR, BIRCH_TRAPDOOR, SPRUCE_TRAPDOOR, JUNGLE_TRAPDOOR, ACACIA_TRAPDOOR, DARK_OAK_TRAPDOOR + -> run { + if (!user.hasBuildAnywhere && !parcel.isNullOr { canBuild(user) || allowInteractInputs }) { + user.sendParcelMessage(nopermit = true, message = "You cannot use inputs in this parcel") + event.isCancelled = true; return@l + } } - } - WHITE_BED, ORANGE_BED, MAGENTA_BED, LIGHT_BLUE_BED, YELLOW_BED, LIME_BED, PINK_BED, GRAY_BED, LIGHT_GRAY_BED, CYAN_BED, PURPLE_BED, BLUE_BED, BROWN_BED, GREEN_BED, RED_BED, BLACK_BED - -> run { - if (world.options.disableExplosions) { - val bed = clickedBlock.blockData as Bed - val head = if (bed == Bed.Part.FOOT) clickedBlock.getRelative(bed.facing) else clickedBlock - when (head.biome) { - Biome.NETHER, Biome.THE_END -> run { - user.sendParcelMessage(nopermit = true, message = "You cannot use this bed because it would explode") - event.isCancelled = true; return@l + WHITE_BED, ORANGE_BED, MAGENTA_BED, LIGHT_BLUE_BED, YELLOW_BED, LIME_BED, PINK_BED, GRAY_BED, LIGHT_GRAY_BED, CYAN_BED, PURPLE_BED, BLUE_BED, BROWN_BED, GREEN_BED, RED_BED, BLACK_BED + -> run { + if (world.options.disableExplosions) { + val bed = clickedBlock.blockData as Bed + val head = if (bed == Bed.Part.FOOT) clickedBlock.getRelative(bed.facing) else clickedBlock + when (head.biome) { + Biome.NETHER, Biome.THE_END -> run { + user.sendParcelMessage(nopermit = true, message = "You cannot use this bed because it would explode") + event.isCancelled = true; return@l + } } + } } - } + onPlayerInteractEvent_RightClick(event, world, parcel) } - Action.RIGHT_CLICK_AIR -> if (event.hasItem()) { - val item = event.item.type - if (world.options.blockedItems.contains(item)) { - user.sendParcelMessage(nopermit = true, message = "You cannot use this bed because it would explode") - event.isCancelled = true; return@l - } - - if (!parcel.canBuildN(user)) { - when (item) { - LAVA_BUCKET, WATER_BUCKET, BUCKET, FLINT_AND_STEEL -> event.isCancelled = true - } - } + Action.RIGHT_CLICK_AIR -> onPlayerInteractEvent_RightClick(event, world, parcel) + Action.PHYSICAL -> if (!user.hasBuildAnywhere && !parcel.isPresentAnd { canBuild(user) || allowInteractInputs }) { + user.sendParcelMessage(nopermit = true, message = "You cannot use inputs in this parcel") + event.isCancelled = true; return@l } + } + } + @Suppress("NON_EXHAUSTIVE_WHEN") + private fun onPlayerInteractEvent_RightClick(event: PlayerInteractEvent, world: ParcelWorld, parcel: Parcel?) { + if (event.hasItem()) { + val item = event.item.type + if (world.options.blockedItems.contains(item)) { + event.player.sendParcelMessage(nopermit = true, message = "You cannot use this item because it is disabled in this world") + event.isCancelled = true; return + } - Action.PHYSICAL -> if (!user.hasBuildAnywhere && !parcel.isPresentAnd { canBuild(user) || allowInteractInputs }) { - event.isCancelled = true; return@l + if (!parcel.canBuildN(event.player)) { + when (item) { + LAVA_BUCKET, WATER_BUCKET, BUCKET, FLINT_AND_STEEL -> event.isCancelled = true + } } } } @@ -352,7 +359,7 @@ class ParcelListeners(val parcelProvider: ParcelProvider, val entityTracker: Par world.weatherDuration = Int.MAX_VALUE } - // TODO: BlockFormEvent, BlockSpreadEvent, BlockFadeEvent +// TODO: BlockFormEvent, BlockSpreadEvent, BlockFadeEvent, Fireworks /* * Prevents natural blocks forming @@ -370,10 +377,10 @@ class ParcelListeners(val parcelProvider: ParcelProvider, val entityTracker: Par val hasEntity = event is EntityBlockFormEvent val player = (event as? EntityBlockFormEvent)?.entity as? Player - val cancel: Boolean = when (block.type) { + val cancel: Boolean = when (event.newState.type) { // prevent ice generation from Frost Walkers enchantment - ICE -> player != null && !ppa.canBuild(player) + FROSTED_ICE -> player != null && !ppa.canBuild(player) // prevent snow generation from weather SNOW -> !hasEntity && wo.options.preventWeatherBlockChanges @@ -406,12 +413,13 @@ class ParcelListeners(val parcelProvider: ParcelProvider, val entityTracker: Par val onVehicleMoveEvent = RegistratorListener l@{ event -> val (wo, ppa) = getWoAndPPa(event.to.block) ?: return@l if (ppa == null) { - event.vehicle.eject() event.vehicle.passengers.forEach { if (it.type == EntityType.PLAYER) { (it as Player).sendParcelMessage(except = true, message = "Your ride ends here") } else it.remove() } + event.vehicle.eject() + event.vehicle.remove() } else if (ppa.hasBlockVisitors) { event.to.subtract(event.to).add(event.from) } diff --git a/src/main/kotlin/io/dico/parcels2/options/GeneratorOptions.kt b/src/main/kotlin/io/dico/parcels2/options/GeneratorOptions.kt index ac4d975..d0626dc 100644 --- a/src/main/kotlin/io/dico/parcels2/options/GeneratorOptions.kt +++ b/src/main/kotlin/io/dico/parcels2/options/GeneratorOptions.kt @@ -10,7 +10,7 @@ import kotlin.reflect.KClass object GeneratorOptionsFactories : PolymorphicOptionsFactories("name", GeneratorOptions::class, DefaultGeneratorOptionsFactory()) -class GeneratorOptions(name: String, options: Any) : PolymorphicOptions(name, options, GeneratorOptionsFactories) { +class GeneratorOptions (name: String = "default", options: Any = DefaultGeneratorOptions()) : PolymorphicOptions(name, options, GeneratorOptionsFactories) { fun newInstance(worldName: String) = factory.newInstance(key, options, worldName) } diff --git a/src/main/kotlin/io/dico/parcels2/options/MigrationOptions.kt b/src/main/kotlin/io/dico/parcels2/options/MigrationOptions.kt index bb4e052..2ad10e9 100644 --- a/src/main/kotlin/io/dico/parcels2/options/MigrationOptions.kt +++ b/src/main/kotlin/io/dico/parcels2/options/MigrationOptions.kt @@ -1,17 +1,21 @@ package io.dico.parcels2.options import io.dico.parcels2.storage.migration.Migration +import io.dico.parcels2.storage.migration.plotme.PlotmeMigration import kotlin.reflect.KClass object MigrationOptionsFactories : PolymorphicOptionsFactories("kind", MigrationOptions::class, PlotmeMigrationFactory()) -class MigrationOptions(kind: String, options: Any) : SimplePolymorphicOptions(kind, options, MigrationOptionsFactories) +class MigrationOptions(kind: String = "plotme-0.17", options: Any = PlotmeMigrationOptions()) : SimplePolymorphicOptions(kind, options, MigrationOptionsFactories) private class PlotmeMigrationFactory : PolymorphicOptionsFactory { override val supportedKeys = listOf("plotme-0.17") - override val optionsClass: KClass get() = TODO() + override val optionsClass: KClass get() = PlotmeMigrationOptions::class override fun newInstance(key: String, options: Any, vararg extra: Any?): Migration { - TODO() + return PlotmeMigration(options as PlotmeMigrationOptions) } } + +class PlotmeMigrationOptions(val worldsFromTo: Map = mapOf("plotworld" to "parcels"), + val storage: StorageOptions = StorageOptions(options = DataConnectionOptions(database = "plotme"))) \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/options/Options.kt b/src/main/kotlin/io/dico/parcels2/options/Options.kt index fb2e4cc..79dbb46 100644 --- a/src/main/kotlin/io/dico/parcels2/options/Options.kt +++ b/src/main/kotlin/io/dico/parcels2/options/Options.kt @@ -10,14 +10,15 @@ import java.util.EnumSet class Options { var worlds: Map = hashMapOf() private set - var storage: StorageOptions = StorageOptions("postgresql", DataConnectionOptions()) + var storage: StorageOptions = StorageOptions() var tickWorktime: TickWorktimeOptions = TickWorktimeOptions(20, 1) + var migration = MigrationOptionsHolder() fun addWorld(name: String, generatorOptions: GeneratorOptions? = null, worldOptions: RuntimeWorldOptions? = null) { val optionsHolder = WorldOptions( - generatorOptions ?: GeneratorOptions("default", DefaultGeneratorOptions()), + generatorOptions ?: GeneratorOptions(), worldOptions ?: RuntimeWorldOptions() ) @@ -49,3 +50,9 @@ class RuntimeWorldOptions(var gameMode: GameMode? = GameMode.CREATIVE, var axisLimit: Int = 10) class DataFileOptions(val location: String = "/flatfile-storage/") + +class MigrationOptionsHolder { + var enabled = false + var disableWhenComplete = true + var instance: MigrationOptions? = MigrationOptions() +} \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/options/StorageOptions.kt b/src/main/kotlin/io/dico/parcels2/options/StorageOptions.kt index 639be9d..3d68701 100644 --- a/src/main/kotlin/io/dico/parcels2/options/StorageOptions.kt +++ b/src/main/kotlin/io/dico/parcels2/options/StorageOptions.kt @@ -3,22 +3,36 @@ package io.dico.parcels2.options import com.zaxxer.hikari.HikariDataSource import io.dico.parcels2.logger import io.dico.parcels2.storage.Storage -import io.dico.parcels2.storage.StorageWithCoroutineBacking +import io.dico.parcels2.storage.BackedStorage import io.dico.parcels2.storage.exposed.ExposedBacking import io.dico.parcels2.storage.getHikariConfig +import javax.sql.DataSource object StorageOptionsFactories : PolymorphicOptionsFactories("dialect", StorageOptions::class, ConnectionStorageFactory()) -class StorageOptions(dialect: String, options: Any) : SimplePolymorphicOptions(dialect, options, StorageOptionsFactories) +class StorageOptions(dialect: String = "mariadb", options: Any = DataConnectionOptions()) : SimplePolymorphicOptions(dialect, options, StorageOptionsFactories) { + + fun getDataSourceFactory(): DataSourceFactory? { + return when (factory) { + is ConnectionStorageFactory -> factory.getDataSourceFactory(key, options) + else -> return null + } + } +} + +typealias DataSourceFactory = () -> DataSource private class ConnectionStorageFactory : PolymorphicOptionsFactory { override val optionsClass = DataConnectionOptions::class override val supportedKeys: List = listOf("postgresql", "mariadb") - override fun newInstance(key: String, options: Any, vararg extra: Any?): Storage { + fun getDataSourceFactory(key: String, options: Any): DataSourceFactory { val hikariConfig = getHikariConfig(key, options as DataConnectionOptions) - val dataSourceFactory = suspend { HikariDataSource(hikariConfig) } - return StorageWithCoroutineBacking(ExposedBacking(dataSourceFactory)) + return { HikariDataSource(hikariConfig) } + } + + override fun newInstance(key: String, options: Any, vararg extra: Any?): Storage { + return BackedStorage(ExposedBacking(getDataSourceFactory(key, options), (options as DataConnectionOptions).poolSize)) } } diff --git a/src/main/kotlin/io/dico/parcels2/storage/Backing.kt b/src/main/kotlin/io/dico/parcels2/storage/Backing.kt index 88ee5fd..bb4cf33 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/Backing.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/Backing.kt @@ -1,6 +1,12 @@ package io.dico.parcels2.storage import io.dico.parcels2.* +import kotlinx.coroutines.experimental.CoroutineDispatcher +import kotlinx.coroutines.experimental.CoroutineScope +import kotlinx.coroutines.experimental.Deferred +import kotlinx.coroutines.experimental.Job +import kotlinx.coroutines.experimental.channels.ProducerScope +import kotlinx.coroutines.experimental.channels.ReceiveChannel import kotlinx.coroutines.experimental.channels.SendChannel import java.util.UUID @@ -10,41 +16,58 @@ interface Backing { val isConnected: Boolean - suspend fun init() + fun launchJob(job: Backing.() -> Unit): Job - suspend fun shutdown() + fun launchFuture(future: Backing.() -> T): Deferred + + fun openChannel(future: Backing.(SendChannel) -> Unit): ReceiveChannel + + + fun init() + + fun shutdown() /** * This producer function is capable of constantly reading parcels from a potentially infinite sequence, * and provide parcel data for it as read from the database. */ - suspend fun produceParcelData(channel: SendChannel, parcels: Sequence) + fun produceParcelData(channel: SendChannel, parcels: Sequence) + + fun produceAllParcelData(channel: SendChannel) + + fun readParcelData(parcel: ParcelId): ParcelData? + + fun getOwnedParcels(user: ParcelOwner): List + + fun getNumParcels(user: ParcelOwner): Int = getOwnedParcels(user).size + - suspend fun produceAllParcelData(channel: SendChannel) + fun setParcelData(parcel: ParcelId, data: ParcelData?) - suspend fun readParcelData(parcel: ParcelId): ParcelData? + fun setParcelOwner(parcel: ParcelId, owner: ParcelOwner?) - suspend fun getOwnedParcels(user: ParcelOwner): List + fun setLocalPlayerStatus(parcel: ParcelId, player: UUID, status: AddedStatus) - suspend fun getNumParcels(user: ParcelOwner): Int = getOwnedParcels(user).size + fun setParcelAllowsInteractInventory(parcel: ParcelId, value: Boolean) + fun setParcelAllowsInteractInputs(parcel: ParcelId, value: Boolean) - suspend fun setParcelData(parcel: ParcelId, data: ParcelData?) - suspend fun setParcelOwner(parcel: ParcelId, owner: ParcelOwner?) + fun produceAllGlobalAddedData(channel: SendChannel>) - suspend fun setLocalPlayerStatus(parcel: ParcelId, player: UUID, status: AddedStatus) + fun readGlobalAddedData(owner: ParcelOwner): MutableAddedDataMap - suspend fun setParcelAllowsInteractInventory(parcel: ParcelId, value: Boolean) + fun setGlobalPlayerStatus(owner: ParcelOwner, player: UUID, status: AddedStatus) - suspend fun setParcelAllowsInteractInputs(parcel: ParcelId, value: Boolean) +} +abstract class AbstractBacking(val dispatcher: CoroutineDispatcher) { - suspend fun produceAllGlobalAddedData(channel: SendChannel>) + fun launchJob(job: Backing.() -> Unit): Job - suspend fun readGlobalAddedData(owner: ParcelOwner): MutableAddedDataMap + fun launchFuture(future: Backing.() -> T): Deferred - suspend fun setGlobalPlayerStatus(owner: ParcelOwner, player: UUID, status: AddedStatus) + fun openChannel(future: Backing.(SendChannel) -> Unit): ReceiveChannel -} \ No newline at end of file +} diff --git a/src/main/kotlin/io/dico/parcels2/storage/Storage.kt b/src/main/kotlin/io/dico/parcels2/storage/Storage.kt index 6c3d68f..6770d99 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/Storage.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/Storage.kt @@ -3,21 +3,16 @@ package io.dico.parcels2.storage import io.dico.parcels2.* -import kotlinx.coroutines.experimental.* -import kotlinx.coroutines.experimental.channels.ProducerScope +import kotlinx.coroutines.experimental.Deferred +import kotlinx.coroutines.experimental.Job import kotlinx.coroutines.experimental.channels.ReceiveChannel -import kotlinx.coroutines.experimental.channels.produce import java.util.UUID -import java.util.concurrent.Executor -import java.util.concurrent.Executors typealias DataPair = Pair typealias AddedDataPair = Pair interface Storage { val name: String - val syncDispatcher: CoroutineDispatcher - val asyncDispatcher: CoroutineDispatcher val isConnected: Boolean fun init(): Job @@ -54,55 +49,39 @@ interface Storage { fun setGlobalAddedStatus(owner: ParcelOwner, player: UUID, status: AddedStatus): Job } -class StorageWithCoroutineBacking internal constructor(val backing: Backing) : Storage { - override val name get() = backing.name - 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 - val channelCapacity = 16 +class BackedStorage internal constructor(val b: Backing) : Storage { + override val name get() = b.name + override val isConnected get() = b.isConnected - private inline fun defer(noinline block: suspend CoroutineScope.() -> T): Deferred { - return async(context = asyncDispatcher, start = CoroutineStart.ATOMIC, block = block) - } + override fun init() = b.launchJob { init() } - private inline fun job(noinline block: suspend CoroutineScope.() -> Unit): Job { - return launch(context = asyncDispatcher, start = CoroutineStart.ATOMIC, block = block) - } + override fun shutdown() = b.launchJob { shutdown() } - private inline fun openChannel(noinline block: suspend ProducerScope.() -> Unit): ReceiveChannel { - return produce(asyncDispatcher, capacity = channelCapacity, block = block) - } - override fun init() = job { backing.init() } + override fun readParcelData(parcel: ParcelId) = b.launchFuture { readParcelData(parcel) } - override fun shutdown() = job { backing.shutdown() } + override fun readParcelData(parcels: Sequence) = b.openChannel { produceParcelData(it, parcels) } + override fun readAllParcelData() = b.openChannel { produceAllParcelData(it) } - override fun readParcelData(parcel: ParcelId) = defer { backing.readParcelData(parcel) } + override fun getOwnedParcels(user: ParcelOwner) = b.launchFuture { getOwnedParcels(user) } - override fun readParcelData(parcels: Sequence) = openChannel { backing.produceParcelData(channel, parcels) } + override fun getNumParcels(user: ParcelOwner) = b.launchFuture { getNumParcels(user) } - override fun readAllParcelData() = openChannel { backing.produceAllParcelData(channel) } + override fun setParcelData(parcel: ParcelId, data: ParcelData?) = b.launchJob { setParcelData(parcel, data) } - override fun getOwnedParcels(user: ParcelOwner) = defer { backing.getOwnedParcels(user) } + override fun setParcelOwner(parcel: ParcelId, owner: ParcelOwner?) = b.launchJob { setParcelOwner(parcel, owner) } - override fun getNumParcels(user: ParcelOwner) = defer { backing.getNumParcels(user) } + override fun setParcelPlayerStatus(parcel: ParcelId, player: UUID, status: AddedStatus) = b.launchJob { setLocalPlayerStatus(parcel, player, status) } - override fun setParcelData(parcel: ParcelId, data: ParcelData?) = job { backing.setParcelData(parcel, data) } + override fun setParcelAllowsInteractInventory(parcel: ParcelId, value: Boolean) = b.launchJob { setParcelAllowsInteractInventory(parcel, value) } - override fun setParcelOwner(parcel: ParcelId, owner: ParcelOwner?) = job { backing.setParcelOwner(parcel, owner) } + override fun setParcelAllowsInteractInputs(parcel: ParcelId, value: Boolean) = b.launchJob { setParcelAllowsInteractInputs(parcel, value) } - override fun setParcelPlayerStatus(parcel: ParcelId, player: UUID, status: AddedStatus) = job { backing.setLocalPlayerStatus(parcel, player, status) } - override fun setParcelAllowsInteractInventory(parcel: ParcelId, value: Boolean) = job { backing.setParcelAllowsInteractInventory(parcel, value) } + override fun readAllGlobalAddedData(): ReceiveChannel> = b.openChannel { produceAllGlobalAddedData(it) } - override fun setParcelAllowsInteractInputs(parcel: ParcelId, value: Boolean) = job { backing.setParcelAllowsInteractInputs(parcel, value) } + override fun readGlobalAddedData(owner: ParcelOwner): Deferred = b.launchFuture { readGlobalAddedData(owner) } - - override fun readAllGlobalAddedData(): ReceiveChannel> = openChannel { backing.produceAllGlobalAddedData(channel) } - - override fun readGlobalAddedData(owner: ParcelOwner): Deferred = defer { backing.readGlobalAddedData(owner) } - - override fun setGlobalAddedStatus(owner: ParcelOwner, player: UUID, status: AddedStatus) = job { backing.setGlobalPlayerStatus(owner, player, status) } + override fun setGlobalAddedStatus(owner: ParcelOwner, player: UUID, status: AddedStatus) = b.launchJob { setGlobalPlayerStatus(owner, player, status) } } diff --git a/src/main/kotlin/io/dico/parcels2/storage/exposed/CoroutineTransactionManager.kt b/src/main/kotlin/io/dico/parcels2/storage/exposed/CoroutineTransactionManager.kt new file mode 100644 index 0000000..ab707af --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/storage/exposed/CoroutineTransactionManager.kt @@ -0,0 +1,118 @@ +package io.dico.parcels2.storage.exposed + +import kotlinx.coroutines.experimental.* +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.statements.StatementContext +import org.jetbrains.exposed.sql.statements.StatementInterceptor +import org.jetbrains.exposed.sql.statements.expandArgs +import org.jetbrains.exposed.sql.transactions.* +import org.slf4j.LoggerFactory +import java.sql.Connection +import kotlin.coroutines.experimental.CoroutineContext + +fun ctransaction(db: Database? = null, statement: suspend Transaction.() -> T): T { + return ctransaction(TransactionManager.manager.defaultIsolationLevel, 3, db, statement) +} + +fun ctransaction(transactionIsolation: Int, repetitionAttempts: Int, db: Database? = null, statement: suspend Transaction.() -> T): T { + return transaction(transactionIsolation, repetitionAttempts, db) { + if (this !is CoroutineTransaction) throw IllegalStateException("ctransaction requires CoroutineTransactionManager.") + + val job = async(context = manager.context, start = CoroutineStart.UNDISPATCHED) { + this@transaction.statement() + } + + if (job.isActive) { + runBlocking(context = Unconfined) { + job.join() + } + } + + job.getCompleted() + } +} + +class CoroutineTransactionManager(private val db: Database, + dispatcher: CoroutineDispatcher, + override var defaultIsolationLevel: Int = DEFAULT_ISOLATION_LEVEL) : TransactionManager { + val context: CoroutineDispatcher = TransactionCoroutineDispatcher(dispatcher) + private val transaction = ThreadLocal() + + override fun currentOrNull(): Transaction? { + + + return transaction.get() + ?: null + } + + override fun newTransaction(isolation: Int): Transaction { + return CoroutineTransaction(this, CoroutineTransactionInterface(db, isolation, transaction)).also { transaction.set(it) } + } + + private inner class TransactionCoroutineDispatcher(val delegate: CoroutineDispatcher) : CoroutineDispatcher() { + + // When the thread changes, move the transaction to the new thread + override fun dispatch(context: CoroutineContext, block: Runnable) { + val existing = transaction.get() + + val newContext: CoroutineContext + if (existing != null) { + transaction.set(null) + newContext = context // + existing + } else { + newContext = context + } + + delegate.dispatch(newContext, Runnable { + if (existing != null) { + transaction.set(existing) + } + + block.run() + }) + } + + } + +} + +private class CoroutineTransaction(val manager: CoroutineTransactionManager, + itf: CoroutineTransactionInterface) : Transaction(itf), CoroutineContext.Element { + companion object Key : CoroutineContext.Key + + override val key: CoroutineContext.Key = Key +} + +private class CoroutineTransactionInterface(override val db: Database, isolation: Int, val threadLocal: ThreadLocal) : TransactionInterface { + private val connectionLazy = lazy(LazyThreadSafetyMode.NONE) { + db.connector().apply { + autoCommit = false + transactionIsolation = isolation + } + } + override val connection: Connection + get() = connectionLazy.value + + override val outerTransaction: CoroutineTransaction? = threadLocal.get() + + override fun commit() { + if (connectionLazy.isInitialized()) + connection.commit() + } + + override fun rollback() { + if (connectionLazy.isInitialized() && !connection.isClosed) { + connection.rollback() + } + } + + override fun close() { + try { + if (connectionLazy.isInitialized()) connection.close() + } finally { + threadLocal.set(outerTransaction) + } + } + +} + diff --git a/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedBacking.kt b/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedBacking.kt index 5685346..01afd94 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedBacking.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedBacking.kt @@ -1,4 +1,4 @@ -@file:Suppress("NOTHING_TO_INLINE", "PARAMETER_NAME_CHANGED_ON_OVERRIDE", "LocalVariableName") +@file:Suppress("NOTHING_TO_INLINE", "PARAMETER_NAME_CHANGED_ON_OVERRIDE", "LocalVariableName", "UNUSED_EXPRESSION") package io.dico.parcels2.storage.exposed @@ -7,10 +7,10 @@ import io.dico.parcels2.* import io.dico.parcels2.storage.Backing import io.dico.parcels2.storage.DataPair import io.dico.parcels2.util.toUUID -import kotlinx.coroutines.experimental.CoroutineStart -import kotlinx.coroutines.experimental.Unconfined +import kotlinx.coroutines.experimental.* +import kotlinx.coroutines.experimental.channels.LinkedListChannel +import kotlinx.coroutines.experimental.channels.ReceiveChannel import kotlinx.coroutines.experimental.channels.SendChannel -import kotlinx.coroutines.experimental.launch import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SchemaUtils.create import org.jetbrains.exposed.sql.transactions.transaction @@ -21,14 +21,27 @@ import javax.sql.DataSource class ExposedDatabaseException(message: String? = null) : Exception(message) -class ExposedBacking(private val dataSourceFactory: suspend () -> DataSource) : Backing { +class ExposedBacking(private val dataSourceFactory: () -> DataSource, + private val poolSize: Int) : Backing { override val name get() = "Exposed" + val dispatcher: CoroutineDispatcher = newFixedThreadPoolContext(4, "Parcels StorageThread") + private var dataSource: DataSource? = null private var database: Database? = null private var isShutdown: Boolean = false - override val isConnected get() = database != null + override fun launchJob(job: Backing.() -> Unit): Job = launch(dispatcher) { transaction { job() } } + override fun launchFuture(future: Backing.() -> T): Deferred = async(dispatcher) { transaction { future() } } + + override fun openChannel(future: Backing.(SendChannel) -> Unit): ReceiveChannel { + val channel = LinkedListChannel() + launchJob { future(channel) } + return channel + } + + private fun transaction(statement: Transaction.() -> T) = transaction(database!!, statement) + companion object { init { Database.registerDialect("mariadb") { @@ -37,24 +50,17 @@ class ExposedBacking(private val dataSourceFactory: suspend () -> DataSource) : } } - private fun transaction(statement: Transaction.() -> T) = transaction(database!!, statement) - - private suspend fun transactionLaunch(statement: suspend Transaction.() -> Unit): Unit = transaction(database!!) { - launch(context = Unconfined, start = CoroutineStart.UNDISPATCHED) { - statement(this@transaction) - } - } + override fun init() { + if (isShutdown || isConnected) throw IllegalStateException() - override suspend fun init() { - if (isShutdown) throw IllegalStateException() dataSource = dataSourceFactory() database = Database.connect(dataSource!!) - transaction(database) { + transaction(database!!) { create(WorldsT, OwnersT, ParcelsT, ParcelOptionsT, AddedLocalT, AddedGlobalT) } } - override suspend fun shutdown() { + override fun shutdown() { if (isShutdown) throw IllegalStateException() dataSource?.let { (it as? HikariDataSource)?.close() @@ -63,15 +69,15 @@ class ExposedBacking(private val dataSourceFactory: suspend () -> DataSource) : isShutdown = true } - override suspend fun produceParcelData(channel: SendChannel, parcels: Sequence) { + override fun produceParcelData(channel: SendChannel, parcels: Sequence) { for (parcel in parcels) { val data = readParcelData(parcel) - channel.send(parcel to data) + channel.offer(parcel to data) } channel.close() } - override suspend fun produceAllParcelData(channel: SendChannel>) = transactionLaunch { + override fun produceAllParcelData(channel: SendChannel>) = ctransaction { ParcelsT.selectAll().forEach { row -> val parcel = ParcelsT.getId(row) ?: return@forEach val data = rowToParcelData(row) @@ -80,12 +86,12 @@ class ExposedBacking(private val dataSourceFactory: suspend () -> DataSource) : channel.close() } - override suspend fun readParcelData(parcel: ParcelId): ParcelData? = transaction { + override fun readParcelData(parcel: ParcelId): ParcelData? = transaction { val row = ParcelsT.getRow(parcel) ?: return@transaction null rowToParcelData(row) } - override suspend fun getOwnedParcels(user: ParcelOwner): List = transaction { + override fun getOwnedParcels(user: ParcelOwner): List = transaction { val user_id = OwnersT.getId(user) ?: return@transaction emptyList() ParcelsT.select { ParcelsT.owner_id eq user_id } .orderBy(ParcelsT.claim_time, isAsc = true) @@ -93,7 +99,7 @@ class ExposedBacking(private val dataSourceFactory: suspend () -> DataSource) : .toList() } - override suspend fun setParcelData(parcel: ParcelId, data: ParcelData?) { + override fun setParcelData(parcel: ParcelId, data: ParcelData?) { if (data == null) { transaction { ParcelsT.getId(parcel)?.let { id -> @@ -125,7 +131,7 @@ class ExposedBacking(private val dataSourceFactory: suspend () -> DataSource) : setParcelAllowsInteractInventory(parcel, data.allowInteractInventory) } - override suspend fun setParcelOwner(parcel: ParcelId, owner: ParcelOwner?) = transaction { + override fun setParcelOwner(parcel: ParcelId, owner: ParcelOwner?) = transaction { val id = if (owner == null) ParcelsT.getId(parcel) ?: return@transaction else @@ -140,11 +146,11 @@ class ExposedBacking(private val dataSourceFactory: suspend () -> DataSource) : } } - override suspend fun setLocalPlayerStatus(parcel: ParcelId, player: UUID, status: AddedStatus) = transaction { + override fun setLocalPlayerStatus(parcel: ParcelId, player: UUID, status: AddedStatus) = transaction { AddedLocalT.setPlayerStatus(parcel, player, status) } - override suspend fun setParcelAllowsInteractInventory(parcel: ParcelId, value: Boolean): Unit = transaction { + override fun setParcelAllowsInteractInventory(parcel: ParcelId, value: Boolean): Unit = transaction { val id = ParcelsT.getOrInitId(parcel) ParcelOptionsT.upsert(ParcelOptionsT.parcel_id) { it[ParcelOptionsT.parcel_id] = id @@ -152,7 +158,7 @@ class ExposedBacking(private val dataSourceFactory: suspend () -> DataSource) : } } - override suspend fun setParcelAllowsInteractInputs(parcel: ParcelId, value: Boolean): Unit = transaction { + override fun setParcelAllowsInteractInputs(parcel: ParcelId, value: Boolean): Unit = transaction { val id = ParcelsT.getOrInitId(parcel) ParcelOptionsT.upsert(ParcelOptionsT.parcel_id) { it[ParcelOptionsT.parcel_id] = id @@ -160,16 +166,16 @@ class ExposedBacking(private val dataSourceFactory: suspend () -> DataSource) : } } - override suspend fun produceAllGlobalAddedData(channel: SendChannel>>) = transactionLaunch { + override fun produceAllGlobalAddedData(channel: SendChannel>>) = ctransaction { AddedGlobalT.sendAllAddedData(channel) channel.close() } - override suspend fun readGlobalAddedData(owner: ParcelOwner): MutableMap = transaction { + override fun readGlobalAddedData(owner: ParcelOwner): MutableMap = transaction { return@transaction AddedGlobalT.readAddedData(OwnersT.getId(owner) ?: return@transaction hashMapOf()) } - override suspend fun setGlobalPlayerStatus(owner: ParcelOwner, player: UUID, status: AddedStatus) = transaction { + override fun setGlobalPlayerStatus(owner: ParcelOwner, player: UUID, status: AddedStatus) = transaction { AddedGlobalT.setPlayerStatus(owner, player, status) } diff --git a/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedExtensions.kt b/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedExtensions.kt index 9f7f599..ce66644 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedExtensions.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedExtensions.kt @@ -6,6 +6,7 @@ import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.Transaction import org.jetbrains.exposed.sql.statements.InsertStatement import org.jetbrains.exposed.sql.transactions.TransactionManager +import org.jetbrains.exposed.sql.transactions.transaction class UpsertStatement(table: Table, conflictColumn: Column<*>? = null, conflictIndex: Index? = null) : InsertStatement(table, false) { @@ -61,3 +62,4 @@ fun Table.indexR(customIndexName: String? = null, isUnique: Boolean = false, var } fun Table.uniqueIndexR(customIndexName: String? = null, vararg columns: Column<*>): Index = indexR(customIndexName, true, *columns) + diff --git a/src/main/kotlin/io/dico/parcels2/storage/migration/Migration.kt b/src/main/kotlin/io/dico/parcels2/storage/migration/Migration.kt index c8bc93c..0db669a 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/migration/Migration.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/migration/Migration.kt @@ -1,8 +1,9 @@ package io.dico.parcels2.storage.migration import io.dico.parcels2.storage.Storage +import kotlinx.coroutines.experimental.Job interface Migration { - fun migrateTo(storage: Storage) + fun migrateTo(storage: Storage): Job } diff --git a/src/main/kotlin/io/dico/parcels2/storage/migration/plotme/PlotmeMigration.kt b/src/main/kotlin/io/dico/parcels2/storage/migration/plotme/PlotmeMigration.kt index e5b7d9d..1f6e49c 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/migration/plotme/PlotmeMigration.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/migration/plotme/PlotmeMigration.kt @@ -1,50 +1,50 @@ +@file:Suppress("RedundantSuspendModifier", "DEPRECATION") + package io.dico.parcels2.storage.migration.plotme import com.zaxxer.hikari.HikariDataSource import io.dico.parcels2.* +import io.dico.parcels2.options.PlotmeMigrationOptions import io.dico.parcels2.storage.Storage import io.dico.parcels2.storage.migration.Migration import io.dico.parcels2.util.Vec2i import io.dico.parcels2.util.isValid import io.dico.parcels2.util.toUUID import io.dico.parcels2.util.uuid -import kotlinx.coroutines.experimental.asCoroutineDispatcher -import kotlinx.coroutines.experimental.launch +import kotlinx.coroutines.experimental.* import org.bukkit.Bukkit import org.jetbrains.exposed.sql.* import org.slf4j.LoggerFactory import java.io.ByteArrayOutputStream import java.sql.Blob import java.util.UUID -import java.util.concurrent.Executors +import java.util.concurrent.ConcurrentHashMap import javax.sql.DataSource +import kotlin.coroutines.experimental.coroutineContext -class PlotmeMigration(val parcelProvider: ParcelProvider, - val worldMapper: Map, - val dataSourceFactory: () -> DataSource) : Migration { +class PlotmeMigration(val options: PlotmeMigrationOptions) : Migration { private var dataSource: DataSource? = null private var database: Database? = null private var isShutdown: Boolean = false - private val dispatcher = Executors.newSingleThreadExecutor { Thread(it, "PlotMe Migration Thread") }.asCoroutineDispatcher() private val mlogger = LoggerFactory.getLogger("PlotMe Migrator") private fun transaction(statement: Transaction.() -> T) = org.jetbrains.exposed.sql.transactions.transaction(database!!, statement) - override fun migrateTo(storage: Storage) { - launch(context = dispatcher) { + override fun migrateTo(storage: Storage): Job { + return launch(context = storage.asyncDispatcher) { init() - doWork(storage) + transaction { launch(context = Unconfined, start = CoroutineStart.UNDISPATCHED) { doWork(storage) } } shutdown() } } - fun init() { + suspend fun init() { if (isShutdown) throw IllegalStateException() - dataSource = dataSourceFactory() + dataSource = options.storage.getDataSourceFactory()!!() database = Database.connect(dataSource!!) } - fun shutdown() { + suspend fun shutdown() { if (isShutdown) throw IllegalStateException() dataSource?.let { (it as? HikariDataSource)?.close() @@ -53,22 +53,23 @@ class PlotmeMigration(val parcelProvider: ParcelProvider, isShutdown = true } - val parcelsCache = hashMapOf>() + private val parcelsCache = hashMapOf>() private fun getMap(worldName: String): MutableMap? { - val mapped = worldMapper[worldName] ?: return null + val mapped = options.worldsFromTo[worldName] ?: return null return parcelsCache.computeIfAbsent(mapped) { mutableMapOf() } } private fun getData(worldName: String, position: Vec2i): ParcelData? { - return getMap(worldName)?.computeIfAbsent(position) { ParcelDataHolder() } + return getMap(worldName)?.computeIfAbsent(position) { ParcelDataHolder(addedMap = ConcurrentHashMap()) } } - fun doWork(target: Storage): Unit = transaction { + suspend fun doWork(target: Storage): Unit { if (!PlotmePlotsT.exists()) { mlogger.warn("Plotme tables don't appear to exist. Exiting.") - return@transaction + return } + parcelsCache.clear() iterPlotmeTable(PlotmePlotsT) { data, row -> @@ -76,22 +77,29 @@ class PlotmeMigration(val parcelProvider: ParcelProvider, data.owner = ParcelOwner(row[owner_uuid]?.toUUID(), row[owner_name]) } - iterPlotmeTable(PlotmeAllowedT) { data, row -> - val uuid = row[player_uuid]?.toUUID() - ?: Bukkit.getOfflinePlayer(row[player_name]).takeIf { it.isValid }?.uuid - ?: return@iterPlotmeTable + launch(context = target.asyncDispatcher) { + iterPlotmeTable(PlotmeAllowedT) { data, row -> + val uuid = row[player_uuid]?.toUUID() + ?: Bukkit.getOfflinePlayer(row[player_name]).takeIf { it.isValid }?.uuid + ?: return@iterPlotmeTable - data.setAddedStatus(uuid, AddedStatus.ALLOWED) + data.setAddedStatus(uuid, AddedStatus.ALLOWED) + } } - iterPlotmeTable(PlotmeDeniedT) { data, row -> - val uuid = row[PlotmeAllowedT.player_uuid]?.toUUID() - ?: Bukkit.getOfflinePlayer(row[PlotmeAllowedT.player_name]).takeIf { it.isValid }?.uuid - ?: return@iterPlotmeTable + launch(context = target.asyncDispatcher) { + iterPlotmeTable(PlotmeDeniedT) { data, row -> + val uuid = row[player_uuid]?.toUUID() + ?: Bukkit.getOfflinePlayer(row[player_name]).takeIf { it.isValid }?.uuid + ?: return@iterPlotmeTable - data.setAddedStatus(uuid, AddedStatus.BANNED) + data.setAddedStatus(uuid, AddedStatus.BANNED) + } } + println(coroutineContext[Job]!!.children) + coroutineContext[Job]!!.joinChildren() + for ((worldName, map) in parcelsCache) { val world = ParcelWorldId(worldName) for ((pos, data) in map) { diff --git a/src/main/kotlin/io/dico/parcels2/storage/migration/plotme/PlotmeTables.kt b/src/main/kotlin/io/dico/parcels2/storage/migration/plotme/PlotmeTables.kt index 3d07955..8564ad3 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/migration/plotme/PlotmeTables.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/migration/plotme/PlotmeTables.kt @@ -7,9 +7,9 @@ const val uppercase: Boolean = false fun String.toCorrectCase() = if (uppercase) this else toLowerCase() sealed class PlotmeTable(name: String) : Table(name) { - val px = PlotmePlotsT.integer("idX").primaryKey() - val pz = PlotmePlotsT.integer("idZ").primaryKey() - val world_name = PlotmePlotsT.varchar("world", 32).primaryKey() + val px = integer("idX").primaryKey() + val pz = integer("idZ").primaryKey() + val world_name = varchar("world", 32).primaryKey() } object PlotmePlotsT : PlotmeTable("plotmePlots".toCorrectCase()) { @@ -18,8 +18,8 @@ object PlotmePlotsT : PlotmeTable("plotmePlots".toCorrectCase()) { } sealed class PlotmePlotPlayerMap(name: String) : PlotmeTable(name) { - val player_name = PlotmePlotsT.varchar("player", 32) - val player_uuid = PlotmePlotsT.blob("playerid").nullable() + val player_name = varchar("player", 32) + val player_uuid = blob("playerid").nullable() } object PlotmeAllowedT : PlotmePlotPlayerMap("plotmeAllowed".toCorrectCase()) diff --git a/src/main/kotlin/io/dico/parcels2/util/MiscExtensions.kt b/src/main/kotlin/io/dico/parcels2/util/MiscExtensions.kt index 7a2504d..a4ab58d 100644 --- a/src/main/kotlin/io/dico/parcels2/util/MiscExtensions.kt +++ b/src/main/kotlin/io/dico/parcels2/util/MiscExtensions.kt @@ -4,9 +4,12 @@ import io.dico.parcels2.logger import java.io.File fun File.tryCreate(): Boolean { + if (exists()) { + return !isDirectory + } val parent = parentFile if (parent == null || !(parent.exists() || parent.mkdirs()) || !createNewFile()) { - logger.warn("Failed to create file ${canonicalPath}") + logger.warn("Failed to create file $canonicalPath") return false } return true diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 65186e5..c2c9ddb 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -6,7 +6,36 @@ - + + + + true + C:/Parcels/sql.log + + + + %magenta(%-8.-8(%thread)) %highlight(%-5level) %boldCyan(%6.-32logger{32}) - %msg + + + + C:/Parcels/sql%i.log + 1 + 3 + + + + 1MB + + + + + + true + + + + + \ No newline at end of file -- cgit v1.2.3 From ba347a805333b84f860150cbf0b8c514472c6f9f Mon Sep 17 00:00:00 2001 From: Dico Date: Sun, 5 Aug 2018 06:46:11 +0100 Subject: Things be working --- src/main/kotlin/io/dico/parcels2/AddedData.kt | 73 +++--- src/main/kotlin/io/dico/parcels2/Parcel.kt | 6 +- .../kotlin/io/dico/parcels2/ParcelGenerator.kt | 7 +- src/main/kotlin/io/dico/parcels2/ParcelOwner.kt | 52 ---- src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt | 1 + src/main/kotlin/io/dico/parcels2/PlayerProfile.kt | 287 +++++++++++++++++++++ .../parcels2/command/AbstractParcelCommands.kt | 4 +- .../parcels2/command/CommandsAddedStatusGlobal.kt | 4 +- .../io/dico/parcels2/command/CommandsGeneral.kt | 6 +- .../io/dico/parcels2/command/ParcelTarget.kt | 54 ++-- .../parcels2/defaultimpl/DefaultParcelGenerator.kt | 16 +- .../defaultimpl/GlobalAddedDataManagerImpl.kt | 15 +- .../io/dico/parcels2/defaultimpl/ParcelImpl.kt | 27 +- .../parcels2/defaultimpl/ParcelProviderImpl.kt | 4 +- .../io/dico/parcels2/listener/ParcelListeners.kt | 5 +- .../kotlin/io/dico/parcels2/storage/Backing.kt | 41 ++- .../kotlin/io/dico/parcels2/storage/Storage.kt | 59 +++-- .../storage/exposed/CoroutineTransactionManager.kt | 118 --------- .../parcels2/storage/exposed/ExposedBacking.kt | 145 +++++++---- .../parcels2/storage/exposed/ExposedExtensions.kt | 19 +- .../io/dico/parcels2/storage/exposed/IdTables.kt | 85 +++--- .../io/dico/parcels2/storage/exposed/ListTables.kt | 60 ++--- .../storage/migration/plotme/PlotmeMigration.kt | 115 ++++----- .../kotlin/io/dico/parcels2/util/MiscExtensions.kt | 3 + .../io/dico/parcels2/util/PlayerExtensions.kt | 2 + src/main/resources/logback.xml | 38 +-- 26 files changed, 709 insertions(+), 537 deletions(-) delete mode 100644 src/main/kotlin/io/dico/parcels2/ParcelOwner.kt create mode 100644 src/main/kotlin/io/dico/parcels2/PlayerProfile.kt delete mode 100644 src/main/kotlin/io/dico/parcels2/storage/exposed/CoroutineTransactionManager.kt diff --git a/src/main/kotlin/io/dico/parcels2/AddedData.kt b/src/main/kotlin/io/dico/parcels2/AddedData.kt index 633fe72..9249c7f 100644 --- a/src/main/kotlin/io/dico/parcels2/AddedData.kt +++ b/src/main/kotlin/io/dico/parcels2/AddedData.kt @@ -1,41 +1,50 @@ package io.dico.parcels2 -import io.dico.parcels2.util.uuid import org.bukkit.OfflinePlayer -import java.util.UUID -typealias MutableAddedDataMap = MutableMap -typealias AddedDataMap = Map +typealias StatusKey = PlayerProfile.Real +typealias MutableAddedDataMap = MutableMap +typealias AddedDataMap = Map + +@Suppress("FunctionName") +fun MutableAddedDataMap(): MutableAddedDataMap = hashMapOf() interface AddedData { val addedMap: AddedDataMap + var addedStatusOfStar: AddedStatus + + fun getAddedStatus(key: StatusKey): AddedStatus + fun setAddedStatus(key: StatusKey, status: AddedStatus): Boolean + + fun compareAndSetAddedStatus(key: StatusKey, expect: AddedStatus, status: AddedStatus): Boolean = + (getAddedStatus(key) == expect).also { if (it) setAddedStatus(key, status) } + + fun isAllowed(key: StatusKey) = getAddedStatus(key) == AddedStatus.ALLOWED + fun allow(key: StatusKey) = setAddedStatus(key, AddedStatus.ALLOWED) + fun disallow(key: StatusKey) = compareAndSetAddedStatus(key, AddedStatus.ALLOWED, AddedStatus.DEFAULT) + fun isBanned(key: StatusKey) = getAddedStatus(key) == AddedStatus.BANNED + fun ban(key: StatusKey) = setAddedStatus(key, AddedStatus.BANNED) + fun unban(key: StatusKey) = compareAndSetAddedStatus(key, AddedStatus.BANNED, AddedStatus.DEFAULT) - fun getAddedStatus(uuid: UUID): AddedStatus - fun setAddedStatus(uuid: UUID, status: AddedStatus): Boolean - - fun compareAndSetAddedStatus(uuid: UUID, expect: AddedStatus, status: AddedStatus): Boolean = - (getAddedStatus(uuid) == expect).also { if (it) setAddedStatus(uuid, status) } - - fun isAllowed(uuid: UUID) = getAddedStatus(uuid) == AddedStatus.ALLOWED - fun allow(uuid: UUID) = setAddedStatus(uuid, AddedStatus.ALLOWED) - fun disallow(uuid: UUID) = compareAndSetAddedStatus(uuid, AddedStatus.ALLOWED, AddedStatus.DEFAULT) - fun isBanned(uuid: UUID) = getAddedStatus(uuid) == AddedStatus.BANNED - fun ban(uuid: UUID) = setAddedStatus(uuid, AddedStatus.BANNED) - fun unban(uuid: UUID) = compareAndSetAddedStatus(uuid, AddedStatus.BANNED, AddedStatus.DEFAULT) - - fun isAllowed(player: OfflinePlayer) = isAllowed(player.uuid) - fun allow(player: OfflinePlayer) = allow(player.uuid) - fun disallow(player: OfflinePlayer) = disallow(player.uuid) - fun isBanned(player: OfflinePlayer) = isBanned(player.uuid) - fun ban(player: OfflinePlayer) = ban(player.uuid) - fun unban(player: OfflinePlayer) = unban(player.uuid) + fun isAllowed(player: OfflinePlayer) = isAllowed(player.statusKey) + fun allow(player: OfflinePlayer) = allow(player.statusKey) + fun disallow(player: OfflinePlayer) = disallow(player.statusKey) + fun isBanned(player: OfflinePlayer) = isBanned(player.statusKey) + fun ban(player: OfflinePlayer) = ban(player.statusKey) + fun unban(player: OfflinePlayer) = unban(player.statusKey) } +inline val OfflinePlayer.statusKey get() = PlayerProfile.nameless(this) + open class AddedDataHolder(override var addedMap: MutableAddedDataMap = mutableMapOf()) : AddedData { - override fun getAddedStatus(uuid: UUID): AddedStatus = addedMap.getOrDefault(uuid, AddedStatus.DEFAULT) - override fun setAddedStatus(uuid: UUID, status: AddedStatus): Boolean = status.takeIf { it != AddedStatus.DEFAULT } - ?.let { addedMap.put(uuid, it) != it } - ?: addedMap.remove(uuid) != null + override var addedStatusOfStar: AddedStatus = AddedStatus.DEFAULT + + override fun getAddedStatus(key: StatusKey): AddedStatus = addedMap.getOrDefault(key, addedStatusOfStar) + + override fun setAddedStatus(key: StatusKey, status: AddedStatus): Boolean { + return if (status.isDefault) addedMap.remove(key) != null + else addedMap.put(key, status) != status + } } enum class AddedStatus { @@ -43,15 +52,15 @@ enum class AddedStatus { ALLOWED, BANNED; - val isDefault get() = this == DEFAULT - val isAllowed get() = this == ALLOWED - val isBanned get() = this == BANNED + inline val isDefault get() = this == DEFAULT + inline val isAllowed get() = this == ALLOWED + inline val isBanned get() = this == BANNED } interface GlobalAddedData : AddedData { - val owner: ParcelOwner + val owner: PlayerProfile } interface GlobalAddedDataManager { - operator fun get(owner: ParcelOwner): GlobalAddedData + operator fun get(owner: PlayerProfile): GlobalAddedData } diff --git a/src/main/kotlin/io/dico/parcels2/Parcel.kt b/src/main/kotlin/io/dico/parcels2/Parcel.kt index 6505a49..e7c5209 100644 --- a/src/main/kotlin/io/dico/parcels2/Parcel.kt +++ b/src/main/kotlin/io/dico/parcels2/Parcel.kt @@ -33,7 +33,7 @@ interface Parcel : ParcelData { } interface ParcelData : AddedData { - var owner: ParcelOwner? + var owner: PlayerProfile? val since: DateTime? fun canBuild(player: OfflinePlayer, checkAdmin: Boolean = true, checkGlobal: Boolean = true): Boolean @@ -48,9 +48,9 @@ interface ParcelData : AddedData { class ParcelDataHolder(addedMap: MutableAddedDataMap = mutableMapOf()) : AddedDataHolder(addedMap), ParcelData { - override var owner: ParcelOwner? = null + override var owner: PlayerProfile? = null override var since: DateTime? = null - override fun canBuild(player: OfflinePlayer, checkAdmin: Boolean, checkGlobal: Boolean) = isAllowed(player.uniqueId) + override fun canBuild(player: OfflinePlayer, checkAdmin: Boolean, checkGlobal: Boolean) = isAllowed(player.statusKey) || owner.let { it != null && it.matches(player, allowNameMatch = false) } || (checkAdmin && player is Player && player.hasBuildAnywhere) diff --git a/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt b/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt index 6f504d0..b9d2e3d 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt @@ -3,8 +3,6 @@ package io.dico.parcels2 import io.dico.parcels2.blockvisitor.RegionTraversal import io.dico.parcels2.blockvisitor.Worker import io.dico.parcels2.blockvisitor.WorktimeLimiter -import io.dico.parcels2.defaultimpl.DefaultParcelGenerator -import io.dico.parcels2.options.GeneratorOptions import io.dico.parcels2.util.Vec2i import org.bukkit.Chunk import org.bukkit.Location @@ -14,9 +12,7 @@ import org.bukkit.block.Block import org.bukkit.entity.Entity import org.bukkit.generator.BlockPopulator import org.bukkit.generator.ChunkGenerator -import java.util.HashMap import java.util.Random -import kotlin.reflect.KClass abstract class ParcelGenerator : ChunkGenerator() { abstract val world: World @@ -40,6 +36,7 @@ abstract class ParcelGenerator : ChunkGenerator() { abstract fun makeParcelLocator(container: ParcelContainer): ParcelLocator } +@Suppress("DeprecatedCallableAddReplaceWith") interface ParcelBlockManager { val world: World val worktimeLimiter: WorktimeLimiter @@ -48,7 +45,7 @@ interface ParcelBlockManager { fun getHomeLocation(parcel: ParcelId): Location - fun setOwnerBlock(parcel: ParcelId, owner: ParcelOwner?) + fun setOwnerBlock(parcel: ParcelId, owner: PlayerProfile?) @Deprecated("") fun getEntities(parcel: ParcelId): Collection = TODO() diff --git a/src/main/kotlin/io/dico/parcels2/ParcelOwner.kt b/src/main/kotlin/io/dico/parcels2/ParcelOwner.kt deleted file mode 100644 index 5a36cac..0000000 --- a/src/main/kotlin/io/dico/parcels2/ParcelOwner.kt +++ /dev/null @@ -1,52 +0,0 @@ -@file:Suppress("unused", "UsePropertyAccessSyntax", "DEPRECATION") - -package io.dico.parcels2 - -import io.dico.parcels2.util.getPlayerNameOrDefault -import io.dico.parcels2.util.isValid -import io.dico.parcels2.util.uuid -import org.bukkit.Bukkit -import org.bukkit.OfflinePlayer -import org.bukkit.entity.Player -import java.util.UUID - -class ParcelOwner(val uuid: UUID?, - val name: String?) { - val notNullName: String by lazy { name ?: getPlayerNameOrDefault(uuid!!) } - - constructor(name: String) : this(null, name) - constructor(uuid: UUID) : this(uuid, null) - constructor(player: OfflinePlayer) : this(player.uuid, player.name) - - companion object { - fun nameless(player: OfflinePlayer) = ParcelOwner(player.uuid, null) - } - - inline val hasUUID: Boolean get() = uuid != null - - val onlinePlayer: Player? get() = uuid?.let { Bukkit.getPlayer(uuid) } - - val onlinePlayerAllowingNameMatch: Player? get() = onlinePlayer ?: name?.let { Bukkit.getPlayerExact(name) } - val offlinePlayer: OfflinePlayer? get() = uuid?.let { Bukkit.getOfflinePlayer(it).takeIf { it.isValid } } - val offlinePlayerAllowingNameMatch: OfflinePlayer? - get() = offlinePlayer ?: Bukkit.getOfflinePlayer(name).takeIf { it.isValid } - - fun matches(player: OfflinePlayer, allowNameMatch: Boolean = false): Boolean { - return uuid?.let { it == player.uniqueId } ?: false - || (allowNameMatch && name?.let { it == player.name } ?: false) - } - - fun equals(other: ParcelOwner): Boolean { - return if (hasUUID) other.hasUUID && uuid == other.uuid - else !other.hasUUID && name == other.name - } - - override fun equals(other: Any?): Boolean { - return other is ParcelOwner && equals(other) - } - - override fun hashCode(): Int { - return if (hasUUID) uuid!!.hashCode() else name!!.hashCode() - } - -} \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt b/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt index f0b5fbc..e9c70f6 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt @@ -42,6 +42,7 @@ class ParcelsPlugin : JavaPlugin() { override fun onEnable() { plogger.info("Debug enabled: ${plogger.isDebugEnabled}") + plogger.debug(System.getProperty("user.dir")) if (!init()) { Bukkit.getPluginManager().disablePlugin(this) } diff --git a/src/main/kotlin/io/dico/parcels2/PlayerProfile.kt b/src/main/kotlin/io/dico/parcels2/PlayerProfile.kt new file mode 100644 index 0000000..c5403cd --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/PlayerProfile.kt @@ -0,0 +1,287 @@ +@file:Suppress("unused", "UsePropertyAccessSyntax", "DEPRECATION") + +package io.dico.parcels2 + +import io.dico.parcels2.storage.Storage +import io.dico.parcels2.util.getPlayerNameOrDefault +import io.dico.parcels2.util.isValid +import io.dico.parcels2.util.uuid +import kotlinx.coroutines.experimental.Deferred +import kotlinx.coroutines.experimental.Unconfined +import kotlinx.coroutines.experimental.async +import org.bukkit.Bukkit +import org.bukkit.OfflinePlayer +import java.util.UUID + +interface PlayerProfile { + val uuid: UUID? get() = null + val name: String? + val notNullName: String + val isStar: Boolean get() = false + + fun matches(player: OfflinePlayer, allowNameMatch: Boolean = false): Boolean + + fun equals(other: PlayerProfile): Boolean + + override fun equals(other: Any?): Boolean + override fun hashCode(): Int + + val isFake: Boolean get() = this is Fake + val isReal: Boolean get() = this is Real + + companion object { + fun safe(uuid: UUID?, name: String?): PlayerProfile? { + if (uuid != null) return Real(uuid, name) + if (name != null) return invoke(name) + return null + } + + operator fun invoke(uuid: UUID?, name: String?): PlayerProfile { + return safe(uuid, name) ?: throw IllegalArgumentException("One of uuid and name must not be null") + } + + operator fun invoke(uuid: UUID): Real { + if (uuid == Star.uuid) return Star + return RealImpl(uuid, null) + } + + operator fun invoke(name: String): PlayerProfile { + if (name == Star.name) return Star + return Fake(name) + } + + operator fun invoke(player: OfflinePlayer): PlayerProfile { + return if (player.isValid) Real(player.uuid, player.name) else Fake(player.name) + } + + fun nameless(player: OfflinePlayer): Real { + if (!player.isValid) throw IllegalArgumentException("The given OfflinePlayer is not valid") + return RealImpl(player.uuid, null) + } + + fun byName(input: String, allowReal: Boolean = true, allowFake: Boolean = false): PlayerProfile { + if (!allowReal) { + if (!allowFake) throw IllegalArgumentException("at least one of allowReal and allowFake must be true") + return Fake(input) + } + + if (input == Star.name) return Star + + return Bukkit.getOfflinePlayer(input).takeIf { it.isValid }?.let { PlayerProfile(it) } + ?: Unresolved(input) + } + } + + interface Real : PlayerProfile { + override val uuid: UUID + override val notNullName: String + get() = name ?: getPlayerNameOrDefault(uuid) + + val player: OfflinePlayer? get() = Bukkit.getOfflinePlayer(uuid).takeIf { it.isValid } + val playerUnchecked: OfflinePlayer get() = Bukkit.getOfflinePlayer(uuid) + + override fun matches(player: OfflinePlayer, allowNameMatch: Boolean): Boolean { + return uuid == player.uuid || (allowNameMatch && name?.let { it == player.name } == true) + } + + override fun equals(other: PlayerProfile): Boolean { + return other is Real && uuid == other.uuid + } + + companion object { + fun byName(name: String): PlayerProfile { + if (name == Star.name) return Star + return Unresolved(name) + } + + operator fun invoke(uuid: UUID, name: String?): Real { + if (name == Star.name || uuid == Star.uuid) return Star + return RealImpl(uuid, name) + } + + fun safe(uuid: UUID?, name: String?): Real? { + if (name == Star.name || uuid == Star.uuid) return Star + if (uuid == null) return null + return RealImpl(uuid, name) + } + + } + } + + object Star : BaseImpl(), Real { + override val name: String = "*" + override val uuid: UUID = UUID.fromString("7d09c4c6-117d-4f36-9778-c4d24618cee1") + override val isStar: Boolean get() = true + + override fun matches(player: OfflinePlayer, allowNameMatch: Boolean): Boolean { + return true + } + } + + abstract class NameOnly(override val name: String) : BaseImpl() { + override val notNullName get() = name + + override fun matches(player: OfflinePlayer, allowNameMatch: Boolean): Boolean { + return allowNameMatch && player.name == name + } + } + + class Fake(name: String) : NameOnly(name) { + override fun equals(other: PlayerProfile): Boolean { + return other is Fake && other.name == name + } + } + + class Unresolved(name: String) : NameOnly(name) { + override fun equals(other: PlayerProfile): Boolean { + return other is Unresolved && name == other.name + } + + fun tryResolve(storage: Storage): Deferred { + return async(Unconfined) { tryResolveSuspendedly(storage) } + } + + suspend fun tryResolveSuspendedly(storage: Storage): Real? { + return storage.getPlayerUuidForName(name).await()?.let { RealImpl(it, name) } + } + + fun resolve(uuid: UUID): Real { + return RealImpl(uuid, name) + } + + fun throwException(): Nothing { + throw IllegalArgumentException("A UUID for the player $name can not be found") + } + } + + abstract class BaseImpl : PlayerProfile { + override fun equals(other: Any?): Boolean { + return this === other || (other is PlayerProfile && equals(other)) + } + + override fun hashCode(): Int { + return uuid?.hashCode() ?: name!!.hashCode() + } + } + + private class RealImpl(override val uuid: UUID, override val name: String?) : BaseImpl(), Real + +} + + +/* + + +/** + * This class can represent: + * + * An existing player + * A fake player (with only a name) + * An existing player who must have its uuid resolved from the database (after checking against Bukkit OfflinePlayer) + * STAR profile, which matches everyone. This profile is considered a REAL player, because it can have an added status. + */ +class PlayerProfile2 private constructor(uuid: UUID?, + val name: String?, + val isReal: Boolean = uuid != null) { + private var _uuid: UUID? = uuid + val notNullName: String get() = name ?: getPlayerNameOrDefault(uuid!!) + + val uuid: UUID? get() = _uuid ?: if (isReal) throw IllegalStateException("This PlayerProfile must be resolved first") else null + + companion object { + // below uuid is just a randomly generated one (version 4). Hopefully no minecraft player will ever have it :) + val star = PlayerProfile(UUID.fromString("7d09c4c6-117d-4f36-9778-c4d24618cee1"), "*", true) + + fun nameless(player: OfflinePlayer): PlayerProfile { + if (!player.isValid) throw IllegalArgumentException("The given OfflinePlayer is not valid") + return PlayerProfile(player.uuid) + } + + fun fromNameAndUuid(name: String?, uuid: UUID?): PlayerProfile? { + if (name == null && uuid == null) return null + if (star.name == name && star._uuid == uuid) return star + return PlayerProfile(uuid, name) + } + + fun realPlayerByName(name: String): PlayerProfile { + return fromString(name, allowReal = true, allowFake = false) + } + + fun fromString(input: String, allowReal: Boolean = true, allowFake: Boolean = false): PlayerProfile { + if (!allowReal) { + if (!allowFake) throw IllegalArgumentException("at least one of allowReal and allowFake must be true") + return PlayerProfile(input) + } + + if (input == star.name) return star + + return Bukkit.getOfflinePlayer(input).takeIf { it.isValid }?.let { PlayerProfile(it) } + ?: PlayerProfile(null, input, !allowFake) + } + + operator fun invoke(name: String): PlayerProfile { + if (name == star.name) return star + return PlayerProfile(null, name) + } + + operator fun invoke(uuid: UUID): PlayerProfile { + if (uuid == star.uuid) return star + return PlayerProfile(uuid, null) + } + + operator fun invoke(player: OfflinePlayer): PlayerProfile { + // avoid UUID comparison against STAR + return if (player.isValid) PlayerProfile(player.uuid, player.name) else invoke(player.name) + } + } + + val isStar: Boolean get() = this === star || (name == star.name && _uuid == star._uuid) + val hasUUID: Boolean get() = _uuid != null + val mustBeResolved: Boolean get() = isReal && _uuid == null + + val onlinePlayer: Player? get() = uuid?.let { Bukkit.getPlayer(uuid) } + + val onlinePlayerAllowingNameMatch: Player? get() = onlinePlayer ?: name?.let { Bukkit.getPlayerExact(name) } + val offlinePlayer: OfflinePlayer? get() = uuid?.let { Bukkit.getOfflinePlayer(it).takeIf { it.isValid } } + val offlinePlayerAllowingNameMatch: OfflinePlayer? + get() = offlinePlayer ?: Bukkit.getOfflinePlayer(name).takeIf { it.isValid } + + fun matches(player: OfflinePlayer, allowNameMatch: Boolean = false): Boolean { + if (isStar) return true + return uuid?.let { it == player.uniqueId } ?: false + || (allowNameMatch && name?.let { it == player.name } ?: false) + } + + fun equals(other: PlayerProfile): Boolean { + return if (_uuid != null) _uuid == other._uuid + else other._uuid == null && isReal == other.isReal && name == other.name + } + + override fun equals(other: Any?): Boolean { + return other is PlayerProfile && equals(other) + } + + override fun hashCode(): Int { + return _uuid?.hashCode() ?: name!!.hashCode() + } + + /** + * resolve the uuid of this player profile if [mustBeResolved], using specified [storage]. + * returns true if the PlayerProfile has a uuid after this call. + */ + suspend fun resolve(storage: Storage): Boolean { + if (mustBeResolved) { + val uuid = storage.getPlayerUuidForName(name!!).await() + _uuid = uuid + return uuid != null + } + return _uuid != null + } + + fun resolve(uuid: UUID) { + if (isReal && _uuid == null) { + _uuid = uuid + } + } +} +*/ diff --git a/src/main/kotlin/io/dico/parcels2/command/AbstractParcelCommands.kt b/src/main/kotlin/io/dico/parcels2/command/AbstractParcelCommands.kt index 2fceb5b..31a551a 100644 --- a/src/main/kotlin/io/dico/parcels2/command/AbstractParcelCommands.kt +++ b/src/main/kotlin/io/dico/parcels2/command/AbstractParcelCommands.kt @@ -3,7 +3,7 @@ package io.dico.parcels2.command import io.dico.dicore.command.CommandException import io.dico.dicore.command.ExecutionContext import io.dico.dicore.command.ICommandReceiver -import io.dico.parcels2.ParcelOwner +import io.dico.parcels2.PlayerProfile import io.dico.parcels2.ParcelWorld import io.dico.parcels2.ParcelsPlugin import io.dico.parcels2.util.hasAdminManage @@ -31,7 +31,7 @@ abstract class AbstractParcelCommands(val plugin: ParcelsPlugin) : ICommandRecei protected suspend fun checkParcelLimit(player: Player, world: ParcelWorld) { if (player.hasAdminManage) return - val numOwnedParcels = plugin.storage.getOwnedParcels(ParcelOwner(player)).await() + val numOwnedParcels = plugin.storage.getOwnedParcels(PlayerProfile(player)).await() .filter { it.worldId.equals(world.id) }.size val limit = player.parcelLimit diff --git a/src/main/kotlin/io/dico/parcels2/command/CommandsAddedStatusGlobal.kt b/src/main/kotlin/io/dico/parcels2/command/CommandsAddedStatusGlobal.kt index d483126..eae6da2 100644 --- a/src/main/kotlin/io/dico/parcels2/command/CommandsAddedStatusGlobal.kt +++ b/src/main/kotlin/io/dico/parcels2/command/CommandsAddedStatusGlobal.kt @@ -5,7 +5,7 @@ import io.dico.dicore.command.annotation.Cmd import io.dico.dicore.command.annotation.Desc import io.dico.parcels2.GlobalAddedData import io.dico.parcels2.GlobalAddedDataManager -import io.dico.parcels2.ParcelOwner +import io.dico.parcels2.PlayerProfile import io.dico.parcels2.ParcelsPlugin import org.bukkit.OfflinePlayer import org.bukkit.entity.Player @@ -13,7 +13,7 @@ import org.bukkit.entity.Player class CommandsAddedStatusGlobal(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) { private inline val data get() = plugin.globalAddedData @Suppress("NOTHING_TO_INLINE") - private inline operator fun GlobalAddedDataManager.get(player: OfflinePlayer): GlobalAddedData = data[ParcelOwner(player)] + private inline operator fun GlobalAddedDataManager.get(player: OfflinePlayer): GlobalAddedData = data[PlayerProfile(player)] @Cmd("allow", aliases = ["add", "permit"]) @Desc("Globally allows a player to build on all", diff --git a/src/main/kotlin/io/dico/parcels2/command/CommandsGeneral.kt b/src/main/kotlin/io/dico/parcels2/command/CommandsGeneral.kt index 5395b1c..7442019 100644 --- a/src/main/kotlin/io/dico/parcels2/command/CommandsGeneral.kt +++ b/src/main/kotlin/io/dico/parcels2/command/CommandsGeneral.kt @@ -6,7 +6,7 @@ import io.dico.dicore.command.annotation.Cmd import io.dico.dicore.command.annotation.Desc import io.dico.dicore.command.annotation.Flag import io.dico.dicore.command.annotation.RequireParameters -import io.dico.parcels2.ParcelOwner +import io.dico.parcels2.PlayerProfile import io.dico.parcels2.ParcelsPlugin import io.dico.parcels2.util.hasAdminManage import io.dico.parcels2.util.hasParcelHomeOthers @@ -25,7 +25,7 @@ class CommandsGeneral(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) { val parcel = world.nextEmptyParcel() ?: error("This world is full, please ask an admin to upsize it") - parcel.owner = ParcelOwner(uuid = player.uuid) + parcel.owner = PlayerProfile(uuid = player.uuid) player.teleport(parcel.world.getHomeLocation(parcel.id)) return "Enjoy your new parcel!" } @@ -72,7 +72,7 @@ class CommandsGeneral(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) { } checkParcelLimit(player, world) - parcel.owner = ParcelOwner(player) + parcel.owner = PlayerProfile(player) return "Enjoy your new parcel!" } diff --git a/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt b/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt index 5504e6b..cbab80a 100644 --- a/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt +++ b/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt @@ -7,14 +7,14 @@ import io.dico.dicore.command.parameter.type.ParameterType import io.dico.parcels2.* import io.dico.parcels2.util.Vec2i import io.dico.parcels2.util.floor -import io.dico.parcels2.util.isValid import kotlinx.coroutines.experimental.Deferred -import org.bukkit.Bukkit import org.bukkit.command.CommandSender import org.bukkit.entity.Player sealed class ParcelTarget(val world: ParcelWorld, val isDefault: Boolean) { + abstract suspend fun ParcelsPlugin.getParcelSuspend(): Parcel? + fun ParcelsPlugin.getParcelDeferred(): Deferred = functionHelper.deferUndispatchedOnMainThread { getParcelSuspend() } class ByID(world: ParcelWorld, val id: Vec2i?, isDefault: Boolean) : ParcelTarget(world, isDefault) { @@ -23,16 +23,35 @@ sealed class ParcelTarget(val world: ParcelWorld, val isDefault: Boolean) { val isPath: Boolean get() = id == null } - class ByOwner(world: ParcelWorld, val owner: ParcelOwner, val index: Int, isDefault: Boolean) : ParcelTarget(world, isDefault) { + class ByOwner(world: ParcelWorld, + owner: PlayerProfile, + val index: Int, + isDefault: Boolean, + val onResolveFailure: (() -> Unit)? = null) : ParcelTarget(world, isDefault) { init { if (index < 0) throw IllegalArgumentException("Invalid parcel home index: $index") } + var owner = owner; private set + override suspend fun ParcelsPlugin.getParcelSuspend(): Parcel? { + onResolveFailure?.let { onFail -> + val owner = owner + if (owner is PlayerProfile.Unresolved) { + val new = owner.tryResolveSuspendedly(storage) + if (new == null) { + onFail() + return@let + } + this@ByOwner.owner = new + } + } + val ownedParcelsSerialized = storage.getOwnedParcels(owner).await() val ownedParcels = ownedParcelsSerialized .map { parcelProvider.getParcelById(it) } .filter { it != null && world == it.world && owner == it.owner } + return ownedParcels.getOrNull(index) } } @@ -80,8 +99,8 @@ sealed class ParcelTarget(val world: ParcelWorld, val isDefault: Boolean) { } if (kind and OWNER == 0) invalidInput(parameter, "You must specify a parcel by ID, that is, the x and z component separated by a comma") - val (owner, index) = getHomeIndex(parameter, sender, input) - return ByOwner(world, owner, index, false) + val (owner, index) = getHomeIndex(parameter, kind, sender, input) + return ByOwner(world, owner, index, false, onResolveFailure = { invalidInput(parameter, "The player $input does not exist") }) } private fun getId(parameter: Parameter<*, *>, input: String): Vec2i { @@ -94,7 +113,7 @@ sealed class ParcelTarget(val world: ParcelWorld, val isDefault: Boolean) { return Vec2i(x, z) } - private fun getHomeIndex(parameter: Parameter<*, Int>, sender: CommandSender, input: String): Pair { + private fun getHomeIndex(parameter: Parameter<*, *>, kind: Int, sender: CommandSender, input: String): Pair { val splitIdx = input.indexOf(':') val ownerString: String val indexString: String @@ -109,9 +128,9 @@ sealed class ParcelTarget(val world: ParcelWorld, val isDefault: Boolean) { } val owner = if (ownerString.isEmpty()) - ParcelOwner(requirePlayer(sender, parameter, "the player")) + PlayerProfile(requirePlayer(sender, parameter, "the player")) else - inputAsOwner(parameter, ownerString) + PlayerProfile.byName(ownerString, allowReal = kind and OWNER_REAL != 0, allowFake = kind and OWNER_FAKE != 0) val index = if (indexString.isEmpty()) 0 else indexString.toIntOrNull() ?: invalidInput(parameter, "The home index must be an integer, $indexString is not an integer") @@ -124,22 +143,6 @@ sealed class ParcelTarget(val world: ParcelWorld, val isDefault: Boolean) { return sender } - @Suppress("DEPRECATION") - private fun inputAsOwner(parameter: Parameter<*, Int>, input: String): ParcelOwner { - val kind = parameter.paramInfo ?: DEFAULT_KIND - if (kind and OWNER_REAL == 0) { - return ParcelOwner(input) - } - - val player = Bukkit.getOfflinePlayer(input).takeIf { it.isValid } - if (player == null) { - if (kind and OWNER_FAKE == 0) invalidInput(parameter, "The player $input does not exist") - return ParcelOwner(input) - } - - return ParcelOwner(player) - } - override fun getDefaultValue(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): ParcelTarget? { val kind = parameter.paramInfo ?: DEFAULT_KIND val useLocation = when { @@ -156,7 +159,8 @@ sealed class ParcelTarget(val world: ParcelWorld, val isDefault: Boolean) { return ByID(world, id, true) } - return ByOwner(world, ParcelOwner(player), 0, true) + return ByOwner(world, PlayerProfile(player), 0, true) } } + } diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt index 3b6bfb5..7e75f52 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt @@ -119,7 +119,7 @@ class DefaultParcelGenerator(val name: String, private val o: DefaultGeneratorOp val modX = absX umod sectionSize val modZ = absZ umod sectionSize if (modX in 0 until parcelSize && modZ in 0 until parcelSize) { - return mapper((absX - modX) / sectionSize, (absZ - modZ) / sectionSize) + return mapper((absX - modX) / sectionSize + 1, (absZ - modZ) / sectionSize + 1) } return null } @@ -140,16 +140,18 @@ class DefaultParcelGenerator(val name: String, private val o: DefaultGeneratorOp override val world: World = this@DefaultParcelGenerator.world override fun getBottomBlock(parcel: ParcelId): Vec2i = Vec2i( - sectionSize * parcel.pos.x + pathOffset + o.offsetX, - sectionSize * parcel.pos.z + pathOffset + o.offsetZ + sectionSize * (parcel.x - 1) + pathOffset + o.offsetX, + sectionSize * (parcel.z - 1) + pathOffset + o.offsetZ ) override fun getHomeLocation(parcel: ParcelId): Location { val bottom = getBottomBlock(parcel) - return Location(world, bottom.x.toDouble() + 0.5, o.floorHeight + 1.0, bottom.z + 0.5 + (o.parcelSize - 1) / 2.0, -90F, 0F) + val x = bottom.x + (o.parcelSize - 1) / 2.0 + val z = bottom.z - 2 + return Location(world, x + 0.5, o.floorHeight + 1.0, z + 0.5, 0F, 0F) } - override fun setOwnerBlock(parcel: ParcelId, owner: ParcelOwner?) { + override fun setOwnerBlock(parcel: ParcelId, owner: PlayerProfile?) { val b = getBottomBlock(parcel) val wallBlock = world.getBlockAt(b.x - 1, o.floorHeight + 1, b.z - 1) @@ -178,8 +180,8 @@ class DefaultParcelGenerator(val name: String, private val o: DefaultGeneratorOp skullBlock.type = Material.PLAYER_HEAD val skull = skullBlock.state as Skull - if (owner.uuid != null) { - skull.owningPlayer = owner.offlinePlayer + if (owner is PlayerProfile.Real) { + skull.owningPlayer = owner.playerUnchecked } else { skull.owner = owner.name } diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/GlobalAddedDataManagerImpl.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/GlobalAddedDataManagerImpl.kt index 1ac053f..ba54375 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/GlobalAddedDataManagerImpl.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/GlobalAddedDataManagerImpl.kt @@ -3,33 +3,32 @@ package io.dico.parcels2.defaultimpl import io.dico.parcels2.* +import io.dico.parcels2.util.alsoIfTrue import java.util.Collections -import java.util.UUID class GlobalAddedDataManagerImpl(val plugin: ParcelsPlugin) : GlobalAddedDataManager { - private val map = mutableMapOf() + private val map = mutableMapOf() - override fun get(owner: ParcelOwner): GlobalAddedData { + override fun get(owner: PlayerProfile): GlobalAddedData { return map[owner] ?: GlobalAddedDataImpl(owner).also { map[owner] = it } } - private inner class GlobalAddedDataImpl(override val owner: ParcelOwner, + private inner class GlobalAddedDataImpl(override val owner: PlayerProfile, data: MutableAddedDataMap = emptyData) : AddedDataHolder(data), GlobalAddedData { private inline var data get() = addedMap; set(value) = run { addedMap = value } private inline val isEmpty get() = data === emptyData - override fun setAddedStatus(uuid: UUID, status: AddedStatus): Boolean { + override fun setAddedStatus(key: StatusKey, status: AddedStatus): Boolean { if (isEmpty) { if (status == AddedStatus.DEFAULT) return false data = mutableMapOf() } - return super.setAddedStatus(uuid, status).also { - if (it) plugin.storage.setGlobalAddedStatus(owner, uuid, status) + return super.setAddedStatus(key, status).alsoIfTrue { + plugin.storage.setGlobalAddedStatus(owner, key, status) } } - } private companion object { diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelImpl.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelImpl.kt index c3d7c22..128705f 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelImpl.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelImpl.kt @@ -3,6 +3,7 @@ package io.dico.parcels2.defaultimpl import io.dico.dicore.Formatting import io.dico.parcels2.* import io.dico.parcels2.util.Vec2i +import io.dico.parcels2.util.alsoIfTrue import io.dico.parcels2.util.getPlayerName import org.bukkit.OfflinePlayer import org.joda.time.DateTime @@ -33,20 +34,24 @@ class ParcelImpl(override val world: ParcelWorld, world.storage.setParcelData(this, null) } - override val addedMap: Map get() = data.addedMap - override fun getAddedStatus(uuid: UUID) = data.getAddedStatus(uuid) - override fun isBanned(uuid: UUID) = data.isBanned(uuid) - override fun isAllowed(uuid: UUID) = data.isAllowed(uuid) + override val addedMap: AddedDataMap get() = data.addedMap + override fun getAddedStatus(key: StatusKey) = data.getAddedStatus(key) + override fun isBanned(key: StatusKey) = data.isBanned(key) + override fun isAllowed(key: StatusKey) = data.isAllowed(key) override fun canBuild(player: OfflinePlayer, checkAdmin: Boolean, checkGlobal: Boolean): Boolean { return (data.canBuild(player, checkAdmin, false)) || checkGlobal && world.globalAddedData[owner ?: return false].isAllowed(player) } - val globalAddedMap: Map? get() = owner?.let { world.globalAddedData[it].addedMap } + override var addedStatusOfStar: AddedStatus + get() = data.addedStatusOfStar + set(value) = run { setAddedStatus(PlayerProfile.Star, value) } + + val globalAddedMap: AddedDataMap? get() = owner?.let { world.globalAddedData[it].addedMap } override val since: DateTime? get() = data.since - override var owner: ParcelOwner? + override var owner: PlayerProfile? get() = data.owner set(value) { if (data.owner != value) { @@ -55,9 +60,9 @@ class ParcelImpl(override val world: ParcelWorld, } } - override fun setAddedStatus(uuid: UUID, status: AddedStatus): Boolean { - return data.setAddedStatus(uuid, status).also { - if (it) world.storage.setParcelPlayerStatus(this, uuid, status) + override fun setAddedStatus(key: StatusKey, status: AddedStatus): Boolean { + return data.setAddedStatus(key, status).alsoIfTrue { + world.storage.setParcelPlayerStatus(this, key, status) } } @@ -102,10 +107,10 @@ private object ParcelInfoStringComputer { append(' ') } - private fun StringBuilder.appendAddedList(local: Map, global: Map, status: AddedStatus, fieldName: String) { + private fun StringBuilder.appendAddedList(local: AddedDataMap, global: AddedDataMap, status: AddedStatus, fieldName: String) { val globalSet = global.filterValues { it == status }.keys val localList = local.filterValues { it == status }.keys.filter { it !in globalSet } - val stringList = globalSet.map(::getPlayerName).map { "(G)$it" } + localList.map(::getPlayerName) + val stringList = globalSet.map(StatusKey::notNullName).map { "(G)$it" } + localList.map(StatusKey::notNullName) if (stringList.isEmpty()) return appendField({ diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt index f7abccd..70d428d 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt @@ -75,7 +75,7 @@ class ParcelProviderImpl(val plugin: ParcelsPlugin) : ParcelProvider { } logger.info("Loading all parcel data...") - val channel = plugin.storage.readAllParcelData() + val channel = plugin.storage.transmitAllParcelData() do { val pair = channel.receiveOrNull() ?: break val parcel = getParcelById(pair.first) ?: continue @@ -119,7 +119,7 @@ class ParcelProviderImpl(val plugin: ParcelsPlugin) : ParcelProvider { } } - val channel = plugin.storage.readAllParcelData() + val channel = plugin.storage.transmitAllParcelData() val job = plugin.functionHelper.launchLazilyOnMainThread { do { val pair = channel.receiveOrNull() ?: break diff --git a/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt b/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt index b2637f5..fa5ccb4 100644 --- a/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt +++ b/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt @@ -6,6 +6,7 @@ import io.dico.dicore.RegistratorListener import io.dico.parcels2.Parcel import io.dico.parcels2.ParcelProvider import io.dico.parcels2.ParcelWorld +import io.dico.parcels2.statusKey import io.dico.parcels2.util.* import org.bukkit.Material.* import org.bukkit.World @@ -51,7 +52,7 @@ class ParcelListeners(val parcelProvider: ParcelProvider, val entityTracker: Par val user = event.player if (user.hasBanBypass) return@l val parcel = parcelProvider.getParcelAt(event.to) ?: return@l - if (parcel.isBanned(user.uuid)) { + if (parcel.isBanned(user.statusKey)) { parcelProvider.getParcelAt(event.from)?.also { user.teleport(it.world.getHomeLocation(it.id)) user.sendParcelMessage(nopermit = true, message = "You are banned from this parcel") @@ -178,7 +179,7 @@ class ParcelListeners(val parcelProvider: ParcelProvider, val entityTracker: Par val clickedBlock = event.clickedBlock val parcel = clickedBlock?.let { world.getParcelAt(it) } - if (!user.hasBuildAnywhere && parcel.isPresentAnd { isBanned(user.uuid) }) { + if (!user.hasBuildAnywhere && parcel.isPresentAnd { isBanned(user.statusKey) }) { user.sendParcelMessage(nopermit = true, message = "You cannot interact with parcels you're banned from") event.isCancelled = true; return@l } diff --git a/src/main/kotlin/io/dico/parcels2/storage/Backing.kt b/src/main/kotlin/io/dico/parcels2/storage/Backing.kt index bb4cf33..b658d10 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/Backing.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/Backing.kt @@ -2,10 +2,8 @@ package io.dico.parcels2.storage import io.dico.parcels2.* import kotlinx.coroutines.experimental.CoroutineDispatcher -import kotlinx.coroutines.experimental.CoroutineScope import kotlinx.coroutines.experimental.Deferred import kotlinx.coroutines.experimental.Job -import kotlinx.coroutines.experimental.channels.ProducerScope import kotlinx.coroutines.experimental.channels.ReceiveChannel import kotlinx.coroutines.experimental.channels.SendChannel import java.util.UUID @@ -16,58 +14,49 @@ interface Backing { val isConnected: Boolean + val dispatcher: CoroutineDispatcher + fun launchJob(job: Backing.() -> Unit): Job fun launchFuture(future: Backing.() -> T): Deferred fun openChannel(future: Backing.(SendChannel) -> Unit): ReceiveChannel + fun openChannelForWriting(future: Backing.(T) -> Unit): SendChannel + fun init() fun shutdown() - /** - * This producer function is capable of constantly reading parcels from a potentially infinite sequence, - * and provide parcel data for it as read from the database. - */ - fun produceParcelData(channel: SendChannel, parcels: Sequence) + fun getPlayerUuidForName(name: String): UUID? - fun produceAllParcelData(channel: SendChannel) + fun transmitParcelData(channel: SendChannel, parcels: Sequence) + + fun transmitAllParcelData(channel: SendChannel) fun readParcelData(parcel: ParcelId): ParcelData? - fun getOwnedParcels(user: ParcelOwner): List + fun getOwnedParcels(user: PlayerProfile): List - fun getNumParcels(user: ParcelOwner): Int = getOwnedParcels(user).size + fun getNumParcels(user: PlayerProfile): Int = getOwnedParcels(user).size fun setParcelData(parcel: ParcelId, data: ParcelData?) - fun setParcelOwner(parcel: ParcelId, owner: ParcelOwner?) + fun setParcelOwner(parcel: ParcelId, owner: PlayerProfile?) - fun setLocalPlayerStatus(parcel: ParcelId, player: UUID, status: AddedStatus) + fun setLocalPlayerStatus(parcel: ParcelId, player: PlayerProfile, status: AddedStatus) fun setParcelAllowsInteractInventory(parcel: ParcelId, value: Boolean) fun setParcelAllowsInteractInputs(parcel: ParcelId, value: Boolean) - fun produceAllGlobalAddedData(channel: SendChannel>) - - fun readGlobalAddedData(owner: ParcelOwner): MutableAddedDataMap - - fun setGlobalPlayerStatus(owner: ParcelOwner, player: UUID, status: AddedStatus) - -} + fun transmitAllGlobalAddedData(channel: SendChannel>) -abstract class AbstractBacking(val dispatcher: CoroutineDispatcher) { - - fun launchJob(job: Backing.() -> Unit): Job - - fun launchFuture(future: Backing.() -> T): Deferred - - fun openChannel(future: Backing.(SendChannel) -> Unit): ReceiveChannel + fun readGlobalAddedData(owner: PlayerProfile): MutableAddedDataMap + fun setGlobalPlayerStatus(owner: PlayerProfile, player: PlayerProfile, status: AddedStatus) } diff --git a/src/main/kotlin/io/dico/parcels2/storage/Storage.kt b/src/main/kotlin/io/dico/parcels2/storage/Storage.kt index 6770d99..2116b46 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/Storage.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/Storage.kt @@ -6,6 +6,8 @@ import io.dico.parcels2.* import kotlinx.coroutines.experimental.Deferred import kotlinx.coroutines.experimental.Job import kotlinx.coroutines.experimental.channels.ReceiveChannel +import kotlinx.coroutines.experimental.channels.SendChannel +import kotlinx.coroutines.experimental.launch import java.util.UUID typealias DataPair = Pair @@ -20,68 +22,77 @@ interface Storage { fun shutdown(): Job + fun getPlayerUuidForName(name: String): Deferred + fun readParcelData(parcel: ParcelId): Deferred - fun readParcelData(parcels: Sequence): ReceiveChannel + fun transmitParcelData(parcels: Sequence): ReceiveChannel - fun readAllParcelData(): ReceiveChannel + fun transmitAllParcelData(): ReceiveChannel - fun getOwnedParcels(user: ParcelOwner): Deferred> + fun getOwnedParcels(user: PlayerProfile): Deferred> - fun getNumParcels(user: ParcelOwner): Deferred + fun getNumParcels(user: PlayerProfile): Deferred fun setParcelData(parcel: ParcelId, data: ParcelData?): Job - fun setParcelOwner(parcel: ParcelId, owner: ParcelOwner?): Job + fun setParcelOwner(parcel: ParcelId, owner: PlayerProfile?): Job - fun setParcelPlayerStatus(parcel: ParcelId, player: UUID, status: AddedStatus): Job + fun setParcelPlayerStatus(parcel: ParcelId, player: PlayerProfile, status: AddedStatus): Job fun setParcelAllowsInteractInventory(parcel: ParcelId, value: Boolean): Job fun setParcelAllowsInteractInputs(parcel: ParcelId, value: Boolean): Job - fun readAllGlobalAddedData(): ReceiveChannel> + fun transmitAllGlobalAddedData(): ReceiveChannel> + + fun readGlobalAddedData(owner: PlayerProfile): Deferred + + fun setGlobalAddedStatus(owner: PlayerProfile, player: PlayerProfile, status: AddedStatus): Job - fun readGlobalAddedData(owner: ParcelOwner): Deferred - fun setGlobalAddedStatus(owner: ParcelOwner, player: UUID, status: AddedStatus): Job + fun getChannelToUpdateParcelData(): SendChannel> } class BackedStorage internal constructor(val b: Backing) : Storage { override val name get() = b.name override val isConnected get() = b.isConnected - override fun init() = b.launchJob { init() } + override fun init() = launch(b.dispatcher) { b.init() } + + override fun shutdown() = launch(b.dispatcher) { b.shutdown() } + - override fun shutdown() = b.launchJob { shutdown() } + override fun getPlayerUuidForName(name: String): Deferred = b.launchFuture { b.getPlayerUuidForName(name) } + override fun readParcelData(parcel: ParcelId) = b.launchFuture { b.readParcelData(parcel) } - override fun readParcelData(parcel: ParcelId) = b.launchFuture { readParcelData(parcel) } + override fun transmitParcelData(parcels: Sequence) = b.openChannel { b.transmitParcelData(it, parcels) } - override fun readParcelData(parcels: Sequence) = b.openChannel { produceParcelData(it, parcels) } + override fun transmitAllParcelData() = b.openChannel { b.transmitAllParcelData(it) } - override fun readAllParcelData() = b.openChannel { produceAllParcelData(it) } + override fun getOwnedParcels(user: PlayerProfile) = b.launchFuture { b.getOwnedParcels(user) } - override fun getOwnedParcels(user: ParcelOwner) = b.launchFuture { getOwnedParcels(user) } + override fun getNumParcels(user: PlayerProfile) = b.launchFuture { b.getNumParcels(user) } - override fun getNumParcels(user: ParcelOwner) = b.launchFuture { getNumParcels(user) } + override fun setParcelData(parcel: ParcelId, data: ParcelData?) = b.launchJob { b.setParcelData(parcel, data) } - override fun setParcelData(parcel: ParcelId, data: ParcelData?) = b.launchJob { setParcelData(parcel, data) } + override fun setParcelOwner(parcel: ParcelId, owner: PlayerProfile?) = b.launchJob { b.setParcelOwner(parcel, owner) } - override fun setParcelOwner(parcel: ParcelId, owner: ParcelOwner?) = b.launchJob { setParcelOwner(parcel, owner) } + override fun setParcelPlayerStatus(parcel: ParcelId, player: PlayerProfile, status: AddedStatus) = b.launchJob { b.setLocalPlayerStatus(parcel, player, status) } - override fun setParcelPlayerStatus(parcel: ParcelId, player: UUID, status: AddedStatus) = b.launchJob { setLocalPlayerStatus(parcel, player, status) } + override fun setParcelAllowsInteractInventory(parcel: ParcelId, value: Boolean) = b.launchJob { b.setParcelAllowsInteractInventory(parcel, value) } - override fun setParcelAllowsInteractInventory(parcel: ParcelId, value: Boolean) = b.launchJob { setParcelAllowsInteractInventory(parcel, value) } + override fun setParcelAllowsInteractInputs(parcel: ParcelId, value: Boolean) = b.launchJob { b.setParcelAllowsInteractInputs(parcel, value) } - override fun setParcelAllowsInteractInputs(parcel: ParcelId, value: Boolean) = b.launchJob { setParcelAllowsInteractInputs(parcel, value) } + override fun transmitAllGlobalAddedData(): ReceiveChannel> = b.openChannel { b.transmitAllGlobalAddedData(it) } - override fun readAllGlobalAddedData(): ReceiveChannel> = b.openChannel { produceAllGlobalAddedData(it) } + override fun readGlobalAddedData(owner: PlayerProfile): Deferred = b.launchFuture { b.readGlobalAddedData(owner) } - override fun readGlobalAddedData(owner: ParcelOwner): Deferred = b.launchFuture { readGlobalAddedData(owner) } + override fun setGlobalAddedStatus(owner: PlayerProfile, player: PlayerProfile, status: AddedStatus) = b.launchJob { b.setGlobalPlayerStatus(owner, player, status) } - override fun setGlobalAddedStatus(owner: ParcelOwner, player: UUID, status: AddedStatus) = b.launchJob { setGlobalPlayerStatus(owner, player, status) } + override fun getChannelToUpdateParcelData(): SendChannel> = b.openChannelForWriting { b.setParcelData(it.first, it.second) } } diff --git a/src/main/kotlin/io/dico/parcels2/storage/exposed/CoroutineTransactionManager.kt b/src/main/kotlin/io/dico/parcels2/storage/exposed/CoroutineTransactionManager.kt deleted file mode 100644 index ab707af..0000000 --- a/src/main/kotlin/io/dico/parcels2/storage/exposed/CoroutineTransactionManager.kt +++ /dev/null @@ -1,118 +0,0 @@ -package io.dico.parcels2.storage.exposed - -import kotlinx.coroutines.experimental.* -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.statements.StatementContext -import org.jetbrains.exposed.sql.statements.StatementInterceptor -import org.jetbrains.exposed.sql.statements.expandArgs -import org.jetbrains.exposed.sql.transactions.* -import org.slf4j.LoggerFactory -import java.sql.Connection -import kotlin.coroutines.experimental.CoroutineContext - -fun ctransaction(db: Database? = null, statement: suspend Transaction.() -> T): T { - return ctransaction(TransactionManager.manager.defaultIsolationLevel, 3, db, statement) -} - -fun ctransaction(transactionIsolation: Int, repetitionAttempts: Int, db: Database? = null, statement: suspend Transaction.() -> T): T { - return transaction(transactionIsolation, repetitionAttempts, db) { - if (this !is CoroutineTransaction) throw IllegalStateException("ctransaction requires CoroutineTransactionManager.") - - val job = async(context = manager.context, start = CoroutineStart.UNDISPATCHED) { - this@transaction.statement() - } - - if (job.isActive) { - runBlocking(context = Unconfined) { - job.join() - } - } - - job.getCompleted() - } -} - -class CoroutineTransactionManager(private val db: Database, - dispatcher: CoroutineDispatcher, - override var defaultIsolationLevel: Int = DEFAULT_ISOLATION_LEVEL) : TransactionManager { - val context: CoroutineDispatcher = TransactionCoroutineDispatcher(dispatcher) - private val transaction = ThreadLocal() - - override fun currentOrNull(): Transaction? { - - - return transaction.get() - ?: null - } - - override fun newTransaction(isolation: Int): Transaction { - return CoroutineTransaction(this, CoroutineTransactionInterface(db, isolation, transaction)).also { transaction.set(it) } - } - - private inner class TransactionCoroutineDispatcher(val delegate: CoroutineDispatcher) : CoroutineDispatcher() { - - // When the thread changes, move the transaction to the new thread - override fun dispatch(context: CoroutineContext, block: Runnable) { - val existing = transaction.get() - - val newContext: CoroutineContext - if (existing != null) { - transaction.set(null) - newContext = context // + existing - } else { - newContext = context - } - - delegate.dispatch(newContext, Runnable { - if (existing != null) { - transaction.set(existing) - } - - block.run() - }) - } - - } - -} - -private class CoroutineTransaction(val manager: CoroutineTransactionManager, - itf: CoroutineTransactionInterface) : Transaction(itf), CoroutineContext.Element { - companion object Key : CoroutineContext.Key - - override val key: CoroutineContext.Key = Key -} - -private class CoroutineTransactionInterface(override val db: Database, isolation: Int, val threadLocal: ThreadLocal) : TransactionInterface { - private val connectionLazy = lazy(LazyThreadSafetyMode.NONE) { - db.connector().apply { - autoCommit = false - transactionIsolation = isolation - } - } - override val connection: Connection - get() = connectionLazy.value - - override val outerTransaction: CoroutineTransaction? = threadLocal.get() - - override fun commit() { - if (connectionLazy.isInitialized()) - connection.commit() - } - - override fun rollback() { - if (connectionLazy.isInitialized() && !connection.isClosed) { - connection.rollback() - } - } - - override fun close() { - try { - if (connectionLazy.isInitialized()) connection.close() - } finally { - threadLocal.set(outerTransaction) - } - } - -} - diff --git a/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedBacking.kt b/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedBacking.kt index 01afd94..8ea6653 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedBacking.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedBacking.kt @@ -4,10 +4,13 @@ package io.dico.parcels2.storage.exposed import com.zaxxer.hikari.HikariDataSource import io.dico.parcels2.* +import io.dico.parcels2.storage.AddedDataPair import io.dico.parcels2.storage.Backing import io.dico.parcels2.storage.DataPair +import io.dico.parcels2.util.synchronized import io.dico.parcels2.util.toUUID import kotlinx.coroutines.experimental.* +import kotlinx.coroutines.experimental.channels.ArrayChannel import kotlinx.coroutines.experimental.channels.LinkedListChannel import kotlinx.coroutines.experimental.channels.ReceiveChannel import kotlinx.coroutines.experimental.channels.SendChannel @@ -21,10 +24,9 @@ import javax.sql.DataSource class ExposedDatabaseException(message: String? = null) : Exception(message) -class ExposedBacking(private val dataSourceFactory: () -> DataSource, - private val poolSize: Int) : Backing { +class ExposedBacking(private val dataSourceFactory: () -> DataSource, val poolSize: Int) : Backing { override val name get() = "Exposed" - val dispatcher: CoroutineDispatcher = newFixedThreadPoolContext(4, "Parcels StorageThread") + override val dispatcher: ThreadPoolDispatcher = newFixedThreadPoolContext(poolSize, "Parcels StorageThread") private var dataSource: DataSource? = null private var database: Database? = null @@ -40,6 +42,24 @@ class ExposedBacking(private val dataSourceFactory: () -> DataSource, return channel } + override fun openChannelForWriting(action: Backing.(T) -> Unit): SendChannel { + val channel = ArrayChannel(poolSize * 2) + + repeat(poolSize) { + launch(dispatcher) { + try { + while (true) { + action(channel.receive()) + } + } catch (ex: Exception) { + // channel closed + } + } + } + + return channel + } + private fun transaction(statement: Transaction.() -> T) = transaction(database!!, statement) companion object { @@ -51,25 +71,54 @@ class ExposedBacking(private val dataSourceFactory: () -> DataSource, } override fun init() { - if (isShutdown || isConnected) throw IllegalStateException() - - dataSource = dataSourceFactory() - database = Database.connect(dataSource!!) - transaction(database!!) { - create(WorldsT, OwnersT, ParcelsT, ParcelOptionsT, AddedLocalT, AddedGlobalT) + synchronized { + if (isShutdown || isConnected) throw IllegalStateException() + dataSource = dataSourceFactory() + database = Database.connect(dataSource!!) + transaction(database!!) { + create(WorldsT, ProfilesT, ParcelsT, ParcelOptionsT, AddedLocalT, AddedGlobalT) + } } } override fun shutdown() { - if (isShutdown) throw IllegalStateException() - dataSource?.let { - (it as? HikariDataSource)?.close() + synchronized { + if (isShutdown) throw IllegalStateException() + dataSource?.let { + (it as? HikariDataSource)?.close() + } + database = null + isShutdown = true } - database = null - isShutdown = true } - override fun produceParcelData(channel: SendChannel, parcels: Sequence) { + private fun PlayerProfile.toOwnerProfile(): PlayerProfile { + if (this is PlayerProfile.Star) return PlayerProfile.Fake(PlayerProfile.Star.name) + return this + } + + private fun PlayerProfile.Unresolved.toResolvedProfile(): PlayerProfile.Real { + return resolve(getPlayerUuidForName(name) ?: throwException()) + } + + private fun PlayerProfile.toResolvedProfile(): PlayerProfile { + if (this is PlayerProfile.Unresolved) return toResolvedProfile() + return this + } + + private fun PlayerProfile.toRealProfile(): PlayerProfile.Real = when (this) { + is PlayerProfile.Real -> this + is PlayerProfile.Fake -> throw IllegalArgumentException("Fake profiles are not accepted") + is PlayerProfile.Unresolved -> toResolvedProfile() + else -> throw InternalError("Case should not be reached") + } + + override fun getPlayerUuidForName(name: String): UUID? { + return ProfilesT.slice(ProfilesT.uuid).select { ProfilesT.name.upperCase() eq name.toUpperCase() } + .firstOrNull()?.let { it[ProfilesT.uuid]?.toUUID() } + } + + override fun transmitParcelData(channel: SendChannel, parcels: Sequence) { for (parcel in parcels) { val data = readParcelData(parcel) channel.offer(parcel to data) @@ -77,25 +126,25 @@ class ExposedBacking(private val dataSourceFactory: () -> DataSource, channel.close() } - override fun produceAllParcelData(channel: SendChannel>) = ctransaction { + override fun transmitAllParcelData(channel: SendChannel) { ParcelsT.selectAll().forEach { row -> - val parcel = ParcelsT.getId(row) ?: return@forEach + val parcel = ParcelsT.getItem(row) ?: return@forEach val data = rowToParcelData(row) - channel.send(parcel to data) + channel.offer(parcel to data) } channel.close() } - override fun readParcelData(parcel: ParcelId): ParcelData? = transaction { - val row = ParcelsT.getRow(parcel) ?: return@transaction null - rowToParcelData(row) + override fun readParcelData(parcel: ParcelId): ParcelData? { + val row = ParcelsT.getRow(parcel) ?: return null + return rowToParcelData(row) } - override fun getOwnedParcels(user: ParcelOwner): List = transaction { - val user_id = OwnersT.getId(user) ?: return@transaction emptyList() - ParcelsT.select { ParcelsT.owner_id eq user_id } + override fun getOwnedParcels(user: PlayerProfile): List { + val user_id = ProfilesT.getId(user.toOwnerProfile()) ?: return emptyList() + return ParcelsT.select { ParcelsT.owner_id eq user_id } .orderBy(ParcelsT.claim_time, isAsc = true) - .mapNotNull(ParcelsT::getId) + .mapNotNull(ParcelsT::getItem) .toList() } @@ -123,21 +172,21 @@ class ExposedBacking(private val dataSourceFactory: () -> DataSource, setParcelOwner(parcel, data.owner) - for ((uuid, status) in data.addedMap) { - setLocalPlayerStatus(parcel, uuid, status) + for ((profile, status) in data.addedMap) { + AddedLocalT.setPlayerStatus(parcel, profile, status) } setParcelAllowsInteractInputs(parcel, data.allowInteractInputs) setParcelAllowsInteractInventory(parcel, data.allowInteractInventory) } - override fun setParcelOwner(parcel: ParcelId, owner: ParcelOwner?) = transaction { + override fun setParcelOwner(parcel: ParcelId, owner: PlayerProfile?) { val id = if (owner == null) - ParcelsT.getId(parcel) ?: return@transaction + ParcelsT.getId(parcel) ?: return else ParcelsT.getOrInitId(parcel) - val owner_id = owner?.let { OwnersT.getOrInitId(it) } + val owner_id = owner?.let { ProfilesT.getOrInitId(it.toOwnerProfile()) } val time = owner?.let { DateTime.now() } ParcelsT.update({ ParcelsT.id eq id }) { @@ -146,11 +195,11 @@ class ExposedBacking(private val dataSourceFactory: () -> DataSource, } } - override fun setLocalPlayerStatus(parcel: ParcelId, player: UUID, status: AddedStatus) = transaction { - AddedLocalT.setPlayerStatus(parcel, player, status) + override fun setLocalPlayerStatus(parcel: ParcelId, player: PlayerProfile, status: AddedStatus) { + AddedLocalT.setPlayerStatus(parcel, player.toRealProfile(), status) } - override fun setParcelAllowsInteractInventory(parcel: ParcelId, value: Boolean): Unit = transaction { + override fun setParcelAllowsInteractInventory(parcel: ParcelId, value: Boolean) { val id = ParcelsT.getOrInitId(parcel) ParcelOptionsT.upsert(ParcelOptionsT.parcel_id) { it[ParcelOptionsT.parcel_id] = id @@ -158,7 +207,7 @@ class ExposedBacking(private val dataSourceFactory: () -> DataSource, } } - override fun setParcelAllowsInteractInputs(parcel: ParcelId, value: Boolean): Unit = transaction { + override fun setParcelAllowsInteractInputs(parcel: ParcelId, value: Boolean) { val id = ParcelsT.getOrInitId(parcel) ParcelOptionsT.upsert(ParcelOptionsT.parcel_id) { it[ParcelOptionsT.parcel_id] = id @@ -166,36 +215,30 @@ class ExposedBacking(private val dataSourceFactory: () -> DataSource, } } - override fun produceAllGlobalAddedData(channel: SendChannel>>) = ctransaction { + override fun transmitAllGlobalAddedData(channel: SendChannel>) { AddedGlobalT.sendAllAddedData(channel) channel.close() } - override fun readGlobalAddedData(owner: ParcelOwner): MutableMap = transaction { - return@transaction AddedGlobalT.readAddedData(OwnersT.getId(owner) ?: return@transaction hashMapOf()) + override fun readGlobalAddedData(owner: PlayerProfile): MutableAddedDataMap { + return AddedGlobalT.readAddedData(ProfilesT.getId(owner.toOwnerProfile()) ?: return hashMapOf()) } - override fun setGlobalPlayerStatus(owner: ParcelOwner, player: UUID, status: AddedStatus) = transaction { - AddedGlobalT.setPlayerStatus(owner, player, status) + override fun setGlobalPlayerStatus(owner: PlayerProfile, player: PlayerProfile, status: AddedStatus) { + AddedGlobalT.setPlayerStatus(owner, player.toRealProfile(), status) } private fun rowToParcelData(row: ResultRow) = ParcelDataHolder().apply { - owner = row[ParcelsT.owner_id]?.let { OwnersT.getId(it) } + owner = row[ParcelsT.owner_id]?.let { ProfilesT.getItem(it) } since = row[ParcelsT.claim_time] - val parcelId = row[ParcelsT.id] - addedMap = AddedLocalT.readAddedData(parcelId) - - AddedLocalT.select { AddedLocalT.attach_id eq parcelId }.forEach { - val uuid = it[AddedLocalT.player_uuid].toUUID() - val status = if (it[AddedLocalT.allowed_flag]) AddedStatus.ALLOWED else AddedStatus.BANNED - setAddedStatus(uuid, status) + val id = row[ParcelsT.id] + ParcelOptionsT.select { ParcelOptionsT.parcel_id eq id }.firstOrNull()?.let { optrow -> + allowInteractInputs = optrow[ParcelOptionsT.interact_inputs] + allowInteractInventory = optrow[ParcelOptionsT.interact_inventory] } - ParcelOptionsT.select { ParcelOptionsT.parcel_id eq parcelId }.firstOrNull()?.let { - allowInteractInputs = it[ParcelOptionsT.interact_inputs] - allowInteractInventory = it[ParcelOptionsT.interact_inventory] - } + addedMap = AddedLocalT.readAddedData(id) } } diff --git a/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedExtensions.kt b/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedExtensions.kt index ce66644..6ebac6d 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedExtensions.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedExtensions.kt @@ -1,12 +1,9 @@ package io.dico.parcels2.storage.exposed -import org.jetbrains.exposed.sql.Column -import org.jetbrains.exposed.sql.Index -import org.jetbrains.exposed.sql.Table -import org.jetbrains.exposed.sql.Transaction +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.Function import org.jetbrains.exposed.sql.statements.InsertStatement import org.jetbrains.exposed.sql.transactions.TransactionManager -import org.jetbrains.exposed.sql.transactions.transaction class UpsertStatement(table: Table, conflictColumn: Column<*>? = null, conflictIndex: Index? = null) : InsertStatement(table, false) { @@ -63,3 +60,15 @@ fun Table.indexR(customIndexName: String? = null, isUnique: Boolean = false, var fun Table.uniqueIndexR(customIndexName: String? = null, vararg columns: Column<*>): Index = indexR(customIndexName, true, *columns) +fun ExpressionWithColumnType.abs(): Function = Abs(this) + +class Abs(val expr: Expression) : Function(IntegerColumnType()) { + override fun toSQL(queryBuilder: QueryBuilder): String = "ABS(${expr.toSQL(queryBuilder)})" +} + +fun > SqlExpressionBuilder.greater(col1: ExpressionWithColumnType, col2: ExpressionWithColumnType): Expression { + return case(col1) + .When(col1.greater(col2), col1) + .Else(col2) +} + diff --git a/src/main/kotlin/io/dico/parcels2/storage/exposed/IdTables.kt b/src/main/kotlin/io/dico/parcels2/storage/exposed/IdTables.kt index ac6e431..33314aa 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/exposed/IdTables.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/exposed/IdTables.kt @@ -3,12 +3,12 @@ package io.dico.parcels2.storage.exposed import io.dico.parcels2.ParcelId -import io.dico.parcels2.ParcelOwner import io.dico.parcels2.ParcelWorldId +import io.dico.parcels2.PlayerProfile import io.dico.parcels2.util.toByteArray import io.dico.parcels2.util.toUUID import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.statements.InsertStatement +import org.jetbrains.exposed.sql.statements.UpdateBuilder import java.util.UUID sealed class IdTransactionsTable, QueryObj>(tableName: String, columnName: String) @@ -23,16 +23,16 @@ sealed class IdTransactionsTable, return select { where(table) }.firstOrNull()?.let { it[id] } } - internal inline fun insertAndGetId(objName: String, noinline body: TableT.(InsertStatement) -> Unit): Int { - return table.insert(body)[id] ?: insertError(objName) + internal inline fun getOrInitId(getId: () -> Int?, noinline body: TableT.(UpdateBuilder<*>) -> Unit, objName: () -> String): Int { + return getId() ?: table.insertIgnore(body)[id] ?: getId() ?: throw ExposedDatabaseException("This should not happen - failed to insert ${objName()} and get its id") } - private inline fun insertError(obj: String): Nothing = throw ExposedDatabaseException("This should not happen - failed to insert $obj and getParcelDeferred its id") - abstract fun getId(obj: QueryObj): Int? abstract fun getOrInitId(obj: QueryObj): Int - fun getId(id: Int): QueryObj? = select { this@IdTransactionsTable.id eq id }.firstOrNull()?.let { getId(it) } - abstract fun getId(row: ResultRow): QueryObj? + fun getItem(id: Int): QueryObj? = select { this@IdTransactionsTable.id eq id }.firstOrNull()?.let { getItem(it) } + abstract fun getItem(row: ResultRow): QueryObj? + + fun getId(obj: QueryObj, init: Boolean): Int? = if (init) getOrInitId(obj) else getId(obj) } object WorldsT : IdTransactionsTable("parcel_worlds", "world_id") { @@ -44,14 +44,16 @@ object WorldsT : IdTransactionsTable("parcel_worlds", "w internal inline fun getId(worldName: String, binaryUid: ByteArray?): Int? = getId { (name eq worldName).let { if (binaryUid == null) it else it or (uid eq binaryUid) } } internal inline fun getId(worldName: String, uid: UUID?): Int? = getId(worldName, uid?.toByteArray()) internal inline fun getOrInitId(worldName: String, worldUid: UUID?): Int = worldUid?.toByteArray().let { binaryUid -> - getId(worldName, binaryUid) - ?: insertAndGetId("world named $worldName") { it[name] = worldName; binaryUid?.let { buid -> it[uid] = buid } } + return getOrInitId( + { getId(worldName, binaryUid) }, + { it[name] = worldName; it[uid] = binaryUid }, + { "world named $worldName" }) } override fun getId(world: ParcelWorldId): Int? = getId(world.name, world.uid) override fun getOrInitId(world: ParcelWorldId): Int = getOrInitId(world.name, world.uid) - override fun getId(row: ResultRow): ParcelWorldId { + override fun getItem(row: ResultRow): ParcelWorldId { return ParcelWorldId(row[name], row[uid]?.toUUID()) } } @@ -60,7 +62,7 @@ object ParcelsT : IdTransactionsTable("parcels", "parcel_id" val world_id = integer("world_id").references(WorldsT.id) val px = integer("px") val pz = integer("pz") - val owner_id = integer("owner_id").references(OwnersT.id).nullable() + val owner_id = integer("owner_id").references(ProfilesT.id).nullable() val claim_time = datetime("claim_time").nullable() val index_location = uniqueIndexR("index_location", world_id, px, pz) @@ -68,8 +70,10 @@ object ParcelsT : IdTransactionsTable("parcels", "parcel_id" private inline fun getId(worldName: String, worldUid: UUID?, parcelX: Int, parcelZ: Int): Int? = WorldsT.getId(worldName, worldUid)?.let { getId(it, parcelX, parcelZ) } private inline fun getOrInitId(worldName: String, worldUid: UUID?, parcelX: Int, parcelZ: Int): Int { val worldId = WorldsT.getOrInitId(worldName, worldUid) - return getId(worldId, parcelX, parcelZ) - ?: insertAndGetId("parcel at $worldName($parcelX, $parcelZ)") { it[world_id] = worldId; it[px] = parcelX; it[pz] = parcelZ } + return getOrInitId( + { getId(worldId, parcelX, parcelZ) }, + { it[world_id] = worldId; it[px] = parcelX; it[pz] = parcelZ }, + { "parcel at $worldName($parcelX, $parcelZ)" }) } override fun getId(parcel: ParcelId): Int? = getId(parcel.worldId.name, parcel.worldId.uid, parcel.x, parcel.z) @@ -78,41 +82,56 @@ object ParcelsT : IdTransactionsTable("parcels", "parcel_id" private inline fun getRow(id: Int): ResultRow? = select { ParcelsT.id eq id }.firstOrNull() fun getRow(parcel: ParcelId): ResultRow? = getId(parcel)?.let { getRow(it) } - override fun getId(row: ResultRow): ParcelId? { + override fun getItem(row: ResultRow): ParcelId? { val worldId = row[world_id] - val world = WorldsT.getId(worldId) ?: return null + val world = WorldsT.getItem(worldId) ?: return null return ParcelId(world, row[px], row[pz]) } } -object OwnersT : IdTransactionsTable("parcel_owners", "owner_id") { +object ProfilesT : IdTransactionsTable("parcel_profiles", "owner_id") { val uuid = binary("uuid", 16).nullable() val name = varchar("name", 32) val index_pair = uniqueIndexR("index_pair", uuid, name) private inline fun getId(binaryUuid: ByteArray) = getId { uuid eq binaryUuid } private inline fun getId(uuid: UUID) = getId(uuid.toByteArray()) - private inline fun getId(nameIn: String) = getId { uuid.isNull() and (name eq nameIn) } + private inline fun getId(nameIn: String) = getId { uuid.isNull() and (name.lowerCase() eq nameIn.toLowerCase()) } + private inline fun getRealId(nameIn: String) = getId { uuid.isNotNull() and (name.lowerCase() eq nameIn.toLowerCase()) } - private inline fun getOrInitId(uuid: UUID, name: String) = uuid.toByteArray().let { binaryUuid -> - getId(binaryUuid) ?: insertAndGetId("owner(uuid = $uuid)") { - it[this@OwnersT.uuid] = binaryUuid - it[this@OwnersT.name] = name - } + private inline fun getOrInitId(uuid: UUID, name: String) = uuid.toByteArray().let { binaryUuid -> getOrInitId( + { getId(binaryUuid) }, + { it[this@ProfilesT.uuid] = binaryUuid; it[this@ProfilesT.name] = name }, + { "profile(uuid = $uuid, name = $name)" }) } - private inline fun getOrInitId(name: String) = - getId(name) ?: insertAndGetId("owner(name = $name)") { it[OwnersT.name] = name } + private inline fun getOrInitId(name: String) = getOrInitId( + { getId(name) }, + { it[ProfilesT.name] = name }, + { "owner(name = $name)" }) - override fun getId(owner: ParcelOwner): Int? = - if (owner.hasUUID) getId(owner.uuid!!) - else getId(owner.name!!) - override fun getOrInitId(owner: ParcelOwner): Int = - if (owner.hasUUID) getOrInitId(owner.uuid!!, owner.notNullName) - else getOrInitId(owner.name!!) + override fun getId(profile: PlayerProfile): Int? = when (profile) { + is PlayerProfile.Real -> getId(profile.uuid) + is PlayerProfile.Fake -> getId(profile.name) + is PlayerProfile.Unresolved -> getRealId(profile.name) + else -> throw IllegalArgumentException() + } + + override fun getOrInitId(profile: PlayerProfile): Int = when (profile) { + is PlayerProfile.Real -> getOrInitId(profile.uuid, profile.notNullName) + is PlayerProfile.Fake -> getOrInitId(profile.name) + else -> throw IllegalArgumentException() + } - override fun getId(row: ResultRow): ParcelOwner { - return row[uuid]?.toUUID()?.let { ParcelOwner(it) } ?: ParcelOwner(row[name]) + override fun getItem(row: ResultRow): PlayerProfile { + return PlayerProfile(row[uuid]?.toUUID(), row[name]) } + + fun getRealItem(id: Int): PlayerProfile.Real? { + return getItem(id) as? PlayerProfile.Real + } + } + +// val ParcelsWithOptionsT = ParcelsT.join(ParcelOptionsT, JoinType.INNER, onColumn = ParcelsT.id, otherColumn = ParcelOptionsT.parcel_id) \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/storage/exposed/ListTables.kt b/src/main/kotlin/io/dico/parcels2/storage/exposed/ListTables.kt index 20b36b1..bbf6872 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/exposed/ListTables.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/exposed/ListTables.kt @@ -2,17 +2,13 @@ package io.dico.parcels2.storage.exposed -import io.dico.parcels2.AddedStatus -import io.dico.parcels2.ParcelId -import io.dico.parcels2.ParcelOwner -import io.dico.parcels2.util.toByteArray -import io.dico.parcels2.util.toUUID +import io.dico.parcels2.* import kotlinx.coroutines.experimental.channels.SendChannel import org.jetbrains.exposed.sql.* import java.util.UUID object AddedLocalT : AddedTable("parcels_added_local", ParcelsT) -object AddedGlobalT : AddedTable("parcels_added_global", OwnersT) +object AddedGlobalT : AddedTable("parcels_added_global", ProfilesT) object ParcelOptionsT : Table("parcel_options") { val parcel_id = integer("parcel_id").primaryKey().references(ParcelsT.id, ReferenceOption.CASCADE) @@ -20,55 +16,59 @@ object ParcelOptionsT : Table("parcel_options") { val interact_inputs = bool("interact_inputs").default(true) } -typealias AddedStatusSendChannel = SendChannel>> +typealias AddedStatusSendChannel = SendChannel> sealed class AddedTable(name: String, val idTable: IdTransactionsTable<*, AttachT>) : Table(name) { val attach_id = integer("attach_id").references(idTable.id, ReferenceOption.CASCADE) - val player_uuid = binary("player_uuid", 16) + val profile_id = integer("profile_id").references(ProfilesT.id, ReferenceOption.CASCADE) val allowed_flag = bool("allowed_flag") - val index_pair = uniqueIndexR("index_pair", attach_id, player_uuid) - - fun setPlayerStatus(attachedOn: AttachT, player: UUID, status: AddedStatus) { - val binaryUuid = player.toByteArray() + val index_pair = uniqueIndexR("index_pair", attach_id, profile_id) + fun setPlayerStatus(attachedOn: AttachT, player: PlayerProfile.Real, status: AddedStatus) { if (status.isDefault) { - idTable.getId(attachedOn)?.let { id -> - deleteWhere { (attach_id eq id) and (player_uuid eq binaryUuid) } + val player_id = ProfilesT.getId(player) ?: return + idTable.getId(attachedOn)?.let { holder -> + deleteWhere { (attach_id eq holder) and (profile_id eq player_id) } } return } - val id = idTable.getOrInitId(attachedOn) + val holder = idTable.getOrInitId(attachedOn) + val player_id = ProfilesT.getOrInitId(player) upsert(conflictIndex = index_pair) { - it[attach_id] = id - it[player_uuid] = binaryUuid + it[attach_id] = holder + it[profile_id] = player_id it[allowed_flag] = status.isAllowed } } - fun readAddedData(id: Int): MutableMap { - return slice(player_uuid, allowed_flag).select { attach_id eq id } - .associateByTo(hashMapOf(), { it[player_uuid].toUUID() }, { it[allowed_flag].asAddedStatus() }) + fun readAddedData(id: Int): MutableAddedDataMap { + val list = slice(profile_id, allowed_flag).select { attach_id eq id } + val result = MutableAddedDataMap() + for (row in list) { + val profile = ProfilesT.getRealItem(row[profile_id]) ?: continue + result[profile] = row[allowed_flag].asAddedStatus() + } + return result } - suspend fun sendAllAddedData(channel: AddedStatusSendChannel) { - /* + fun sendAllAddedData(channel: AddedStatusSendChannel) { val iterator = selectAll().orderBy(attach_id).iterator() if (iterator.hasNext()) { val firstRow = iterator.next() var id: Int = firstRow[attach_id] - var attach: SerializableT? = null - var map: MutableMap? = null + var attach: AttachT? = null + var map: MutableAddedDataMap? = null fun initAttachAndMap() { - attach = idTable.getId(id) + attach = idTable.getItem(id) map = attach?.let { mutableMapOf() } } - suspend fun sendIfPresent() { + fun sendIfPresent() { if (attach != null && map != null && map!!.isNotEmpty()) { - channel.send(attach!! to map!!) + channel.offer(attach!! to map!!) } attach = null map = null @@ -88,13 +88,13 @@ sealed class AddedTable(name: String, val idTable: IdTransactionsTable< continue // owner not found for this owner id } - val player_uuid = row[player_uuid].toUUID() + val profile = ProfilesT.getRealItem(row[profile_id]) ?: continue val status = row[allowed_flag].asAddedStatus() - map!![player_uuid] = status + map!![profile] = status } sendIfPresent() - }*/ + } } private inline fun Boolean?.asAddedStatus() = if (this == null) AddedStatus.DEFAULT else if (this) AddedStatus.ALLOWED else AddedStatus.BANNED diff --git a/src/main/kotlin/io/dico/parcels2/storage/migration/plotme/PlotmeMigration.kt b/src/main/kotlin/io/dico/parcels2/storage/migration/plotme/PlotmeMigration.kt index 1f6e49c..f0c0cd8 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/migration/plotme/PlotmeMigration.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/migration/plotme/PlotmeMigration.kt @@ -6,45 +6,43 @@ import com.zaxxer.hikari.HikariDataSource import io.dico.parcels2.* import io.dico.parcels2.options.PlotmeMigrationOptions import io.dico.parcels2.storage.Storage +import io.dico.parcels2.storage.exposed.abs +import io.dico.parcels2.storage.exposed.greater import io.dico.parcels2.storage.migration.Migration -import io.dico.parcels2.util.Vec2i -import io.dico.parcels2.util.isValid import io.dico.parcels2.util.toUUID -import io.dico.parcels2.util.uuid -import kotlinx.coroutines.experimental.* -import org.bukkit.Bukkit +import kotlinx.coroutines.experimental.Job +import kotlinx.coroutines.experimental.launch +import kotlinx.coroutines.experimental.newFixedThreadPoolContext import org.jetbrains.exposed.sql.* import org.slf4j.LoggerFactory -import java.io.ByteArrayOutputStream import java.sql.Blob import java.util.UUID -import java.util.concurrent.ConcurrentHashMap import javax.sql.DataSource -import kotlin.coroutines.experimental.coroutineContext class PlotmeMigration(val options: PlotmeMigrationOptions) : Migration { private var dataSource: DataSource? = null private var database: Database? = null private var isShutdown: Boolean = false private val mlogger = LoggerFactory.getLogger("PlotMe Migrator") + val dispatcher = newFixedThreadPoolContext(1, "PlotMe Migration Thread") private fun transaction(statement: Transaction.() -> T) = org.jetbrains.exposed.sql.transactions.transaction(database!!, statement) override fun migrateTo(storage: Storage): Job { - return launch(context = storage.asyncDispatcher) { + return launch(dispatcher) { init() - transaction { launch(context = Unconfined, start = CoroutineStart.UNDISPATCHED) { doWork(storage) } } + doWork(storage) shutdown() } } - suspend fun init() { - if (isShutdown) throw IllegalStateException() + fun init() { + if (isShutdown || database != null) throw IllegalStateException() dataSource = options.storage.getDataSourceFactory()!!() database = Database.connect(dataSource!!) } - suspend fun shutdown() { + fun shutdown() { if (isShutdown) throw IllegalStateException() dataSource?.let { (it as? HikariDataSource)?.close() @@ -53,74 +51,61 @@ class PlotmeMigration(val options: PlotmeMigrationOptions) : Migration { isShutdown = true } - private val parcelsCache = hashMapOf>() - - private fun getMap(worldName: String): MutableMap? { - val mapped = options.worldsFromTo[worldName] ?: return null - return parcelsCache.computeIfAbsent(mapped) { mutableMapOf() } - } - - private fun getData(worldName: String, position: Vec2i): ParcelData? { - return getMap(worldName)?.computeIfAbsent(position) { ParcelDataHolder(addedMap = ConcurrentHashMap()) } - } - - suspend fun doWork(target: Storage): Unit { - if (!PlotmePlotsT.exists()) { - mlogger.warn("Plotme tables don't appear to exist. Exiting.") - return + suspend fun doWork(target: Storage) { + val exit = transaction { + (!PlotmePlotsT.exists()).also { + if (it) mlogger.warn("Plotme tables don't appear to exist. Exiting.") + } } + if (exit) return - parcelsCache.clear() + val worldCache = options.worldsFromTo.mapValues { ParcelWorldId(it.value) } - iterPlotmeTable(PlotmePlotsT) { data, row -> - // in practice, owner_uuid is not null for any plot currently. It will convert well. - data.owner = ParcelOwner(row[owner_uuid]?.toUUID(), row[owner_name]) + fun getParcelId(table: PlotmeTable, row: ResultRow): ParcelId? { + val world = worldCache[row[table.world_name]] ?: return null + return ParcelId(world, row[table.px], row[table.pz]) } - launch(context = target.asyncDispatcher) { - iterPlotmeTable(PlotmeAllowedT) { data, row -> - val uuid = row[player_uuid]?.toUUID() - ?: Bukkit.getOfflinePlayer(row[player_name]).takeIf { it.isValid }?.uuid - ?: return@iterPlotmeTable - - data.setAddedStatus(uuid, AddedStatus.ALLOWED) + fun PlotmePlotPlayerMap.transmitPlotmeAddedTable(kind: AddedStatus) { + selectAll().forEach { row -> + val parcel = getParcelId(this, row) ?: return@forEach + val profile = StatusKey.safe(row[player_uuid]?.toUUID(), row[player_name]) ?: return@forEach + target.setParcelPlayerStatus(parcel, profile, kind) } } - launch(context = target.asyncDispatcher) { - iterPlotmeTable(PlotmeDeniedT) { data, row -> - val uuid = row[player_uuid]?.toUUID() - ?: Bukkit.getOfflinePlayer(row[player_name]).takeIf { it.isValid }?.uuid - ?: return@iterPlotmeTable - - data.setAddedStatus(uuid, AddedStatus.BANNED) - } + mlogger.info("Transmitting data from plotmeplots table") + transaction { + PlotmePlotsT.selectAll() + .orderBy(PlotmePlotsT.world_name) + .orderBy(with(SqlExpressionBuilder) { greater(PlotmePlotsT.px.abs(), PlotmePlotsT.pz.abs()) }) + .forEach { row -> + val parcel = getParcelId(PlotmePlotsT, row) ?: return@forEach + val owner = PlayerProfile.safe(row[PlotmePlotsT.owner_uuid]?.toUUID(), row[PlotmePlotsT.owner_name]) + target.setParcelOwner(parcel, owner) + } } - println(coroutineContext[Job]!!.children) - coroutineContext[Job]!!.joinChildren() + mlogger.info("Transmitting data from plotmeallowed table") + transaction { + PlotmeAllowedT.transmitPlotmeAddedTable(AddedStatus.ALLOWED) + } - for ((worldName, map) in parcelsCache) { - val world = ParcelWorldId(worldName) - for ((pos, data) in map) { - val parcel = ParcelId(world, pos) - target.setParcelData(parcel, data) - } + mlogger.info("Transmitting data from plotmedenied table") + transaction { + PlotmeDeniedT.transmitPlotmeAddedTable(AddedStatus.BANNED) } + mlogger.warn("Data has been **transmitted**.") + mlogger.warn("Loading parcel data might take a while as enqueued transactions from this migration are completed.") } - private fun Blob.toUUID(): UUID { - val out = ByteArrayOutputStream(16) - binaryStream.copyTo(out, bufferSize = 16) - return out.toByteArray().toUUID() + private fun Blob.toUUID(): UUID? { + val ba = ByteArray(16) + val count = binaryStream.read(ba, 0, 16) + if (count < 16) return null + return ba.toUUID() } - private inline fun iterPlotmeTable(table: T, block: T.(ParcelData, ResultRow) -> Unit) { - table.selectAll().forEach { row -> - val data = getData(row[table.world_name], Vec2i(row[table.px], row[table.pz])) ?: return@forEach - table.block(data, row) - } - } } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/util/MiscExtensions.kt b/src/main/kotlin/io/dico/parcels2/util/MiscExtensions.kt index a4ab58d..877d1cc 100644 --- a/src/main/kotlin/io/dico/parcels2/util/MiscExtensions.kt +++ b/src/main/kotlin/io/dico/parcels2/util/MiscExtensions.kt @@ -15,6 +15,9 @@ fun File.tryCreate(): Boolean { return true } +inline fun Boolean.alsoIfTrue(block: () -> Unit): Boolean = also { if (it) block() } +inline fun Boolean.alsoIfFalse(block: () -> Unit): Boolean = also { if (!it) block() } + inline fun Any.synchronized(block: () -> R): R = synchronized(this, block) inline fun T?.isNullOr(condition: T.() -> Boolean): Boolean = this == null || condition() diff --git a/src/main/kotlin/io/dico/parcels2/util/PlayerExtensions.kt b/src/main/kotlin/io/dico/parcels2/util/PlayerExtensions.kt index 8713da7..9604365 100644 --- a/src/main/kotlin/io/dico/parcels2/util/PlayerExtensions.kt +++ b/src/main/kotlin/io/dico/parcels2/util/PlayerExtensions.kt @@ -2,12 +2,14 @@ package io.dico.parcels2.util import io.dico.dicore.Formatting import io.dico.parcels2.ParcelsPlugin +import io.dico.parcels2.PlayerProfile import io.dico.parcels2.logger import org.bukkit.OfflinePlayer import org.bukkit.entity.Player import org.bukkit.plugin.java.JavaPlugin inline val OfflinePlayer.uuid get() = uniqueId + @Suppress("UsePropertyAccessSyntax") inline val OfflinePlayer.isValid get() = isOnline() || hasPlayedBefore() diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index c2c9ddb..eff524c 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,41 +1,17 @@ - - %magenta(%-8.-8(%thread)) %highlight(%-5level) %boldCyan(%6.-32logger{32}) - %msg + + %magenta(%-16.-16(%thread)) %highlight(%-5level) %boldCyan(%6.-32logger{32}) - %msg - + - - true - C:/Parcels/sql.log - - - - %magenta(%-8.-8(%thread)) %highlight(%-5level) %boldCyan(%6.-32logger{32}) - %msg - - - - C:/Parcels/sql%i.log - 1 - 3 - - - - 1MB - - - - - - true - - - - - + + + + \ No newline at end of file -- cgit v1.2.3