diff options
author | Dico200 <dico.karssiens@gmail.com> | 2018-07-22 07:12:53 +0200 |
---|---|---|
committer | Dico200 <dico.karssiens@gmail.com> | 2018-07-22 07:12:53 +0200 |
commit | 9f81a74bd9e89541448bf79d73d0b9bc297e72ee (patch) | |
tree | d32eb5e641bf8f81c172d866fdf05d22018c08ba /src/main/kotlin/io/dico/parcels2 | |
parent | dbcc90ac8a938e9098dc93206f061cc2a4765ad6 (diff) |
port RedstonerPlots a bit
Diffstat (limited to 'src/main/kotlin/io/dico/parcels2')
-rw-r--r-- | src/main/kotlin/io/dico/parcels2/Options.kt | 105 | ||||
-rw-r--r-- | src/main/kotlin/io/dico/parcels2/Parcel.kt | 31 | ||||
-rw-r--r-- | src/main/kotlin/io/dico/parcels2/ParcelWorld.kt | 172 | ||||
-rw-r--r-- | src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt | 9 | ||||
-rw-r--r-- | src/main/kotlin/io/dico/parcels2/WorldGenerator.kt | 263 | ||||
-rw-r--r-- | src/main/kotlin/io/dico/parcels2/math/NumberExtensions.kt | 32 | ||||
-rw-r--r-- | src/main/kotlin/io/dico/parcels2/storage/Backing.kt | 37 | ||||
-rw-r--r-- | src/main/kotlin/io/dico/parcels2/storage/Exposed.kt | 85 | ||||
-rw-r--r-- | src/main/kotlin/io/dico/parcels2/storage/Hikari.kt | 51 | ||||
-rw-r--r-- | src/main/kotlin/io/dico/parcels2/storage/Jackson.kt | 95 | ||||
-rw-r--r-- | src/main/kotlin/io/dico/parcels2/storage/SerializableTypes.kt | 28 | ||||
-rw-r--r-- | src/main/kotlin/io/dico/parcels2/storage/Storage.kt | 62 | ||||
-rw-r--r-- | src/main/kotlin/io/dico/parcels2/storage/StorageFactory.kt | 46 | ||||
-rw-r--r-- | src/main/kotlin/io/dico/parcels2/storage/backing/Exposed.kt | 41 | ||||
-rw-r--r-- | src/main/kotlin/io/dico/parcels2/util/PlayerExtensions.kt | 22 |
15 files changed, 986 insertions, 93 deletions
diff --git a/src/main/kotlin/io/dico/parcels2/Options.kt b/src/main/kotlin/io/dico/parcels2/Options.kt new file mode 100644 index 0000000..4324df3 --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/Options.kt @@ -0,0 +1,105 @@ +package io.dico.parcels2 + + +import com.fasterxml.jackson.annotation.JsonIgnore +import io.dico.parcels2.storage.Storage +import io.dico.parcels2.storage.StorageFactory +import io.dico.parcels2.storage.yamlObjectMapper +import org.bukkit.Bukkit +import org.bukkit.Bukkit.createBlockData +import org.bukkit.GameMode +import org.bukkit.Material +import org.bukkit.block.Biome +import org.bukkit.block.data.BlockData +import java.io.Reader +import java.io.Writer +import java.util.* + +class Options { + var worlds: Map<String, WorldOptions> = HashMap() + private set + var storage: StorageOptions = StorageOptions("mysql", DataConnectionOptions()) + + fun addWorld(name: String, options: WorldOptions) = (worlds as MutableMap).put(name, options) + + fun addDefaultWorld() = addWorld("plotworld", WorldOptions()) + + fun writeTo(writer: Writer) = yamlObjectMapper.writeValue(writer, this) + + fun mergeFrom(reader: Reader) = yamlObjectMapper.readerForUpdating(this).readValue<Options>(reader) + + override fun toString(): String = yamlObjectMapper.writeValueAsString(this) + +} + +data class WorldOptions(var gameMode: GameMode? = GameMode.CREATIVE, + var dayTime: Boolean = true, + var noWeather: Boolean = true, + var dropEntityItems: Boolean = true, + var doTileDrops: Boolean = false, + var disableExplosions: Boolean = true, + var blockPortalCreation: Boolean = true, + var blockMobSpawning: Boolean = true, + var blockedItems: Set<Material> = EnumSet.of(Material.FLINT_AND_STEEL, Material.SNOWBALL), + var axisLimit: Int = 10, + var generator: GeneratorOptions = DefaultGeneratorOptions()) { + +} + +abstract class GeneratorOptions { + + abstract fun generatorFactory(): GeneratorFactory + + fun getGenerator(worldName: String) = generatorFactory().newParcelGenerator(worldName, this) + +} + +data class DefaultGeneratorOptions(var defaultBiome: Biome = Biome.JUNGLE, + var wallType: BlockData = createBlockData(Material.STONE_SLAB), + var floorType: BlockData = createBlockData(Material.QUARTZ_BLOCK), + var fillType: BlockData = createBlockData(Material.QUARTZ_BLOCK), + var pathMainType: BlockData = createBlockData(Material.SANDSTONE), + var pathAltType: BlockData = 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 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<String, Int>? { + 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).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/")
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/Parcel.kt b/src/main/kotlin/io/dico/parcels2/Parcel.kt index 6fd8cf3..0ec2c0b 100644 --- a/src/main/kotlin/io/dico/parcels2/Parcel.kt +++ b/src/main/kotlin/io/dico/parcels2/Parcel.kt @@ -2,6 +2,7 @@ package io.dico.parcels2 import io.dico.parcels2.math.Vec2i import io.dico.parcels2.util.getPlayerName +import io.dico.parcels2.util.hasBuildAnywhere import org.bukkit.Bukkit import org.bukkit.entity.Player import java.util.* @@ -10,13 +11,29 @@ class Parcel(val world: ParcelWorld, val pos: Vec2i, var data: ParcelData = ParcelData()) { - + val id get() = "${pos.x}:${pos.z}" } class ParcelData { - val owner: ParcelOwner? = null - val added = mutableMapOf<UUID, Boolean>() + private val added = mutableMapOf<UUID, AddedStatus>() + var owner: ParcelOwner? = null + + fun setAddedStatus(uuid: UUID): AddedStatus = added.getOrDefault(uuid, AddedStatus.DEFAULT) + fun setAddedStatus(uuid: UUID, state: AddedStatus) = state.takeIf { it != AddedStatus.DEFAULT }?.let { added[uuid] = it } + ?: added.remove(uuid) + + fun isBanned(uuid: UUID) = setAddedStatus(uuid) == AddedStatus.BANNED + fun isAllowed(uuid: UUID) = setAddedStatus(uuid) == AddedStatus.ALLOWED + fun canBuild(player: Player) = isAllowed(player.uniqueId) + || owner?.matches(player, allowNameMatch = false) ?: false + || player.hasBuildAnywhere +} + +enum class AddedStatus { + DEFAULT, + ALLOWED, + BANNED } data class ParcelOwner(val uuid: UUID? = null, @@ -29,11 +46,13 @@ data class ParcelOwner(val uuid: UUID? = null, val playerName get() = getPlayerName(uuid, name) @Suppress("DEPRECATION") - val offlinePlayer get() = (uuid?.let { Bukkit.getOfflinePlayer(it) } ?: Bukkit.getOfflinePlayer(name)) - ?.takeIf { it.isOnline() || it.hasPlayedBefore() } + val offlinePlayer + get() = (uuid?.let { Bukkit.getOfflinePlayer(it) } ?: Bukkit.getOfflinePlayer(name)) + ?.takeIf { it.isOnline() || it.hasPlayedBefore() } fun matches(player: Player, allowNameMatch: Boolean = false): Boolean { - return player.uniqueId == uuid || (allowNameMatch && player.name == name) + return uuid?.let { it == player.uniqueId } ?: false + || (allowNameMatch && name?.let { it == player.name } ?: false) } }
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt b/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt index 9832ce8..97e3942 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt @@ -1,4 +1,174 @@ package io.dico.parcels2 -class ParcelWorld { +import io.dico.parcels2.math.Vec2i +import io.dico.parcels2.math.floor +import kotlinx.coroutines.experimental.launch +import org.bukkit.Bukkit +import org.bukkit.Location +import org.bukkit.World +import org.bukkit.WorldCreator +import org.bukkit.block.Block +import org.bukkit.entity.Entity +import org.bukkit.entity.Player +import java.util.* +import kotlin.coroutines.experimental.buildSequence + +val worlds: Map<String, ParcelWorld> get() = _worlds +private val _worlds: MutableMap<String, ParcelWorld> = HashMap() + +fun getWorld(name: String): ParcelWorld? = _worlds.get(name) + +fun getWorld(world: World): ParcelWorld? = getWorld(world.name) + +fun getParcelAt(block: Block): Parcel? = getParcelAt(block.world, block.x, block.z) + +fun getParcelAt(player: Player): Parcel? = getParcelAt(player.location) + +fun getParcelAt(location: Location): Parcel? = getParcelAt(location.world, location.x.floor(), location.z.floor()) + +fun getParcelAt(world: World, x: Int, z: Int): Parcel? = getParcelAt(world.name, x, z) + +fun getParcelAt(world: String, x: Int, z: Int): Parcel? { + with (getWorld(world) ?: return null) { + return generator.parcelAt(x, z) + } +} + +fun loadWorlds(options: Options) { + for ((worldName, worldOptions) in options.worlds.entries) { + val world: ParcelWorld + try { + world = ParcelWorld(worldName, worldOptions, worldOptions.generator.getGenerator(worldName)) + } catch (ex: Exception) { + ex.printStackTrace() + continue + } + + _worlds.put(worldName, world) + + if (Bukkit.getWorld(worldName) == null) { + val bworld = WorldCreator(worldName).generator(world.generator).createWorld() + val spawn = world.generator.getFixedSpawnLocation(bworld, null) + bworld.setSpawnLocation(spawn.x.floor(), spawn.y.floor(), spawn.z.floor()) + } + + } + +} + +interface ParcelProvider { + + fun parcelAt(x: Int, z: Int): Parcel? + + fun parcelAt(vec: Vec2i): Parcel? = parcelAt(vec.x, vec.z) + + fun parcelAt(loc: Location): Parcel? = parcelAt(loc.x.floor(), loc.z.floor()) + + fun parcelAt(entity: Entity): Parcel? = parcelAt(entity.location) + + fun parcelAt(block: Block): Parcel? = parcelAt(block.x, block.z) +} + +class ParcelWorld(val name: String, + val options: WorldOptions, + val generator: ParcelGenerator) : ParcelProvider by generator { + val world: World by lazy { + val tmp = Bukkit.getWorld(name) + if (tmp == null) { + throw NullPointerException("World $name does not appear to be loaded") + } + tmp + } + + val container: ParcelContainer = DefaultParcelContainer(this) + + fun parcelByID(x: Int, z: Int): Parcel? { + TODO("not implemented") + } + + fun parcelByID(id: Vec2i): Parcel? = parcelByID(id.x, id.z) + + fun enforceOptionsIfApplicable() { + val world = world + val options = options + if (options.dayTime) { + world.setGameRuleValue("doDaylightCycle", "false") + world.setTime(6000) + } + + if (options.noWeather) { + world.setStorm(false) + world.setThundering(false) + world.weatherDuration = Integer.MAX_VALUE + } + + world.setGameRuleValue("doTileDrops", "${options.doTileDrops}") + } + +} + +abstract class ParcelContainer { + + abstract fun ployByID(x: Int, z: Int): Parcel? + + abstract fun nextEmptyParcel(): Parcel? + +} + +class DefaultParcelContainer(private val world: ParcelWorld) : ParcelContainer() { + private var parcels: Array<Array<Parcel>> + + init { + parcels = initArray(world.options.axisLimit, world) + } + + fun resizeIfSizeChanged() { + if (parcels.size / 2 != world.options.axisLimit) { + resize(world.options.axisLimit) + } + } + + fun resize(axisLimit: Int) { + parcels = initArray(axisLimit, world, this) + } + + fun initArray(axisLimit: Int, world: ParcelWorld, cur: DefaultParcelContainer? = null): Array<Array<Parcel>> { + val arraySize = 2 * axisLimit + 1 + return Array(arraySize, { + val x = it - axisLimit + Array(arraySize, { + val z = it - axisLimit + cur?.ployByID(x, z) ?: Parcel(world, Vec2i(x, z)) + }) + }) + } + + override fun ployByID(x: Int, z: Int): Parcel? { + return parcels[x][z] + } + + override fun nextEmptyParcel(): Parcel? { + TODO() + } + + fun allParcels(): Sequence<Parcel> = buildSequence { + for (array in parcels) { + yieldAll(array.iterator()) + } + } + + fun loadAllData() { + /* + val channel = Main.instance.storage.readParcelData(allParcels(), 100).channel + launch(Main.instance.storage.asyncDispatcher) { + for ((parcel, data) in channel) { + if (data != null) { + parcel.data = data + } + } + } + */ + TODO() + } + }
\ 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 6c1d6e9..5ffce7c 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt @@ -1,12 +1,15 @@ package io.dico.parcels2 import org.bukkit.plugin.java.JavaPlugin +import org.slf4j.LoggerFactory -class ParcelsPlugin : JavaPlugin() { - - +val logger = LoggerFactory.getLogger("ParcelsPlugin") +class ParcelsPlugin : JavaPlugin() { + override fun onEnable() { + super.onEnable() + } }
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/WorldGenerator.kt b/src/main/kotlin/io/dico/parcels2/WorldGenerator.kt new file mode 100644 index 0000000..8f6a3aa --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/WorldGenerator.kt @@ -0,0 +1,263 @@ +package io.dico.parcels2 + +import io.dico.parcels2.math.Vec2i +import io.dico.parcels2.math.clamp +import io.dico.parcels2.math.even +import io.dico.parcels2.math.umod +import org.bukkit.* +import org.bukkit.Bukkit.createBlockData +import org.bukkit.block.Biome +import org.bukkit.block.Block +import org.bukkit.block.BlockFace +import org.bukkit.block.Skull +import org.bukkit.block.data.BlockData +import org.bukkit.block.data.type.Sign +import org.bukkit.block.data.type.Slab +import org.bukkit.entity.Entity +import org.bukkit.generator.BlockPopulator +import org.bukkit.generator.ChunkGenerator +import java.util.* +import kotlin.coroutines.experimental.buildIterator +import kotlin.reflect.KClass + +abstract class ParcelGenerator : ChunkGenerator(), ParcelProvider { + abstract val world: ParcelWorld + + abstract val factory: GeneratorFactory + + abstract override fun generateChunkData(world: World?, random: Random?, chunkX: Int, chunkZ: Int, biome: BiomeGrid?): ChunkData + + abstract fun populate(world: World?, random: Random?, chunk: Chunk?) + + abstract override fun getFixedSpawnLocation(world: World?, random: Random?): Location + + override fun getDefaultPopulators(world: World?): MutableList<BlockPopulator> { + return Collections.singletonList(object : BlockPopulator() { + override fun populate(world: World?, random: Random?, chunk: Chunk?) { + this@ParcelGenerator.populate(world, random, chunk) + } + }) + } + + abstract fun updateOwner(parcel: Parcel) + + abstract fun getBottomCoord(parcel: Parcel): Vec2i + + abstract fun getHomeLocation(parcel: Parcel): Location + + abstract fun setBiome(parcel: Parcel, biome: Biome) + + abstract fun getEntities(parcel: Parcel): Collection<Entity> + + abstract fun getBlocks(parcel: Parcel, yRange: IntRange = 0..255): Iterator<Block> + +} + +interface GeneratorFactory { + companion object GeneratorFactories { + private val map: MutableMap<String, GeneratorFactory> = HashMap() + + fun registerFactory(generator: GeneratorFactory): Boolean = map.putIfAbsent(generator.name, generator) == null + + fun getFactory(name: String): GeneratorFactory? = map.get(name) + + init { + registerFactory(DefaultParcelGenerator.Factory) + } + + } + + val name: String + + val optionsClass: KClass<out GeneratorOptions> + + fun newParcelGenerator(worldName: String, options: GeneratorOptions): ParcelGenerator + +} + +class DefaultParcelGenerator(name: String, private val o: DefaultGeneratorOptions) : ParcelGenerator() { + override val world: ParcelWorld by lazy { TODO() } + override val factory = Factory + + 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 + val makePathAlt = o.pathSize > 4 + + private inline fun <T> generate(chunkX: Int, + chunkZ: Int, + floor: T, wall: + T, pathMain: T, + pathAlt: T, + fill: T, + setter: (Int, Int, Int, T) -> Unit) { + + val floorHeight = o.floorHeight + val parcelSize = o.parcelSize + val sectionSize = sectionSize + val pathOffset = pathOffset + val makePathMain = makePathMain + val makePathAlt = makePathAlt + + // parcel bottom x and z + // umod is unsigned %: the result is always >= 0 + val pbx = ((chunkX shl 4) - o.offsetX) umod sectionSize + val pbz = ((chunkZ shl 4) - o.offsetZ) umod sectionSize + + var curHeight: Int + var x: Int + var z: Int + for (cx in 0..15) { + for (cz in 0..15) { + x = (pbx + cx) % sectionSize - pathOffset + z = (pbz + cz) % sectionSize - pathOffset + curHeight = floorHeight + + val type = when { + (x in 0 until parcelSize && z in 0 until parcelSize) -> floor + (x in -1..parcelSize && z in -1..parcelSize) -> { + curHeight++ + wall + } + (makePathAlt && x in -2 until parcelSize + 2 && z in -2 until parcelSize + 2) -> pathAlt + (makePathMain) -> pathMain + else -> { + curHeight++ + wall + } + } + + for (y in 0 until curHeight) { + setter(cx, y, cz, fill) + } + setter(cx, curHeight, cz, type) + } + } + } + + override fun generateChunkData(world: World?, random: Random?, chunkX: Int, chunkZ: Int, biome: BiomeGrid?): ChunkData { + val out = Bukkit.createChunkData(world) + generate(chunkX, chunkZ, o.floorType, o.wallType, o.pathMainType, o.pathAltType, o.fillType) { x, y, z, type -> + out.setBlock(x, y, z, type) + } + return out + } + + + override fun populate(world: World?, random: Random?, chunk: Chunk?) { + /* + generate(chunk!!.x, chunk.z, o.floorType.data, o.wallType.data, o.pathMainType.data, o.pathAltType.data, o.fillType.data) { x, y, z, type -> + if (type == 0.toByte()) chunk.getBlock(x, y, z).setData(type, false) + } + */ + } + + override fun getFixedSpawnLocation(world: World?, random: Random?): Location { + val fix = if (o.parcelSize.even) 0.5 else 0.0 + return Location(world, o.offsetX + fix, o.floorHeight + 1.0, o.offsetZ + fix) + } + + override fun parcelAt(x: Int, z: Int): Parcel? { + val sectionSize = sectionSize + val parcelSize = o.parcelSize + val absX = x - o.offsetX - pathOffset + val absZ = z - o.offsetZ - pathOffset + val modX = absX umod sectionSize + val modZ = absZ umod sectionSize + if (0 <= modX && modX < parcelSize && 0 <= modZ && modZ < parcelSize) { + return world.parcelByID((absX - modX) / sectionSize, (absZ - modZ) / sectionSize) + } + return null + } + + override fun getBottomCoord(parcel: Parcel): Vec2i = Vec2i(sectionSize * parcel.pos.x + pathOffset + o.offsetX, + sectionSize * parcel.pos.z + pathOffset + o.offsetZ) + + override fun getHomeLocation(parcel: Parcel): Location { + val bottom = getBottomCoord(parcel) + return Location(world.world, bottom.x.toDouble(), o.floorHeight + 1.0, bottom.z + (o.parcelSize - 1) / 2.0, -90F, 0F) + } + + override fun updateOwner(parcel: Parcel) { + val world = this.world.world + val b = getBottomCoord(parcel) + + val wallBlock = world.getBlockAt(b.x - 1, o.floorHeight + 1, b.z - 1) + val signBlock = world.getBlockAt(b.x - 2, o.floorHeight + 1, b.z - 1) + val skullBlock = world.getBlockAt(b.x - 1, o.floorHeight + 2, b.z - 1) + + val owner = parcel.data?.owner + if (owner == null) { + wallBlock.blockData = o.wallType + signBlock.type = Material.AIR + skullBlock.type = Material.AIR + } else { + + val wallBlockType: BlockData = if (o.wallType is Slab) + (o.wallType.clone() as Slab).apply { type = Slab.Type.DOUBLE } + else + o.wallType + + wallBlock.blockData = wallBlockType + + signBlock.blockData = (createBlockData(Material.WALL_SIGN) as Sign).apply { rotation = BlockFace.NORTH } + + val sign = signBlock.state as org.bukkit.block.Sign + sign.setLine(0, parcel.id) + sign.setLine(2, owner.playerName) + sign.update() + + skullBlock.type = Material.PLAYER_HEAD + val skull = skullBlock.state as Skull + if (owner.uuid != null) { + skull.owningPlayer = owner.offlinePlayer + } else { + skull.owner = owner.name + } + skull.rotation = BlockFace.WEST + skull.update() + } + } + + override fun setBiome(parcel: Parcel, biome: Biome) { + val world = this.world.world + val b = getBottomCoord(parcel) + val parcelSize = o.parcelSize + for (x in b.x until b.x + parcelSize) { + for (z in b.z until b.z + parcelSize) { + world.setBiome(x, z, biome) + } + } + } + + override fun getEntities(parcel: Parcel): Collection<Entity> { + val world = this.world.world + val b = getBottomCoord(parcel) + val parcelSize = o.parcelSize + val center = Location(world, (b.x + parcelSize) / 2.0, 128.0, (b.z + parcelSize) / 2.0) + return world.getNearbyEntities(center, parcelSize / 2.0 + 0.2, 128.0, parcelSize / 2.0 + 0.2) + } + + override fun getBlocks(parcel: Parcel, yRange: IntRange): Iterator<Block> = buildIterator { + val range = yRange.clamp(0, 255) + val world = this@DefaultParcelGenerator.world.world + val b = getBottomCoord(parcel) + val parcelSize = o.parcelSize + for (x in b.x until b.x + parcelSize) { + for (z in b.z until b.z + parcelSize) { + for (y in range) { + yield(world.getBlockAt(x, y, z)) + } + } + } + } + +}
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/math/NumberExtensions.kt b/src/main/kotlin/io/dico/parcels2/math/NumberExtensions.kt new file mode 100644 index 0000000..faf2939 --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/math/NumberExtensions.kt @@ -0,0 +1,32 @@ +package io.dico.parcels2.math + +fun Double.floor(): Int { + val down = toInt() + if (down.toDouble() != this && (java.lang.Double.doubleToRawLongBits(this).ushr(63).toInt()) == 1) { + return down-1 + } + return down +} + +infix fun Int.umod(divisor: Int): Int { + val out = this % divisor + if (out < 0) { + return out + divisor + } + return out +} + +val Int.even: Boolean get() = and(1) == 0 + +fun IntRange.clamp(min: Int, max: Int): IntRange { + if (first < min) { + if (last > max) { + return IntRange(min, max) + } + return IntRange(min, last) + } + if (last > max) { + return IntRange(first, max) + } + return this +}
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/storage/Backing.kt b/src/main/kotlin/io/dico/parcels2/storage/Backing.kt new file mode 100644 index 0000000..cd33b3d --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/storage/Backing.kt @@ -0,0 +1,37 @@ +package io.dico.parcels2.storage + +import io.dico.parcels2.Parcel +import io.dico.parcels2.ParcelData +import io.dico.parcels2.ParcelOwner +import io.dico.parcels2.storage.SerializableParcel +import kotlinx.coroutines.experimental.channels.ProducerScope +import java.util.* + +interface Backing { + + val name: String + + suspend fun init() + + suspend fun shutdown() + + /** + * This producer function is capable of constantly reading plots from a potentially infinite sequence, + * and provide plotdata for it as read from the database. + */ + + suspend fun ProducerScope<Pair<Parcel, ParcelData?>>.produceParcelData(parcels: Sequence<Parcel>) + + suspend fun readParcelData(plotFor: Parcel): ParcelData? + + suspend fun getOwnedParcels(user: ParcelOwner): List<SerializableParcel> + + suspend fun setParcelOwner(plotFor: Parcel, owner: ParcelOwner?) + + suspend fun setParcelPlayerState(plotFor: Parcel, player: UUID, state: Boolean?) + + suspend fun setParcelAllowsInteractInventory(plot: Parcel, value: Boolean) + + suspend fun setParcelAllowsInteractInputs(plot: Parcel, value: Boolean) + +}
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/storage/Exposed.kt b/src/main/kotlin/io/dico/parcels2/storage/Exposed.kt new file mode 100644 index 0000000..ea02b8b --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/storage/Exposed.kt @@ -0,0 +1,85 @@ +package io.dico.parcels2.storage + +import com.zaxxer.hikari.HikariDataSource +import io.dico.parcels2.Parcel +import io.dico.parcels2.ParcelData +import io.dico.parcels2.ParcelOwner +import kotlinx.coroutines.experimental.channels.ProducerScope +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.ReferenceOption +import org.jetbrains.exposed.sql.Table +import java.util.* +import javax.sql.DataSource + +object ParcelsTable : Table() { + val id = integer("id").autoIncrement().primaryKey() + val px = integer("px") + val pz = integer("pz") + val world_uuid = binary("world_uuid", 16).also { uniqueIndex("location", it, px, pz) } + val world = varchar("world", 32).nullable() + val owner_uuid = binary("owner_uuid", 16).nullable() + val owner = varchar("owner", 16).nullable() +} + +object ParcelsAddedTable : Table() { + val id = integer("id").references(ParcelsTable.id, ReferenceOption.CASCADE) + val player_uuid = binary("player_uuid", 16).also { uniqueIndex("pair", id, it) } + val allowed_flag = bool("allowed_flag") +} + +object PlayerAddedTable : Table() { + val owner_uuid = binary("owner_uuid", 16) + val player_uuid = binary("player_uuid", 16).also { uniqueIndex("pair", owner_uuid, it) } + val allowed_flag = bool("allowed_flag") +} + +class ExposedBacking(val dataSource: DataSource) : Backing { + override val name get() = "Exposed" + lateinit var database: Database + + override suspend fun init() { + database = Database.connect(dataSource) + } + + override suspend fun shutdown() { + if (dataSource is HikariDataSource) { + dataSource.close() + } + } + + override suspend fun ProducerScope<Pair<Parcel, ParcelData?>>.produceParcelData(parcels: Sequence<Parcel>) { + TODO() + } + + override suspend fun readParcelData(plotFor: Parcel): ParcelData? { + TODO() + } + + override suspend fun getOwnedParcels(user: ParcelOwner): List<SerializableParcel> { + TODO() + } + + override suspend fun setParcelOwner(plotFor: Parcel, owner: ParcelOwner?) { + TODO() + } + + override suspend fun setParcelPlayerState(plotFor: Parcel, player: UUID, state: Boolean?) { + TODO() + } + + override suspend fun setParcelAllowsInteractInventory(plot: Parcel, value: Boolean) { + TODO() + } + + override suspend fun setParcelAllowsInteractInputs(plot: Parcel, value: Boolean) { + TODO() + } + +} + + + + + + + diff --git a/src/main/kotlin/io/dico/parcels2/storage/Hikari.kt b/src/main/kotlin/io/dico/parcels2/storage/Hikari.kt new file mode 100644 index 0000000..7e4fb7f --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/storage/Hikari.kt @@ -0,0 +1,51 @@ +package io.dico.parcels2.storage + +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import io.dico.parcels2.DataConnectionOptions +import javax.sql.DataSource + +fun getHikariDataSource(dialectName: String, + driver: String, + dco: DataConnectionOptions): DataSource = with(HikariConfig()) { + + val (address, port) = dco.splitAddressAndPort() ?: throw IllegalArgumentException("Invalid address: ${dco.address}") + + poolName = "redstonerplots" + maximumPoolSize = dco.poolSize + dataSourceClassName = driver + username = dco.username + password = dco.password + connectionTimeout = 15000 + leakDetectionThreshold = 10000 + connectionTestQuery = "SELECT 1" + + addDataSourceProperty("serverName", address) + addDataSourceProperty("port", port.toString()) + addDataSourceProperty("databaseName", dco.database) + + // copied from github.com/lucko/LuckPerms + if (dialectName.toLowerCase() == "mariadb") { + addDataSourceProperty("properties", "useUnicode=true;characterEncoding=utf8") + } else { + // doesn't exist on the MariaDB driver + addDataSourceProperty("cachePrepStmts", "true") + addDataSourceProperty("alwaysSendSetIsolation", "false") + addDataSourceProperty("cacheServerConfiguration", "true") + addDataSourceProperty("elideSetAutoCommits", "true") + addDataSourceProperty("useLocalSessionState", "true") + + // already set as default on mariadb + addDataSourceProperty("useServerPrepStmts", "true") + addDataSourceProperty("prepStmtCacheSize", "250") + addDataSourceProperty("prepStmtCacheSqlLimit", "2048") + addDataSourceProperty("cacheCallableStmts", "true") + + // make sure unicode characters can be used. + addDataSourceProperty("characterEncoding", "utf8") + addDataSourceProperty("useUnicode", "true") + } + + HikariDataSource(this) + +} diff --git a/src/main/kotlin/io/dico/parcels2/storage/Jackson.kt b/src/main/kotlin/io/dico/parcels2/storage/Jackson.kt new file mode 100644 index 0000000..18ad31f --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/storage/Jackson.kt @@ -0,0 +1,95 @@ +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.* +import org.bukkit.Bukkit +import org.bukkit.block.data.BlockData +import kotlin.reflect.KClass +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<*> { + if (GeneratorOptions::class.isSuperclassOf(beanDesc?.beanClass?.kotlin as KClass<*>)) { + return GeneratorOptionsSerializer(serializer as JsonSerializer<GeneratorOptions>) + } + + return super.modifySerializer(config, beanDesc, serializer) + } + }) + + 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>(BlockData::class.java) { + + override fun serialize(value: BlockData, gen: JsonGenerator, provider: SerializerProvider) { + gen.writeString(value.asString) + } + +} + +private class BlockDataDeserializer : StdDeserializer<BlockData>(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<StorageOptions>() { + + override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): StorageOptions { + val node = p!!.readValueAsTree<JsonNode>() + 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, factory, options) + } + +} + +class StorageOptionsSerializer : StdSerializer<StorageOptions>(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() + } + } + +} +*/ diff --git a/src/main/kotlin/io/dico/parcels2/storage/SerializableTypes.kt b/src/main/kotlin/io/dico/parcels2/storage/SerializableTypes.kt new file mode 100644 index 0000000..4e467b1 --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/storage/SerializableTypes.kt @@ -0,0 +1,28 @@ +package io.dico.parcels2.storage + +import io.dico.parcels2.Parcel +import io.dico.parcels2.ParcelWorld +import io.dico.parcels2.math.Vec2i +import org.bukkit.Bukkit +import org.bukkit.World +import java.util.* + +data class SerializableWorld(val name: String? = null, + val uid: UUID? = null) { + + init { + uid ?: name ?: throw IllegalArgumentException("uuid and/or name must be present") + } + + val world: World? by lazy { uid?.let { Bukkit.getWorld(it) } ?: name?.let { Bukkit.getWorld(it) } } + val parcelWorld: ParcelWorld? by lazy { TODO() } +} + +/** + * Used by storage backing options to encompass the location of a parcel + */ +data class SerializableParcel(val world: SerializableWorld, + val coord: Vec2i) { + + val parcel: Parcel? by lazy { TODO() } +}
\ 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 7414b88..36f5400 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/Storage.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/Storage.kt @@ -1,69 +1,65 @@ package io.dico.parcels2.storage -import kotlinx.coroutines.experimental.CoroutineDispatcher -import kotlinx.coroutines.experimental.CoroutineScope -import kotlinx.coroutines.experimental.CoroutineStart -import kotlinx.coroutines.experimental.asCoroutineDispatcher -import kotlinx.coroutines.experimental.channels.ProducerJob +import io.dico.parcels2.Parcel +import io.dico.parcels2.ParcelData +import io.dico.parcels2.ParcelOwner +import kotlinx.coroutines.experimental.* +import kotlinx.coroutines.experimental.channels.ReceiveChannel import kotlinx.coroutines.experimental.channels.produce import java.util.* -import java.util.concurrent.CompletableFuture import java.util.concurrent.Executor import java.util.concurrent.Executors -/* interface Storage { - val name: String - val syncDispatcher: CoroutineDispatcher - val asyncDispatcher: CoroutineDispatcher - fun init(): CompletableFuture<Unit> + fun init(): Deferred<Unit> - fun shutdown(): CompletableFuture<Unit> + fun shutdown(): Deferred<Unit> - fun readPlotData(plotFor: Plot): CompletableFuture<PlotData?> + fun readParcelData(parcelFor: Parcel): Deferred<ParcelData?> - fun readPlotData(plotsFor: Sequence<Plot>, channelCapacity: Int): ProducerJob<Pair<Plot, PlotData?>> + fun readParcelData(parcelsFor: Sequence<Parcel>, channelCapacity: Int): ReceiveChannel<Pair<Parcel, ParcelData?>> - fun getOwnedPlots(user: PlotOwner): CompletableFuture<List<SerializablePlot>> + fun getOwnedParcels(user: ParcelOwner): Deferred<List<SerializableParcel>> - fun setPlotOwner(plotFor: Plot, owner: PlotOwner?): CompletableFuture<Unit> + fun setParcelOwner(parcelFor: Parcel, owner: ParcelOwner?): Deferred<Unit> - fun setPlotPlayerState(plotFor: Plot, player: UUID, state: Boolean?): CompletableFuture<Unit> + fun setParcelPlayerState(parcelFor: Parcel, player: UUID, state: Boolean?): Deferred<Unit> - fun setPlotAllowsInteractInventory(plot: Plot, value: Boolean): CompletableFuture<Unit> + fun setParcelAllowsInteractInventory(parcel: Parcel, value: Boolean): Deferred<Unit> - fun setPlotAllowsInteractInputs(plot: Plot, value: Boolean): CompletableFuture<Unit> + fun setParcelAllowsInteractInputs(parcel: Parcel, value: Boolean): Deferred<Unit> } -class StorageWithBacking internal constructor(val backing: Backing) : Storage { +class StorageWithCoroutineBacking internal constructor(val backing: Backing) : Storage { override val name get() = backing.name override val syncDispatcher = Executor { it.run() }.asCoroutineDispatcher() - override val asyncDispatcher = Executors.newFixedThreadPool(4) { Thread(it, "AbstractStorageThread") }.asCoroutineDispatcher() + val poolSize: Int get() = 4 + override val asyncDispatcher = Executors.newFixedThreadPool(poolSize) { Thread(it, "Parcels2_StorageThread") }.asCoroutineDispatcher() - private fun <T> future(block: suspend CoroutineScope.() -> T) = kotlinx.coroutines.experimental.future.future(asyncDispatcher, CoroutineStart.ATOMIC, block) + private fun <T> future(block: suspend CoroutineScope.() -> T) = async(context = asyncDispatcher, start = CoroutineStart.ATOMIC, block = block) - override fun init(): CompletableFuture<Unit> = future { backing.init() } + override fun init() = future { backing.init() } - override fun shutdown(): CompletableFuture<Unit> = future { backing.shutdown() } + override fun shutdown() = future { backing.shutdown() } - override fun readPlotData(plotFor: Plot) = future { backing.readPlotData(plotFor) } + override fun readParcelData(parcelFor: Parcel) = future { backing.readParcelData(parcelFor) } - override fun readPlotData(plotsFor: Sequence<Plot>, channelCapacity: Int) = - produce<Pair<Plot, PlotData?>>(asyncDispatcher, capacity = channelCapacity) { backing.producePlotData(this, plotsFor) } + override fun readParcelData(parcelsFor: Sequence<Parcel>, channelCapacity: Int) = produce(asyncDispatcher, capacity = channelCapacity) { + with(backing) { produceParcelData(parcelsFor) } + } - override fun getOwnedPlots(user: PlotOwner) = future { backing.getOwnedPlots(user) } + override fun getOwnedParcels(user: ParcelOwner) = future { backing.getOwnedParcels(user) } - override fun setPlotOwner(plotFor: Plot, owner: PlotOwner?) = future { backing.setPlotOwner(plotFor, owner) } + override fun setParcelOwner(parcelFor: Parcel, owner: ParcelOwner?) = future { backing.setParcelOwner(parcelFor, owner) } - override fun setPlotPlayerState(plotFor: Plot, player: UUID, state: Boolean?) = future { backing.setPlotPlayerState(plotFor, player, state) } + override fun setParcelPlayerState(parcelFor: Parcel, player: UUID, state: Boolean?) = future { backing.setParcelPlayerState(parcelFor, player, state) } - override fun setPlotAllowsInteractInventory(plot: Plot, value: Boolean) = future { backing.setPlotAllowsInteractInventory(plot, value) } + override fun setParcelAllowsInteractInventory(parcel: Parcel, value: Boolean) = future { backing.setParcelAllowsInteractInventory(parcel, value) } - override fun setPlotAllowsInteractInputs(plot: Plot, value: Boolean) = future { backing.setPlotAllowsInteractInputs(plot, value) } + override fun setParcelAllowsInteractInputs(parcel: Parcel, value: Boolean) = future { backing.setParcelAllowsInteractInputs(parcel, value) } } - */
\ 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 new file mode 100644 index 0000000..7429351 --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/storage/StorageFactory.kt @@ -0,0 +1,46 @@ +package io.dico.parcels2.storage + +import io.dico.parcels2.DataConnectionOptions +import kotlin.reflect.KClass + +interface StorageFactory { + companion object StorageFactories { + private val map: MutableMap<String, StorageFactory> = HashMap() + + fun registerFactory(method: String, generator: StorageFactory): Boolean = map.putIfAbsent(method.toLowerCase(), generator) == null + + fun getFactory(method: String): StorageFactory? = map[method.toLowerCase()] + + init { + // have to write the code like this in kotlin. + // This code is absolutely disgusting + ConnectionStorageFactory().register(this) + } + } + + val optionsClass: KClass<out Any> + + fun newStorageInstance(method: String, options: Any): Storage + +} + +class ConnectionStorageFactory : StorageFactory { + override val optionsClass = DataConnectionOptions::class + + private val types: Map<String, String> = with(HashMap<String, String>()) { + put("mysql", "com.mysql.jdbc.jdbc2.optional.MysqlDataSource") + this + } + + fun register(companion: StorageFactory.StorageFactories) { + types.keys.forEach { + companion.registerFactory(it, this) + } + } + + override fun newStorageInstance(dialect: String, options: Any): Storage { + val driverClass = types[dialect.toLowerCase()] ?: throw IllegalArgumentException("Storage dialect $dialect is not supported") + return StorageWithCoroutineBacking(ExposedBacking(getHikariDataSource(dialect, driverClass, options as DataConnectionOptions))) + } + +}
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/storage/backing/Exposed.kt b/src/main/kotlin/io/dico/parcels2/storage/backing/Exposed.kt deleted file mode 100644 index 404e48c..0000000 --- a/src/main/kotlin/io/dico/parcels2/storage/backing/Exposed.kt +++ /dev/null @@ -1,41 +0,0 @@ -package io.dico.parcels2.storage.backing - -import org.jetbrains.exposed.sql.ReferenceOption -import org.jetbrains.exposed.sql.Table - -object ParcelsTable : Table() { - val id = integer("id").autoIncrement().primaryKey() - val px = integer("px") - val pz = integer("pz") - val world_uuid = binary("world_uuid", 16).also { uniqueIndex("location", it, px, pz) } - val world = varchar("world", 32).nullable() - val owner_uuid = binary("owner_uuid", 16).nullable() - val owner = varchar("owner", 16).nullable() -} - -object ParcelsAddedTable : Table() { - val id = integer("id").references(ParcelsTable.id, ReferenceOption.CASCADE) - val player_uuid = binary("player_uuid", 16).also { uniqueIndex("pair", id, it) } - val allowed_flag = bool("allowed_flag") -} - -object PlayerAddedTable : Table() { - val owner_uuid = binary("owner_uuid", 16) - val player_uuid = binary("player_uuid", 16).also { uniqueIndex("pair", owner_uuid, it) } - val allowed_flag = bool("allowed_flag") -} - -class AbstractParcelsDatabase { - - - - - -} - - - - - - - diff --git a/src/main/kotlin/io/dico/parcels2/util/PlayerExtensions.kt b/src/main/kotlin/io/dico/parcels2/util/PlayerExtensions.kt index 47bbf8c..3ef3c89 100644 --- a/src/main/kotlin/io/dico/parcels2/util/PlayerExtensions.kt +++ b/src/main/kotlin/io/dico/parcels2/util/PlayerExtensions.kt @@ -1,13 +1,17 @@ package io.dico.parcels2.util +import io.dico.dicore.Formatting +import io.dico.parcels2.ParcelsPlugin +import io.dico.parcels2.logger import org.bukkit.entity.Player +import org.bukkit.plugin.java.JavaPlugin -val Player.hasBanBypass get() = hasPermission("plots.admin.bypass.ban") -val Player.hasBuildAnywhere get() = hasPermission("plots.admin.bypass.build") -val Player.hasGamemodeBypass get() = hasPermission("plots.admin.bypass.gamemode") -val Player.hasAdminManage get() = hasPermission("plots.admin.manage") -val Player.hasPlotHomeOthers get() = hasPermission("plots.command.home.others") -val Player.hasRandomSpecific get() = hasPermission("plots.command.random.specific") +inline val Player.hasBanBypass get() = hasPermission("plots.admin.bypass.ban") +inline val Player.hasBuildAnywhere get() = hasPermission("plots.admin.bypass.build") +inline val Player.hasGamemodeBypass get() = hasPermission("plots.admin.bypass.gamemode") +inline val Player.hasAdminManage get() = hasPermission("plots.admin.manage") +inline val Player.hasPlotHomeOthers get() = hasPermission("plots.command.home.others") +inline val Player.hasRandomSpecific get() = hasPermission("plots.command.random.specific") val Player.plotLimit: Int get() { for (info in effectivePermissions) { @@ -18,15 +22,15 @@ val Player.plotLimit: Int return Int.MAX_VALUE } return limitString.toIntOrNull() ?: DEFAULT_LIMIT.also { - Main.instance.logger.severe("$name has permission '$perm'. The suffix can not be parsed to an integer (or *).") + logger.warn("$name has permission '$perm'. The suffix can not be parsed to an integer (or *).") } } } return DEFAULT_LIMIT } -val DEFAULT_LIMIT = 1 -internal val prefix = Formatting.translateChars('&', "&4[&c${Main.instance.name}&4] &a") +private const val DEFAULT_LIMIT = 1 +private val prefix = Formatting.translateChars('&', "&4[&c${JavaPlugin.getPlugin(ParcelsPlugin::class.java).name}&4] &a") fun Player.sendPlotMessage(except: Boolean = false, nopermit: Boolean = false, message: String) { if (except) { |