From 0f196f59c6a4cb76ab8409da62ff1f35505f94a8 Mon Sep 17 00:00:00 2001 From: Dico Karssiens Date: Sun, 11 Nov 2018 14:06:45 +0000 Subject: Changes I made before breaking my local repository. Hoping this works. --- src/main/kotlin/io/dico/parcels2/Interactable.kt | 346 ++--- src/main/kotlin/io/dico/parcels2/JobDispatcher.kt | 674 +++++----- src/main/kotlin/io/dico/parcels2/Parcel.kt | 132 +- .../kotlin/io/dico/parcels2/ParcelGenerator.kt | 206 +-- src/main/kotlin/io/dico/parcels2/ParcelId.kt | 112 +- src/main/kotlin/io/dico/parcels2/ParcelWorld.kt | 206 +-- src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt | 304 ++--- src/main/kotlin/io/dico/parcels2/PlayerProfile.kt | 366 +++--- src/main/kotlin/io/dico/parcels2/Privilege.kt | 250 ++-- src/main/kotlin/io/dico/parcels2/Privileges.kt | 128 +- .../io/dico/parcels2/blockvisitor/Attachables.kt | 122 +- .../dico/parcels2/blockvisitor/ExtraBlockChange.kt | 76 +- .../dico/parcels2/blockvisitor/RegionTraverser.kt | 646 +++++----- .../io/dico/parcels2/blockvisitor/Schematic.kt | 242 ++-- .../parcels2/command/AbstractParcelCommands.kt | 126 +- .../io/dico/parcels2/command/CommandsAdmin.kt | 188 +-- .../command/CommandsAdminPrivilegesGlobal.kt | 262 ++-- .../io/dico/parcels2/command/CommandsDebug.kt | 322 ++--- .../io/dico/parcels2/command/CommandsGeneral.kt | 282 ++--- .../parcels2/command/CommandsPrivilegesGlobal.kt | 152 +-- .../parcels2/command/CommandsPrivilegesLocal.kt | 286 ++--- .../dico/parcels2/command/ParcelCommandBuilder.kt | 274 ++-- .../parcels2/command/ParcelCommandReceivers.kt | 136 +- .../command/ParcelOptionsInteractCommand.kt | 112 +- .../dico/parcels2/command/ParcelParameterTypes.kt | 166 +-- .../io/dico/parcels2/command/ParcelTarget.kt | 382 +++--- .../io/dico/parcels2/command/ParcelsChatHandler.kt | 46 +- .../parcels2/defaultimpl/DefaultParcelContainer.kt | 144 +-- .../parcels2/defaultimpl/DefaultParcelGenerator.kt | 754 +++++------ .../defaultimpl/GlobalPrivilegesManagerImpl.kt | 54 +- .../io/dico/parcels2/defaultimpl/InfoBuilder.kt | 174 +-- .../parcels2/defaultimpl/ParcelProviderImpl.kt | 444 +++---- .../dico/parcels2/defaultimpl/ParcelWorldImpl.kt | 150 +-- .../dico/parcels2/listener/ParcelEntityTracker.kt | 120 +- .../io/dico/parcels2/listener/ParcelListeners.kt | 1320 ++++++++++---------- .../io/dico/parcels2/listener/WorldEditListener.kt | 156 +-- .../io/dico/parcels2/options/GeneratorOptions.kt | 70 +- .../io/dico/parcels2/options/MigrationOptions.kt | 42 +- .../kotlin/io/dico/parcels2/options/Options.kt | 114 +- .../io/dico/parcels2/options/OptionsMapper.kt | 132 +- .../io/dico/parcels2/options/PolymorphicOptions.kt | 176 +-- .../io/dico/parcels2/options/StorageOptions.kt | 116 +- .../kotlin/io/dico/parcels2/storage/Backing.kt | 138 +- .../io/dico/parcels2/storage/DataConverters.kt | 74 +- src/main/kotlin/io/dico/parcels2/storage/Hikari.kt | 146 +-- .../kotlin/io/dico/parcels2/storage/Storage.kt | 228 ++-- .../parcels2/storage/exposed/ExposedBacking.kt | 564 ++++----- .../parcels2/storage/exposed/ExposedExtensions.kt | 148 +-- .../io/dico/parcels2/storage/exposed/IdTables.kt | 328 ++--- .../io/dico/parcels2/storage/exposed/ListTables.kt | 222 ++-- .../dico/parcels2/storage/migration/Migration.kt | 18 +- .../storage/migration/plotme/PlotmeMigration.kt | 234 ++-- .../storage/migration/plotme/PlotmeTables.kt | 62 +- .../kotlin/io/dico/parcels2/util/BukkitUtil.kt | 28 +- .../io/dico/parcels2/util/MainThreadDispatcher.kt | 84 +- .../io/dico/parcels2/util/PluginScheduler.kt | 40 +- .../kotlin/io/dico/parcels2/util/ext/Material.kt | 214 ++-- src/main/kotlin/io/dico/parcels2/util/ext/Misc.kt | 162 +-- .../kotlin/io/dico/parcels2/util/ext/Player.kt | 114 +- .../kotlin/io/dico/parcels2/util/math/Dimension.kt | 36 +- src/main/kotlin/io/dico/parcels2/util/math/Math.kt | 82 +- .../kotlin/io/dico/parcels2/util/math/Region.kt | 72 +- .../kotlin/io/dico/parcels2/util/math/Vec2i.kt | 18 +- .../kotlin/io/dico/parcels2/util/math/Vec3d.kt | 104 +- .../kotlin/io/dico/parcels2/util/math/Vec3i.kt | 210 ++-- src/main/resources/logback.xml | 32 +- src/main/resources/plugin.yml | 10 +- 67 files changed, 6939 insertions(+), 6939 deletions(-) (limited to 'src') diff --git a/src/main/kotlin/io/dico/parcels2/Interactable.kt b/src/main/kotlin/io/dico/parcels2/Interactable.kt index c301340..f25b796 100644 --- a/src/main/kotlin/io/dico/parcels2/Interactable.kt +++ b/src/main/kotlin/io/dico/parcels2/Interactable.kt @@ -1,174 +1,174 @@ -package io.dico.parcels2 - -import io.dico.parcels2.util.math.ceilDiv -import io.dico.parcels2.util.ext.getMaterialsWithWoodTypePrefix -import org.bukkit.Material -import java.util.EnumMap - -class Interactables -private constructor( - val id: Int, - val name: String, - val interactableByDefault: Boolean, - vararg val materials: Material -) { - - companion object { - val classesById: List - val classesByName: Map - val listedMaterials: Map - - init { - val array = getClassesArray() - classesById = array.asList() - classesByName = mapOf(*array.map { it.name to it }.toTypedArray()) - listedMaterials = EnumMap(mapOf(*array.flatMap { clazz -> clazz.materials.map { it to clazz.id } }.toTypedArray())) - } - - operator fun get(material: Material): Interactables? { - val id = listedMaterials[material] ?: return null - return classesById[id] - } - - operator fun get(name: String): Interactables { - return classesByName[name] ?: throw IllegalArgumentException("Interactables class does not exist: $name") - } - - operator fun get(id: Int): Interactables { - return classesById[id] - } - - private fun getClassesArray() = run { - var id = 0 - @Suppress("UNUSED_CHANGED_VALUE") - arrayOf( - Interactables( - id++, "buttons", true, - Material.STONE_BUTTON, - *getMaterialsWithWoodTypePrefix("BUTTON") - ), - - Interactables( - id++, "levers", true, - Material.LEVER - ), - - Interactables( - id++, "pressure_plates", true, - Material.STONE_PRESSURE_PLATE, - *getMaterialsWithWoodTypePrefix("PRESSURE_PLATE"), - Material.HEAVY_WEIGHTED_PRESSURE_PLATE, - Material.LIGHT_WEIGHTED_PRESSURE_PLATE - ), - - Interactables( - id++, "redstone", false, - Material.COMPARATOR, - Material.REPEATER - ), - - Interactables( - id++, "containers", false, - Material.CHEST, - Material.TRAPPED_CHEST, - Material.DISPENSER, - Material.DROPPER, - Material.HOPPER, - Material.FURNACE - ), - - Interactables( - id++, "gates", true, - *getMaterialsWithWoodTypePrefix("DOOR"), - *getMaterialsWithWoodTypePrefix("TRAPDOOR"), - *getMaterialsWithWoodTypePrefix("FENCE_GATE") - ) - ) - } - - } - -} - -val Parcel?.effectiveInteractableConfig: InteractableConfiguration - get() = this?.interactableConfig ?: pathInteractableConfig - -val pathInteractableConfig: InteractableConfiguration = run { - val data = BitmaskInteractableConfiguration().apply { - Interactables.classesById.forEach { - setInteractable(it, false) - } - } - object : InteractableConfiguration by data { - override fun setInteractable(clazz: Interactables, interactable: Boolean) = - throw IllegalStateException("pathInteractableConfig is immutable") - - override fun clear() = - throw IllegalStateException("pathInteractableConfig is immutable") - - override fun copyFrom(other: InteractableConfiguration) = - throw IllegalStateException("pathInteractableConfig is immutable") - } -} - -interface InteractableConfiguration { - val interactableClasses: List get() = Interactables.classesById.filter { isInteractable(it) } - - fun isInteractable(material: Material): Boolean - fun isInteractable(clazz: Interactables): Boolean - fun isDefault(): Boolean - - fun setInteractable(clazz: Interactables, interactable: Boolean): Boolean - fun clear(): Boolean - fun copyFrom(other: InteractableConfiguration) = - Interactables.classesById.fold(false) { cur, elem -> setInteractable(elem, other.isInteractable(elem) || cur) } - - operator fun invoke(material: Material) = isInteractable(material) - operator fun invoke(className: String) = isInteractable(Interactables[className]) -} - -fun InteractableConfiguration.isInteractable(clazz: Interactables?) = clazz != null && isInteractable(clazz) - -class BitmaskInteractableConfiguration : InteractableConfiguration { - val bitmaskArray = IntArray(Interactables.classesById.size ceilDiv Int.SIZE_BITS) - - private fun isBitSet(classId: Int): Boolean { - val idx = classId.ushr(5) - return idx < bitmaskArray.size && bitmaskArray[idx].and(0x1.shl(classId.and(0x1F))) != 0 - } - - override fun isInteractable(material: Material): Boolean { - val classId = Interactables.listedMaterials[material] ?: return false - return isBitSet(classId) != Interactables.classesById[classId].interactableByDefault - } - - override fun isInteractable(clazz: Interactables): Boolean { - return isBitSet(clazz.id) != clazz.interactableByDefault - } - - override fun isDefault(): Boolean { - for (x in bitmaskArray) { - if (x != 0) return false - } - return true - } - - override fun setInteractable(clazz: Interactables, interactable: Boolean): Boolean { - val idx = clazz.id.ushr(5) - if (idx >= bitmaskArray.size) return false - val bit = 0x1.shl(clazz.id.and(0x1F)) - val oldBitmask = bitmaskArray[idx] - bitmaskArray[idx] = if (interactable != clazz.interactableByDefault) oldBitmask.or(bit) else oldBitmask.and(bit.inv()) - return bitmaskArray[idx] != oldBitmask - } - - override fun clear(): Boolean { - var change = false - for (i in bitmaskArray.indices) { - if (!change && bitmaskArray[i] != 0) change = true - bitmaskArray[i] = 0 - } - return change - } - +package io.dico.parcels2 + +import io.dico.parcels2.util.math.ceilDiv +import io.dico.parcels2.util.ext.getMaterialsWithWoodTypePrefix +import org.bukkit.Material +import java.util.EnumMap + +class Interactables +private constructor( + val id: Int, + val name: String, + val interactableByDefault: Boolean, + vararg val materials: Material +) { + + companion object { + val classesById: List + val classesByName: Map + val listedMaterials: Map + + init { + val array = getClassesArray() + classesById = array.asList() + classesByName = mapOf(*array.map { it.name to it }.toTypedArray()) + listedMaterials = EnumMap(mapOf(*array.flatMap { clazz -> clazz.materials.map { it to clazz.id } }.toTypedArray())) + } + + operator fun get(material: Material): Interactables? { + val id = listedMaterials[material] ?: return null + return classesById[id] + } + + operator fun get(name: String): Interactables { + return classesByName[name] ?: throw IllegalArgumentException("Interactables class does not exist: $name") + } + + operator fun get(id: Int): Interactables { + return classesById[id] + } + + private fun getClassesArray() = run { + var id = 0 + @Suppress("UNUSED_CHANGED_VALUE") + arrayOf( + Interactables( + id++, "buttons", true, + Material.STONE_BUTTON, + *getMaterialsWithWoodTypePrefix("BUTTON") + ), + + Interactables( + id++, "levers", true, + Material.LEVER + ), + + Interactables( + id++, "pressure_plates", true, + Material.STONE_PRESSURE_PLATE, + *getMaterialsWithWoodTypePrefix("PRESSURE_PLATE"), + Material.HEAVY_WEIGHTED_PRESSURE_PLATE, + Material.LIGHT_WEIGHTED_PRESSURE_PLATE + ), + + Interactables( + id++, "redstone", false, + Material.COMPARATOR, + Material.REPEATER + ), + + Interactables( + id++, "containers", false, + Material.CHEST, + Material.TRAPPED_CHEST, + Material.DISPENSER, + Material.DROPPER, + Material.HOPPER, + Material.FURNACE + ), + + Interactables( + id++, "gates", true, + *getMaterialsWithWoodTypePrefix("DOOR"), + *getMaterialsWithWoodTypePrefix("TRAPDOOR"), + *getMaterialsWithWoodTypePrefix("FENCE_GATE") + ) + ) + } + + } + +} + +val Parcel?.effectiveInteractableConfig: InteractableConfiguration + get() = this?.interactableConfig ?: pathInteractableConfig + +val pathInteractableConfig: InteractableConfiguration = run { + val data = BitmaskInteractableConfiguration().apply { + Interactables.classesById.forEach { + setInteractable(it, false) + } + } + object : InteractableConfiguration by data { + override fun setInteractable(clazz: Interactables, interactable: Boolean) = + throw IllegalStateException("pathInteractableConfig is immutable") + + override fun clear() = + throw IllegalStateException("pathInteractableConfig is immutable") + + override fun copyFrom(other: InteractableConfiguration) = + throw IllegalStateException("pathInteractableConfig is immutable") + } +} + +interface InteractableConfiguration { + val interactableClasses: List get() = Interactables.classesById.filter { isInteractable(it) } + + fun isInteractable(material: Material): Boolean + fun isInteractable(clazz: Interactables): Boolean + fun isDefault(): Boolean + + fun setInteractable(clazz: Interactables, interactable: Boolean): Boolean + fun clear(): Boolean + fun copyFrom(other: InteractableConfiguration) = + Interactables.classesById.fold(false) { cur, elem -> setInteractable(elem, other.isInteractable(elem) || cur) } + + operator fun invoke(material: Material) = isInteractable(material) + operator fun invoke(className: String) = isInteractable(Interactables[className]) +} + +fun InteractableConfiguration.isInteractable(clazz: Interactables?) = clazz != null && isInteractable(clazz) + +class BitmaskInteractableConfiguration : InteractableConfiguration { + val bitmaskArray = IntArray(Interactables.classesById.size ceilDiv Int.SIZE_BITS) + + private fun isBitSet(classId: Int): Boolean { + val idx = classId.ushr(5) + return idx < bitmaskArray.size && bitmaskArray[idx].and(0x1.shl(classId.and(0x1F))) != 0 + } + + override fun isInteractable(material: Material): Boolean { + val classId = Interactables.listedMaterials[material] ?: return false + return isBitSet(classId) != Interactables.classesById[classId].interactableByDefault + } + + override fun isInteractable(clazz: Interactables): Boolean { + return isBitSet(clazz.id) != clazz.interactableByDefault + } + + override fun isDefault(): Boolean { + for (x in bitmaskArray) { + if (x != 0) return false + } + return true + } + + override fun setInteractable(clazz: Interactables, interactable: Boolean): Boolean { + val idx = clazz.id.ushr(5) + if (idx >= bitmaskArray.size) return false + val bit = 0x1.shl(clazz.id.and(0x1F)) + val oldBitmask = bitmaskArray[idx] + bitmaskArray[idx] = if (interactable != clazz.interactableByDefault) oldBitmask.or(bit) else oldBitmask.and(bit.inv()) + return bitmaskArray[idx] != oldBitmask + } + + override fun clear(): Boolean { + var change = false + for (i in bitmaskArray.indices) { + if (!change && bitmaskArray[i] != 0) change = true + bitmaskArray[i] = 0 + } + return change + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/JobDispatcher.kt b/src/main/kotlin/io/dico/parcels2/JobDispatcher.kt index 10da0da..12be89a 100644 --- a/src/main/kotlin/io/dico/parcels2/JobDispatcher.kt +++ b/src/main/kotlin/io/dico/parcels2/JobDispatcher.kt @@ -1,337 +1,337 @@ -package io.dico.parcels2 - -import io.dico.parcels2.util.math.clampMin -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart.LAZY -import kotlinx.coroutines.Job as CoroutineJob -import kotlinx.coroutines.launch -import org.bukkit.scheduler.BukkitTask -import java.lang.System.currentTimeMillis -import java.util.LinkedList -import kotlin.coroutines.Continuation -import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED -import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn -import kotlin.coroutines.resume - -typealias JobFunction = suspend JobScope.() -> Unit -typealias JobUpdateLister = Job.(Double, Long) -> Unit - -data class TickJobtimeOptions(var jobTime: Int, var tickInterval: Int) - -interface JobDispatcher { - /** - * Submit a [function] that should be run synchronously, but limited such that it does not stall the server - */ - fun dispatch(function: JobFunction): Job - - /** - * Get a list of all jobs - */ - val jobs: List - - /** - * Attempts to complete any remaining tasks immediately, without suspension. - */ - fun completeAllTasks() -} - -interface JobAndScopeMembersUnion { - /** - * The time that elapsed since this job was dispatched, in milliseconds - */ - val elapsedTime: Long - - /** - * A value indicating the progress of this job, in the range 0.0 <= progress <= 1.0 - * with no guarantees to its accuracy. - */ - val progress: Double -} - -interface Job : JobAndScopeMembersUnion { - /** - * The coroutine associated with this job - */ - val coroutine: CoroutineJob - - /** - * true if this job has completed - */ - val isComplete: Boolean - - /** - * If an exception was thrown during the execution of this task, - * returns that exception. Returns null otherwise. - */ - val completionException: Throwable? - - /** - * Calls the given [block] whenever the progress of this job is updated, - * if [minInterval] milliseconds expired since the last call. - * The first call occurs after at least [minDelay] milliseconds in a likewise manner. - * Repeated invocations of this method result in an [IllegalStateException] - * - * if [asCompletionListener] is true, [onCompleted] is called with the same [block] - */ - fun onProgressUpdate(minDelay: Int, minInterval: Int, asCompletionListener: Boolean = true, block: JobUpdateLister): Job - - /** - * Calls the given [block] when this job completes, with the progress value 1.0. - * Multiple listeners may be registered to this function. - */ - fun onCompleted(block: JobUpdateLister): Job - - /** - * Await completion of this job - */ - suspend fun awaitCompletion() -} - -interface JobScope : JobAndScopeMembersUnion { - /** - * A task should call this frequently during its execution, such that the timer can suspend it when necessary. - */ - suspend fun markSuspensionPoint() - - /** - * A task should call this method to indicate its progress - */ - fun setProgress(progress: Double) - - /** - * Indicate that this job is complete - */ - fun markComplete() = setProgress(1.0) - - /** - * Get a [JobScope] that is responsible for [portion] part of the progress - * If [portion] is negative, the remaining progress is used - */ - fun delegateProgress(portion: Double = -1.0): JobScope -} - -inline fun JobScope.delegateWork(portion: Double = -1.0, block: JobScope.() -> T): T { - delegateProgress(portion).apply { - val result = block() - markComplete() - return result - } -} - -interface JobInternal : Job, JobScope { - /** - * Start or resumes the execution of this job - * and returns true if the job completed - * - * [worktime] is the maximum amount of time, in milliseconds, - * that this job may run for until suspension. - * - * If [worktime] is not positive, the job will complete - * without suspension and this method will always return true. - */ - fun resume(worktime: Long): Boolean -} - -/** - * An object that controls one or more jobs, ensuring that they don't stall the server too much. - * There is a configurable maxiumum amount of milliseconds that can be allocated to all jobs together in each server tick - * This object attempts to split that maximum amount of milliseconds equally between all jobs - */ -class BukkitJobDispatcher(private val plugin: ParcelsPlugin, var options: TickJobtimeOptions) : JobDispatcher { - // The currently registered bukkit scheduler task - private var bukkitTask: BukkitTask? = null - // The jobs. - private val _jobs = LinkedList() - override val jobs: List = _jobs - - override fun dispatch(function: JobFunction): Job { - val job: JobInternal = JobImpl(plugin, function) - - if (bukkitTask == null) { - val completed = job.resume(options.jobTime.toLong()) - if (completed) return job - bukkitTask = plugin.scheduleRepeating(0, options.tickInterval) { tickCoroutineJobs() } - } - _jobs.addFirst(job) - return job - } - - private fun tickCoroutineJobs() { - val jobs = _jobs - if (jobs.isEmpty()) return - val tickStartTime = System.currentTimeMillis() - - val iterator = jobs.listIterator(index = 0) - while (iterator.hasNext()) { - val time = System.currentTimeMillis() - val timeElapsed = time - tickStartTime - val timeLeft = options.jobTime - timeElapsed - if (timeLeft <= 0) return - - val count = jobs.size - iterator.nextIndex() - val timePerJob = (timeLeft + count - 1) / count - val job = iterator.next() - val completed = job.resume(timePerJob) - if (completed) { - iterator.remove() - } - } - - if (jobs.isEmpty()) { - bukkitTask?.cancel() - bukkitTask = null - } - } - - override fun completeAllTasks() { - _jobs.forEach { - it.resume(-1) - } - _jobs.clear() - bukkitTask?.cancel() - bukkitTask = null - } - -} - -private class JobImpl(scope: CoroutineScope, task: JobFunction) : JobInternal { - override val coroutine: CoroutineJob = scope.launch(start = LAZY) { task() } - - private var continuation: Continuation? = null - private var nextSuspensionTime: Long = 0L - private var completeForcefully = false - private var isStarted = false - - override val elapsedTime - get() = - if (coroutine.isCompleted) startTimeOrElapsedTime - else currentTimeMillis() - startTimeOrElapsedTime - - override val isComplete get() = coroutine.isCompleted - - private var _progress = 0.0 - override val progress get() = _progress - override var completionException: Throwable? = null; private set - - private var startTimeOrElapsedTime: Long = 0L // startTime before completed, elapsed time otherwise - private var onProgressUpdate: JobUpdateLister? = null - private var progressUpdateInterval: Int = 0 - private var lastUpdateTime: Long = 0L - private var onCompleted: JobUpdateLister? = null - - init { - coroutine.invokeOnCompletion { exception -> - // report any error that occurred - completionException = exception?.also { - if (it !is CancellationException) - logger.error("JobFunction generated an exception", it) - } - - // convert to elapsed time here - startTimeOrElapsedTime = System.currentTimeMillis() - startTimeOrElapsedTime - onCompleted?.let { it(1.0, elapsedTime) } - - onCompleted = null - onProgressUpdate = { prog, el -> } - } - } - - override fun onProgressUpdate(minDelay: Int, minInterval: Int, asCompletionListener: Boolean, block: JobUpdateLister): Job { - onProgressUpdate?.let { throw IllegalStateException() } - if (asCompletionListener) onCompleted(block) - if (isComplete) return this - onProgressUpdate = block - progressUpdateInterval = minInterval - lastUpdateTime = System.currentTimeMillis() + minDelay - minInterval - - return this - } - - override fun onCompleted(block: JobUpdateLister): Job { - if (isComplete) { - block(1.0, startTimeOrElapsedTime) - return this - } - - val cur = onCompleted - onCompleted = if (cur == null) { - block - } else { - fun Job.(prog: Double, el: Long) { - cur(prog, el) - block(prog, el) - } - } - return this - } - - override suspend fun markSuspensionPoint() { - if (System.currentTimeMillis() >= nextSuspensionTime && !completeForcefully) - suspendCoroutineUninterceptedOrReturn { cont: Continuation -> - continuation = cont - COROUTINE_SUSPENDED - } - } - - override fun setProgress(progress: Double) { - this._progress = progress - val onProgressUpdate = onProgressUpdate ?: return - val time = System.currentTimeMillis() - if (time > lastUpdateTime + progressUpdateInterval) { - onProgressUpdate(progress, elapsedTime) - lastUpdateTime = time - } - } - - override fun resume(worktime: Long): Boolean { - if (isComplete) return true - - if (worktime > 0) { - nextSuspensionTime = currentTimeMillis() + worktime - } else { - completeForcefully = true - } - - if (isStarted) { - continuation?.let { - continuation = null - it.resume(Unit) - return continuation == null - } - return true - } - - isStarted = true - startTimeOrElapsedTime = System.currentTimeMillis() - coroutine.start() - - return continuation == null - } - - override suspend fun awaitCompletion() { - coroutine.join() - } - - private fun delegateProgress(curPortion: Double, portion: Double): JobScope = - DelegateScope(progress, curPortion * (if (portion < 0) 1.0 - progress else portion).clampMin(0.0)) - - override fun delegateProgress(portion: Double): JobScope = delegateProgress(1.0, portion) - - private inner class DelegateScope(val progressStart: Double, val portion: Double) : JobScope { - override val elapsedTime: Long - get() = this@JobImpl.elapsedTime - - override suspend fun markSuspensionPoint() = - this@JobImpl.markSuspensionPoint() - - override val progress: Double - get() = (this@JobImpl.progress - progressStart) / portion - - override fun setProgress(progress: Double) = - this@JobImpl.setProgress(progressStart + progress * portion) - - override fun delegateProgress(portion: Double): JobScope = - this@JobImpl.delegateProgress(this.portion, portion) - } -} +package io.dico.parcels2 + +import io.dico.parcels2.util.math.clampMin +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart.LAZY +import kotlinx.coroutines.Job as CoroutineJob +import kotlinx.coroutines.launch +import org.bukkit.scheduler.BukkitTask +import java.lang.System.currentTimeMillis +import java.util.LinkedList +import kotlin.coroutines.Continuation +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn +import kotlin.coroutines.resume + +typealias JobFunction = suspend JobScope.() -> Unit +typealias JobUpdateLister = Job.(Double, Long) -> Unit + +data class TickJobtimeOptions(var jobTime: Int, var tickInterval: Int) + +interface JobDispatcher { + /** + * Submit a [function] that should be run synchronously, but limited such that it does not stall the server + */ + fun dispatch(function: JobFunction): Job + + /** + * Get a list of all jobs + */ + val jobs: List + + /** + * Attempts to complete any remaining tasks immediately, without suspension. + */ + fun completeAllTasks() +} + +interface JobAndScopeMembersUnion { + /** + * The time that elapsed since this job was dispatched, in milliseconds + */ + val elapsedTime: Long + + /** + * A value indicating the progress of this job, in the range 0.0 <= progress <= 1.0 + * with no guarantees to its accuracy. + */ + val progress: Double +} + +interface Job : JobAndScopeMembersUnion { + /** + * The coroutine associated with this job + */ + val coroutine: CoroutineJob + + /** + * true if this job has completed + */ + val isComplete: Boolean + + /** + * If an exception was thrown during the execution of this task, + * returns that exception. Returns null otherwise. + */ + val completionException: Throwable? + + /** + * Calls the given [block] whenever the progress of this job is updated, + * if [minInterval] milliseconds expired since the last call. + * The first call occurs after at least [minDelay] milliseconds in a likewise manner. + * Repeated invocations of this method result in an [IllegalStateException] + * + * if [asCompletionListener] is true, [onCompleted] is called with the same [block] + */ + fun onProgressUpdate(minDelay: Int, minInterval: Int, asCompletionListener: Boolean = true, block: JobUpdateLister): Job + + /** + * Calls the given [block] when this job completes, with the progress value 1.0. + * Multiple listeners may be registered to this function. + */ + fun onCompleted(block: JobUpdateLister): Job + + /** + * Await completion of this job + */ + suspend fun awaitCompletion() +} + +interface JobScope : JobAndScopeMembersUnion { + /** + * A task should call this frequently during its execution, such that the timer can suspend it when necessary. + */ + suspend fun markSuspensionPoint() + + /** + * A task should call this method to indicate its progress + */ + fun setProgress(progress: Double) + + /** + * Indicate that this job is complete + */ + fun markComplete() = setProgress(1.0) + + /** + * Get a [JobScope] that is responsible for [portion] part of the progress + * If [portion] is negative, the remaining progress is used + */ + fun delegateProgress(portion: Double = -1.0): JobScope +} + +inline fun JobScope.delegateWork(portion: Double = -1.0, block: JobScope.() -> T): T { + delegateProgress(portion).apply { + val result = block() + markComplete() + return result + } +} + +interface JobInternal : Job, JobScope { + /** + * Start or resumes the execution of this job + * and returns true if the job completed + * + * [worktime] is the maximum amount of time, in milliseconds, + * that this job may run for until suspension. + * + * If [worktime] is not positive, the job will complete + * without suspension and this method will always return true. + */ + fun resume(worktime: Long): Boolean +} + +/** + * An object that controls one or more jobs, ensuring that they don't stall the server too much. + * There is a configurable maxiumum amount of milliseconds that can be allocated to all jobs together in each server tick + * This object attempts to split that maximum amount of milliseconds equally between all jobs + */ +class BukkitJobDispatcher(private val plugin: ParcelsPlugin, var options: TickJobtimeOptions) : JobDispatcher { + // The currently registered bukkit scheduler task + private var bukkitTask: BukkitTask? = null + // The jobs. + private val _jobs = LinkedList() + override val jobs: List = _jobs + + override fun dispatch(function: JobFunction): Job { + val job: JobInternal = JobImpl(plugin, function) + + if (bukkitTask == null) { + val completed = job.resume(options.jobTime.toLong()) + if (completed) return job + bukkitTask = plugin.scheduleRepeating(0, options.tickInterval) { tickCoroutineJobs() } + } + _jobs.addFirst(job) + return job + } + + private fun tickCoroutineJobs() { + val jobs = _jobs + if (jobs.isEmpty()) return + val tickStartTime = System.currentTimeMillis() + + val iterator = jobs.listIterator(index = 0) + while (iterator.hasNext()) { + val time = System.currentTimeMillis() + val timeElapsed = time - tickStartTime + val timeLeft = options.jobTime - timeElapsed + if (timeLeft <= 0) return + + val count = jobs.size - iterator.nextIndex() + val timePerJob = (timeLeft + count - 1) / count + val job = iterator.next() + val completed = job.resume(timePerJob) + if (completed) { + iterator.remove() + } + } + + if (jobs.isEmpty()) { + bukkitTask?.cancel() + bukkitTask = null + } + } + + override fun completeAllTasks() { + _jobs.forEach { + it.resume(-1) + } + _jobs.clear() + bukkitTask?.cancel() + bukkitTask = null + } + +} + +private class JobImpl(scope: CoroutineScope, task: JobFunction) : JobInternal { + override val coroutine: CoroutineJob = scope.launch(start = LAZY) { task() } + + private var continuation: Continuation? = null + private var nextSuspensionTime: Long = 0L + private var completeForcefully = false + private var isStarted = false + + override val elapsedTime + get() = + if (coroutine.isCompleted) startTimeOrElapsedTime + else currentTimeMillis() - startTimeOrElapsedTime + + override val isComplete get() = coroutine.isCompleted + + private var _progress = 0.0 + override val progress get() = _progress + override var completionException: Throwable? = null; private set + + private var startTimeOrElapsedTime: Long = 0L // startTime before completed, elapsed time otherwise + private var onProgressUpdate: JobUpdateLister? = null + private var progressUpdateInterval: Int = 0 + private var lastUpdateTime: Long = 0L + private var onCompleted: JobUpdateLister? = null + + init { + coroutine.invokeOnCompletion { exception -> + // report any error that occurred + completionException = exception?.also { + if (it !is CancellationException) + logger.error("JobFunction generated an exception", it) + } + + // convert to elapsed time here + startTimeOrElapsedTime = System.currentTimeMillis() - startTimeOrElapsedTime + onCompleted?.let { it(1.0, elapsedTime) } + + onCompleted = null + onProgressUpdate = { prog, el -> } + } + } + + override fun onProgressUpdate(minDelay: Int, minInterval: Int, asCompletionListener: Boolean, block: JobUpdateLister): Job { + onProgressUpdate?.let { throw IllegalStateException() } + if (asCompletionListener) onCompleted(block) + if (isComplete) return this + onProgressUpdate = block + progressUpdateInterval = minInterval + lastUpdateTime = System.currentTimeMillis() + minDelay - minInterval + + return this + } + + override fun onCompleted(block: JobUpdateLister): Job { + if (isComplete) { + block(1.0, startTimeOrElapsedTime) + return this + } + + val cur = onCompleted + onCompleted = if (cur == null) { + block + } else { + fun Job.(prog: Double, el: Long) { + cur(prog, el) + block(prog, el) + } + } + return this + } + + override suspend fun markSuspensionPoint() { + if (System.currentTimeMillis() >= nextSuspensionTime && !completeForcefully) + suspendCoroutineUninterceptedOrReturn { cont: Continuation -> + continuation = cont + COROUTINE_SUSPENDED + } + } + + override fun setProgress(progress: Double) { + this._progress = progress + val onProgressUpdate = onProgressUpdate ?: return + val time = System.currentTimeMillis() + if (time > lastUpdateTime + progressUpdateInterval) { + onProgressUpdate(progress, elapsedTime) + lastUpdateTime = time + } + } + + override fun resume(worktime: Long): Boolean { + if (isComplete) return true + + if (worktime > 0) { + nextSuspensionTime = currentTimeMillis() + worktime + } else { + completeForcefully = true + } + + if (isStarted) { + continuation?.let { + continuation = null + it.resume(Unit) + return continuation == null + } + return true + } + + isStarted = true + startTimeOrElapsedTime = System.currentTimeMillis() + coroutine.start() + + return continuation == null + } + + override suspend fun awaitCompletion() { + coroutine.join() + } + + private fun delegateProgress(curPortion: Double, portion: Double): JobScope = + DelegateScope(progress, curPortion * (if (portion < 0) 1.0 - progress else portion).clampMin(0.0)) + + override fun delegateProgress(portion: Double): JobScope = delegateProgress(1.0, portion) + + private inner class DelegateScope(val progressStart: Double, val portion: Double) : JobScope { + override val elapsedTime: Long + get() = this@JobImpl.elapsedTime + + override suspend fun markSuspensionPoint() = + this@JobImpl.markSuspensionPoint() + + override val progress: Double + get() = (this@JobImpl.progress - progressStart) / portion + + override fun setProgress(progress: Double) = + this@JobImpl.setProgress(progressStart + progress * portion) + + override fun delegateProgress(portion: Double): JobScope = + this@JobImpl.delegateProgress(this.portion, portion) + } +} diff --git a/src/main/kotlin/io/dico/parcels2/Parcel.kt b/src/main/kotlin/io/dico/parcels2/Parcel.kt index 4f89fe0..3527e15 100644 --- a/src/main/kotlin/io/dico/parcels2/Parcel.kt +++ b/src/main/kotlin/io/dico/parcels2/Parcel.kt @@ -1,66 +1,66 @@ -package io.dico.parcels2 - -import io.dico.parcels2.util.math.Vec2i -import io.dico.parcels2.util.math.Vec3i -import org.bukkit.Location -import org.joda.time.DateTime -import java.util.UUID - -/** - * Parcel implementation of ParcelData will update the database when changes are made. - * To change the data without updating the database, defer to the data delegate instance. - * - * This should be used for example in database query callbacks. - * However, this implementation is intentionally not thread-safe. - * Therefore, database query callbacks should schedule their updates using the bukkit scheduler. - */ -interface Parcel : ParcelData, Privileges { - val id: ParcelId - val world: ParcelWorld - val pos: Vec2i - val x: Int - val z: Int - val data: ParcelDataHolder - val infoString: String - val hasBlockVisitors: Boolean - val globalPrivileges: GlobalPrivileges? - - override val keyOfOwner: PlayerProfile.Real? - get() = owner as? PlayerProfile.Real - - fun copyData(newData: ParcelDataHolder, callerIsDatabase: Boolean = false) - - fun dispose() = copyData(ParcelDataHolder()) - - fun updateOwnerSign(force: Boolean = false) - - val homeLocation: Location get() = world.blockManager.getHomeLocation(id) -} - - - -interface ParcelData : RawPrivileges { - var owner: PlayerProfile? - val lastClaimTime: DateTime? - var isOwnerSignOutdated: Boolean - var interactableConfig: InteractableConfiguration - - //fun canBuild(player: OfflinePlayer, checkAdmin: Boolean = true, checkGlobal: Boolean = true): Boolean - - fun isOwner(uuid: UUID): Boolean { - return owner?.uuid == uuid - } - - fun isOwner(profile: PlayerProfile?): Boolean { - return owner == profile - } -} - -class ParcelDataHolder(addedMap: MutablePrivilegeMap = mutableMapOf()) - : ParcelData, PrivilegesHolder(addedMap) { - override var owner: PlayerProfile? = null - override var lastClaimTime: DateTime? = null - override var isOwnerSignOutdated = false - override var interactableConfig: InteractableConfiguration = BitmaskInteractableConfiguration() -} - +package io.dico.parcels2 + +import io.dico.parcels2.util.math.Vec2i +import io.dico.parcels2.util.math.Vec3i +import org.bukkit.Location +import org.joda.time.DateTime +import java.util.UUID + +/** + * Parcel implementation of ParcelData will update the database when changes are made. + * To change the data without updating the database, defer to the data delegate instance. + * + * This should be used for example in database query callbacks. + * However, this implementation is intentionally not thread-safe. + * Therefore, database query callbacks should schedule their updates using the bukkit scheduler. + */ +interface Parcel : ParcelData, Privileges { + val id: ParcelId + val world: ParcelWorld + val pos: Vec2i + val x: Int + val z: Int + val data: ParcelDataHolder + val infoString: String + val hasBlockVisitors: Boolean + val globalPrivileges: GlobalPrivileges? + + override val keyOfOwner: PlayerProfile.Real? + get() = owner as? PlayerProfile.Real + + fun copyData(newData: ParcelDataHolder, callerIsDatabase: Boolean = false) + + fun dispose() = copyData(ParcelDataHolder()) + + fun updateOwnerSign(force: Boolean = false) + + val homeLocation: Location get() = world.blockManager.getHomeLocation(id) +} + + + +interface ParcelData : RawPrivileges { + var owner: PlayerProfile? + val lastClaimTime: DateTime? + var isOwnerSignOutdated: Boolean + var interactableConfig: InteractableConfiguration + + //fun canBuild(player: OfflinePlayer, checkAdmin: Boolean = true, checkGlobal: Boolean = true): Boolean + + fun isOwner(uuid: UUID): Boolean { + return owner?.uuid == uuid + } + + fun isOwner(profile: PlayerProfile?): Boolean { + return owner == profile + } +} + +class ParcelDataHolder(addedMap: MutablePrivilegeMap = mutableMapOf()) + : ParcelData, PrivilegesHolder(addedMap) { + override var owner: PlayerProfile? = null + override var lastClaimTime: DateTime? = null + override var isOwnerSignOutdated = false + override var interactableConfig: InteractableConfiguration = BitmaskInteractableConfiguration() +} + diff --git a/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt b/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt index 109c5dc..63ec02c 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt @@ -1,103 +1,103 @@ -package io.dico.parcels2 - -import io.dico.parcels2.blockvisitor.RegionTraverser -import io.dico.parcels2.util.math.Region -import io.dico.parcels2.util.math.Vec2i -import io.dico.parcels2.util.math.Vec3i -import io.dico.parcels2.util.math.get -import kotlinx.coroutines.CoroutineScope -import org.bukkit.Chunk -import org.bukkit.Location -import org.bukkit.Material -import org.bukkit.World -import org.bukkit.block.Biome -import org.bukkit.block.Block -import org.bukkit.block.BlockFace -import org.bukkit.entity.Entity -import org.bukkit.generator.BlockPopulator -import org.bukkit.generator.ChunkGenerator -import java.util.Random - -abstract class ParcelGenerator : ChunkGenerator() { - abstract val worldName: String - - abstract val world: World - - 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 { - return mutableListOf(object : BlockPopulator() { - override fun populate(world: World?, random: Random?, chunk: Chunk?) { - this@ParcelGenerator.populate(world, random, chunk) - } - }) - } - - abstract fun makeParcelLocatorAndBlockManager( - parcelProvider: ParcelProvider, - container: ParcelContainer, - coroutineScope: CoroutineScope, - jobDispatcher: JobDispatcher - ): Pair -} - -interface ParcelBlockManager { - val world: World - val jobDispatcher: JobDispatcher - val parcelTraverser: RegionTraverser - - fun getRegionOrigin(parcel: ParcelId) = getRegion(parcel).origin.toVec2i() - - fun getHomeLocation(parcel: ParcelId): Location - - fun getRegion(parcel: ParcelId): Region - - fun getEntities(parcel: ParcelId): Collection - - fun isParcelInfoSectionLoaded(parcel: ParcelId): Boolean - - fun updateParcelInfo(parcel: ParcelId, owner: PlayerProfile?) - - fun getParcelForInfoBlockInteraction(block: Vec3i, type: Material, face: BlockFace): Parcel? - - fun setBiome(parcel: ParcelId, biome: Biome): Job? - - fun clearParcel(parcel: ParcelId): Job? - - /** - * Used to update owner blocks in the corner of the parcel - */ - fun getParcelsWithOwnerBlockIn(chunk: Chunk): Collection -} - -inline fun ParcelBlockManager.tryDoBlockOperation( - parcelProvider: ParcelProvider, - parcel: ParcelId, - traverser: RegionTraverser, - crossinline operation: suspend JobScope.(Block) -> Unit -) = parcelProvider.trySubmitBlockVisitor(Permit(), arrayOf(parcel)) { - val region = getRegion(parcel) - val blockCount = region.blockCount.toDouble() - val blocks = traverser.traverseRegion(region) - for ((index, vec) in blocks.withIndex()) { - markSuspensionPoint() - operation(world[vec]) - setProgress((index + 1) / blockCount) - } -} - -abstract class ParcelBlockManagerBase : ParcelBlockManager { - - override fun getEntities(parcel: ParcelId): Collection { - val region = getRegion(parcel) - val center = region.center - val centerLoc = Location(world, center.x, center.y, center.z) - val centerDist = (center - region.origin).add(0.2, 0.2, 0.2) - return world.getNearbyEntities(centerLoc, centerDist.x, centerDist.y, centerDist.z) - } - -} +package io.dico.parcels2 + +import io.dico.parcels2.blockvisitor.RegionTraverser +import io.dico.parcels2.util.math.Region +import io.dico.parcels2.util.math.Vec2i +import io.dico.parcels2.util.math.Vec3i +import io.dico.parcels2.util.math.get +import kotlinx.coroutines.CoroutineScope +import org.bukkit.Chunk +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.World +import org.bukkit.block.Biome +import org.bukkit.block.Block +import org.bukkit.block.BlockFace +import org.bukkit.entity.Entity +import org.bukkit.generator.BlockPopulator +import org.bukkit.generator.ChunkGenerator +import java.util.Random + +abstract class ParcelGenerator : ChunkGenerator() { + abstract val worldName: String + + abstract val world: World + + 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 { + return mutableListOf(object : BlockPopulator() { + override fun populate(world: World?, random: Random?, chunk: Chunk?) { + this@ParcelGenerator.populate(world, random, chunk) + } + }) + } + + abstract fun makeParcelLocatorAndBlockManager( + parcelProvider: ParcelProvider, + container: ParcelContainer, + coroutineScope: CoroutineScope, + jobDispatcher: JobDispatcher + ): Pair +} + +interface ParcelBlockManager { + val world: World + val jobDispatcher: JobDispatcher + val parcelTraverser: RegionTraverser + + fun getRegionOrigin(parcel: ParcelId) = getRegion(parcel).origin.toVec2i() + + fun getHomeLocation(parcel: ParcelId): Location + + fun getRegion(parcel: ParcelId): Region + + fun getEntities(parcel: ParcelId): Collection + + fun isParcelInfoSectionLoaded(parcel: ParcelId): Boolean + + fun updateParcelInfo(parcel: ParcelId, owner: PlayerProfile?) + + fun getParcelForInfoBlockInteraction(block: Vec3i, type: Material, face: BlockFace): Parcel? + + fun setBiome(parcel: ParcelId, biome: Biome): Job? + + fun clearParcel(parcel: ParcelId): Job? + + /** + * Used to update owner blocks in the corner of the parcel + */ + fun getParcelsWithOwnerBlockIn(chunk: Chunk): Collection +} + +inline fun ParcelBlockManager.tryDoBlockOperation( + parcelProvider: ParcelProvider, + parcel: ParcelId, + traverser: RegionTraverser, + crossinline operation: suspend JobScope.(Block) -> Unit +) = parcelProvider.trySubmitBlockVisitor(Permit(), arrayOf(parcel)) { + val region = getRegion(parcel) + val blockCount = region.blockCount.toDouble() + val blocks = traverser.traverseRegion(region) + for ((index, vec) in blocks.withIndex()) { + markSuspensionPoint() + operation(world[vec]) + setProgress((index + 1) / blockCount) + } +} + +abstract class ParcelBlockManagerBase : ParcelBlockManager { + + override fun getEntities(parcel: ParcelId): Collection { + val region = getRegion(parcel) + val center = region.center + val centerLoc = Location(world, center.x, center.y, center.z) + val centerDist = (center - region.origin).add(0.2, 0.2, 0.2) + return world.getNearbyEntities(centerLoc, centerDist.x, centerDist.y, centerDist.z) + } + +} diff --git a/src/main/kotlin/io/dico/parcels2/ParcelId.kt b/src/main/kotlin/io/dico/parcels2/ParcelId.kt index eef7129..77a1835 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelId.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelId.kt @@ -1,56 +1,56 @@ -@file:Suppress("FunctionName") - -package io.dico.parcels2 - -import io.dico.parcels2.util.math.Vec2i -import org.bukkit.Bukkit -import org.bukkit.World -import java.util.UUID - -/** - * Used by storage backing options to encompass the identity of a world - * Does NOT support equality operator. - */ -interface ParcelWorldId { - val name: String - val uid: UUID? - fun equals(id: ParcelWorldId): Boolean = name == id.name || (uid != null && uid == id.uid) - - val bukkitWorld: World? get() = Bukkit.getWorld(name) ?: uid?.let { Bukkit.getWorld(it) } -} - -fun ParcelWorldId.parcelWorldIdToString() = "ParcelWorld($name)" - -fun ParcelWorldId(worldName: String, worldUid: UUID? = null): ParcelWorldId = ParcelWorldIdImpl(worldName, worldUid) -fun ParcelWorldId(world: World) = ParcelWorldId(world.name, world.uid) - -/** - * Used by storage backing options to encompass the location of a parcel - * Does NOT support equality operator. - */ -interface ParcelId { - val worldId: ParcelWorldId - val x: Int - val z: Int - val pos: Vec2i get() = Vec2i(x, z) - val idString get() = "$x,$z" - fun equals(id: ParcelId): Boolean = x == id.x && z == id.z && worldId.equals(id.worldId) -} - -fun ParcelId.parcelIdToString() = "Parcel(${worldId.name},$idString)" - -fun ParcelId(worldId: ParcelWorldId, pos: Vec2i) = ParcelId(worldId, pos.x, pos.z) -fun ParcelId(worldName: String, worldUid: UUID?, pos: Vec2i) = ParcelId(worldName, worldUid, pos.x, pos.z) -fun ParcelId(worldName: String, worldUid: UUID?, x: Int, z: Int) = ParcelId(ParcelWorldId(worldName, worldUid), x, z) -fun ParcelId(worldId: ParcelWorldId, x: Int, z: Int): ParcelId = ParcelIdImpl(worldId, x, z) - -private class ParcelWorldIdImpl(override val name: String, - override val uid: UUID?) : ParcelWorldId { - override fun toString() = parcelWorldIdToString() -} - -private class ParcelIdImpl(override val worldId: ParcelWorldId, - override val x: Int, - override val z: Int) : ParcelId { - override fun toString() = parcelIdToString() -} +@file:Suppress("FunctionName") + +package io.dico.parcels2 + +import io.dico.parcels2.util.math.Vec2i +import org.bukkit.Bukkit +import org.bukkit.World +import java.util.UUID + +/** + * Used by storage backing options to encompass the identity of a world + * Does NOT support equality operator. + */ +interface ParcelWorldId { + val name: String + val uid: UUID? + fun equals(id: ParcelWorldId): Boolean = name == id.name || (uid != null && uid == id.uid) + + val bukkitWorld: World? get() = Bukkit.getWorld(name) ?: uid?.let { Bukkit.getWorld(it) } +} + +fun ParcelWorldId.parcelWorldIdToString() = "ParcelWorld($name)" + +fun ParcelWorldId(worldName: String, worldUid: UUID? = null): ParcelWorldId = ParcelWorldIdImpl(worldName, worldUid) +fun ParcelWorldId(world: World) = ParcelWorldId(world.name, world.uid) + +/** + * Used by storage backing options to encompass the location of a parcel + * Does NOT support equality operator. + */ +interface ParcelId { + val worldId: ParcelWorldId + val x: Int + val z: Int + val pos: Vec2i get() = Vec2i(x, z) + val idString get() = "$x,$z" + fun equals(id: ParcelId): Boolean = x == id.x && z == id.z && worldId.equals(id.worldId) +} + +fun ParcelId.parcelIdToString() = "Parcel(${worldId.name},$idString)" + +fun ParcelId(worldId: ParcelWorldId, pos: Vec2i) = ParcelId(worldId, pos.x, pos.z) +fun ParcelId(worldName: String, worldUid: UUID?, pos: Vec2i) = ParcelId(worldName, worldUid, pos.x, pos.z) +fun ParcelId(worldName: String, worldUid: UUID?, x: Int, z: Int) = ParcelId(ParcelWorldId(worldName, worldUid), x, z) +fun ParcelId(worldId: ParcelWorldId, x: Int, z: Int): ParcelId = ParcelIdImpl(worldId, x, z) + +private class ParcelWorldIdImpl(override val name: String, + override val uid: UUID?) : ParcelWorldId { + override fun toString() = parcelWorldIdToString() +} + +private class ParcelIdImpl(override val worldId: ParcelWorldId, + override val x: Int, + override val z: Int) : ParcelId { + override fun toString() = parcelIdToString() +} diff --git a/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt b/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt index c31b11a..36dfe1c 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt @@ -1,103 +1,103 @@ -package io.dico.parcels2 - -import io.dico.parcels2.options.RuntimeWorldOptions -import io.dico.parcels2.storage.Storage -import io.dico.parcels2.util.math.Vec2i -import io.dico.parcels2.util.math.floor -import org.bukkit.Location -import org.bukkit.World -import org.bukkit.block.Block -import org.bukkit.entity.Entity -import org.joda.time.DateTime -import java.lang.IllegalStateException -import java.util.UUID - -class Permit - -interface ParcelProvider { - val worlds: Map - - fun getWorldById(id: ParcelWorldId): ParcelWorld? - - fun getParcelById(id: ParcelId): Parcel? - - fun getWorld(name: String): ParcelWorld? - - fun getWorld(world: World): ParcelWorld? = getWorld(world.name) - - fun getWorld(block: Block): ParcelWorld? = getWorld(block.world) - - fun getWorld(loc: Location): ParcelWorld? = getWorld(loc.world) - - fun getWorld(entity: Entity): ParcelWorld? = getWorld(entity.location) - - fun getParcelAt(worldName: String, x: Int, z: Int): Parcel? = getWorld(worldName)?.locator?.getParcelAt(x, z) - - fun getParcelAt(world: World, x: Int, z: Int): Parcel? = getParcelAt(world.name, x, z) - - fun getParcelAt(world: World, vec: Vec2i): Parcel? = getParcelAt(world, vec.x, vec.z) - - fun getParcelAt(loc: Location): Parcel? = getParcelAt(loc.world, loc.x.floor(), loc.z.floor()) - - fun getParcelAt(entity: Entity): Parcel? = getParcelAt(entity.location) - - fun getParcelAt(block: Block): Parcel? = getParcelAt(block.world, block.x, block.z) - - fun getWorldGenerator(worldName: String): ParcelGenerator? - - fun loadWorlds() - - fun acquireBlockVisitorPermit(parcelId: ParcelId, with: Permit): Boolean - - @Throws(IllegalStateException::class) - fun releaseBlockVisitorPermit(parcelId: ParcelId, with: Permit) - - fun trySubmitBlockVisitor(permit: Permit, parcelIds: Array, function: JobFunction): Job? - - fun swapParcels(parcelId1: ParcelId, parcelId2: ParcelId): Job? -} - -interface ParcelLocator { - val world: World - - fun getParcelIdAt(x: Int, z: Int): ParcelId? - - fun getParcelAt(x: Int, z: Int): Parcel? - - fun getParcelAt(vec: Vec2i): Parcel? = getParcelAt(vec.x, vec.z) - - fun getParcelAt(loc: Location): Parcel? = getParcelAt(loc.x.floor(), loc.z.floor()).takeIf { loc.world == world } - - fun getParcelAt(entity: Entity): Parcel? = getParcelAt(entity.location).takeIf { entity.world == world } - - fun getParcelAt(block: Block): Parcel? = getParcelAt(block.x, block.z).takeIf { block.world == world } -} - -typealias ParcelContainerFactory = (ParcelWorld) -> ParcelContainer - -interface ParcelContainer { - - fun getParcelById(x: Int, z: Int): Parcel? - - fun getParcelById(id: Vec2i): Parcel? = getParcelById(id.x, id.z) - - fun getParcelById(id: ParcelId): Parcel? - - fun nextEmptyParcel(): Parcel? - -} - -interface ParcelWorld : ParcelLocator, ParcelContainer { - val id: ParcelWorldId - val name: String - val uid: UUID? - val options: RuntimeWorldOptions - val generator: ParcelGenerator - val storage: Storage - val container: ParcelContainer - val locator: ParcelLocator - val blockManager: ParcelBlockManager - val globalPrivileges: GlobalPrivilegesManager - - val creationTime: DateTime? -} +package io.dico.parcels2 + +import io.dico.parcels2.options.RuntimeWorldOptions +import io.dico.parcels2.storage.Storage +import io.dico.parcels2.util.math.Vec2i +import io.dico.parcels2.util.math.floor +import org.bukkit.Location +import org.bukkit.World +import org.bukkit.block.Block +import org.bukkit.entity.Entity +import org.joda.time.DateTime +import java.lang.IllegalStateException +import java.util.UUID + +class Permit + +interface ParcelProvider { + val worlds: Map + + fun getWorldById(id: ParcelWorldId): ParcelWorld? + + fun getParcelById(id: ParcelId): Parcel? + + fun getWorld(name: String): ParcelWorld? + + fun getWorld(world: World): ParcelWorld? = getWorld(world.name) + + fun getWorld(block: Block): ParcelWorld? = getWorld(block.world) + + fun getWorld(loc: Location): ParcelWorld? = getWorld(loc.world) + + fun getWorld(entity: Entity): ParcelWorld? = getWorld(entity.location) + + fun getParcelAt(worldName: String, x: Int, z: Int): Parcel? = getWorld(worldName)?.locator?.getParcelAt(x, z) + + fun getParcelAt(world: World, x: Int, z: Int): Parcel? = getParcelAt(world.name, x, z) + + fun getParcelAt(world: World, vec: Vec2i): Parcel? = getParcelAt(world, vec.x, vec.z) + + fun getParcelAt(loc: Location): Parcel? = getParcelAt(loc.world, loc.x.floor(), loc.z.floor()) + + fun getParcelAt(entity: Entity): Parcel? = getParcelAt(entity.location) + + fun getParcelAt(block: Block): Parcel? = getParcelAt(block.world, block.x, block.z) + + fun getWorldGenerator(worldName: String): ParcelGenerator? + + fun loadWorlds() + + fun acquireBlockVisitorPermit(parcelId: ParcelId, with: Permit): Boolean + + @Throws(IllegalStateException::class) + fun releaseBlockVisitorPermit(parcelId: ParcelId, with: Permit) + + fun trySubmitBlockVisitor(permit: Permit, parcelIds: Array, function: JobFunction): Job? + + fun swapParcels(parcelId1: ParcelId, parcelId2: ParcelId): Job? +} + +interface ParcelLocator { + val world: World + + fun getParcelIdAt(x: Int, z: Int): ParcelId? + + fun getParcelAt(x: Int, z: Int): Parcel? + + fun getParcelAt(vec: Vec2i): Parcel? = getParcelAt(vec.x, vec.z) + + fun getParcelAt(loc: Location): Parcel? = getParcelAt(loc.x.floor(), loc.z.floor()).takeIf { loc.world == world } + + fun getParcelAt(entity: Entity): Parcel? = getParcelAt(entity.location).takeIf { entity.world == world } + + fun getParcelAt(block: Block): Parcel? = getParcelAt(block.x, block.z).takeIf { block.world == world } +} + +typealias ParcelContainerFactory = (ParcelWorld) -> ParcelContainer + +interface ParcelContainer { + + fun getParcelById(x: Int, z: Int): Parcel? + + fun getParcelById(id: Vec2i): Parcel? = getParcelById(id.x, id.z) + + fun getParcelById(id: ParcelId): Parcel? + + fun nextEmptyParcel(): Parcel? + +} + +interface ParcelWorld : ParcelLocator, ParcelContainer { + val id: ParcelWorldId + val name: String + val uid: UUID? + val options: RuntimeWorldOptions + val generator: ParcelGenerator + val storage: Storage + val container: ParcelContainer + val locator: ParcelLocator + val blockManager: ParcelBlockManager + val globalPrivileges: GlobalPrivilegesManager + + val creationTime: DateTime? +} diff --git a/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt b/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt index a6ebcd8..b2d52a9 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt @@ -1,153 +1,153 @@ -package io.dico.parcels2 - -import io.dico.dicore.Registrator -import io.dico.dicore.command.EOverridePolicy -import io.dico.dicore.command.ICommandDispatcher -import io.dico.parcels2.command.getParcelCommands -import io.dico.parcels2.defaultimpl.GlobalPrivilegesManagerImpl -import io.dico.parcels2.defaultimpl.ParcelProviderImpl -import io.dico.parcels2.listener.ParcelEntityTracker -import io.dico.parcels2.listener.ParcelListeners -import io.dico.parcels2.listener.WorldEditListener -import io.dico.parcels2.options.Options -import io.dico.parcels2.options.optionsMapper -import io.dico.parcels2.storage.Storage -import io.dico.parcels2.util.MainThreadDispatcher -import io.dico.parcels2.util.PluginScheduler -import io.dico.parcels2.util.ext.tryCreate -import io.dico.parcels2.util.isServerThread -import kotlinx.coroutines.CoroutineScope -import org.bukkit.Bukkit -import org.bukkit.generator.ChunkGenerator -import org.bukkit.plugin.Plugin -import org.bukkit.plugin.java.JavaPlugin -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.io.File -import kotlin.coroutines.CoroutineContext - -val logger: Logger = LoggerFactory.getLogger("ParcelsPlugin") -private inline val plogger get() = logger - -class ParcelsPlugin : JavaPlugin(), CoroutineScope, PluginScheduler { - lateinit var optionsFile: File; private set - lateinit var options: Options; private set - lateinit var parcelProvider: ParcelProvider; private set - lateinit var storage: Storage; private set - lateinit var globalPrivileges: GlobalPrivilegesManager; private set - - val registrator = Registrator(this) - lateinit var entityTracker: ParcelEntityTracker; private set - private var listeners: ParcelListeners? = null - private var cmdDispatcher: ICommandDispatcher? = null - - override val coroutineContext: CoroutineContext = MainThreadDispatcher(this) - override val plugin: Plugin get() = this - val jobDispatcher: JobDispatcher by lazy { BukkitJobDispatcher(this, options.tickJobtime) } - - override fun onEnable() { - plogger.info("Is server thread: ${isServerThread()}") - plogger.info("Debug enabled: ${plogger.isDebugEnabled}") - plogger.debug(System.getProperty("user.dir")) - if (!init()) { - Bukkit.getPluginManager().disablePlugin(this) - } - } - - override fun onDisable() { - val hasWorkers = jobDispatcher.jobs.isNotEmpty() - if (hasWorkers) { - plogger.warn("Parcels is attempting to complete all ${jobDispatcher.jobs.size} remaining jobs before shutdown...") - } - jobDispatcher.completeAllTasks() - if (hasWorkers) { - plogger.info("Parcels has completed the remaining jobs.") - } - - cmdDispatcher?.unregisterFromCommandMap() - } - - private fun init(): Boolean { - optionsFile = File(dataFolder, "options.yml") - options = Options() - parcelProvider = ParcelProviderImpl(this) - - try { - if (!loadOptions()) return false - - try { - storage = options.storage.newInstance() - storage.init() - } catch (ex: Exception) { - plogger.error("Failed to connect to database", ex) - return false - } - - globalPrivileges = GlobalPrivilegesManagerImpl(this) - entityTracker = ParcelEntityTracker(parcelProvider) - } catch (ex: Exception) { - plogger.error("Error loading options", ex) - return false - } - - registerListeners() - registerCommands() - - parcelProvider.loadWorlds() - return true - } - - fun loadOptions(): Boolean { - when { - optionsFile.exists() -> optionsMapper.readerForUpdating(options).readValue(optionsFile) - else -> run { - options.addWorld("parcels") - 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}") - } - 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) - } - - private fun registerCommands() { - cmdDispatcher = getParcelCommands(this).apply { - registerToCommandMap("parcels:", EOverridePolicy.FALLBACK_ONLY) - } - } - - private fun registerListeners() { - if (listeners == null) { - listeners = ParcelListeners(parcelProvider, entityTracker, storage) - registrator.registerListeners(listeners!!) - - val worldEditPlugin = server.pluginManager.getPlugin("WorldEdit") - if (worldEditPlugin != null) { - WorldEditListener.register(this, worldEditPlugin) - } - } - - scheduleRepeating(100, 5, entityTracker::tick) - } - +package io.dico.parcels2 + +import io.dico.dicore.Registrator +import io.dico.dicore.command.EOverridePolicy +import io.dico.dicore.command.ICommandDispatcher +import io.dico.parcels2.command.getParcelCommands +import io.dico.parcels2.defaultimpl.GlobalPrivilegesManagerImpl +import io.dico.parcels2.defaultimpl.ParcelProviderImpl +import io.dico.parcels2.listener.ParcelEntityTracker +import io.dico.parcels2.listener.ParcelListeners +import io.dico.parcels2.listener.WorldEditListener +import io.dico.parcels2.options.Options +import io.dico.parcels2.options.optionsMapper +import io.dico.parcels2.storage.Storage +import io.dico.parcels2.util.MainThreadDispatcher +import io.dico.parcels2.util.PluginScheduler +import io.dico.parcels2.util.ext.tryCreate +import io.dico.parcels2.util.isServerThread +import kotlinx.coroutines.CoroutineScope +import org.bukkit.Bukkit +import org.bukkit.generator.ChunkGenerator +import org.bukkit.plugin.Plugin +import org.bukkit.plugin.java.JavaPlugin +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.File +import kotlin.coroutines.CoroutineContext + +val logger: Logger = LoggerFactory.getLogger("ParcelsPlugin") +private inline val plogger get() = logger + +class ParcelsPlugin : JavaPlugin(), CoroutineScope, PluginScheduler { + lateinit var optionsFile: File; private set + lateinit var options: Options; private set + lateinit var parcelProvider: ParcelProvider; private set + lateinit var storage: Storage; private set + lateinit var globalPrivileges: GlobalPrivilegesManager; private set + + val registrator = Registrator(this) + lateinit var entityTracker: ParcelEntityTracker; private set + private var listeners: ParcelListeners? = null + private var cmdDispatcher: ICommandDispatcher? = null + + override val coroutineContext: CoroutineContext = MainThreadDispatcher(this) + override val plugin: Plugin get() = this + val jobDispatcher: JobDispatcher by lazy { BukkitJobDispatcher(this, options.tickJobtime) } + + override fun onEnable() { + plogger.info("Is server thread: ${isServerThread()}") + plogger.info("Debug enabled: ${plogger.isDebugEnabled}") + plogger.debug(System.getProperty("user.dir")) + if (!init()) { + Bukkit.getPluginManager().disablePlugin(this) + } + } + + override fun onDisable() { + val hasWorkers = jobDispatcher.jobs.isNotEmpty() + if (hasWorkers) { + plogger.warn("Parcels is attempting to complete all ${jobDispatcher.jobs.size} remaining jobs before shutdown...") + } + jobDispatcher.completeAllTasks() + if (hasWorkers) { + plogger.info("Parcels has completed the remaining jobs.") + } + + cmdDispatcher?.unregisterFromCommandMap() + } + + private fun init(): Boolean { + optionsFile = File(dataFolder, "options.yml") + options = Options() + parcelProvider = ParcelProviderImpl(this) + + try { + if (!loadOptions()) return false + + try { + storage = options.storage.newInstance() + storage.init() + } catch (ex: Exception) { + plogger.error("Failed to connect to database", ex) + return false + } + + globalPrivileges = GlobalPrivilegesManagerImpl(this) + entityTracker = ParcelEntityTracker(parcelProvider) + } catch (ex: Exception) { + plogger.error("Error loading options", ex) + return false + } + + registerListeners() + registerCommands() + + parcelProvider.loadWorlds() + return true + } + + fun loadOptions(): Boolean { + when { + optionsFile.exists() -> optionsMapper.readerForUpdating(options).readValue(optionsFile) + else -> run { + options.addWorld("parcels") + 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}") + } + 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) + } + + private fun registerCommands() { + cmdDispatcher = getParcelCommands(this).apply { + registerToCommandMap("parcels:", EOverridePolicy.FALLBACK_ONLY) + } + } + + private fun registerListeners() { + if (listeners == null) { + listeners = ParcelListeners(parcelProvider, entityTracker, storage) + registrator.registerListeners(listeners!!) + + val worldEditPlugin = server.pluginManager.getPlugin("WorldEdit") + if (worldEditPlugin != null) { + WorldEditListener.register(this, worldEditPlugin) + } + } + + scheduleRepeating(100, 5, entityTracker::tick) + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/PlayerProfile.kt b/src/main/kotlin/io/dico/parcels2/PlayerProfile.kt index e3e0f55..6c30c27 100644 --- a/src/main/kotlin/io/dico/parcels2/PlayerProfile.kt +++ b/src/main/kotlin/io/dico/parcels2/PlayerProfile.kt @@ -1,184 +1,184 @@ -@file:Suppress("unused", "UsePropertyAccessSyntax", "DEPRECATION") - -package io.dico.parcels2 - -import io.dico.parcels2.storage.Storage -import io.dico.parcels2.util.ext.PLAYER_NAME_PLACEHOLDER -import io.dico.parcels2.util.ext.isValid -import io.dico.parcels2.util.ext.uuid -import io.dico.parcels2.util.getOfflinePlayer -import io.dico.parcels2.util.getPlayerName -import org.bukkit.Bukkit -import org.bukkit.OfflinePlayer -import java.util.UUID - -interface PlayerProfile { - val uuid: UUID? get() = null - val name: String? - val nameOrBukkitName: String? - val notNullName: String - val isStar: Boolean get() = this is Star - val exists: Boolean get() = this is RealImpl - - 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 getOfflinePlayer(input)?.let { PlayerProfile(it) } ?: Unresolved(input) - } - } - - interface Real : PlayerProfile { - override val uuid: UUID - override val nameOrBukkitName: String? - // If a player is online, their name is prioritized to get name changes right immediately - get() = Bukkit.getPlayer(uuid)?.name ?: name ?: getPlayerName(uuid) - override val notNullName: String - get() = nameOrBukkitName ?: PLAYER_NAME_PLACEHOLDER - - val player: OfflinePlayer? get() = 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 get() = "*" - override val nameOrBukkitName get() = name - override val notNullName get() = name - - // hopefully nobody will have this random UUID :) - override val uuid: UUID = UUID.fromString("7d09c4c6-117d-4f36-9778-c4d24618cee1") - - override fun matches(player: OfflinePlayer, allowNameMatch: Boolean): Boolean { - return true - } - - override fun toString() = "Star" - } - - abstract class NameOnly(override val name: String) : BaseImpl() { - override val notNullName get() = name - override val nameOrBukkitName: String get() = name - - override fun matches(player: OfflinePlayer, allowNameMatch: Boolean): Boolean { - return allowNameMatch && player.name == name - } - - override fun toString() = "${javaClass.simpleName}($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 - } - - suspend fun tryResolveSuspendedly(storage: Storage): Real? { - return storage.getPlayerUuidForName(name).await()?.let { resolve(it) } - } - - 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 { - override fun toString() = "Real($notNullName)" - } - -} - -suspend fun PlayerProfile.resolved(storage: Storage, resolveToFake: Boolean = false): PlayerProfile? = - when (this) { - is PlayerProfile.Unresolved -> tryResolveSuspendedly(storage) - ?: if (resolveToFake) PlayerProfile.Fake(name) else null - else -> this +@file:Suppress("unused", "UsePropertyAccessSyntax", "DEPRECATION") + +package io.dico.parcels2 + +import io.dico.parcels2.storage.Storage +import io.dico.parcels2.util.ext.PLAYER_NAME_PLACEHOLDER +import io.dico.parcels2.util.ext.isValid +import io.dico.parcels2.util.ext.uuid +import io.dico.parcels2.util.getOfflinePlayer +import io.dico.parcels2.util.getPlayerName +import org.bukkit.Bukkit +import org.bukkit.OfflinePlayer +import java.util.UUID + +interface PlayerProfile { + val uuid: UUID? get() = null + val name: String? + val nameOrBukkitName: String? + val notNullName: String + val isStar: Boolean get() = this is Star + val exists: Boolean get() = this is RealImpl + + 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 getOfflinePlayer(input)?.let { PlayerProfile(it) } ?: Unresolved(input) + } + } + + interface Real : PlayerProfile { + override val uuid: UUID + override val nameOrBukkitName: String? + // If a player is online, their name is prioritized to get name changes right immediately + get() = Bukkit.getPlayer(uuid)?.name ?: name ?: getPlayerName(uuid) + override val notNullName: String + get() = nameOrBukkitName ?: PLAYER_NAME_PLACEHOLDER + + val player: OfflinePlayer? get() = 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 get() = "*" + override val nameOrBukkitName get() = name + override val notNullName get() = name + + // hopefully nobody will have this random UUID :) + override val uuid: UUID = UUID.fromString("7d09c4c6-117d-4f36-9778-c4d24618cee1") + + override fun matches(player: OfflinePlayer, allowNameMatch: Boolean): Boolean { + return true + } + + override fun toString() = "Star" + } + + abstract class NameOnly(override val name: String) : BaseImpl() { + override val notNullName get() = name + override val nameOrBukkitName: String get() = name + + override fun matches(player: OfflinePlayer, allowNameMatch: Boolean): Boolean { + return allowNameMatch && player.name == name + } + + override fun toString() = "${javaClass.simpleName}($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 + } + + suspend fun tryResolveSuspendedly(storage: Storage): Real? { + return storage.getPlayerUuidForName(name).await()?.let { resolve(it) } + } + + 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 { + override fun toString() = "Real($notNullName)" + } + +} + +suspend fun PlayerProfile.resolved(storage: Storage, resolveToFake: Boolean = false): PlayerProfile? = + when (this) { + is PlayerProfile.Unresolved -> tryResolveSuspendedly(storage) + ?: if (resolveToFake) PlayerProfile.Fake(name) else null + else -> this } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/Privilege.kt b/src/main/kotlin/io/dico/parcels2/Privilege.kt index 135d7c1..892ed4c 100644 --- a/src/main/kotlin/io/dico/parcels2/Privilege.kt +++ b/src/main/kotlin/io/dico/parcels2/Privilege.kt @@ -1,125 +1,125 @@ -package io.dico.parcels2 - -import io.dico.parcels2.Privilege.DEFAULT -import java.util.Collections - -typealias PrivilegeKey = PlayerProfile.Real -typealias PrivilegeMap = Map -typealias MutablePrivilegeMap = MutableMap - -@Suppress("FunctionName") -fun MutablePrivilegeMap(): MutablePrivilegeMap = hashMapOf() - -@Suppress("UNCHECKED_CAST") -val EmptyPrivilegeMap = Collections.emptyMap() as MutablePrivilegeMap - -enum class Privilege( - val number: Int, - val transient: Boolean = false -) { - BANNED(1), - DEFAULT(2), - CAN_BUILD(3), - CAN_MANAGE(4), - - OWNER(-1, transient = true), - ADMIN(-1, transient = true); - - fun implies(other: Privilege): Boolean = - when { - other > DEFAULT -> this >= other - other == DEFAULT -> this == other - else -> this <= other - } - - fun isChangeInDirection(positiveDirection: Boolean, update: Privilege): Boolean = - if (positiveDirection) update > this - else update < this - - fun requireNonTransient(): Privilege { - if (transient) { - throw IllegalArgumentException("Transient privilege $this is invalid") - } - return this - } - - companion object { - fun getByNumber(id: Int) = - when (id) { - 1 -> BANNED - 2 -> DEFAULT - 3 -> CAN_BUILD - 4 -> CAN_MANAGE - else -> null - } - } -} - -interface RawPrivileges { - val privilegeMap: PrivilegeMap - var privilegeOfStar: Privilege - - fun getRawStoredPrivilege(key: PrivilegeKey): Privilege - fun setRawStoredPrivilege(key: PrivilegeKey, privilege: Privilege): Boolean - fun hasAnyDeclaredPrivileges(): Boolean -} - -open class PrivilegesHolder(override var privilegeMap: MutablePrivilegeMap = EmptyPrivilegeMap) : RawPrivileges { - private var _privilegeOfStar: Privilege = DEFAULT - - override /*open*/ var privilegeOfStar: Privilege - get() = _privilegeOfStar - set(value) = run { _privilegeOfStar = value } - - protected val isEmpty - inline get() = privilegeMap === EmptyPrivilegeMap - - override fun getRawStoredPrivilege(key: PrivilegeKey) = - if (key.isStar) _privilegeOfStar - else privilegeMap.getOrDefault(key, _privilegeOfStar) - - override fun setRawStoredPrivilege(key: PrivilegeKey, privilege: Privilege): Boolean { - privilege.requireNonTransient() - - if (key.isStar) { - if (_privilegeOfStar == privilege) return false - _privilegeOfStar = privilege - return true - } - - if (isEmpty) { - if (privilege == DEFAULT) return false - privilegeMap = MutablePrivilegeMap() - } - - return if (privilege == DEFAULT) privilegeMap.remove(key) != null - else privilegeMap.put(key, privilege) != privilege - } - - override fun hasAnyDeclaredPrivileges(): Boolean { - return privilegeMap.isNotEmpty() || privilegeOfStar != DEFAULT - } - - fun copyPrivilegesFrom(other: PrivilegesHolder) { - privilegeMap = other.privilegeMap - privilegeOfStar = other.privilegeOfStar - } - -} - -private fun MutableMap.put(key: K, value: V, override: Boolean) { - if (override) this[key] = value - else putIfAbsent(key, value) -} - -fun RawPrivileges.filterProfilesWithPrivilegeTo(map: MutableMap, privilege: Privilege) { - if (privilegeOfStar.implies(privilege)) { - map.putIfAbsent(PlayerProfile.Star, privilegeOfStar) - } - - for ((profile, declaredPrivilege) in privilegeMap) { - if (declaredPrivilege.implies(privilege)) { - map.putIfAbsent(profile, declaredPrivilege) - } - } -} +package io.dico.parcels2 + +import io.dico.parcels2.Privilege.DEFAULT +import java.util.Collections + +typealias PrivilegeKey = PlayerProfile.Real +typealias PrivilegeMap = Map +typealias MutablePrivilegeMap = MutableMap + +@Suppress("FunctionName") +fun MutablePrivilegeMap(): MutablePrivilegeMap = hashMapOf() + +@Suppress("UNCHECKED_CAST") +val EmptyPrivilegeMap = Collections.emptyMap() as MutablePrivilegeMap + +enum class Privilege( + val number: Int, + val transient: Boolean = false +) { + BANNED(1), + DEFAULT(2), + CAN_BUILD(3), + CAN_MANAGE(4), + + OWNER(-1, transient = true), + ADMIN(-1, transient = true); + + fun implies(other: Privilege): Boolean = + when { + other > DEFAULT -> this >= other + other == DEFAULT -> this == other + else -> this <= other + } + + fun isChangeInDirection(positiveDirection: Boolean, update: Privilege): Boolean = + if (positiveDirection) update > this + else update < this + + fun requireNonTransient(): Privilege { + if (transient) { + throw IllegalArgumentException("Transient privilege $this is invalid") + } + return this + } + + companion object { + fun getByNumber(id: Int) = + when (id) { + 1 -> BANNED + 2 -> DEFAULT + 3 -> CAN_BUILD + 4 -> CAN_MANAGE + else -> null + } + } +} + +interface RawPrivileges { + val privilegeMap: PrivilegeMap + var privilegeOfStar: Privilege + + fun getRawStoredPrivilege(key: PrivilegeKey): Privilege + fun setRawStoredPrivilege(key: PrivilegeKey, privilege: Privilege): Boolean + fun hasAnyDeclaredPrivileges(): Boolean +} + +open class PrivilegesHolder(override var privilegeMap: MutablePrivilegeMap = EmptyPrivilegeMap) : RawPrivileges { + private var _privilegeOfStar: Privilege = DEFAULT + + override /*open*/ var privilegeOfStar: Privilege + get() = _privilegeOfStar + set(value) = run { _privilegeOfStar = value } + + protected val isEmpty + inline get() = privilegeMap === EmptyPrivilegeMap + + override fun getRawStoredPrivilege(key: PrivilegeKey) = + if (key.isStar) _privilegeOfStar + else privilegeMap.getOrDefault(key, _privilegeOfStar) + + override fun setRawStoredPrivilege(key: PrivilegeKey, privilege: Privilege): Boolean { + privilege.requireNonTransient() + + if (key.isStar) { + if (_privilegeOfStar == privilege) return false + _privilegeOfStar = privilege + return true + } + + if (isEmpty) { + if (privilege == DEFAULT) return false + privilegeMap = MutablePrivilegeMap() + } + + return if (privilege == DEFAULT) privilegeMap.remove(key) != null + else privilegeMap.put(key, privilege) != privilege + } + + override fun hasAnyDeclaredPrivileges(): Boolean { + return privilegeMap.isNotEmpty() || privilegeOfStar != DEFAULT + } + + fun copyPrivilegesFrom(other: PrivilegesHolder) { + privilegeMap = other.privilegeMap + privilegeOfStar = other.privilegeOfStar + } + +} + +private fun MutableMap.put(key: K, value: V, override: Boolean) { + if (override) this[key] = value + else putIfAbsent(key, value) +} + +fun RawPrivileges.filterProfilesWithPrivilegeTo(map: MutableMap, privilege: Privilege) { + if (privilegeOfStar.implies(privilege)) { + map.putIfAbsent(PlayerProfile.Star, privilegeOfStar) + } + + for ((profile, declaredPrivilege) in privilegeMap) { + if (declaredPrivilege.implies(privilege)) { + map.putIfAbsent(profile, declaredPrivilege) + } + } +} diff --git a/src/main/kotlin/io/dico/parcels2/Privileges.kt b/src/main/kotlin/io/dico/parcels2/Privileges.kt index 632f1a4..6cdd6d0 100644 --- a/src/main/kotlin/io/dico/parcels2/Privileges.kt +++ b/src/main/kotlin/io/dico/parcels2/Privileges.kt @@ -1,64 +1,64 @@ -package io.dico.parcels2 - -import io.dico.parcels2.PrivilegeChangeResult.* -import io.dico.parcels2.util.ext.PERM_ADMIN_MANAGE -import io.dico.parcels2.util.ext.PERM_BAN_BYPASS -import io.dico.parcels2.util.ext.PERM_BUILD_ANYWHERE -import org.bukkit.OfflinePlayer -import org.bukkit.entity.Player - -interface Privileges : RawPrivileges { - val keyOfOwner: PlayerProfile.Real? - - fun getStoredPrivilege(key: PrivilegeKey): Privilege { - return if (key == keyOfOwner) Privilege.OWNER - else getRawStoredPrivilege(key) - } - - override var privilegeOfStar: Privilege - get() = getStoredPrivilege(PlayerProfile.Star) - set(value) { - setRawStoredPrivilege(PlayerProfile.Star, value) - } -} - -val OfflinePlayer.privilegeKey: PrivilegeKey - inline get() = PlayerProfile.nameless(this) - -fun Privileges.getEffectivePrivilege(player: OfflinePlayer, adminPerm: String): Privilege = - if (player is Player && player.hasPermission(adminPerm)) Privilege.ADMIN - else getStoredPrivilege(player.privilegeKey) - -fun Privileges.canManage(player: OfflinePlayer) = getEffectivePrivilege(player, PERM_ADMIN_MANAGE) >= Privilege.CAN_MANAGE -fun Privileges.canManageFast(player: OfflinePlayer) = getStoredPrivilege(player.privilegeKey) >= Privilege.CAN_MANAGE -fun Privileges.canBuild(player: OfflinePlayer) = getEffectivePrivilege(player, PERM_BUILD_ANYWHERE) >= Privilege.CAN_BUILD -fun Privileges.canBuildFast(player: OfflinePlayer) = getStoredPrivilege(player.privilegeKey) >= Privilege.CAN_BUILD -fun Privileges.canEnter(player: OfflinePlayer) = getEffectivePrivilege(player, PERM_BAN_BYPASS) >= Privilege.DEFAULT -fun Privileges.canEnterFast(player: OfflinePlayer) = getStoredPrivilege(player.privilegeKey) >= Privilege.DEFAULT - -enum class PrivilegeChangeResult { - SUCCESS, FAIL, FAIL_OWNER -} - -fun Privileges.changePrivilege(key: PrivilegeKey, positive: Boolean, update: Privilege): PrivilegeChangeResult = - when { - key == keyOfOwner -> FAIL_OWNER - getRawStoredPrivilege(key).isChangeInDirection(positive, update) && setRawStoredPrivilege(key, update) -> SUCCESS - else -> FAIL - } - -fun Privileges.allowManage(key: PrivilegeKey) = changePrivilege(key, true, Privilege.CAN_MANAGE) -fun Privileges.disallowManage(key: PrivilegeKey) = changePrivilege(key, false, Privilege.CAN_BUILD) -fun Privileges.allowBuild(key: PrivilegeKey) = changePrivilege(key, true, Privilege.CAN_BUILD) -fun Privileges.disallowBuild(key: PrivilegeKey) = changePrivilege(key, false, Privilege.DEFAULT) -fun Privileges.allowEnter(key: PrivilegeKey) = changePrivilege(key, true, Privilege.DEFAULT) -fun Privileges.disallowEnter(key: PrivilegeKey) = changePrivilege(key, false, Privilege.BANNED) - -interface GlobalPrivileges : RawPrivileges, Privileges { - override val keyOfOwner: PlayerProfile.Real -} - -interface GlobalPrivilegesManager { - operator fun get(owner: PlayerProfile.Real): GlobalPrivileges - operator fun get(owner: OfflinePlayer): GlobalPrivileges = get(owner.privilegeKey) -} +package io.dico.parcels2 + +import io.dico.parcels2.PrivilegeChangeResult.* +import io.dico.parcels2.util.ext.PERM_ADMIN_MANAGE +import io.dico.parcels2.util.ext.PERM_BAN_BYPASS +import io.dico.parcels2.util.ext.PERM_BUILD_ANYWHERE +import org.bukkit.OfflinePlayer +import org.bukkit.entity.Player + +interface Privileges : RawPrivileges { + val keyOfOwner: PlayerProfile.Real? + + fun getStoredPrivilege(key: PrivilegeKey): Privilege { + return if (key == keyOfOwner) Privilege.OWNER + else getRawStoredPrivilege(key) + } + + override var privilegeOfStar: Privilege + get() = getStoredPrivilege(PlayerProfile.Star) + set(value) { + setRawStoredPrivilege(PlayerProfile.Star, value) + } +} + +val OfflinePlayer.privilegeKey: PrivilegeKey + inline get() = PlayerProfile.nameless(this) + +fun Privileges.getEffectivePrivilege(player: OfflinePlayer, adminPerm: String): Privilege = + if (player is Player && player.hasPermission(adminPerm)) Privilege.ADMIN + else getStoredPrivilege(player.privilegeKey) + +fun Privileges.canManage(player: OfflinePlayer) = getEffectivePrivilege(player, PERM_ADMIN_MANAGE) >= Privilege.CAN_MANAGE +fun Privileges.canManageFast(player: OfflinePlayer) = getStoredPrivilege(player.privilegeKey) >= Privilege.CAN_MANAGE +fun Privileges.canBuild(player: OfflinePlayer) = getEffectivePrivilege(player, PERM_BUILD_ANYWHERE) >= Privilege.CAN_BUILD +fun Privileges.canBuildFast(player: OfflinePlayer) = getStoredPrivilege(player.privilegeKey) >= Privilege.CAN_BUILD +fun Privileges.canEnter(player: OfflinePlayer) = getEffectivePrivilege(player, PERM_BAN_BYPASS) >= Privilege.DEFAULT +fun Privileges.canEnterFast(player: OfflinePlayer) = getStoredPrivilege(player.privilegeKey) >= Privilege.DEFAULT + +enum class PrivilegeChangeResult { + SUCCESS, FAIL, FAIL_OWNER +} + +fun Privileges.changePrivilege(key: PrivilegeKey, positive: Boolean, update: Privilege): PrivilegeChangeResult = + when { + key == keyOfOwner -> FAIL_OWNER + getRawStoredPrivilege(key).isChangeInDirection(positive, update) && setRawStoredPrivilege(key, update) -> SUCCESS + else -> FAIL + } + +fun Privileges.allowManage(key: PrivilegeKey) = changePrivilege(key, true, Privilege.CAN_MANAGE) +fun Privileges.disallowManage(key: PrivilegeKey) = changePrivilege(key, false, Privilege.CAN_BUILD) +fun Privileges.allowBuild(key: PrivilegeKey) = changePrivilege(key, true, Privilege.CAN_BUILD) +fun Privileges.disallowBuild(key: PrivilegeKey) = changePrivilege(key, false, Privilege.DEFAULT) +fun Privileges.allowEnter(key: PrivilegeKey) = changePrivilege(key, true, Privilege.DEFAULT) +fun Privileges.disallowEnter(key: PrivilegeKey) = changePrivilege(key, false, Privilege.BANNED) + +interface GlobalPrivileges : RawPrivileges, Privileges { + override val keyOfOwner: PlayerProfile.Real +} + +interface GlobalPrivilegesManager { + operator fun get(owner: PlayerProfile.Real): GlobalPrivileges + operator fun get(owner: OfflinePlayer): GlobalPrivileges = get(owner.privilegeKey) +} diff --git a/src/main/kotlin/io/dico/parcels2/blockvisitor/Attachables.kt b/src/main/kotlin/io/dico/parcels2/blockvisitor/Attachables.kt index 8f2c565..83567c5 100644 --- a/src/main/kotlin/io/dico/parcels2/blockvisitor/Attachables.kt +++ b/src/main/kotlin/io/dico/parcels2/blockvisitor/Attachables.kt @@ -1,61 +1,61 @@ -package io.dico.parcels2.blockvisitor - -import io.dico.parcels2.util.math.Vec3i -import io.dico.parcels2.util.ext.getMaterialsWithWoodTypePrefix -import io.dico.parcels2.util.ext.getMaterialsWithWoolColorPrefix -import org.bukkit.Material -import org.bukkit.Material.* -import org.bukkit.block.BlockFace -import org.bukkit.block.data.BlockData -import org.bukkit.block.data.Directional -import java.util.EnumSet - -private val attachables = EnumSet.of( - REPEATER, COMPARATOR, - *getMaterialsWithWoodTypePrefix("PRESSURE_PLATE"), - STONE_PRESSURE_PLATE, LIGHT_WEIGHTED_PRESSURE_PLATE, HEAVY_WEIGHTED_PRESSURE_PLATE, - *getMaterialsWithWoodTypePrefix("BUTTON"), - STONE_BUTTON, LEVER, - *getMaterialsWithWoodTypePrefix("DOOR"), IRON_DOOR, - ACTIVATOR_RAIL, POWERED_RAIL, DETECTOR_RAIL, RAIL, - PISTON, STICKY_PISTON, - REDSTONE_TORCH, REDSTONE_WALL_TORCH, REDSTONE_WIRE, - TRIPWIRE, TRIPWIRE_HOOK, - - BROWN_MUSHROOM, RED_MUSHROOM, CACTUS, CARROT, COCOA, - WHEAT, DEAD_BUSH, CHORUS_FLOWER, DANDELION, SUGAR_CANE, - TALL_GRASS, TALL_SEAGRASS, NETHER_WART, MELON_STEM, - PUMPKIN_STEM, SUNFLOWER, POTATO, LILY_PAD, VINE, - *getMaterialsWithWoodTypePrefix("SAPLING"), - - SAND, RED_SAND, DRAGON_EGG, ANVIL, - *getMaterialsWithWoolColorPrefix("CONCRETE_POWDER"), - - *getMaterialsWithWoolColorPrefix("CARPET"), - CAKE, FIRE, - FLOWER_POT, - LADDER, - // NETHER_PORTAL, fuck nether portals - FLOWER_POT, - SNOW, - TORCH, WALL_TORCH, - *getMaterialsWithWoolColorPrefix("BANNER"), - *getMaterialsWithWoolColorPrefix("WALL_BANNER"), - SIGN, WALL_SIGN -) - -fun isAttachable(type: Material) = attachables.contains(type) - -fun getSupportingBlock(data: BlockData): Vec3i = when (data) { - //is MultipleFacing -> // fuck it xD this is good enough - - is Directional -> Vec3i.convert(when (data.material) { - // exceptions - COCOA -> data.facing - OAK_DOOR, BIRCH_DOOR, SPRUCE_DOOR, JUNGLE_DOOR, ACACIA_DOOR, DARK_OAK_DOOR, IRON_DOOR -> BlockFace.DOWN - - else -> data.facing.oppositeFace - }) - - else -> Vec3i.down -} +package io.dico.parcels2.blockvisitor + +import io.dico.parcels2.util.math.Vec3i +import io.dico.parcels2.util.ext.getMaterialsWithWoodTypePrefix +import io.dico.parcels2.util.ext.getMaterialsWithWoolColorPrefix +import org.bukkit.Material +import org.bukkit.Material.* +import org.bukkit.block.BlockFace +import org.bukkit.block.data.BlockData +import org.bukkit.block.data.Directional +import java.util.EnumSet + +private val attachables = EnumSet.of( + REPEATER, COMPARATOR, + *getMaterialsWithWoodTypePrefix("PRESSURE_PLATE"), + STONE_PRESSURE_PLATE, LIGHT_WEIGHTED_PRESSURE_PLATE, HEAVY_WEIGHTED_PRESSURE_PLATE, + *getMaterialsWithWoodTypePrefix("BUTTON"), + STONE_BUTTON, LEVER, + *getMaterialsWithWoodTypePrefix("DOOR"), IRON_DOOR, + ACTIVATOR_RAIL, POWERED_RAIL, DETECTOR_RAIL, RAIL, + PISTON, STICKY_PISTON, + REDSTONE_TORCH, REDSTONE_WALL_TORCH, REDSTONE_WIRE, + TRIPWIRE, TRIPWIRE_HOOK, + + BROWN_MUSHROOM, RED_MUSHROOM, CACTUS, CARROT, COCOA, + WHEAT, DEAD_BUSH, CHORUS_FLOWER, DANDELION, SUGAR_CANE, + TALL_GRASS, TALL_SEAGRASS, NETHER_WART, MELON_STEM, + PUMPKIN_STEM, SUNFLOWER, POTATO, LILY_PAD, VINE, + *getMaterialsWithWoodTypePrefix("SAPLING"), + + SAND, RED_SAND, DRAGON_EGG, ANVIL, + *getMaterialsWithWoolColorPrefix("CONCRETE_POWDER"), + + *getMaterialsWithWoolColorPrefix("CARPET"), + CAKE, FIRE, + FLOWER_POT, + LADDER, + // NETHER_PORTAL, fuck nether portals + FLOWER_POT, + SNOW, + TORCH, WALL_TORCH, + *getMaterialsWithWoolColorPrefix("BANNER"), + *getMaterialsWithWoolColorPrefix("WALL_BANNER"), + SIGN, WALL_SIGN +) + +fun isAttachable(type: Material) = attachables.contains(type) + +fun getSupportingBlock(data: BlockData): Vec3i = when (data) { + //is MultipleFacing -> // fuck it xD this is good enough + + is Directional -> Vec3i.convert(when (data.material) { + // exceptions + COCOA -> data.facing + OAK_DOOR, BIRCH_DOOR, SPRUCE_DOOR, JUNGLE_DOOR, ACACIA_DOOR, DARK_OAK_DOOR, IRON_DOOR -> BlockFace.DOWN + + else -> data.facing.oppositeFace + }) + + else -> Vec3i.down +} diff --git a/src/main/kotlin/io/dico/parcels2/blockvisitor/ExtraBlockChange.kt b/src/main/kotlin/io/dico/parcels2/blockvisitor/ExtraBlockChange.kt index 3f7e070..ddfec27 100644 --- a/src/main/kotlin/io/dico/parcels2/blockvisitor/ExtraBlockChange.kt +++ b/src/main/kotlin/io/dico/parcels2/blockvisitor/ExtraBlockChange.kt @@ -1,38 +1,38 @@ -package io.dico.parcels2.blockvisitor - -import org.bukkit.block.Block -import org.bukkit.block.BlockState -import org.bukkit.block.Sign -import kotlin.reflect.KClass - -interface ExtraBlockChange { - fun update(block: Block) -} - -abstract class BlockStateChange : ExtraBlockChange { - abstract val stateClass: KClass - - abstract fun update(state: T) - - override fun update(block: Block) { - val state = block.state - if (stateClass.isInstance(state)) { - @Suppress("UNCHECKED_CAST") - update(state as T) - } - } -} - -class SignStateChange(state: Sign) : BlockStateChange() { - val lines = state.lines - - override val stateClass: KClass - get() = Sign::class - - override fun update(state: Sign) { - for (i in lines.indices) { - val line = lines[i] - state.setLine(i, line) - } - } -} +package io.dico.parcels2.blockvisitor + +import org.bukkit.block.Block +import org.bukkit.block.BlockState +import org.bukkit.block.Sign +import kotlin.reflect.KClass + +interface ExtraBlockChange { + fun update(block: Block) +} + +abstract class BlockStateChange : ExtraBlockChange { + abstract val stateClass: KClass + + abstract fun update(state: T) + + override fun update(block: Block) { + val state = block.state + if (stateClass.isInstance(state)) { + @Suppress("UNCHECKED_CAST") + update(state as T) + } + } +} + +class SignStateChange(state: Sign) : BlockStateChange() { + val lines = state.lines + + override val stateClass: KClass + get() = Sign::class + + override fun update(state: Sign) { + for (i in lines.indices) { + val line = lines[i] + state.setLine(i, line) + } + } +} diff --git a/src/main/kotlin/io/dico/parcels2/blockvisitor/RegionTraverser.kt b/src/main/kotlin/io/dico/parcels2/blockvisitor/RegionTraverser.kt index b749b36..6525655 100644 --- a/src/main/kotlin/io/dico/parcels2/blockvisitor/RegionTraverser.kt +++ b/src/main/kotlin/io/dico/parcels2/blockvisitor/RegionTraverser.kt @@ -1,323 +1,323 @@ -package io.dico.parcels2.blockvisitor - -import io.dico.parcels2.util.math.Dimension -import io.dico.parcels2.util.math.Region -import io.dico.parcels2.util.math.Vec3i -import io.dico.parcels2.util.math.clampMax - -private typealias Scope = SequenceScope -/* -class ParcelTraverser( - val parcelProvider: ParcelProvider, - val delegate: RegionTraverser, - scope: CoroutineScope -) : RegionTraverser(), CoroutineScope by scope { - - class OccupiedException(parcelId: ParcelId) : Exception("Parcel $parcelId is occupied") - - /** - * Traverse the blocks of parcel's land - * The iterator must be exhausted, else the permit to traverse it will not be reclaimed. - * - * @throws OccupiedException if a parcel is maintained with the given parcel id and an - * iterator exists for it that has not been exhausted - */ - fun traverseParcel(parcelId: ParcelId): Iterator { - val world = parcelProvider.getWorldById(parcelId.worldId) - ?: throw IllegalArgumentException() - val parcel = parcelProvider.getParcelById(parcelId) - - val medium = if (parcel != null) { - if (parcel.hasBlockVisitors || parcel !is ParcelImpl) { - throw OccupiedException(parcelId) - } - parcel.hasBlockVisitors = true - TraverserMedium { parcel.hasBlockVisitors = false } - } else { - TraverserMedium.DoNothing - } - - val region = world.blockManager.getRegion(parcelId) - return traverseRegion(region, world.world.maxHeight, medium) - } - - override suspend fun Scope.build(region: Region, medium: TraverserMedium) { - with(delegate) { - return build(region, medium) - } - } - -} - -@Suppress("FunctionName") -inline fun TraverserMedium(crossinline whenComplete: () -> Unit) = - object : TraverserMedium { - override fun iterationCompleted() { - whenComplete() - } - } - -/** - * An object that is able to communicate with an iterator returned by [RegionTraverser] - * - */ -interface TraverserMedium { - - /** - * Called by the traverser during first [Iterator.hasNext] call that returns false - */ - fun iterationCompleted() - - /** - * The default [TraverserMedium], which does nothing. - */ - object DoNothing : TraverserMedium { - override fun iterationCompleted() {} - } -}*/ - -sealed class RegionTraverser { - - /** - * Get an iterator traversing [region] using this traverser. - * Depending on the implementation, [region] might be traversed in a specific order and direction. - */ - fun traverseRegion( - region: Region, - worldHeight: Int = 256/*, - medium: TraverserMedium = TraverserMedium.DoNothing*/ - ): Iterator = iterator { build(validify(region, worldHeight)/*, medium*/) } - - abstract suspend fun Scope.build(region: Region/*, medium: TraverserMedium = TraverserMedium.DoNothing*/) - - companion object { - val upward = Directional(TraverseDirection(1, 1, 1), TraverseOrderFactory.createWith(Dimension.Y, Dimension.X)) - val downward = Directional(TraverseDirection(1, -1, 1), TraverseOrderFactory.createWith(Dimension.Y, Dimension.X)) - val toClear get() = downward - val toFill get() = upward - - /** - * The returned [RegionTraverser] will traverse the regions - * * below and including absolute level [y] first, in [upward] direction. - * * above absolute level [y] last, in [downward] direction. - */ - fun convergingTo(y: Int) = Slicing(y, upward, downward, true) - - /** - * The returned [RegionTraverser] will traverse the regions - * * above absolute level [y] first, in [upward] direction. - * * below and including absolute level [y] second, in [downward] direction. - */ - fun separatingFrom(y: Int) = Slicing(y, downward, upward, false) - - private fun validify(region: Region, worldHeight: Int): Region { - if (region.origin.y < 0) { - val origin = region.origin withY 0 - val size = region.size.withY((region.size.y + region.origin.y).clampMax(worldHeight)) - return Region(origin, size) - } - - if (region.origin.y + region.size.y > worldHeight) { - val size = region.size.withY(worldHeight - region.origin.y) - return Region(region.origin, size) - } - - return region - } - - } - - class Directional( - val direction: TraverseDirection, - val order: TraverseOrder - ) : RegionTraverser() { - - private inline fun iterate(max: Int, increasing: Boolean, action: (Int) -> Unit) { - for (i in 0..max) { - action(if (increasing) i else max - i) - } - } - - override suspend fun Scope.build(region: Region/*, medium: TraverserMedium*/) { - val order = order - val (primary, secondary, tertiary) = order.toArray() - val (origin, size) = region - - val maxOfPrimary = size[primary] - 1 - val maxOfSecondary = size[secondary] - 1 - val maxOfTertiary = size[tertiary] - 1 - - val isPrimaryIncreasing = direction.isIncreasing(primary) - val isSecondaryIncreasing = direction.isIncreasing(secondary) - val isTertiaryIncreasing = direction.isIncreasing(tertiary) - - iterate(maxOfPrimary, isPrimaryIncreasing) { p -> - iterate(maxOfSecondary, isSecondaryIncreasing) { s -> - iterate(maxOfTertiary, isTertiaryIncreasing) { t -> - yield(order.add(origin, p, s, t)) - } - } - } - - /*medium.iterationCompleted()*/ - } - - } - - class Slicing( - val bottomSectionMaxY: Int, - val bottomTraverser: RegionTraverser, - val topTraverser: RegionTraverser, - val bottomFirst: Boolean = true - ) : RegionTraverser() { - - private fun slice(region: Region, atY: Int): Pair { - if (atY < region.size.y + 1) { - val bottom = Region(region.origin, region.size.withY(atY + 1)) - val top = Region(region.origin.withY(atY + 1), region.size.addY(-atY - 1)) - return bottom to top - } - return region to null - } - - override suspend fun Scope.build(region: Region/*, medium: TraverserMedium*/) { - val (bottom, top) = slice(region, bottomSectionMaxY) - - if (bottomFirst) { - with(bottomTraverser) { build(bottom) } - top?.let { with(topTraverser) { build(it) } } - } else { - top?.let { with(topTraverser) { build(it) } } - with(bottomTraverser) { build(bottom) } - } - - /*medium.iterationCompleted()*/ - } - } - - /** - * Returns [Directional] instance that would be responsible for - * emitting the given position if it is contained in a region. - * [Directional] instance has a set order and direction - */ - fun childForPosition(position: Vec3i): Directional { - var cur = this - while (true) { - when (cur) { - /*is ParcelTraverser -> cur = cur.delegate*/ - is Directional -> return cur - is Slicing -> - cur = - if (position.y <= cur.bottomSectionMaxY) cur.bottomTraverser - else cur.topTraverser - } - } - } - - /** - * Returns true if and only if this traverser would visit the given - * [block] position before the given [current] position. - * If at least one of [block] and [current] is not contained in a - * region being traversed the result is undefined. - */ - fun comesFirst(current: Vec3i, block: Vec3i): Boolean { - var cur = this - while (true) { - when (cur) { - /*is ParcelTraverser -> cur = cur.delegate*/ - is Directional -> return cur.direction.comesFirst(current, block) - is Slicing -> { - val border = cur.bottomSectionMaxY - cur = when { - current.y <= border && block.y <= border -> cur.bottomTraverser - current.y <= border -> return !cur.bottomFirst - block.y <= border -> return cur.bottomFirst - else -> cur.topTraverser - } - } - } - } - } - -} - -object TraverseOrderFactory { - private fun isSwap(primary: Dimension, secondary: Dimension) = secondary.ordinal != (primary.ordinal + 1) % 3 - - fun createWith(primary: Dimension, secondary: Dimension): TraverseOrder { - // tertiary is implicit - if (primary == secondary) throw IllegalArgumentException() - return TraverseOrder(primary, isSwap(primary, secondary)) - } -} - -inline class TraverseOrder(val orderNum: Int) { - constructor(first: Dimension, swap: Boolean) - : this(if (swap) first.ordinal + 3 else first.ordinal) - - @Suppress("NOTHING_TO_INLINE") - private inline fun element(index: Int) = Dimension[(orderNum + index) % 3] - - private val swap inline get() = orderNum >= 3 - - /** - * The slowest changing dimension - */ - val primary: Dimension get() = element(0) - - /** - * Second slowest changing dimension - */ - val secondary: Dimension get() = element(if (swap) 2 else 1) - - /** - * Dimension that changes every block - */ - val tertiary: Dimension get() = element(if (swap) 1 else 2) - - /** - * All 3 dimensions in this order - */ - fun toArray() = arrayOf(primary, secondary, tertiary) - - fun add(vec: Vec3i, p: Int, s: Int, t: Int): Vec3i = - // optimize this, will be called lots - when (orderNum) { - 0 -> vec.add(p, s, t) // xyz - 1 -> vec.add(t, p, s) // yzx - 2 -> vec.add(s, t, p) // zxy - 3 -> vec.add(p, t, s) // xzy - 4 -> vec.add(s, p, t) // yxz - 5 -> vec.add(t, s, p) // zyx - else -> error("Invalid orderNum $orderNum") - } -} - -inline class TraverseDirection(val bits: Int) { - fun isIncreasing(dimension: Dimension) = (1 shl dimension.ordinal) and bits != 0 - - fun comesFirst(current: Vec3i, block: Vec3i, dimension: Dimension): Boolean = - if (isIncreasing(dimension)) - block[dimension] <= current[dimension] - else - block[dimension] >= current[dimension] - - fun comesFirst(current: Vec3i, block: Vec3i) = - comesFirst(current, block, Dimension.X) - && comesFirst(current, block, Dimension.Y) - && comesFirst(current, block, Dimension.Z) - - companion object { - operator fun invoke(x: Int, y: Int, z: Int) = invoke(Vec3i(x, y, z)) - - operator fun invoke(block: Vec3i): TraverseDirection { - if (block.x == 0 || block.y == 0 || block.z == 0) throw IllegalArgumentException() - var bits = 0 - if (block.x > 0) bits = bits or 1 - if (block.y > 0) bits = bits or 2 - if (block.z > 0) bits = bits or 4 - return TraverseDirection(bits) - } - } - -} +package io.dico.parcels2.blockvisitor + +import io.dico.parcels2.util.math.Dimension +import io.dico.parcels2.util.math.Region +import io.dico.parcels2.util.math.Vec3i +import io.dico.parcels2.util.math.clampMax + +private typealias Scope = SequenceScope +/* +class ParcelTraverser( + val parcelProvider: ParcelProvider, + val delegate: RegionTraverser, + scope: CoroutineScope +) : RegionTraverser(), CoroutineScope by scope { + + class OccupiedException(parcelId: ParcelId) : Exception("Parcel $parcelId is occupied") + + /** + * Traverse the blocks of parcel's land + * The iterator must be exhausted, else the permit to traverse it will not be reclaimed. + * + * @throws OccupiedException if a parcel is maintained with the given parcel id and an + * iterator exists for it that has not been exhausted + */ + fun traverseParcel(parcelId: ParcelId): Iterator { + val world = parcelProvider.getWorldById(parcelId.worldId) + ?: throw IllegalArgumentException() + val parcel = parcelProvider.getParcelById(parcelId) + + val medium = if (parcel != null) { + if (parcel.hasBlockVisitors || parcel !is ParcelImpl) { + throw OccupiedException(parcelId) + } + parcel.hasBlockVisitors = true + TraverserMedium { parcel.hasBlockVisitors = false } + } else { + TraverserMedium.DoNothing + } + + val region = world.blockManager.getRegion(parcelId) + return traverseRegion(region, world.world.maxHeight, medium) + } + + override suspend fun Scope.build(region: Region, medium: TraverserMedium) { + with(delegate) { + return build(region, medium) + } + } + +} + +@Suppress("FunctionName") +inline fun TraverserMedium(crossinline whenComplete: () -> Unit) = + object : TraverserMedium { + override fun iterationCompleted() { + whenComplete() + } + } + +/** + * An object that is able to communicate with an iterator returned by [RegionTraverser] + * + */ +interface TraverserMedium { + + /** + * Called by the traverser during first [Iterator.hasNext] call that returns false + */ + fun iterationCompleted() + + /** + * The default [TraverserMedium], which does nothing. + */ + object DoNothing : TraverserMedium { + override fun iterationCompleted() {} + } +}*/ + +sealed class RegionTraverser { + + /** + * Get an iterator traversing [region] using this traverser. + * Depending on the implementation, [region] might be traversed in a specific order and direction. + */ + fun traverseRegion( + region: Region, + worldHeight: Int = 256/*, + medium: TraverserMedium = TraverserMedium.DoNothing*/ + ): Iterator = iterator { build(validify(region, worldHeight)/*, medium*/) } + + abstract suspend fun Scope.build(region: Region/*, medium: TraverserMedium = TraverserMedium.DoNothing*/) + + companion object { + val upward = Directional(TraverseDirection(1, 1, 1), TraverseOrderFactory.createWith(Dimension.Y, Dimension.X)) + val downward = Directional(TraverseDirection(1, -1, 1), TraverseOrderFactory.createWith(Dimension.Y, Dimension.X)) + val toClear get() = downward + val toFill get() = upward + + /** + * The returned [RegionTraverser] will traverse the regions + * * below and including absolute level [y] first, in [upward] direction. + * * above absolute level [y] last, in [downward] direction. + */ + fun convergingTo(y: Int) = Slicing(y, upward, downward, true) + + /** + * The returned [RegionTraverser] will traverse the regions + * * above absolute level [y] first, in [upward] direction. + * * below and including absolute level [y] second, in [downward] direction. + */ + fun separatingFrom(y: Int) = Slicing(y, downward, upward, false) + + private fun validify(region: Region, worldHeight: Int): Region { + if (region.origin.y < 0) { + val origin = region.origin withY 0 + val size = region.size.withY((region.size.y + region.origin.y).clampMax(worldHeight)) + return Region(origin, size) + } + + if (region.origin.y + region.size.y > worldHeight) { + val size = region.size.withY(worldHeight - region.origin.y) + return Region(region.origin, size) + } + + return region + } + + } + + class Directional( + val direction: TraverseDirection, + val order: TraverseOrder + ) : RegionTraverser() { + + private inline fun iterate(max: Int, increasing: Boolean, action: (Int) -> Unit) { + for (i in 0..max) { + action(if (increasing) i else max - i) + } + } + + override suspend fun Scope.build(region: Region/*, medium: TraverserMedium*/) { + val order = order + val (primary, secondary, tertiary) = order.toArray() + val (origin, size) = region + + val maxOfPrimary = size[primary] - 1 + val maxOfSecondary = size[secondary] - 1 + val maxOfTertiary = size[tertiary] - 1 + + val isPrimaryIncreasing = direction.isIncreasing(primary) + val isSecondaryIncreasing = direction.isIncreasing(secondary) + val isTertiaryIncreasing = direction.isIncreasing(tertiary) + + iterate(maxOfPrimary, isPrimaryIncreasing) { p -> + iterate(maxOfSecondary, isSecondaryIncreasing) { s -> + iterate(maxOfTertiary, isTertiaryIncreasing) { t -> + yield(order.add(origin, p, s, t)) + } + } + } + + /*medium.iterationCompleted()*/ + } + + } + + class Slicing( + val bottomSectionMaxY: Int, + val bottomTraverser: RegionTraverser, + val topTraverser: RegionTraverser, + val bottomFirst: Boolean = true + ) : RegionTraverser() { + + private fun slice(region: Region, atY: Int): Pair { + if (atY < region.size.y + 1) { + val bottom = Region(region.origin, region.size.withY(atY + 1)) + val top = Region(region.origin.withY(atY + 1), region.size.addY(-atY - 1)) + return bottom to top + } + return region to null + } + + override suspend fun Scope.build(region: Region/*, medium: TraverserMedium*/) { + val (bottom, top) = slice(region, bottomSectionMaxY) + + if (bottomFirst) { + with(bottomTraverser) { build(bottom) } + top?.let { with(topTraverser) { build(it) } } + } else { + top?.let { with(topTraverser) { build(it) } } + with(bottomTraverser) { build(bottom) } + } + + /*medium.iterationCompleted()*/ + } + } + + /** + * Returns [Directional] instance that would be responsible for + * emitting the given position if it is contained in a region. + * [Directional] instance has a set order and direction + */ + fun childForPosition(position: Vec3i): Directional { + var cur = this + while (true) { + when (cur) { + /*is ParcelTraverser -> cur = cur.delegate*/ + is Directional -> return cur + is Slicing -> + cur = + if (position.y <= cur.bottomSectionMaxY) cur.bottomTraverser + else cur.topTraverser + } + } + } + + /** + * Returns true if and only if this traverser would visit the given + * [block] position before the given [current] position. + * If at least one of [block] and [current] is not contained in a + * region being traversed the result is undefined. + */ + fun comesFirst(current: Vec3i, block: Vec3i): Boolean { + var cur = this + while (true) { + when (cur) { + /*is ParcelTraverser -> cur = cur.delegate*/ + is Directional -> return cur.direction.comesFirst(current, block) + is Slicing -> { + val border = cur.bottomSectionMaxY + cur = when { + current.y <= border && block.y <= border -> cur.bottomTraverser + current.y <= border -> return !cur.bottomFirst + block.y <= border -> return cur.bottomFirst + else -> cur.topTraverser + } + } + } + } + } + +} + +object TraverseOrderFactory { + private fun isSwap(primary: Dimension, secondary: Dimension) = secondary.ordinal != (primary.ordinal + 1) % 3 + + fun createWith(primary: Dimension, secondary: Dimension): TraverseOrder { + // tertiary is implicit + if (primary == secondary) throw IllegalArgumentException() + return TraverseOrder(primary, isSwap(primary, secondary)) + } +} + +inline class TraverseOrder(val orderNum: Int) { + constructor(first: Dimension, swap: Boolean) + : this(if (swap) first.ordinal + 3 else first.ordinal) + + @Suppress("NOTHING_TO_INLINE") + private inline fun element(index: Int) = Dimension[(orderNum + index) % 3] + + private val swap inline get() = orderNum >= 3 + + /** + * The slowest changing dimension + */ + val primary: Dimension get() = element(0) + + /** + * Second slowest changing dimension + */ + val secondary: Dimension get() = element(if (swap) 2 else 1) + + /** + * Dimension that changes every block + */ + val tertiary: Dimension get() = element(if (swap) 1 else 2) + + /** + * All 3 dimensions in this order + */ + fun toArray() = arrayOf(primary, secondary, tertiary) + + fun add(vec: Vec3i, p: Int, s: Int, t: Int): Vec3i = + // optimize this, will be called lots + when (orderNum) { + 0 -> vec.add(p, s, t) // xyz + 1 -> vec.add(t, p, s) // yzx + 2 -> vec.add(s, t, p) // zxy + 3 -> vec.add(p, t, s) // xzy + 4 -> vec.add(s, p, t) // yxz + 5 -> vec.add(t, s, p) // zyx + else -> error("Invalid orderNum $orderNum") + } +} + +inline class TraverseDirection(val bits: Int) { + fun isIncreasing(dimension: Dimension) = (1 shl dimension.ordinal) and bits != 0 + + fun comesFirst(current: Vec3i, block: Vec3i, dimension: Dimension): Boolean = + if (isIncreasing(dimension)) + block[dimension] <= current[dimension] + else + block[dimension] >= current[dimension] + + fun comesFirst(current: Vec3i, block: Vec3i) = + comesFirst(current, block, Dimension.X) + && comesFirst(current, block, Dimension.Y) + && comesFirst(current, block, Dimension.Z) + + companion object { + operator fun invoke(x: Int, y: Int, z: Int) = invoke(Vec3i(x, y, z)) + + operator fun invoke(block: Vec3i): TraverseDirection { + if (block.x == 0 || block.y == 0 || block.z == 0) throw IllegalArgumentException() + var bits = 0 + if (block.x > 0) bits = bits or 1 + if (block.y > 0) bits = bits or 2 + if (block.z > 0) bits = bits or 4 + return TraverseDirection(bits) + } + } + +} diff --git a/src/main/kotlin/io/dico/parcels2/blockvisitor/Schematic.kt b/src/main/kotlin/io/dico/parcels2/blockvisitor/Schematic.kt index df3cfab..39b4345 100644 --- a/src/main/kotlin/io/dico/parcels2/blockvisitor/Schematic.kt +++ b/src/main/kotlin/io/dico/parcels2/blockvisitor/Schematic.kt @@ -1,121 +1,121 @@ -package io.dico.parcels2.blockvisitor - -import io.dico.parcels2.JobFunction -import io.dico.parcels2.JobScope -import io.dico.parcels2.util.math.Region -import io.dico.parcels2.util.math.Vec3i -import io.dico.parcels2.util.math.get -import org.bukkit.Bukkit -import org.bukkit.Material -import org.bukkit.World -import org.bukkit.block.Sign -import org.bukkit.block.data.BlockData - -private val air = Bukkit.createBlockData(Material.AIR) - -class Schematic { - val size: Vec3i get() = _size!! - private var _size: Vec3i? = null - set(value) { - field?.let { throw IllegalStateException() } - field = value - } - - private var blockDatas: Array? = null - private val extra = mutableListOf>() - private var isLoaded = false; private set - private val traverser: RegionTraverser = RegionTraverser.upward - - suspend fun JobScope.load(world: World, region: Region) { - _size = region.size - - val data = arrayOfNulls(region.blockCount).also { blockDatas = it } - val blocks = traverser.traverseRegion(region) - val total = region.blockCount.toDouble() - - loop@ for ((index, vec) in blocks.withIndex()) { - markSuspensionPoint() - setProgress(index / total) - - val block = world[vec] - if (block.y > 255) continue - val blockData = block.blockData - data[index] = blockData - - val extraChange = when (blockData.material) { - Material.SIGN, - Material.WALL_SIGN -> SignStateChange(block.state as Sign) - else -> continue@loop - } - - extra += (vec - region.origin) to extraChange - } - - isLoaded = true - } - - suspend fun JobScope.paste(world: World, position: Vec3i) { - if (!isLoaded) throw IllegalStateException() - - val region = Region(position, _size!!) - val blocks = traverser.traverseRegion(region, worldHeight = world.maxHeight) - val blockDatas = blockDatas!! - var postponed = hashMapOf() - - val total = region.blockCount.toDouble() - var processed = 0 - - for ((index, vec) in blocks.withIndex()) { - markSuspensionPoint() - setProgress(index / total) - - val block = world[vec] - val type = blockDatas[index] ?: air - if (type !== air && isAttachable(type.material)) { - val supportingBlock = vec + getSupportingBlock(type) - - if (!postponed.containsKey(supportingBlock) && traverser.comesFirst(vec, supportingBlock)) { - block.blockData = type - setProgress(++processed / total) - } else { - postponed[vec] = type - } - - } else { - block.blockData = type - setProgress(++processed / total) - } - } - - while (!postponed.isEmpty()) { - markSuspensionPoint() - val newMap = hashMapOf() - for ((vec, type) in postponed) { - val supportingBlock = vec + getSupportingBlock(type) - if (supportingBlock in postponed && supportingBlock != vec) { - newMap[vec] = type - } else { - world[vec].blockData = type - setProgress(++processed / total) - } - } - postponed = newMap - } - - // Should be negligible so we don't track progress - for ((vec, extraChange) in extra) { - markSuspensionPoint() - val block = world[position + vec] - extraChange.update(block) - } - } - - fun getLoadTask(world: World, region: Region): JobFunction = { - load(world, region) - } - - fun getPasteTask(world: World, position: Vec3i): JobFunction = { - paste(world, position) - } - -} +package io.dico.parcels2.blockvisitor + +import io.dico.parcels2.JobFunction +import io.dico.parcels2.JobScope +import io.dico.parcels2.util.math.Region +import io.dico.parcels2.util.math.Vec3i +import io.dico.parcels2.util.math.get +import org.bukkit.Bukkit +import org.bukkit.Material +import org.bukkit.World +import org.bukkit.block.Sign +import org.bukkit.block.data.BlockData + +private val air = Bukkit.createBlockData(Material.AIR) + +class Schematic { + val size: Vec3i get() = _size!! + private var _size: Vec3i? = null + set(value) { + field?.let { throw IllegalStateException() } + field = value + } + + private var blockDatas: Array? = null + private val extra = mutableListOf>() + private var isLoaded = false; private set + private val traverser: RegionTraverser = RegionTraverser.upward + + suspend fun JobScope.load(world: World, region: Region) { + _size = region.size + + val data = arrayOfNulls(region.blockCount).also { blockDatas = it } + val blocks = traverser.traverseRegion(region) + val total = region.blockCount.toDouble() + + loop@ for ((index, vec) in blocks.withIndex()) { + markSuspensionPoint() + setProgress(index / total) + + val block = world[vec] + if (block.y > 255) continue + val blockData = block.blockData + data[index] = blockData + + val extraChange = when (blockData.material) { + Material.SIGN, + Material.WALL_SIGN -> SignStateChange(block.state as Sign) + else -> continue@loop + } + + extra += (vec - region.origin) to extraChange + } + + isLoaded = true + } + + suspend fun JobScope.paste(world: World, position: Vec3i) { + if (!isLoaded) throw IllegalStateException() + + val region = Region(position, _size!!) + val blocks = traverser.traverseRegion(region, worldHeight = world.maxHeight) + val blockDatas = blockDatas!! + var postponed = hashMapOf() + + val total = region.blockCount.toDouble() + var processed = 0 + + for ((index, vec) in blocks.withIndex()) { + markSuspensionPoint() + setProgress(index / total) + + val block = world[vec] + val type = blockDatas[index] ?: air + if (type !== air && isAttachable(type.material)) { + val supportingBlock = vec + getSupportingBlock(type) + + if (!postponed.containsKey(supportingBlock) && traverser.comesFirst(vec, supportingBlock)) { + block.blockData = type + setProgress(++processed / total) + } else { + postponed[vec] = type + } + + } else { + block.blockData = type + setProgress(++processed / total) + } + } + + while (!postponed.isEmpty()) { + markSuspensionPoint() + val newMap = hashMapOf() + for ((vec, type) in postponed) { + val supportingBlock = vec + getSupportingBlock(type) + if (supportingBlock in postponed && supportingBlock != vec) { + newMap[vec] = type + } else { + world[vec].blockData = type + setProgress(++processed / total) + } + } + postponed = newMap + } + + // Should be negligible so we don't track progress + for ((vec, extraChange) in extra) { + markSuspensionPoint() + val block = world[position + vec] + extraChange.update(block) + } + } + + fun getLoadTask(world: World, region: Region): JobFunction = { + load(world, region) + } + + fun getPasteTask(world: World, position: Vec3i): JobFunction = { + paste(world, position) + } + +} diff --git a/src/main/kotlin/io/dico/parcels2/command/AbstractParcelCommands.kt b/src/main/kotlin/io/dico/parcels2/command/AbstractParcelCommands.kt index eaa9f57..32f4299 100644 --- a/src/main/kotlin/io/dico/parcels2/command/AbstractParcelCommands.kt +++ b/src/main/kotlin/io/dico/parcels2/command/AbstractParcelCommands.kt @@ -1,64 +1,64 @@ -package io.dico.parcels2.command - -import io.dico.dicore.command.* -import io.dico.dicore.command.registration.reflect.ICommandInterceptor -import io.dico.dicore.command.registration.reflect.ICommandReceiver -import io.dico.parcels2.* -import io.dico.parcels2.PlayerProfile.Real -import io.dico.parcels2.PlayerProfile.Unresolved -import io.dico.parcels2.util.ext.hasPermAdminManage -import io.dico.parcels2.util.ext.parcelLimit -import org.bukkit.entity.Player -import org.bukkit.plugin.Plugin -import java.lang.reflect.Method - -abstract class AbstractParcelCommands(val plugin: ParcelsPlugin) : ICommandInterceptor { - - override fun getReceiver(context: ExecutionContext, target: Method, cmdName: String): ICommandReceiver { - return getParcelCommandReceiver(plugin.parcelProvider, context, target, cmdName) - } - - override fun getCoroutineContext(context: ExecutionContext?, target: Method?, cmdName: String?): Any { - return plugin.coroutineContext - } - - protected fun checkConnected(action: String) { - if (!plugin.storage.isConnected) err("Parcels cannot $action right now because of a database error") - } - - protected suspend fun checkParcelLimit(player: Player, world: ParcelWorld) { - if (player.hasPermAdminManage) return - val numOwnedParcels = plugin.storage.getOwnedParcels(PlayerProfile(player)).await() - .filter { it.worldId.equals(world.id) }.size - - val limit = player.parcelLimit - if (numOwnedParcels >= limit) { - err("You have enough plots for now") - } - } - - protected suspend fun toPrivilegeKey(profile: PlayerProfile): PrivilegeKey = when (profile) { - is Real -> profile - is Unresolved -> profile.tryResolveSuspendedly(plugin.storage) - ?: throw CommandException() - else -> throw CommandException() - } - - protected fun areYouSureMessage(context: ExecutionContext): String { - val command = (context.route + context.original).joinToString(" ") + " -sure" - return "Are you sure? You cannot undo this action!\n" + - "Run \"/$command\" if you want to go through with this." - } - - protected fun Job.reportProgressUpdates(context: ExecutionContext, action: String): Job = - onProgressUpdate(1000, 1000) { progress, elapsedTime -> - val alt = context.getFormat(EMessageType.NUMBER) - val main = context.getFormat(EMessageType.INFORMATIVE) - context.sendMessage( - EMessageType.INFORMATIVE, false, "$action progress: $alt%.02f$main%%, $alt%.2f${main}s elapsed" - .format(progress * 100, elapsedTime / 1000.0) - ) - } -} - +package io.dico.parcels2.command + +import io.dico.dicore.command.* +import io.dico.dicore.command.registration.reflect.ICommandInterceptor +import io.dico.dicore.command.registration.reflect.ICommandReceiver +import io.dico.parcels2.* +import io.dico.parcels2.PlayerProfile.Real +import io.dico.parcels2.PlayerProfile.Unresolved +import io.dico.parcels2.util.ext.hasPermAdminManage +import io.dico.parcels2.util.ext.parcelLimit +import org.bukkit.entity.Player +import org.bukkit.plugin.Plugin +import java.lang.reflect.Method + +abstract class AbstractParcelCommands(val plugin: ParcelsPlugin) : ICommandInterceptor { + + override fun getReceiver(context: ExecutionContext, target: Method, cmdName: String): ICommandReceiver { + return getParcelCommandReceiver(plugin.parcelProvider, context, target, cmdName) + } + + override fun getCoroutineContext(context: ExecutionContext?, target: Method?, cmdName: String?): Any { + return plugin.coroutineContext + } + + protected fun checkConnected(action: String) { + if (!plugin.storage.isConnected) err("Parcels cannot $action right now because of a database error") + } + + protected suspend fun checkParcelLimit(player: Player, world: ParcelWorld) { + if (player.hasPermAdminManage) return + val numOwnedParcels = plugin.storage.getOwnedParcels(PlayerProfile(player)).await() + .filter { it.worldId.equals(world.id) }.size + + val limit = player.parcelLimit + if (numOwnedParcels >= limit) { + err("You have enough plots for now") + } + } + + protected suspend fun toPrivilegeKey(profile: PlayerProfile): PrivilegeKey = when (profile) { + is Real -> profile + is Unresolved -> profile.tryResolveSuspendedly(plugin.storage) + ?: throw CommandException() + else -> throw CommandException() + } + + protected fun areYouSureMessage(context: ExecutionContext): String { + val command = (context.route + context.original).joinToString(" ") + " -sure" + return "Are you sure? You cannot undo this action!\n" + + "Run \"/$command\" if you want to go through with this." + } + + protected fun Job.reportProgressUpdates(context: ExecutionContext, action: String): Job = + onProgressUpdate(1000, 1000) { progress, elapsedTime -> + val alt = context.getFormat(EMessageType.NUMBER) + val main = context.getFormat(EMessageType.INFORMATIVE) + context.sendMessage( + EMessageType.INFORMATIVE, false, "$action progress: $alt%.02f$main%%, $alt%.2f${main}s elapsed" + .format(progress * 100, elapsedTime / 1000.0) + ) + } +} + fun err(message: String): Nothing = throw CommandException(message) \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/command/CommandsAdmin.kt b/src/main/kotlin/io/dico/parcels2/command/CommandsAdmin.kt index e700d6d..38adc43 100644 --- a/src/main/kotlin/io/dico/parcels2/command/CommandsAdmin.kt +++ b/src/main/kotlin/io/dico/parcels2/command/CommandsAdmin.kt @@ -1,95 +1,95 @@ -package io.dico.parcels2.command - -import io.dico.dicore.command.CommandException -import io.dico.dicore.command.ExecutionContext -import io.dico.dicore.command.Validate -import io.dico.dicore.command.annotation.Cmd -import io.dico.dicore.command.annotation.Flag -import io.dico.parcels2.* -import io.dico.parcels2.command.ParcelTarget.TargetKind -import io.dico.parcels2.defaultimpl.DefaultParcelContainer -import io.dico.parcels2.util.ext.PERM_ADMIN_MANAGE - -class CommandsAdmin(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) { - - @Cmd("setowner") - @RequireParcelPrivilege(Privilege.ADMIN) - suspend fun ParcelScope.cmdSetowner(@ProfileKind(ProfileKind.ANY) target: PlayerProfile): Any? { - val profile = target.resolved(plugin.storage, resolveToFake = true)!! - parcel.owner = profile - - val fakeString = if (profile.isFake) " (fake)" else "" - return "${profile.notNullName}$fakeString is the new owner of (${parcel.id.idString})" - } - - @Cmd("update_all_owner_signs") - fun cmdUpdateAllOwnerSigns(context: ExecutionContext): Any? { - Validate.isAuthorized(context.sender, PERM_ADMIN_MANAGE) - plugin.jobDispatcher.dispatch { - fun getParcelCount(world: ParcelWorld) = (world.options.axisLimit * 2 + 1).let { it * it } - val parcelCount = plugin.parcelProvider.worlds.values.sumBy { getParcelCount(it) }.toDouble() - var processed = 0 - for (world in plugin.parcelProvider.worlds.values) { - markSuspensionPoint() - - val container = world.container as? DefaultParcelContainer - if (container == null) { - processed += getParcelCount(world) - setProgress(processed / parcelCount) - continue - } - - for (parcel in container.getAllParcels()) { - parcel.updateOwnerSign(force = true) - processed++ - setProgress(processed / parcelCount) - } - } - }.reportProgressUpdates(context, "Updating") - return null - } - - @Cmd("dispose") - @RequireParcelPrivilege(Privilege.ADMIN) - fun ParcelScope.cmdDispose(): Any? { - parcel.dispose() - return "Data of (${parcel.id.idString}) has been disposed" - } - - @Cmd("reset") - @RequireParcelPrivilege(Privilege.ADMIN) - fun ParcelScope.cmdReset(context: ExecutionContext, @Flag sure: Boolean): Any? { - Validate.isTrue(!parcel.hasBlockVisitors, "A process is already running in this parcel") - if (!sure) return areYouSureMessage(context) - - parcel.dispose() - world.blockManager.clearParcel(parcel.id)?.reportProgressUpdates(context, "Reset") - return "Data of (${parcel.id.idString}) has been disposed" - } - - @Cmd("swap") - @RequireParcelPrivilege(Privilege.ADMIN) - fun ParcelScope.cmdSwap( - context: ExecutionContext, - @TargetKind(TargetKind.ID) target: ParcelTarget, - @Flag sure: Boolean - ): Any? { - Validate.isTrue(!parcel.hasBlockVisitors, "A process is already running in this parcel") - if (!sure) return areYouSureMessage(context) - - val parcel2 = (target as ParcelTarget.ByID).getParcel() - ?: throw CommandException("Invalid parcel target") - - // Validate.isTrue(parcel2.world == world, "Parcel must be in the same world") - Validate.isTrue(!parcel2.hasBlockVisitors, "A process is already running in that parcel") - - val data = parcel.data - parcel.copyData(parcel2.data) - parcel2.copyData(data) - - val job = plugin.parcelProvider.swapParcels(parcel.id, parcel2.id)?.reportProgressUpdates(context, "Swap") - Validate.notNull(job, "A process is already running in some parcel (internal error)") - return null - } - +package io.dico.parcels2.command + +import io.dico.dicore.command.CommandException +import io.dico.dicore.command.ExecutionContext +import io.dico.dicore.command.Validate +import io.dico.dicore.command.annotation.Cmd +import io.dico.dicore.command.annotation.Flag +import io.dico.parcels2.* +import io.dico.parcels2.command.ParcelTarget.TargetKind +import io.dico.parcels2.defaultimpl.DefaultParcelContainer +import io.dico.parcels2.util.ext.PERM_ADMIN_MANAGE + +class CommandsAdmin(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) { + + @Cmd("setowner") + @RequireParcelPrivilege(Privilege.ADMIN) + suspend fun ParcelScope.cmdSetowner(@ProfileKind(ProfileKind.ANY) target: PlayerProfile): Any? { + val profile = target.resolved(plugin.storage, resolveToFake = true)!! + parcel.owner = profile + + val fakeString = if (profile.isFake) " (fake)" else "" + return "${profile.notNullName}$fakeString is the new owner of (${parcel.id.idString})" + } + + @Cmd("update_all_owner_signs") + fun cmdUpdateAllOwnerSigns(context: ExecutionContext): Any? { + Validate.isAuthorized(context.sender, PERM_ADMIN_MANAGE) + plugin.jobDispatcher.dispatch { + fun getParcelCount(world: ParcelWorld) = (world.options.axisLimit * 2 + 1).let { it * it } + val parcelCount = plugin.parcelProvider.worlds.values.sumBy { getParcelCount(it) }.toDouble() + var processed = 0 + for (world in plugin.parcelProvider.worlds.values) { + markSuspensionPoint() + + val container = world.container as? DefaultParcelContainer + if (container == null) { + processed += getParcelCount(world) + setProgress(processed / parcelCount) + continue + } + + for (parcel in container.getAllParcels()) { + parcel.updateOwnerSign(force = true) + processed++ + setProgress(processed / parcelCount) + } + } + }.reportProgressUpdates(context, "Updating") + return null + } + + @Cmd("dispose") + @RequireParcelPrivilege(Privilege.ADMIN) + fun ParcelScope.cmdDispose(): Any? { + parcel.dispose() + return "Data of (${parcel.id.idString}) has been disposed" + } + + @Cmd("reset") + @RequireParcelPrivilege(Privilege.ADMIN) + fun ParcelScope.cmdReset(context: ExecutionContext, @Flag sure: Boolean): Any? { + Validate.isTrue(!parcel.hasBlockVisitors, "A process is already running in this parcel") + if (!sure) return areYouSureMessage(context) + + parcel.dispose() + world.blockManager.clearParcel(parcel.id)?.reportProgressUpdates(context, "Reset") + return "Data of (${parcel.id.idString}) has been disposed" + } + + @Cmd("swap") + @RequireParcelPrivilege(Privilege.ADMIN) + fun ParcelScope.cmdSwap( + context: ExecutionContext, + @TargetKind(TargetKind.ID) target: ParcelTarget, + @Flag sure: Boolean + ): Any? { + Validate.isTrue(!parcel.hasBlockVisitors, "A process is already running in this parcel") + if (!sure) return areYouSureMessage(context) + + val parcel2 = (target as ParcelTarget.ByID).getParcel() + ?: throw CommandException("Invalid parcel target") + + // Validate.isTrue(parcel2.world == world, "Parcel must be in the same world") + Validate.isTrue(!parcel2.hasBlockVisitors, "A process is already running in that parcel") + + val data = parcel.data + parcel.copyData(parcel2.data) + parcel2.copyData(data) + + val job = plugin.parcelProvider.swapParcels(parcel.id, parcel2.id)?.reportProgressUpdates(context, "Swap") + Validate.notNull(job, "A process is already running in some parcel (internal error)") + return null + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/command/CommandsAdminPrivilegesGlobal.kt b/src/main/kotlin/io/dico/parcels2/command/CommandsAdminPrivilegesGlobal.kt index cee3e62..a2bd8d1 100644 --- a/src/main/kotlin/io/dico/parcels2/command/CommandsAdminPrivilegesGlobal.kt +++ b/src/main/kotlin/io/dico/parcels2/command/CommandsAdminPrivilegesGlobal.kt @@ -1,132 +1,132 @@ -@file:Suppress("NON_EXHAUSTIVE_WHEN") - -package io.dico.parcels2.command - -import io.dico.dicore.command.ExecutionContext -import io.dico.dicore.command.Validate -import io.dico.dicore.command.annotation.Cmd -import io.dico.dicore.command.annotation.Desc -import io.dico.parcels2.* -import io.dico.parcels2.Privilege.BANNED -import io.dico.parcels2.Privilege.CAN_BUILD -import io.dico.parcels2.PrivilegeChangeResult.* -import io.dico.parcels2.defaultimpl.InfoBuilder -import io.dico.parcels2.util.ext.PERM_ADMIN_MANAGE -import org.bukkit.OfflinePlayer - -class CommandsAdminPrivilegesGlobal(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) { - private val data - inline get() = plugin.globalPrivileges - - private fun checkContext(context: ExecutionContext, owner: OfflinePlayer, changing: Boolean = true): OfflinePlayer { - if (changing) { - checkConnected("have privileges changed") - } - val sender = context.sender - if (sender !== owner) { - Validate.isAuthorized(sender, PERM_ADMIN_MANAGE) - } - return owner - } - - @Cmd("list", aliases = ["l"]) - @Desc( - "List globally declared privileges, players you", - "allowed to build on or banned from all your parcels", - shortVersion = "lists globally declared privileges" - ) - fun cmdList(context: ExecutionContext, owner: OfflinePlayer): Any? { - checkContext(context, owner, changing = false) - val map = plugin.globalPrivileges[owner] - Validate.isTrue(map.hasAnyDeclaredPrivileges(), "This user has not declared any global privileges") - - return StringBuilder().apply { - with(InfoBuilder) { - appendProfilesWithPrivilege("Globally Allowed", map, null, CAN_BUILD) - appendProfilesWithPrivilege("Globally Banned", map, null, BANNED) - } - }.toString() - } - - @Cmd("entrust") - @Desc( - "Allows a player to manage globally", - shortVersion = "allows a player to manage globally" - ) - suspend fun cmdEntrust(context: ExecutionContext, owner: OfflinePlayer, player: PlayerProfile): Any? = - when (data[checkContext(context, owner)].allowManage(toPrivilegeKey(player))) { - FAIL_OWNER -> err("The target cannot be the owner themselves") - FAIL -> err("${player.name} is already allowed to manage globally") - SUCCESS -> "${player.name} is now allowed to manage globally" - } - - @Cmd("distrust") - @Desc( - "Disallows a player to manage globally,", - "they will still be able to build", - shortVersion = "disallows a player to manage globally" - ) - suspend fun cmdDistrust(context: ExecutionContext, owner: OfflinePlayer, player: PlayerProfile): Any? = - when (data[checkContext(context, owner)].disallowManage(toPrivilegeKey(player))) { - FAIL_OWNER -> err("The target cannot be the owner themselves") - FAIL -> err("${player.name} is not currently allowed to manage globally") - SUCCESS -> "${player.name} is not allowed to manage globally anymore" - } - - @Cmd("allow", aliases = ["add", "permit"]) - @Desc( - "Globally allows a player to build on all", - "the parcels that you own.", - shortVersion = "globally allows a player to build on your parcels" - ) - suspend fun cmdAllow(context: ExecutionContext, owner: OfflinePlayer, player: PlayerProfile): Any? = - when (data[checkContext(context, owner)].allowBuild(toPrivilegeKey(player))) { - FAIL_OWNER -> err("The target cannot be the owner themselves") - FAIL -> err("${player.name} is already allowed globally") - SUCCESS -> "${player.name} is now allowed to build globally" - } - - @Cmd("disallow", aliases = ["remove", "forbid"]) - @Desc( - "Globally disallows a player to build on", - "the parcels that you own.", - "If the player is allowed to build on specific", - "parcels, they can still build there.", - shortVersion = "globally disallows a player to build on your parcels" - ) - suspend fun cmdDisallow(context: ExecutionContext, owner: OfflinePlayer, player: PlayerProfile): Any? = - when (data[checkContext(context, owner)].disallowBuild(toPrivilegeKey(player))) { - FAIL_OWNER -> err("The target cannot be the owner themselves") - FAIL -> err("${player.name} is not currently allowed globally") - SUCCESS -> "${player.name} is not allowed to build globally anymore" - } - - @Cmd("ban", aliases = ["deny"]) - @Desc( - "Globally bans a player from all the parcels", - "that you own, making them unable to enter.", - shortVersion = "globally bans a player from your parcels" - ) - suspend fun cmdBan(context: ExecutionContext, owner: OfflinePlayer, player: PlayerProfile): Any? = - when (data[checkContext(context, owner)].disallowEnter(toPrivilegeKey(player))) { - FAIL_OWNER -> err("The target cannot be the owner themselves") - FAIL -> err("${player.name} is already banned globally") - SUCCESS -> "${player.name} is now banned globally" - } - - @Cmd("unban", aliases = ["undeny"]) - @Desc( - "Globally unbans a player from all the parcels", - "that you own, they can enter again.", - "If the player is banned from specific parcels,", - "they will still be banned there.", - shortVersion = "globally unbans a player from your parcels" - ) - suspend fun cmdUnban(context: ExecutionContext, owner: OfflinePlayer, player: PlayerProfile): Any? = - when (data[checkContext(context, owner)].allowEnter(toPrivilegeKey(player))) { - FAIL_OWNER -> err("The target cannot be the owner themselves") - FAIL -> err("${player.name} is not currently banned globally") - SUCCESS -> "${player.name} is not banned globally anymore" - } - +@file:Suppress("NON_EXHAUSTIVE_WHEN") + +package io.dico.parcels2.command + +import io.dico.dicore.command.ExecutionContext +import io.dico.dicore.command.Validate +import io.dico.dicore.command.annotation.Cmd +import io.dico.dicore.command.annotation.Desc +import io.dico.parcels2.* +import io.dico.parcels2.Privilege.BANNED +import io.dico.parcels2.Privilege.CAN_BUILD +import io.dico.parcels2.PrivilegeChangeResult.* +import io.dico.parcels2.defaultimpl.InfoBuilder +import io.dico.parcels2.util.ext.PERM_ADMIN_MANAGE +import org.bukkit.OfflinePlayer + +class CommandsAdminPrivilegesGlobal(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) { + private val data + inline get() = plugin.globalPrivileges + + private fun checkContext(context: ExecutionContext, owner: OfflinePlayer, changing: Boolean = true): OfflinePlayer { + if (changing) { + checkConnected("have privileges changed") + } + val sender = context.sender + if (sender !== owner) { + Validate.isAuthorized(sender, PERM_ADMIN_MANAGE) + } + return owner + } + + @Cmd("list", aliases = ["l"]) + @Desc( + "List globally declared privileges, players you", + "allowed to build on or banned from all your parcels", + shortVersion = "lists globally declared privileges" + ) + fun cmdList(context: ExecutionContext, owner: OfflinePlayer): Any? { + checkContext(context, owner, changing = false) + val map = plugin.globalPrivileges[owner] + Validate.isTrue(map.hasAnyDeclaredPrivileges(), "This user has not declared any global privileges") + + return StringBuilder().apply { + with(InfoBuilder) { + appendProfilesWithPrivilege("Globally Allowed", map, null, CAN_BUILD) + appendProfilesWithPrivilege("Globally Banned", map, null, BANNED) + } + }.toString() + } + + @Cmd("entrust") + @Desc( + "Allows a player to manage globally", + shortVersion = "allows a player to manage globally" + ) + suspend fun cmdEntrust(context: ExecutionContext, owner: OfflinePlayer, player: PlayerProfile): Any? = + when (data[checkContext(context, owner)].allowManage(toPrivilegeKey(player))) { + FAIL_OWNER -> err("The target cannot be the owner themselves") + FAIL -> err("${player.name} is already allowed to manage globally") + SUCCESS -> "${player.name} is now allowed to manage globally" + } + + @Cmd("distrust") + @Desc( + "Disallows a player to manage globally,", + "they will still be able to build", + shortVersion = "disallows a player to manage globally" + ) + suspend fun cmdDistrust(context: ExecutionContext, owner: OfflinePlayer, player: PlayerProfile): Any? = + when (data[checkContext(context, owner)].disallowManage(toPrivilegeKey(player))) { + FAIL_OWNER -> err("The target cannot be the owner themselves") + FAIL -> err("${player.name} is not currently allowed to manage globally") + SUCCESS -> "${player.name} is not allowed to manage globally anymore" + } + + @Cmd("allow", aliases = ["add", "permit"]) + @Desc( + "Globally allows a player to build on all", + "the parcels that you own.", + shortVersion = "globally allows a player to build on your parcels" + ) + suspend fun cmdAllow(context: ExecutionContext, owner: OfflinePlayer, player: PlayerProfile): Any? = + when (data[checkContext(context, owner)].allowBuild(toPrivilegeKey(player))) { + FAIL_OWNER -> err("The target cannot be the owner themselves") + FAIL -> err("${player.name} is already allowed globally") + SUCCESS -> "${player.name} is now allowed to build globally" + } + + @Cmd("disallow", aliases = ["remove", "forbid"]) + @Desc( + "Globally disallows a player to build on", + "the parcels that you own.", + "If the player is allowed to build on specific", + "parcels, they can still build there.", + shortVersion = "globally disallows a player to build on your parcels" + ) + suspend fun cmdDisallow(context: ExecutionContext, owner: OfflinePlayer, player: PlayerProfile): Any? = + when (data[checkContext(context, owner)].disallowBuild(toPrivilegeKey(player))) { + FAIL_OWNER -> err("The target cannot be the owner themselves") + FAIL -> err("${player.name} is not currently allowed globally") + SUCCESS -> "${player.name} is not allowed to build globally anymore" + } + + @Cmd("ban", aliases = ["deny"]) + @Desc( + "Globally bans a player from all the parcels", + "that you own, making them unable to enter.", + shortVersion = "globally bans a player from your parcels" + ) + suspend fun cmdBan(context: ExecutionContext, owner: OfflinePlayer, player: PlayerProfile): Any? = + when (data[checkContext(context, owner)].disallowEnter(toPrivilegeKey(player))) { + FAIL_OWNER -> err("The target cannot be the owner themselves") + FAIL -> err("${player.name} is already banned globally") + SUCCESS -> "${player.name} is now banned globally" + } + + @Cmd("unban", aliases = ["undeny"]) + @Desc( + "Globally unbans a player from all the parcels", + "that you own, they can enter again.", + "If the player is banned from specific parcels,", + "they will still be banned there.", + shortVersion = "globally unbans a player from your parcels" + ) + suspend fun cmdUnban(context: ExecutionContext, owner: OfflinePlayer, player: PlayerProfile): Any? = + when (data[checkContext(context, owner)].allowEnter(toPrivilegeKey(player))) { + FAIL_OWNER -> err("The target cannot be the owner themselves") + FAIL -> err("${player.name} is not currently banned globally") + SUCCESS -> "${player.name} is not banned globally anymore" + } + } \ 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 e0bde7d..3f166ad 100644 --- a/src/main/kotlin/io/dico/parcels2/command/CommandsDebug.kt +++ b/src/main/kotlin/io/dico/parcels2/command/CommandsDebug.kt @@ -1,162 +1,162 @@ -package io.dico.parcels2.command - -import io.dico.dicore.Formatting -import io.dico.dicore.command.* -import io.dico.dicore.command.IContextFilter.Priority.PERMISSION -import io.dico.dicore.command.annotation.Cmd -import io.dico.dicore.command.annotation.PreprocessArgs -import io.dico.dicore.command.annotation.RequireParameters -import io.dico.dicore.command.parameter.ArgumentBuffer -import io.dico.parcels2.* -import io.dico.parcels2.blockvisitor.RegionTraverser -import io.dico.parcels2.util.ext.PERM_ADMIN_MANAGE -import io.dico.parcels2.util.ext.PERM_BAN_BYPASS -import io.dico.parcels2.util.ext.PERM_BUILD_ANYWHERE -import kotlinx.coroutines.launch -import org.bukkit.Bukkit -import org.bukkit.Material -import org.bukkit.block.BlockFace -import org.bukkit.block.data.Directional -import org.bukkit.command.CommandSender -import org.bukkit.entity.Player -import java.util.Random - -class CommandsDebug(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) { - - @Cmd("reloadoptions") - fun reloadOptions() { - plugin.loadOptions() - } - - @Cmd("tpworld") - fun tpWorld(sender: Player, worldName: String): String { - if (worldName == "list") { - return Bukkit.getWorlds().joinToString("\n- ", "- ", "") - } - val world = Bukkit.getWorld(worldName) ?: err("World $worldName is not loaded") - sender.teleport(world.spawnLocation) - return "Teleported you to $worldName spawn" - } - - @Cmd("make_mess") - @RequireParcelPrivilege(Privilege.OWNER) - fun ParcelScope.cmdMakeMess(context: ExecutionContext) { - Validate.isTrue(!parcel.hasBlockVisitors, "A process is already running in this parcel") - - val server = plugin.server - val blockDatas = arrayOf( - server.createBlockData(Material.BLUE_WOOL), - server.createBlockData(Material.LIME_WOOL), - server.createBlockData(Material.GLASS), - server.createBlockData(Material.STONE_SLAB), - server.createBlockData(Material.STONE), - server.createBlockData(Material.QUARTZ_BLOCK), - server.createBlockData(Material.BROWN_CONCRETE) - ) - val random = Random() - - world.blockManager.tryDoBlockOperation(plugin.parcelProvider, parcel.id, traverser = RegionTraverser.upward) { block -> - 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) - ) - } - } - - @Cmd("directionality", aliases = ["dir"]) - fun cmdDirectionality(sender: Player, context: ExecutionContext, material: Material): Any? { - val senderLoc = sender.location - val block = senderLoc.add(senderLoc.direction.setY(0).normalize().multiply(2).toLocation(sender.world)).block - - val blockData = Bukkit.createBlockData(material) - if (blockData is Directional) { - blockData.facing = BlockFace.SOUTH - } - - block.blockData = blockData - return if (blockData is Directional) "The block is facing south" else "The block is not directional, however it implements " + - blockData.javaClass.interfaces!!.contentToString() - } - - @Cmd("jobs") - fun cmdJobs(): Any? { - val workers = plugin.jobDispatcher.jobs - println(workers.map { it.coroutine }.joinToString(separator = "\n")) - return "Task count: ${workers.size}" - } - - @Cmd("complete_jobs") - fun cmdCompleteJobs(): Any? = cmdJobs().also { - plugin.launch { plugin.jobDispatcher.completeAllTasks() } - } - - @Cmd("message") - @PreprocessArgs - fun cmdMessage(sender: CommandSender, message: String): Any? { - // testing @PreprocessArgs which merges "hello there" into a single argument - sender.sendMessage(Formatting.translate(message)) - return null - } - - @Cmd("hasperm") - fun cmdHasperm(target: Player, permission: String): Any? { - return target.hasPermission(permission).toString() - } - - @Cmd("permissions") - fun cmdPermissions(context: ExecutionContext, of: Player, vararg address: String): Any? { - val target = context.address.dispatcherForTree.getDeepChild(ArgumentBuffer(address)) - Validate.isTrue(target.depth == address.size && target.hasCommand(), "Not found: /${address.joinToString(separator = " ")}") - return getPermissionsOf(target).joinToString(separator = "\n") { "$it: ${of.hasPermission(it)}" } - } - - @Cmd("privilege") - @RequireParameters(1) - suspend fun ParcelScope.cmdPrivilege(target: PlayerProfile, adminPerm: String?): Any? { - val key = toPrivilegeKey(target) - - val perm = when (adminPerm) { - "none" -> null - "build" -> PERM_BUILD_ANYWHERE - "manage", null -> PERM_ADMIN_MANAGE - "enter" -> PERM_BAN_BYPASS - else -> err("adminPerm should be build, manager or enter") - } - - val privilege = if (perm == null) { - parcel.getStoredPrivilege(key) - } else { - if (key is PlayerProfile.Star) err("* can't have permissions") - parcel.getEffectivePrivilege(key.player!!, perm) - } - - return privilege.toString() - } - - private fun getPermissionsOf(address: ICommandAddress) = getPermissionsOf(address, emptyArray(), mutableListOf()) - - private fun getPermissionsOf(address: ICommandAddress, path: Array, result: MutableList): List { - val command = address.command ?: return result - - var inherited = false - for (filter in command.contextFilters) { - when (filter) { - is PermissionContextFilter -> { - if (path.isEmpty()) result.add(filter.permission) - else if (filter.isInheritable) result.add(filter.getInheritedPermission(path)) - } - is InheritingContextFilter -> { - if (filter.priority == PERMISSION && address.hasParent() && !inherited) { - inherited = true - getPermissionsOf(address.parent, arrayOf(address.mainKey, *path), result) - } - } - } - } - - return result - } - +package io.dico.parcels2.command + +import io.dico.dicore.Formatting +import io.dico.dicore.command.* +import io.dico.dicore.command.IContextFilter.Priority.PERMISSION +import io.dico.dicore.command.annotation.Cmd +import io.dico.dicore.command.annotation.PreprocessArgs +import io.dico.dicore.command.annotation.RequireParameters +import io.dico.dicore.command.parameter.ArgumentBuffer +import io.dico.parcels2.* +import io.dico.parcels2.blockvisitor.RegionTraverser +import io.dico.parcels2.util.ext.PERM_ADMIN_MANAGE +import io.dico.parcels2.util.ext.PERM_BAN_BYPASS +import io.dico.parcels2.util.ext.PERM_BUILD_ANYWHERE +import kotlinx.coroutines.launch +import org.bukkit.Bukkit +import org.bukkit.Material +import org.bukkit.block.BlockFace +import org.bukkit.block.data.Directional +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player +import java.util.Random + +class CommandsDebug(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) { + + @Cmd("reloadoptions") + fun reloadOptions() { + plugin.loadOptions() + } + + @Cmd("tpworld") + fun tpWorld(sender: Player, worldName: String): String { + if (worldName == "list") { + return Bukkit.getWorlds().joinToString("\n- ", "- ", "") + } + val world = Bukkit.getWorld(worldName) ?: err("World $worldName is not loaded") + sender.teleport(world.spawnLocation) + return "Teleported you to $worldName spawn" + } + + @Cmd("make_mess") + @RequireParcelPrivilege(Privilege.OWNER) + fun ParcelScope.cmdMakeMess(context: ExecutionContext) { + Validate.isTrue(!parcel.hasBlockVisitors, "A process is already running in this parcel") + + val server = plugin.server + val blockDatas = arrayOf( + server.createBlockData(Material.BLUE_WOOL), + server.createBlockData(Material.LIME_WOOL), + server.createBlockData(Material.GLASS), + server.createBlockData(Material.STONE_SLAB), + server.createBlockData(Material.STONE), + server.createBlockData(Material.QUARTZ_BLOCK), + server.createBlockData(Material.BROWN_CONCRETE) + ) + val random = Random() + + world.blockManager.tryDoBlockOperation(plugin.parcelProvider, parcel.id, traverser = RegionTraverser.upward) { block -> + 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) + ) + } + } + + @Cmd("directionality", aliases = ["dir"]) + fun cmdDirectionality(sender: Player, context: ExecutionContext, material: Material): Any? { + val senderLoc = sender.location + val block = senderLoc.add(senderLoc.direction.setY(0).normalize().multiply(2).toLocation(sender.world)).block + + val blockData = Bukkit.createBlockData(material) + if (blockData is Directional) { + blockData.facing = BlockFace.SOUTH + } + + block.blockData = blockData + return if (blockData is Directional) "The block is facing south" else "The block is not directional, however it implements " + + blockData.javaClass.interfaces!!.contentToString() + } + + @Cmd("jobs") + fun cmdJobs(): Any? { + val workers = plugin.jobDispatcher.jobs + println(workers.map { it.coroutine }.joinToString(separator = "\n")) + return "Task count: ${workers.size}" + } + + @Cmd("complete_jobs") + fun cmdCompleteJobs(): Any? = cmdJobs().also { + plugin.launch { plugin.jobDispatcher.completeAllTasks() } + } + + @Cmd("message") + @PreprocessArgs + fun cmdMessage(sender: CommandSender, message: String): Any? { + // testing @PreprocessArgs which merges "hello there" into a single argument + sender.sendMessage(Formatting.translate(message)) + return null + } + + @Cmd("hasperm") + fun cmdHasperm(target: Player, permission: String): Any? { + return target.hasPermission(permission).toString() + } + + @Cmd("permissions") + fun cmdPermissions(context: ExecutionContext, of: Player, vararg address: String): Any? { + val target = context.address.dispatcherForTree.getDeepChild(ArgumentBuffer(address)) + Validate.isTrue(target.depth == address.size && target.hasCommand(), "Not found: /${address.joinToString(separator = " ")}") + return getPermissionsOf(target).joinToString(separator = "\n") { "$it: ${of.hasPermission(it)}" } + } + + @Cmd("privilege") + @RequireParameters(1) + suspend fun ParcelScope.cmdPrivilege(target: PlayerProfile, adminPerm: String?): Any? { + val key = toPrivilegeKey(target) + + val perm = when (adminPerm) { + "none" -> null + "build" -> PERM_BUILD_ANYWHERE + "manage", null -> PERM_ADMIN_MANAGE + "enter" -> PERM_BAN_BYPASS + else -> err("adminPerm should be build, manager or enter") + } + + val privilege = if (perm == null) { + parcel.getStoredPrivilege(key) + } else { + if (key is PlayerProfile.Star) err("* can't have permissions") + parcel.getEffectivePrivilege(key.player!!, perm) + } + + return privilege.toString() + } + + private fun getPermissionsOf(address: ICommandAddress) = getPermissionsOf(address, emptyArray(), mutableListOf()) + + private fun getPermissionsOf(address: ICommandAddress, path: Array, result: MutableList): List { + val command = address.command ?: return result + + var inherited = false + for (filter in command.contextFilters) { + when (filter) { + is PermissionContextFilter -> { + if (path.isEmpty()) result.add(filter.permission) + else if (filter.isInheritable) result.add(filter.getInheritedPermission(path)) + } + is InheritingContextFilter -> { + if (filter.priority == PERMISSION && address.hasParent() && !inherited) { + inherited = true + getPermissionsOf(address.parent, arrayOf(address.mainKey, *path), result) + } + } + } + } + + return result + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/command/CommandsGeneral.kt b/src/main/kotlin/io/dico/parcels2/command/CommandsGeneral.kt index c918b80..61e8d9e 100644 --- a/src/main/kotlin/io/dico/parcels2/command/CommandsGeneral.kt +++ b/src/main/kotlin/io/dico/parcels2/command/CommandsGeneral.kt @@ -1,142 +1,142 @@ -package io.dico.parcels2.command - -import io.dico.dicore.command.ExecutionContext -import io.dico.dicore.command.Validate -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.ParcelsPlugin -import io.dico.parcels2.PlayerProfile -import io.dico.parcels2.Privilege -import io.dico.parcels2.command.ParcelTarget.TargetKind -import io.dico.parcels2.util.ext.hasParcelHomeOthers -import io.dico.parcels2.util.ext.hasPermAdminManage -import io.dico.parcels2.util.ext.uuid -import org.bukkit.block.Biome -import org.bukkit.entity.Player - -class CommandsGeneral(plugin: ParcelsPlugin, parent: SpecialCommandAddress) : AbstractParcelCommands(plugin) { - - @Cmd("auto") - @Desc( - "Finds the unclaimed parcel nearest to origin,", - "and gives it to you", - shortVersion = "sets you up with a fresh, unclaimed parcel" - ) - suspend fun WorldScope.cmdAuto(player: Player): Any? { - checkConnected("be claimed") - checkParcelLimit(player, world) - - val parcel = world.nextEmptyParcel() - ?: err("This world is full, please ask an admin to upsize it") - parcel.owner = PlayerProfile(uuid = player.uuid) - player.teleport(parcel.homeLocation) - return "Enjoy your new parcel!" - } - - @Cmd("info", aliases = ["i"]) - @Desc( - "Displays general information", - "about the parcel you're on", - shortVersion = "displays information about this parcel" - ) - fun ParcelScope.cmdInfo(player: Player) = parcel.infoString - - init { - parent.addSpeciallyTreatedKeys("home", "h") - } - - @Cmd("home", aliases = ["h"]) - @Desc( - "Teleports you to your parcels,", - "unless another player was specified.", - "You can specify an index number if you have", - "more than one parcel", - shortVersion = "teleports you to parcels" - ) - @RequireParameters(0) - suspend fun cmdHome( - player: Player, - @TargetKind(TargetKind.OWNER_REAL) target: ParcelTarget - ): Any? { - return cmdGoto(player, target) - } - - @Cmd("tp", aliases = ["teleport"]) - suspend fun cmdTp( - player: Player, - @TargetKind(TargetKind.ID) target: ParcelTarget - ): Any? { - return cmdGoto(player, target) - } - - @Cmd("goto") - suspend fun cmdGoto( - player: Player, - @TargetKind(TargetKind.ANY) target: ParcelTarget - ): Any? { - if (target is ParcelTarget.ByOwner) { - target.resolveOwner(plugin.storage) - if (!target.owner.matches(player) && !player.hasParcelHomeOthers) { - err("You do not have permission to teleport to other people's parcels") - } - } - - val match = target.getParcelSuspend(plugin.storage) - ?: err("The specified parcel could not be matched") - player.teleport(match.homeLocation) - return null - } - - @Cmd("goto_fake") - suspend fun cmdGotoFake( - player: Player, - @TargetKind(TargetKind.OWNER_FAKE) target: ParcelTarget - ): Any? { - return cmdGoto(player, target) - } - - @Cmd("claim") - @Desc( - "If this parcel is unowned, makes you the owner", - shortVersion = "claims this parcel" - ) - suspend fun ParcelScope.cmdClaim(player: Player): Any? { - checkConnected("be claimed") - parcel.owner.takeIf { !player.hasPermAdminManage }?.let { - err(if (it.matches(player)) "You already own this parcel" else "This parcel is not available") - } - - checkParcelLimit(player, world) - parcel.owner = PlayerProfile(player) - return "Enjoy your new parcel!" - } - - @Cmd("unclaim") - @Desc("Unclaims this parcel") - @RequireParcelPrivilege(Privilege.OWNER) - fun ParcelScope.cmdUnclaim(player: Player): Any? { - checkConnected("be unclaimed") - parcel.dispose() - return "Your parcel has been disposed" - } - - @Cmd("clear") - @RequireParcelPrivilege(Privilege.OWNER) - fun ParcelScope.cmdClear(context: ExecutionContext, @Flag sure: Boolean): Any? { - Validate.isTrue(!parcel.hasBlockVisitors, "A process is already running in this parcel") - if (!sure) return areYouSureMessage(context) - world.blockManager.clearParcel(parcel.id)?.reportProgressUpdates(context, "Clear") - return null - } - - @Cmd("setbiome") - @RequireParcelPrivilege(Privilege.OWNER) - fun ParcelScope.cmdSetbiome(context: ExecutionContext, biome: Biome): Any? { - Validate.isTrue(!parcel.hasBlockVisitors, "A process is already running in this parcel") - world.blockManager.setBiome(parcel.id, biome)?.reportProgressUpdates(context, "Biome change") - return null - } - +package io.dico.parcels2.command + +import io.dico.dicore.command.ExecutionContext +import io.dico.dicore.command.Validate +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.ParcelsPlugin +import io.dico.parcels2.PlayerProfile +import io.dico.parcels2.Privilege +import io.dico.parcels2.command.ParcelTarget.TargetKind +import io.dico.parcels2.util.ext.hasParcelHomeOthers +import io.dico.parcels2.util.ext.hasPermAdminManage +import io.dico.parcels2.util.ext.uuid +import org.bukkit.block.Biome +import org.bukkit.entity.Player + +class CommandsGeneral(plugin: ParcelsPlugin, parent: SpecialCommandAddress) : AbstractParcelCommands(plugin) { + + @Cmd("auto") + @Desc( + "Finds the unclaimed parcel nearest to origin,", + "and gives it to you", + shortVersion = "sets you up with a fresh, unclaimed parcel" + ) + suspend fun WorldScope.cmdAuto(player: Player): Any? { + checkConnected("be claimed") + checkParcelLimit(player, world) + + val parcel = world.nextEmptyParcel() + ?: err("This world is full, please ask an admin to upsize it") + parcel.owner = PlayerProfile(uuid = player.uuid) + player.teleport(parcel.homeLocation) + return "Enjoy your new parcel!" + } + + @Cmd("info", aliases = ["i"]) + @Desc( + "Displays general information", + "about the parcel you're on", + shortVersion = "displays information about this parcel" + ) + fun ParcelScope.cmdInfo(player: Player) = parcel.infoString + + init { + parent.addSpeciallyTreatedKeys("home", "h") + } + + @Cmd("home", aliases = ["h"]) + @Desc( + "Teleports you to your parcels,", + "unless another player was specified.", + "You can specify an index number if you have", + "more than one parcel", + shortVersion = "teleports you to parcels" + ) + @RequireParameters(0) + suspend fun cmdHome( + player: Player, + @TargetKind(TargetKind.OWNER_REAL) target: ParcelTarget + ): Any? { + return cmdGoto(player, target) + } + + @Cmd("tp", aliases = ["teleport"]) + suspend fun cmdTp( + player: Player, + @TargetKind(TargetKind.ID) target: ParcelTarget + ): Any? { + return cmdGoto(player, target) + } + + @Cmd("goto") + suspend fun cmdGoto( + player: Player, + @TargetKind(TargetKind.ANY) target: ParcelTarget + ): Any? { + if (target is ParcelTarget.ByOwner) { + target.resolveOwner(plugin.storage) + if (!target.owner.matches(player) && !player.hasParcelHomeOthers) { + err("You do not have permission to teleport to other people's parcels") + } + } + + val match = target.getParcelSuspend(plugin.storage) + ?: err("The specified parcel could not be matched") + player.teleport(match.homeLocation) + return null + } + + @Cmd("goto_fake") + suspend fun cmdGotoFake( + player: Player, + @TargetKind(TargetKind.OWNER_FAKE) target: ParcelTarget + ): Any? { + return cmdGoto(player, target) + } + + @Cmd("claim") + @Desc( + "If this parcel is unowned, makes you the owner", + shortVersion = "claims this parcel" + ) + suspend fun ParcelScope.cmdClaim(player: Player): Any? { + checkConnected("be claimed") + parcel.owner.takeIf { !player.hasPermAdminManage }?.let { + err(if (it.matches(player)) "You already own this parcel" else "This parcel is not available") + } + + checkParcelLimit(player, world) + parcel.owner = PlayerProfile(player) + return "Enjoy your new parcel!" + } + + @Cmd("unclaim") + @Desc("Unclaims this parcel") + @RequireParcelPrivilege(Privilege.OWNER) + fun ParcelScope.cmdUnclaim(player: Player): Any? { + checkConnected("be unclaimed") + parcel.dispose() + return "Your parcel has been disposed" + } + + @Cmd("clear") + @RequireParcelPrivilege(Privilege.OWNER) + fun ParcelScope.cmdClear(context: ExecutionContext, @Flag sure: Boolean): Any? { + Validate.isTrue(!parcel.hasBlockVisitors, "A process is already running in this parcel") + if (!sure) return areYouSureMessage(context) + world.blockManager.clearParcel(parcel.id)?.reportProgressUpdates(context, "Clear") + return null + } + + @Cmd("setbiome") + @RequireParcelPrivilege(Privilege.OWNER) + fun ParcelScope.cmdSetbiome(context: ExecutionContext, biome: Biome): Any? { + Validate.isTrue(!parcel.hasBlockVisitors, "A process is already running in this parcel") + world.blockManager.setBiome(parcel.id, biome)?.reportProgressUpdates(context, "Biome change") + return null + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/command/CommandsPrivilegesGlobal.kt b/src/main/kotlin/io/dico/parcels2/command/CommandsPrivilegesGlobal.kt index 3b4234c..33ffb8b 100644 --- a/src/main/kotlin/io/dico/parcels2/command/CommandsPrivilegesGlobal.kt +++ b/src/main/kotlin/io/dico/parcels2/command/CommandsPrivilegesGlobal.kt @@ -1,77 +1,77 @@ -package io.dico.parcels2.command - -import io.dico.dicore.command.ExecutionContext -import io.dico.dicore.command.annotation.Cmd -import io.dico.dicore.command.annotation.Desc -import io.dico.parcels2.ParcelsPlugin -import io.dico.parcels2.PlayerProfile -import org.bukkit.entity.Player - -class CommandsPrivilegesGlobal(plugin: ParcelsPlugin, - val adminVersion: CommandsAdminPrivilegesGlobal) : AbstractParcelCommands(plugin) { - @Cmd("list", aliases = ["l"]) - @Desc( - "List globally declared privileges, players you", - "allowed to build on or banned from all your parcels", - shortVersion = "lists globally declared privileges" - ) - fun cmdList(sender: Player, context: ExecutionContext) = - adminVersion.cmdList(context, sender) - - @Cmd("entrust") - @Desc( - "Allows a player to manage globally", - shortVersion = "allows a player to manage globally" - ) - suspend fun cmdEntrust(sender: Player, context: ExecutionContext, player: PlayerProfile) = - adminVersion.cmdEntrust(context, sender, player) - - @Cmd("distrust") - @Desc( - "Disallows a player to manage globally,", - "they will still be able to build", - shortVersion = "disallows a player to manage globally" - ) - suspend fun cmdDistrust(sender: Player, context: ExecutionContext, player: PlayerProfile) = - adminVersion.cmdDistrust(context, sender, player) - - @Cmd("allow", aliases = ["add", "permit"]) - @Desc( - "Globally allows a player to build on all", - "the parcels that you own.", - shortVersion = "globally allows a player to build on your parcels" - ) - suspend fun cmdAllow(sender: Player, context: ExecutionContext, player: PlayerProfile) = - adminVersion.cmdAllow(context, sender, player) - - @Cmd("disallow", aliases = ["remove", "forbid"]) - @Desc( - "Globally disallows a player to build on", - "the parcels that you own.", - "If the player is allowed to build on specific", - "parcels, they can still build there.", - shortVersion = "globally disallows a player to build on your parcels" - ) - suspend fun cmdDisallow(sender: Player, context: ExecutionContext, player: PlayerProfile) = - adminVersion.cmdDisallow(context, sender, player) - - @Cmd("ban", aliases = ["deny"]) - @Desc( - "Globally bans a player from all the parcels", - "that you own, making them unable to enter.", - shortVersion = "globally bans a player from your parcels" - ) - suspend fun cmdBan(sender: Player, context: ExecutionContext, player: PlayerProfile) = - adminVersion.cmdBan(context, sender, player) - - @Cmd("unban", aliases = ["undeny"]) - @Desc( - "Globally unbans a player from all the parcels", - "that you own, they can enter again.", - "If the player is banned from specific parcels,", - "they will still be banned there.", - shortVersion = "globally unbans a player from your parcels" - ) - suspend fun cmdUnban(sender: Player, context: ExecutionContext, player: PlayerProfile) = - adminVersion.cmdUnban(context, sender, player) +package io.dico.parcels2.command + +import io.dico.dicore.command.ExecutionContext +import io.dico.dicore.command.annotation.Cmd +import io.dico.dicore.command.annotation.Desc +import io.dico.parcels2.ParcelsPlugin +import io.dico.parcels2.PlayerProfile +import org.bukkit.entity.Player + +class CommandsPrivilegesGlobal(plugin: ParcelsPlugin, + val adminVersion: CommandsAdminPrivilegesGlobal) : AbstractParcelCommands(plugin) { + @Cmd("list", aliases = ["l"]) + @Desc( + "List globally declared privileges, players you", + "allowed to build on or banned from all your parcels", + shortVersion = "lists globally declared privileges" + ) + fun cmdList(sender: Player, context: ExecutionContext) = + adminVersion.cmdList(context, sender) + + @Cmd("entrust") + @Desc( + "Allows a player to manage globally", + shortVersion = "allows a player to manage globally" + ) + suspend fun cmdEntrust(sender: Player, context: ExecutionContext, player: PlayerProfile) = + adminVersion.cmdEntrust(context, sender, player) + + @Cmd("distrust") + @Desc( + "Disallows a player to manage globally,", + "they will still be able to build", + shortVersion = "disallows a player to manage globally" + ) + suspend fun cmdDistrust(sender: Player, context: ExecutionContext, player: PlayerProfile) = + adminVersion.cmdDistrust(context, sender, player) + + @Cmd("allow", aliases = ["add", "permit"]) + @Desc( + "Globally allows a player to build on all", + "the parcels that you own.", + shortVersion = "globally allows a player to build on your parcels" + ) + suspend fun cmdAllow(sender: Player, context: ExecutionContext, player: PlayerProfile) = + adminVersion.cmdAllow(context, sender, player) + + @Cmd("disallow", aliases = ["remove", "forbid"]) + @Desc( + "Globally disallows a player to build on", + "the parcels that you own.", + "If the player is allowed to build on specific", + "parcels, they can still build there.", + shortVersion = "globally disallows a player to build on your parcels" + ) + suspend fun cmdDisallow(sender: Player, context: ExecutionContext, player: PlayerProfile) = + adminVersion.cmdDisallow(context, sender, player) + + @Cmd("ban", aliases = ["deny"]) + @Desc( + "Globally bans a player from all the parcels", + "that you own, making them unable to enter.", + shortVersion = "globally bans a player from your parcels" + ) + suspend fun cmdBan(sender: Player, context: ExecutionContext, player: PlayerProfile) = + adminVersion.cmdBan(context, sender, player) + + @Cmd("unban", aliases = ["undeny"]) + @Desc( + "Globally unbans a player from all the parcels", + "that you own, they can enter again.", + "If the player is banned from specific parcels,", + "they will still be banned there.", + shortVersion = "globally unbans a player from your parcels" + ) + suspend fun cmdUnban(sender: Player, context: ExecutionContext, player: PlayerProfile) = + adminVersion.cmdUnban(context, sender, player) } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/command/CommandsPrivilegesLocal.kt b/src/main/kotlin/io/dico/parcels2/command/CommandsPrivilegesLocal.kt index 21910b1..e3aba22 100644 --- a/src/main/kotlin/io/dico/parcels2/command/CommandsPrivilegesLocal.kt +++ b/src/main/kotlin/io/dico/parcels2/command/CommandsPrivilegesLocal.kt @@ -1,144 +1,144 @@ -package io.dico.parcels2.command - -import io.dico.dicore.command.Validate -import io.dico.dicore.command.annotation.Cmd -import io.dico.dicore.command.annotation.Desc -import io.dico.parcels2.* -import io.dico.parcels2.PrivilegeChangeResult.* -import io.dico.parcels2.util.ext.PERM_ADMIN_MANAGE -import io.dico.parcels2.util.ext.hasPermAdminManage -import org.bukkit.entity.Player - -class CommandsPrivilegesLocal(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) { - - private fun ParcelScope.checkPrivilege(sender: Player, key: PrivilegeKey) { - val senderPrivilege = parcel.getEffectivePrivilege(sender, PERM_ADMIN_MANAGE) - val targetPrivilege = parcel.getStoredPrivilege(key) - Validate.isTrue(senderPrivilege > targetPrivilege, "You may not change the privilege of ${key.notNullName}") - } - - private fun ParcelScope.checkOwned(sender: Player) { - Validate.isTrue(parcel.owner != null || sender.hasPermAdminManage, "This parcel is unowned") - } - - @Cmd("entrust") - @Desc( - "Allows a player to manage this parcel", - shortVersion = "allows a player to manage this parcel" - ) - @RequireParcelPrivilege(Privilege.OWNER) - suspend fun ParcelScope.cmdEntrust(sender: Player, player: PlayerProfile): Any? { - checkConnected("have privileges changed") - checkOwned(sender) - - val key = toPrivilegeKey(player) - return when (parcel.allowManage(key)) { - FAIL_OWNER -> err("The target already owns the parcel") - FAIL -> err("${player.name} is already allowed to manage this parcel") - SUCCESS -> "${player.name} is now allowed to manage this parcel" - } - } - - @Cmd("distrust") - @Desc( - "Disallows a player to manage this parcel,", - "they will still be able to build", - shortVersion = "disallows a player to manage this parcel" - ) - @RequireParcelPrivilege(Privilege.OWNER) - suspend fun ParcelScope.cmdDistrust(sender: Player, player: PlayerProfile): Any? { - checkConnected("have privileges changed") - checkOwned(sender) - - val key = toPrivilegeKey(player) - return when (parcel.disallowManage(key)) { - FAIL_OWNER -> err("The target owns the parcel and can't be distrusted") - FAIL -> err("${player.name} is not currently allowed to manage this parcel") - SUCCESS -> "${player.name} is not allowed to manage this parcel anymore" - } - } - - @Cmd("allow", aliases = ["add", "permit"]) - @Desc( - "Allows a player to build on this parcel", - shortVersion = "allows a player to build on this parcel" - ) - @RequireParcelPrivilege(Privilege.CAN_MANAGE) - suspend fun ParcelScope.cmdAllow(sender: Player, player: PlayerProfile): Any? { - checkConnected("have privileges changed") - checkOwned(sender) - - val key = toPrivilegeKey(player) - checkPrivilege(sender, key) - - return when (parcel.allowBuild(key)) { - FAIL_OWNER -> err("The target already owns the parcel") - FAIL -> err("${player.name} is already allowed to build on this parcel") - SUCCESS -> "${player.name} is now allowed to build on this parcel" - } - } - - @Cmd("disallow", aliases = ["remove", "forbid"]) - @Desc( - "Disallows a player to build on this parcel,", - "they won't be allowed to anymore", - shortVersion = "disallows a player to build on this parcel" - ) - @RequireParcelPrivilege(Privilege.CAN_MANAGE) - suspend fun ParcelScope.cmdDisallow(sender: Player, player: PlayerProfile): Any? { - checkConnected("have privileges changed") - checkOwned(sender) - - val key = toPrivilegeKey(player) - checkPrivilege(sender, key) - - return when (parcel.disallowBuild(key)) { - FAIL_OWNER -> err("The target owns the parcel") - FAIL -> err("${player.name} is not currently allowed to build on this parcel") - SUCCESS -> "${player.name} is not allowed to build on this parcel anymore" - } - } - - @Cmd("ban", aliases = ["deny"]) - @Desc( - "Bans a player from this parcel,", - "making them unable to enter", - shortVersion = "bans a player from this parcel" - ) - @RequireParcelPrivilege(Privilege.CAN_MANAGE) - suspend fun ParcelScope.cmdBan(sender: Player, player: PlayerProfile): Any? { - checkConnected("have privileges changed") - checkOwned(sender) - - val key = toPrivilegeKey(player) - checkPrivilege(sender, key) - - return when (parcel.disallowEnter(key)) { - FAIL_OWNER -> err("The target owns the parcel") - FAIL -> err("${player.name} is already banned from this parcel") - SUCCESS -> "${player.name} is now banned from this parcel" - } - } - - @Cmd("unban", aliases = ["undeny"]) - @Desc( - "Unbans a player from this parcel,", - "they will be able to enter it again", - shortVersion = "unbans a player from this parcel" - ) - @RequireParcelPrivilege(Privilege.CAN_MANAGE) - suspend fun ParcelScope.cmdUnban(sender: Player, player: PlayerProfile): Any? { - checkConnected("have privileges changed") - checkOwned(sender) - - val key = toPrivilegeKey(player) - checkPrivilege(sender, key) - - return when (parcel.allowEnter(key)) { - FAIL_OWNER -> err("The target owns the parcel") - FAIL -> err("${player.name} is not currently banned from this parcel") - SUCCESS -> "${player.name} is not banned from this parcel anymore" - } - } - +package io.dico.parcels2.command + +import io.dico.dicore.command.Validate +import io.dico.dicore.command.annotation.Cmd +import io.dico.dicore.command.annotation.Desc +import io.dico.parcels2.* +import io.dico.parcels2.PrivilegeChangeResult.* +import io.dico.parcels2.util.ext.PERM_ADMIN_MANAGE +import io.dico.parcels2.util.ext.hasPermAdminManage +import org.bukkit.entity.Player + +class CommandsPrivilegesLocal(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) { + + private fun ParcelScope.checkPrivilege(sender: Player, key: PrivilegeKey) { + val senderPrivilege = parcel.getEffectivePrivilege(sender, PERM_ADMIN_MANAGE) + val targetPrivilege = parcel.getStoredPrivilege(key) + Validate.isTrue(senderPrivilege > targetPrivilege, "You may not change the privilege of ${key.notNullName}") + } + + private fun ParcelScope.checkOwned(sender: Player) { + Validate.isTrue(parcel.owner != null || sender.hasPermAdminManage, "This parcel is unowned") + } + + @Cmd("entrust") + @Desc( + "Allows a player to manage this parcel", + shortVersion = "allows a player to manage this parcel" + ) + @RequireParcelPrivilege(Privilege.OWNER) + suspend fun ParcelScope.cmdEntrust(sender: Player, player: PlayerProfile): Any? { + checkConnected("have privileges changed") + checkOwned(sender) + + val key = toPrivilegeKey(player) + return when (parcel.allowManage(key)) { + FAIL_OWNER -> err("The target already owns the parcel") + FAIL -> err("${player.name} is already allowed to manage this parcel") + SUCCESS -> "${player.name} is now allowed to manage this parcel" + } + } + + @Cmd("distrust") + @Desc( + "Disallows a player to manage this parcel,", + "they will still be able to build", + shortVersion = "disallows a player to manage this parcel" + ) + @RequireParcelPrivilege(Privilege.OWNER) + suspend fun ParcelScope.cmdDistrust(sender: Player, player: PlayerProfile): Any? { + checkConnected("have privileges changed") + checkOwned(sender) + + val key = toPrivilegeKey(player) + return when (parcel.disallowManage(key)) { + FAIL_OWNER -> err("The target owns the parcel and can't be distrusted") + FAIL -> err("${player.name} is not currently allowed to manage this parcel") + SUCCESS -> "${player.name} is not allowed to manage this parcel anymore" + } + } + + @Cmd("allow", aliases = ["add", "permit"]) + @Desc( + "Allows a player to build on this parcel", + shortVersion = "allows a player to build on this parcel" + ) + @RequireParcelPrivilege(Privilege.CAN_MANAGE) + suspend fun ParcelScope.cmdAllow(sender: Player, player: PlayerProfile): Any? { + checkConnected("have privileges changed") + checkOwned(sender) + + val key = toPrivilegeKey(player) + checkPrivilege(sender, key) + + return when (parcel.allowBuild(key)) { + FAIL_OWNER -> err("The target already owns the parcel") + FAIL -> err("${player.name} is already allowed to build on this parcel") + SUCCESS -> "${player.name} is now allowed to build on this parcel" + } + } + + @Cmd("disallow", aliases = ["remove", "forbid"]) + @Desc( + "Disallows a player to build on this parcel,", + "they won't be allowed to anymore", + shortVersion = "disallows a player to build on this parcel" + ) + @RequireParcelPrivilege(Privilege.CAN_MANAGE) + suspend fun ParcelScope.cmdDisallow(sender: Player, player: PlayerProfile): Any? { + checkConnected("have privileges changed") + checkOwned(sender) + + val key = toPrivilegeKey(player) + checkPrivilege(sender, key) + + return when (parcel.disallowBuild(key)) { + FAIL_OWNER -> err("The target owns the parcel") + FAIL -> err("${player.name} is not currently allowed to build on this parcel") + SUCCESS -> "${player.name} is not allowed to build on this parcel anymore" + } + } + + @Cmd("ban", aliases = ["deny"]) + @Desc( + "Bans a player from this parcel,", + "making them unable to enter", + shortVersion = "bans a player from this parcel" + ) + @RequireParcelPrivilege(Privilege.CAN_MANAGE) + suspend fun ParcelScope.cmdBan(sender: Player, player: PlayerProfile): Any? { + checkConnected("have privileges changed") + checkOwned(sender) + + val key = toPrivilegeKey(player) + checkPrivilege(sender, key) + + return when (parcel.disallowEnter(key)) { + FAIL_OWNER -> err("The target owns the parcel") + FAIL -> err("${player.name} is already banned from this parcel") + SUCCESS -> "${player.name} is now banned from this parcel" + } + } + + @Cmd("unban", aliases = ["undeny"]) + @Desc( + "Unbans a player from this parcel,", + "they will be able to enter it again", + shortVersion = "unbans a player from this parcel" + ) + @RequireParcelPrivilege(Privilege.CAN_MANAGE) + suspend fun ParcelScope.cmdUnban(sender: Player, player: PlayerProfile): Any? { + checkConnected("have privileges changed") + checkOwned(sender) + + val key = toPrivilegeKey(player) + checkPrivilege(sender, key) + + return when (parcel.allowEnter(key)) { + FAIL_OWNER -> err("The target owns the parcel") + FAIL -> err("${player.name} is not currently banned from this parcel") + SUCCESS -> "${player.name} is not banned from this parcel anymore" + } + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt b/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt index 721ce2d..155f4f1 100644 --- a/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt +++ b/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt @@ -1,137 +1,137 @@ -package io.dico.parcels2.command - -import io.dico.dicore.command.* -import io.dico.dicore.command.parameter.ArgumentBuffer -import io.dico.dicore.command.predef.DefaultGroupCommand -import io.dico.dicore.command.registration.reflect.ReflectiveRegistration -import io.dico.parcels2.Interactables -import io.dico.parcels2.ParcelsPlugin -import io.dico.parcels2.logger -import io.dico.parcels2.util.ext.hasPermAdminManage -import org.bukkit.command.CommandSender -import java.util.LinkedList -import java.util.Queue - -@Suppress("UsePropertyAccessSyntax") -fun getParcelCommands(plugin: ParcelsPlugin): ICommandDispatcher = CommandBuilder().apply { - val parcelsAddress = SpecialCommandAddress() - - setChatHandler(ParcelsChatHandler()) - addParameterType(false, ParcelParameterType(plugin.parcelProvider)) - addParameterType(false, ProfileParameterType()) - addParameterType(true, ParcelTarget.PType(plugin.parcelProvider, parcelsAddress)) - - group(parcelsAddress, "parcel", "plot", "plots", "p") { - addContextFilter(IContextFilter.inheritablePermission("parcels.command")) - registerCommands(CommandsGeneral(plugin, parcelsAddress)) - registerCommands(CommandsPrivilegesLocal(plugin)) - - group("option", "opt", "o") { - setGroupDescription( - "changes interaction options for this parcel", - "Sets whether players who are not allowed to", - "build here can interact with certain things." - ) - - group("interact", "i") { - val command = ParcelOptionsInteractCommand(plugin) - Interactables.classesById.forEach { - addSubCommand(it.name, command) - } - } - } - - val adminPrivilegesGlobal = CommandsAdminPrivilegesGlobal(plugin) - - group("global", "g") { - registerCommands(CommandsPrivilegesGlobal(plugin, adminVersion = adminPrivilegesGlobal)) - } - - group("admin", "a") { - setCommand(AdminGroupCommand()) - registerCommands(CommandsAdmin(plugin)) - - group("global", "g") { - registerCommands(adminPrivilegesGlobal) - } - } - - if (!logger.isDebugEnabled) return@group - - group("debug", "d") { - registerCommands(CommandsDebug(plugin)) - } - } - - generateHelpAndSyntaxCommands(parcelsAddress) -}.getDispatcher() - -private inline fun CommandBuilder.group(name: String, vararg aliases: String, config: CommandBuilder.() -> Unit) { - group(name, *aliases) - config() - parent() -} - -private inline fun CommandBuilder.group(address: ICommandAddress, name: String, vararg aliases: String, config: CommandBuilder.() -> Unit) { - group(address, name, *aliases) - config() - parent() -} - -private fun CommandBuilder.generateHelpAndSyntaxCommands(root: ICommandAddress): CommandBuilder { - generateCommands(root, "help", "syntax") - return this -} - -private fun generateCommands(address: ICommandAddress, vararg names: String) { - val addresses: Queue = LinkedList() - addresses.offer(address) - - while (addresses.isNotEmpty()) { - val cur = addresses.poll() - cur.childrenMainKeys.mapTo(addresses) { cur.getChild(it) } - if (cur.hasCommand()) { - ReflectiveRegistration.generateCommands(cur, names) - } - } -} - -class SpecialCommandAddress : ChildCommandAddress() { - private val speciallyTreatedKeys = mutableListOf() - - // Used to allow /p h:1 syntax, which is the same as what PlotMe uses. - var speciallyParsedIndex: Int? = null; private set - - fun addSpeciallyTreatedKeys(vararg keys: String) { - for (key in keys) { - speciallyTreatedKeys.add(key + ":") - } - } - - // h:1 - @Throws(CommandException::class) - override fun getChild(context: ExecutionContext, buffer: ArgumentBuffer): ChildCommandAddress? { - speciallyParsedIndex = null - - val key = buffer.next() ?: return null - for (specialKey in speciallyTreatedKeys) { - if (key.startsWith(specialKey)) { - val result = getChild(specialKey.substring(0, specialKey.length - 1)) - ?: return null - - val text = key.substring(specialKey.length) - val num = text.toIntOrNull() ?: throw CommandException("$text is not a number") - speciallyParsedIndex = num - - return result - } - } - - return super.getChild(key) - } - -} - -private class AdminGroupCommand : DefaultGroupCommand() { - override fun isVisibleTo(sender: CommandSender) = sender.hasPermAdminManage -} +package io.dico.parcels2.command + +import io.dico.dicore.command.* +import io.dico.dicore.command.parameter.ArgumentBuffer +import io.dico.dicore.command.predef.DefaultGroupCommand +import io.dico.dicore.command.registration.reflect.ReflectiveRegistration +import io.dico.parcels2.Interactables +import io.dico.parcels2.ParcelsPlugin +import io.dico.parcels2.logger +import io.dico.parcels2.util.ext.hasPermAdminManage +import org.bukkit.command.CommandSender +import java.util.LinkedList +import java.util.Queue + +@Suppress("UsePropertyAccessSyntax") +fun getParcelCommands(plugin: ParcelsPlugin): ICommandDispatcher = CommandBuilder().apply { + val parcelsAddress = SpecialCommandAddress() + + setChatHandler(ParcelsChatHandler()) + addParameterType(false, ParcelParameterType(plugin.parcelProvider)) + addParameterType(false, ProfileParameterType()) + addParameterType(true, ParcelTarget.PType(plugin.parcelProvider, parcelsAddress)) + + group(parcelsAddress, "parcel", "plot", "plots", "p") { + addContextFilter(IContextFilter.inheritablePermission("parcels.command")) + registerCommands(CommandsGeneral(plugin, parcelsAddress)) + registerCommands(CommandsPrivilegesLocal(plugin)) + + group("option", "opt", "o") { + setGroupDescription( + "changes interaction options for this parcel", + "Sets whether players who are not allowed to", + "build here can interact with certain things." + ) + + group("interact", "i") { + val command = ParcelOptionsInteractCommand(plugin) + Interactables.classesById.forEach { + addSubCommand(it.name, command) + } + } + } + + val adminPrivilegesGlobal = CommandsAdminPrivilegesGlobal(plugin) + + group("global", "g") { + registerCommands(CommandsPrivilegesGlobal(plugin, adminVersion = adminPrivilegesGlobal)) + } + + group("admin", "a") { + setCommand(AdminGroupCommand()) + registerCommands(CommandsAdmin(plugin)) + + group("global", "g") { + registerCommands(adminPrivilegesGlobal) + } + } + + if (!logger.isDebugEnabled) return@group + + group("debug", "d") { + registerCommands(CommandsDebug(plugin)) + } + } + + generateHelpAndSyntaxCommands(parcelsAddress) +}.getDispatcher() + +private inline fun CommandBuilder.group(name: String, vararg aliases: String, config: CommandBuilder.() -> Unit) { + group(name, *aliases) + config() + parent() +} + +private inline fun CommandBuilder.group(address: ICommandAddress, name: String, vararg aliases: String, config: CommandBuilder.() -> Unit) { + group(address, name, *aliases) + config() + parent() +} + +private fun CommandBuilder.generateHelpAndSyntaxCommands(root: ICommandAddress): CommandBuilder { + generateCommands(root, "help", "syntax") + return this +} + +private fun generateCommands(address: ICommandAddress, vararg names: String) { + val addresses: Queue = LinkedList() + addresses.offer(address) + + while (addresses.isNotEmpty()) { + val cur = addresses.poll() + cur.childrenMainKeys.mapTo(addresses) { cur.getChild(it) } + if (cur.hasCommand()) { + ReflectiveRegistration.generateCommands(cur, names) + } + } +} + +class SpecialCommandAddress : ChildCommandAddress() { + private val speciallyTreatedKeys = mutableListOf() + + // Used to allow /p h:1 syntax, which is the same as what PlotMe uses. + var speciallyParsedIndex: Int? = null; private set + + fun addSpeciallyTreatedKeys(vararg keys: String) { + for (key in keys) { + speciallyTreatedKeys.add(key + ":") + } + } + + // h:1 + @Throws(CommandException::class) + override fun getChild(context: ExecutionContext, buffer: ArgumentBuffer): ChildCommandAddress? { + speciallyParsedIndex = null + + val key = buffer.next() ?: return null + for (specialKey in speciallyTreatedKeys) { + if (key.startsWith(specialKey)) { + val result = getChild(specialKey.substring(0, specialKey.length - 1)) + ?: return null + + val text = key.substring(specialKey.length) + val num = text.toIntOrNull() ?: throw CommandException("$text is not a number") + speciallyParsedIndex = num + + return result + } + } + + return super.getChild(key) + } + +} + +private class AdminGroupCommand : DefaultGroupCommand() { + override fun isVisibleTo(sender: CommandSender) = sender.hasPermAdminManage +} diff --git a/src/main/kotlin/io/dico/parcels2/command/ParcelCommandReceivers.kt b/src/main/kotlin/io/dico/parcels2/command/ParcelCommandReceivers.kt index 15548b4..e41fd37 100644 --- a/src/main/kotlin/io/dico/parcels2/command/ParcelCommandReceivers.kt +++ b/src/main/kotlin/io/dico/parcels2/command/ParcelCommandReceivers.kt @@ -1,68 +1,68 @@ -package io.dico.parcels2.command - -import io.dico.dicore.command.CommandException -import io.dico.dicore.command.ExecutionContext -import io.dico.dicore.command.registration.reflect.ICommandReceiver -import io.dico.dicore.command.Validate -import io.dico.parcels2.* -import io.dico.parcels2.Privilege.* -import io.dico.parcels2.util.ext.hasPermAdminManage -import io.dico.parcels2.util.ext.uuid -import org.bukkit.entity.Player -import java.lang.reflect.Method -import kotlin.reflect.full.extensionReceiverParameter -import kotlin.reflect.full.findAnnotation -import kotlin.reflect.jvm.jvmErasure -import kotlin.reflect.jvm.kotlinFunction - -@Target(AnnotationTarget.FUNCTION) -@Retention(AnnotationRetention.RUNTIME) -annotation class RequireParcelPrivilege(val privilege: Privilege) - -/* -@Target(AnnotationTarget.FUNCTION) -@Retention(AnnotationRetention.RUNTIME) -annotation class SuspensionTimeout(val millis: Int) -*/ - -open class WorldScope(val world: ParcelWorld) : ICommandReceiver -open class ParcelScope(val parcel: Parcel) : WorldScope(parcel.world) { - fun checkCanManage(player: Player, action: String) = Validate.isTrue(parcel.canManage(player), "You must own this parcel to $action") -} - -fun getParcelCommandReceiver(parcelProvider: ParcelProvider, context: ExecutionContext, method: Method, cmdName: String): ICommandReceiver { - val player = context.sender as Player - val function = method.kotlinFunction!! - val receiverType = function.extensionReceiverParameter!!.type - val require = function.findAnnotation() - - return when (receiverType.jvmErasure) { - ParcelScope::class -> ParcelScope(parcelProvider.getParcelRequired(player, require?.privilege)) - WorldScope::class -> WorldScope(parcelProvider.getWorldRequired(player, require?.privilege == ADMIN)) - else -> throw InternalError("Invalid command receiver type") - } -} - -fun ParcelProvider.getWorldRequired(player: Player, admin: Boolean = false): ParcelWorld { - if (admin) Validate.isTrue(player.hasPermAdminManage, "You must have admin rights to use that command") - return getWorld(player.world) - ?: throw CommandException("You must be in a parcel world to use that command") -} - -fun ParcelProvider.getParcelRequired(player: Player, privilege: Privilege? = null): Parcel { - val parcel = getWorldRequired(player, admin = privilege == ADMIN).getParcelAt(player) - ?: throw CommandException("You must be in a parcel to use that command") - - if (!player.hasPermAdminManage) { - @Suppress("NON_EXHAUSTIVE_WHEN") - when (privilege) { - OWNER -> - Validate.isTrue(parcel.isOwner(player.uuid), "You must own this parcel to use that command") - CAN_MANAGE -> - Validate.isTrue(parcel.canManage(player), "You must have management privileges on this parcel to use that command") - } - } - - return parcel -} - +package io.dico.parcels2.command + +import io.dico.dicore.command.CommandException +import io.dico.dicore.command.ExecutionContext +import io.dico.dicore.command.registration.reflect.ICommandReceiver +import io.dico.dicore.command.Validate +import io.dico.parcels2.* +import io.dico.parcels2.Privilege.* +import io.dico.parcels2.util.ext.hasPermAdminManage +import io.dico.parcels2.util.ext.uuid +import org.bukkit.entity.Player +import java.lang.reflect.Method +import kotlin.reflect.full.extensionReceiverParameter +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.jvm.jvmErasure +import kotlin.reflect.jvm.kotlinFunction + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class RequireParcelPrivilege(val privilege: Privilege) + +/* +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class SuspensionTimeout(val millis: Int) +*/ + +open class WorldScope(val world: ParcelWorld) : ICommandReceiver +open class ParcelScope(val parcel: Parcel) : WorldScope(parcel.world) { + fun checkCanManage(player: Player, action: String) = Validate.isTrue(parcel.canManage(player), "You must own this parcel to $action") +} + +fun getParcelCommandReceiver(parcelProvider: ParcelProvider, context: ExecutionContext, method: Method, cmdName: String): ICommandReceiver { + val player = context.sender as Player + val function = method.kotlinFunction!! + val receiverType = function.extensionReceiverParameter!!.type + val require = function.findAnnotation() + + return when (receiverType.jvmErasure) { + ParcelScope::class -> ParcelScope(parcelProvider.getParcelRequired(player, require?.privilege)) + WorldScope::class -> WorldScope(parcelProvider.getWorldRequired(player, require?.privilege == ADMIN)) + else -> throw InternalError("Invalid command receiver type") + } +} + +fun ParcelProvider.getWorldRequired(player: Player, admin: Boolean = false): ParcelWorld { + if (admin) Validate.isTrue(player.hasPermAdminManage, "You must have admin rights to use that command") + return getWorld(player.world) + ?: throw CommandException("You must be in a parcel world to use that command") +} + +fun ParcelProvider.getParcelRequired(player: Player, privilege: Privilege? = null): Parcel { + val parcel = getWorldRequired(player, admin = privilege == ADMIN).getParcelAt(player) + ?: throw CommandException("You must be in a parcel to use that command") + + if (!player.hasPermAdminManage) { + @Suppress("NON_EXHAUSTIVE_WHEN") + when (privilege) { + OWNER -> + Validate.isTrue(parcel.isOwner(player.uuid), "You must own this parcel to use that command") + CAN_MANAGE -> + Validate.isTrue(parcel.canManage(player), "You must have management privileges on this parcel to use that command") + } + } + + return parcel +} + diff --git a/src/main/kotlin/io/dico/parcels2/command/ParcelOptionsInteractCommand.kt b/src/main/kotlin/io/dico/parcels2/command/ParcelOptionsInteractCommand.kt index 23087dc..fbe4333 100644 --- a/src/main/kotlin/io/dico/parcels2/command/ParcelOptionsInteractCommand.kt +++ b/src/main/kotlin/io/dico/parcels2/command/ParcelOptionsInteractCommand.kt @@ -1,56 +1,56 @@ -package io.dico.parcels2.command - -import io.dico.dicore.command.* -import io.dico.dicore.command.parameter.type.ParameterTypes -import io.dico.parcels2.Interactables -import io.dico.parcels2.ParcelsPlugin -import io.dico.parcels2.Privilege -import org.bukkit.command.CommandSender -import org.bukkit.entity.Player - -class ParcelOptionsInteractCommand(val plugin: ParcelsPlugin) : Command() { - - init { - setShortDescription("View and/or change the setting") - setDescription(shortDescription) - addContextFilter(IContextFilter.PLAYER_ONLY) - addContextFilter(IContextFilter.INHERIT_PERMISSIONS) - addParameter("allowed", "new setting", ParameterTypes.BOOLEAN) - requiredParameters(0) - } - - override fun execute(sender: CommandSender, context: ExecutionContext): String? { - if (!plugin.storage.isConnected) err("Parcels cannot have their options changed right now because of a database error") - - val interactableClass = Interactables[context.address.mainKey] - val allowed: Boolean? = context.get("allowed") - - val parcel = plugin.parcelProvider.getParcelRequired(sender as Player, - if (allowed == null) Privilege.DEFAULT else Privilege.CAN_MANAGE) - - if (allowed == null) { - val setting = parcel.interactableConfig.isInteractable(interactableClass) - val default = setting == interactableClass.interactableByDefault - - val canColor = context.address.chatHandler.getChatFormatForType(EMessageType.BAD_NEWS) - val cannotColor = context.address.chatHandler.getChatFormatForType(EMessageType.GOOD_NEWS) - val resetColor = context.address.chatHandler.getChatFormatForType(EMessageType.RESULT) - - val settingString = (if (setting) "${canColor}can" else "${cannotColor}cannot") + resetColor - val defaultString = if (default) " (default)" else "" - - return "Players $settingString interact with ${interactableClass.name} on this parcel$defaultString" - } - - val change = parcel.interactableConfig.setInteractable(interactableClass, allowed) - - val interactableClassName = interactableClass.name - return when { - allowed && change -> "Other players can now interact with $interactableClassName" - allowed && !change -> err("Other players could already interact with $interactableClassName") - change -> "Other players can not interact with $interactableClassName anymore" - else -> err("Other players were not allowed to interact with $interactableClassName") - } - } - -} +package io.dico.parcels2.command + +import io.dico.dicore.command.* +import io.dico.dicore.command.parameter.type.ParameterTypes +import io.dico.parcels2.Interactables +import io.dico.parcels2.ParcelsPlugin +import io.dico.parcels2.Privilege +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + +class ParcelOptionsInteractCommand(val plugin: ParcelsPlugin) : Command() { + + init { + setShortDescription("View and/or change the setting") + setDescription(shortDescription) + addContextFilter(IContextFilter.PLAYER_ONLY) + addContextFilter(IContextFilter.INHERIT_PERMISSIONS) + addParameter("allowed", "new setting", ParameterTypes.BOOLEAN) + requiredParameters(0) + } + + override fun execute(sender: CommandSender, context: ExecutionContext): String? { + if (!plugin.storage.isConnected) err("Parcels cannot have their options changed right now because of a database error") + + val interactableClass = Interactables[context.address.mainKey] + val allowed: Boolean? = context.get("allowed") + + val parcel = plugin.parcelProvider.getParcelRequired(sender as Player, + if (allowed == null) Privilege.DEFAULT else Privilege.CAN_MANAGE) + + if (allowed == null) { + val setting = parcel.interactableConfig.isInteractable(interactableClass) + val default = setting == interactableClass.interactableByDefault + + val canColor = context.address.chatHandler.getChatFormatForType(EMessageType.BAD_NEWS) + val cannotColor = context.address.chatHandler.getChatFormatForType(EMessageType.GOOD_NEWS) + val resetColor = context.address.chatHandler.getChatFormatForType(EMessageType.RESULT) + + val settingString = (if (setting) "${canColor}can" else "${cannotColor}cannot") + resetColor + val defaultString = if (default) " (default)" else "" + + return "Players $settingString interact with ${interactableClass.name} on this parcel$defaultString" + } + + val change = parcel.interactableConfig.setInteractable(interactableClass, allowed) + + val interactableClassName = interactableClass.name + return when { + allowed && change -> "Other players can now interact with $interactableClassName" + allowed && !change -> err("Other players could already interact with $interactableClassName") + change -> "Other players can not interact with $interactableClassName anymore" + else -> err("Other players were not allowed to interact with $interactableClassName") + } + } + +} diff --git a/src/main/kotlin/io/dico/parcels2/command/ParcelParameterTypes.kt b/src/main/kotlin/io/dico/parcels2/command/ParcelParameterTypes.kt index c7083a1..730625e 100644 --- a/src/main/kotlin/io/dico/parcels2/command/ParcelParameterTypes.kt +++ b/src/main/kotlin/io/dico/parcels2/command/ParcelParameterTypes.kt @@ -1,83 +1,83 @@ -package io.dico.parcels2.command - -import io.dico.dicore.command.CommandException -import io.dico.dicore.command.parameter.ArgumentBuffer -import io.dico.dicore.command.parameter.Parameter -import io.dico.dicore.command.parameter.type.ParameterConfig -import io.dico.dicore.command.parameter.type.ParameterType -import io.dico.parcels2.* -import io.dico.parcels2.command.ProfileKind.Companion.ANY -import io.dico.parcels2.command.ProfileKind.Companion.FAKE -import io.dico.parcels2.command.ProfileKind.Companion.REAL -import org.bukkit.Location -import org.bukkit.command.CommandSender -import org.bukkit.entity.Player - -fun invalidInput(parameter: Parameter<*, *>, message: String): Nothing { - throw CommandException("invalid input for ${parameter.name}: $message") -} - -fun ParcelProvider.getTargetWorld(input: String?, sender: CommandSender, parameter: Parameter<*, *>): ParcelWorld { - val worldName = input - ?.takeUnless { it.isEmpty() } - ?: (sender as? Player)?.world?.name - ?: invalidInput(parameter, "console cannot omit the world name") - - return getWorld(worldName) - ?: invalidInput(parameter, "$worldName is not a parcel world") -} - -class ParcelParameterType(val parcelProvider: ParcelProvider) : ParameterType(Parcel::class.java) { - val regex = Regex.fromLiteral("((.+)->)?([0-9]+):([0-9]+)") - - override fun parse(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): Parcel { - val matchResult = regex.matchEntire(buffer.next()!!) - ?: invalidInput(parameter, "must match (w->)?a:b (/${regex.pattern}/)") - - val world = parcelProvider.getTargetWorld(matchResult.groupValues[2], sender, parameter) - - val x = matchResult.groupValues[3].toIntOrNull() - ?: invalidInput(parameter, "couldn't parse int") - - val z = matchResult.groupValues[4].toIntOrNull() - ?: invalidInput(parameter, "couldn't parse int") - - return world.getParcelById(x, z) - ?: invalidInput(parameter, "parcel id is out of range") - } - -} - -annotation class ProfileKind(val kind: Int) { - companion object : ParameterConfig(ProfileKind::class.java) { - const val REAL = 1 - const val FAKE = 2 - const val ANY = REAL or FAKE - - override fun toParameterInfo(annotation: ProfileKind): Int { - return annotation.kind - } - } -} - -class ProfileParameterType : ParameterType(PlayerProfile::class.java, ProfileKind) { - - override fun parse(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): PlayerProfile { - val info = parameter.paramInfo ?: REAL - val allowReal = (info and REAL) != 0 - val allowFake = (info and FAKE) != 0 - - val input = buffer.next()!! - return PlayerProfile.byName(input, allowReal, allowFake) - } - - override fun complete( - parameter: Parameter, - sender: CommandSender, - location: Location?, - buffer: ArgumentBuffer - ): MutableList { - logger.info("Completing PlayerProfile: ${buffer.next()}") - return super.complete(parameter, sender, location, buffer) - } -} +package io.dico.parcels2.command + +import io.dico.dicore.command.CommandException +import io.dico.dicore.command.parameter.ArgumentBuffer +import io.dico.dicore.command.parameter.Parameter +import io.dico.dicore.command.parameter.type.ParameterConfig +import io.dico.dicore.command.parameter.type.ParameterType +import io.dico.parcels2.* +import io.dico.parcels2.command.ProfileKind.Companion.ANY +import io.dico.parcels2.command.ProfileKind.Companion.FAKE +import io.dico.parcels2.command.ProfileKind.Companion.REAL +import org.bukkit.Location +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + +fun invalidInput(parameter: Parameter<*, *>, message: String): Nothing { + throw CommandException("invalid input for ${parameter.name}: $message") +} + +fun ParcelProvider.getTargetWorld(input: String?, sender: CommandSender, parameter: Parameter<*, *>): ParcelWorld { + val worldName = input + ?.takeUnless { it.isEmpty() } + ?: (sender as? Player)?.world?.name + ?: invalidInput(parameter, "console cannot omit the world name") + + return getWorld(worldName) + ?: invalidInput(parameter, "$worldName is not a parcel world") +} + +class ParcelParameterType(val parcelProvider: ParcelProvider) : ParameterType(Parcel::class.java) { + val regex = Regex.fromLiteral("((.+)->)?([0-9]+):([0-9]+)") + + override fun parse(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): Parcel { + val matchResult = regex.matchEntire(buffer.next()!!) + ?: invalidInput(parameter, "must match (w->)?a:b (/${regex.pattern}/)") + + val world = parcelProvider.getTargetWorld(matchResult.groupValues[2], sender, parameter) + + val x = matchResult.groupValues[3].toIntOrNull() + ?: invalidInput(parameter, "couldn't parse int") + + val z = matchResult.groupValues[4].toIntOrNull() + ?: invalidInput(parameter, "couldn't parse int") + + return world.getParcelById(x, z) + ?: invalidInput(parameter, "parcel id is out of range") + } + +} + +annotation class ProfileKind(val kind: Int) { + companion object : ParameterConfig(ProfileKind::class.java) { + const val REAL = 1 + const val FAKE = 2 + const val ANY = REAL or FAKE + + override fun toParameterInfo(annotation: ProfileKind): Int { + return annotation.kind + } + } +} + +class ProfileParameterType : ParameterType(PlayerProfile::class.java, ProfileKind) { + + override fun parse(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): PlayerProfile { + val info = parameter.paramInfo ?: REAL + val allowReal = (info and REAL) != 0 + val allowFake = (info and FAKE) != 0 + + val input = buffer.next()!! + return PlayerProfile.byName(input, allowReal, allowFake) + } + + override fun complete( + parameter: Parameter, + sender: CommandSender, + location: Location?, + buffer: ArgumentBuffer + ): MutableList { + logger.info("Completing PlayerProfile: ${buffer.next()}") + return super.complete(parameter, sender, location, buffer) + } +} diff --git a/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt b/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt index 8891912..c39c4b6 100644 --- a/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt +++ b/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt @@ -1,191 +1,191 @@ -package io.dico.parcels2.command - -import io.dico.dicore.command.parameter.ArgumentBuffer -import io.dico.dicore.command.parameter.Parameter -import io.dico.dicore.command.parameter.type.ParameterConfig -import io.dico.dicore.command.parameter.type.ParameterType -import io.dico.parcels2.Parcel -import io.dico.parcels2.ParcelProvider -import io.dico.parcels2.ParcelWorld -import io.dico.parcels2.PlayerProfile -import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.DEFAULT_KIND -import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.ID -import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.OWNER -import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.OWNER_FAKE -import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.OWNER_REAL -import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.PREFER_OWNED_FOR_DEFAULT -import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.REAL -import io.dico.parcels2.storage.Storage -import io.dico.parcels2.util.math.Vec2i -import io.dico.parcels2.util.math.floor -import org.bukkit.command.CommandSender -import org.bukkit.entity.Player - -sealed class ParcelTarget(val world: ParcelWorld, val parsedKind: Int, val isDefault: Boolean) { - - abstract suspend fun getParcelSuspend(storage: Storage): Parcel? - - class ByID(world: ParcelWorld, val id: Vec2i?, parsedKind: Int, isDefault: Boolean) : ParcelTarget(world, parsedKind, isDefault) { - override suspend fun getParcelSuspend(storage: Storage): Parcel? = getParcel() - fun getParcel() = id?.let { world.getParcelById(it) } - val isPath: Boolean get() = id == null - } - - class ByOwner( - world: ParcelWorld, - owner: PlayerProfile, - val index: Int, - parsedKind: Int, - isDefault: Boolean, - val onResolveFailure: (() -> Unit)? = null - ) : ParcelTarget(world, parsedKind, isDefault) { - init { - if (index < 0) throw IllegalArgumentException("Invalid parcel home index: $index") - } - - var owner = owner; private set - - suspend fun resolveOwner(storage: Storage): Boolean { - val owner = owner - if (owner is PlayerProfile.Unresolved) { - this.owner = owner.tryResolveSuspendedly(storage) ?: if (parsedKind and OWNER_FAKE != 0) PlayerProfile.Fake(owner.name) - else run { onResolveFailure?.invoke(); return false } - } - return true - } - - override suspend fun getParcelSuspend(storage: Storage): Parcel? { - onResolveFailure?.let { resolveOwner(storage) } - - val ownedParcelsSerialized = storage.getOwnedParcels(owner).await() - val ownedParcels = ownedParcelsSerialized - .filter { it.worldId.equals(world.id) } - .map { world.getParcelById(it.x, it.z) } - - return ownedParcels.getOrNull(index) - } - } - - annotation class TargetKind(val kind: Int) { - companion object : ParameterConfig(TargetKind::class.java) { - const val ID = 1 // ID - const val OWNER_REAL = 2 // an owner backed by a UUID - const val OWNER_FAKE = 4 // an owner not backed by a UUID - - const val OWNER = OWNER_REAL or OWNER_FAKE // any owner - const val ANY = ID or OWNER_REAL or OWNER_FAKE // any - const val REAL = ID or OWNER_REAL // no owner not backed by a UUID - - const val DEFAULT_KIND = REAL - - const val PREFER_OWNED_FOR_DEFAULT = 8 // if the kind can be ID and OWNER_REAL, prefer OWNER_REAL for default - // instead of parcel that the player is in - - override fun toParameterInfo(annotation: TargetKind): Int { - return annotation.kind - } - } - } - - class PType(val parcelProvider: ParcelProvider, val parcelAddress: SpecialCommandAddress? = null) : - ParameterType(ParcelTarget::class.java, TargetKind) { - - override fun parse(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): ParcelTarget { - var input = buffer.next()!! - val worldString = input.substringBefore("/", missingDelimiterValue = "") - input = input.substringAfter("/") - - val world = if (worldString.isEmpty()) { - val player = requirePlayer(sender, parameter, "the world") - parcelProvider.getWorld(player.world) - ?: invalidInput(parameter, "You cannot omit the world if you're not in a parcel world") - } else { - parcelProvider.getWorld(worldString) ?: invalidInput(parameter, "$worldString is not a parcel world") - } - - val kind = parameter.paramInfo ?: DEFAULT_KIND - if (input.contains(',')) { - if (kind and ID == 0) invalidInput(parameter, "You must specify a parcel by OWNER, that is, an owner and index") - return ByID(world, getId(parameter, input), kind, false) - } - - 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, kind, sender, input) - return ByOwner(world, owner, index, kind, false, onResolveFailure = { invalidInput(parameter, "The player $input does not exist") }) - } - - private fun getId(parameter: Parameter<*, *>, input: String): Vec2i { - val x = input.substringBefore(',').run { - toIntOrNull() ?: invalidInput(parameter, "ID(x) must be an integer, $this is not an integer") - } - val z = input.substringAfter(',').run { - toIntOrNull() ?: invalidInput(parameter, "ID(z) must be an integer, $this is not an integer") - } - return Vec2i(x, z) - } - - private fun getHomeIndex(parameter: Parameter<*, *>, kind: Int, sender: CommandSender, input: String): Pair { - val splitIdx = input.indexOf(':') - val ownerString: String - val index: Int? - - val speciallyParsedIndex = parcelAddress?.speciallyParsedIndex - - if (splitIdx == -1) { - - if (speciallyParsedIndex == null) { - // just the index. - index = input.toIntOrNull() - ownerString = if (index == null) input else "" - } else { - // just the owner. - index = speciallyParsedIndex - ownerString = input - } - - } else { - if (speciallyParsedIndex != null) { - invalidInput(parameter, "Duplicate home index") - } - - ownerString = input.substring(0, splitIdx) - - val indexString = input.substring(splitIdx + 1) - index = indexString.toIntOrNull() - ?: invalidInput(parameter, "The home index must be an integer, $indexString is not an integer") - } - - val owner = if (ownerString.isEmpty()) - PlayerProfile(requirePlayer(sender, parameter, "the player")) - else - PlayerProfile.byName(ownerString, allowReal = kind and OWNER_REAL != 0, allowFake = kind and OWNER_FAKE != 0) - - return owner to (index ?: 0) - } - - private fun requirePlayer(sender: CommandSender, parameter: Parameter<*, *>, objName: String): Player { - if (sender !is Player) invalidInput(parameter, "console cannot omit the $objName") - return sender - } - - override fun getDefaultValue(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): ParcelTarget? { - val kind = parameter.paramInfo ?: DEFAULT_KIND - val useLocation = when { - kind and REAL == REAL -> kind and PREFER_OWNED_FOR_DEFAULT == 0 - kind and ID != 0 -> true - kind and OWNER_REAL != 0 -> false - else -> return null - } - - val player = requirePlayer(sender, parameter, "the parcel") - val world = parcelProvider.getWorld(player.world) ?: invalidInput(parameter, "You must be in a parcel world to omit the parcel") - if (useLocation) { - val id = player.location.let { world.getParcelIdAt(it.x.floor(), it.z.floor())?.pos } - return ByID(world, id, kind, true) - } - - return ByOwner(world, PlayerProfile(player), parcelAddress?.speciallyParsedIndex ?: 0, kind, true) - } - } - -} +package io.dico.parcels2.command + +import io.dico.dicore.command.parameter.ArgumentBuffer +import io.dico.dicore.command.parameter.Parameter +import io.dico.dicore.command.parameter.type.ParameterConfig +import io.dico.dicore.command.parameter.type.ParameterType +import io.dico.parcels2.Parcel +import io.dico.parcels2.ParcelProvider +import io.dico.parcels2.ParcelWorld +import io.dico.parcels2.PlayerProfile +import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.DEFAULT_KIND +import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.ID +import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.OWNER +import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.OWNER_FAKE +import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.OWNER_REAL +import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.PREFER_OWNED_FOR_DEFAULT +import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.REAL +import io.dico.parcels2.storage.Storage +import io.dico.parcels2.util.math.Vec2i +import io.dico.parcels2.util.math.floor +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + +sealed class ParcelTarget(val world: ParcelWorld, val parsedKind: Int, val isDefault: Boolean) { + + abstract suspend fun getParcelSuspend(storage: Storage): Parcel? + + class ByID(world: ParcelWorld, val id: Vec2i?, parsedKind: Int, isDefault: Boolean) : ParcelTarget(world, parsedKind, isDefault) { + override suspend fun getParcelSuspend(storage: Storage): Parcel? = getParcel() + fun getParcel() = id?.let { world.getParcelById(it) } + val isPath: Boolean get() = id == null + } + + class ByOwner( + world: ParcelWorld, + owner: PlayerProfile, + val index: Int, + parsedKind: Int, + isDefault: Boolean, + val onResolveFailure: (() -> Unit)? = null + ) : ParcelTarget(world, parsedKind, isDefault) { + init { + if (index < 0) throw IllegalArgumentException("Invalid parcel home index: $index") + } + + var owner = owner; private set + + suspend fun resolveOwner(storage: Storage): Boolean { + val owner = owner + if (owner is PlayerProfile.Unresolved) { + this.owner = owner.tryResolveSuspendedly(storage) ?: if (parsedKind and OWNER_FAKE != 0) PlayerProfile.Fake(owner.name) + else run { onResolveFailure?.invoke(); return false } + } + return true + } + + override suspend fun getParcelSuspend(storage: Storage): Parcel? { + onResolveFailure?.let { resolveOwner(storage) } + + val ownedParcelsSerialized = storage.getOwnedParcels(owner).await() + val ownedParcels = ownedParcelsSerialized + .filter { it.worldId.equals(world.id) } + .map { world.getParcelById(it.x, it.z) } + + return ownedParcels.getOrNull(index) + } + } + + annotation class TargetKind(val kind: Int) { + companion object : ParameterConfig(TargetKind::class.java) { + const val ID = 1 // ID + const val OWNER_REAL = 2 // an owner backed by a UUID + const val OWNER_FAKE = 4 // an owner not backed by a UUID + + const val OWNER = OWNER_REAL or OWNER_FAKE // any owner + const val ANY = ID or OWNER_REAL or OWNER_FAKE // any + const val REAL = ID or OWNER_REAL // no owner not backed by a UUID + + const val DEFAULT_KIND = REAL + + const val PREFER_OWNED_FOR_DEFAULT = 8 // if the kind can be ID and OWNER_REAL, prefer OWNER_REAL for default + // instead of parcel that the player is in + + override fun toParameterInfo(annotation: TargetKind): Int { + return annotation.kind + } + } + } + + class PType(val parcelProvider: ParcelProvider, val parcelAddress: SpecialCommandAddress? = null) : + ParameterType(ParcelTarget::class.java, TargetKind) { + + override fun parse(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): ParcelTarget { + var input = buffer.next()!! + val worldString = input.substringBefore("/", missingDelimiterValue = "") + input = input.substringAfter("/") + + val world = if (worldString.isEmpty()) { + val player = requirePlayer(sender, parameter, "the world") + parcelProvider.getWorld(player.world) + ?: invalidInput(parameter, "You cannot omit the world if you're not in a parcel world") + } else { + parcelProvider.getWorld(worldString) ?: invalidInput(parameter, "$worldString is not a parcel world") + } + + val kind = parameter.paramInfo ?: DEFAULT_KIND + if (input.contains(',')) { + if (kind and ID == 0) invalidInput(parameter, "You must specify a parcel by OWNER, that is, an owner and index") + return ByID(world, getId(parameter, input), kind, false) + } + + 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, kind, sender, input) + return ByOwner(world, owner, index, kind, false, onResolveFailure = { invalidInput(parameter, "The player $input does not exist") }) + } + + private fun getId(parameter: Parameter<*, *>, input: String): Vec2i { + val x = input.substringBefore(',').run { + toIntOrNull() ?: invalidInput(parameter, "ID(x) must be an integer, $this is not an integer") + } + val z = input.substringAfter(',').run { + toIntOrNull() ?: invalidInput(parameter, "ID(z) must be an integer, $this is not an integer") + } + return Vec2i(x, z) + } + + private fun getHomeIndex(parameter: Parameter<*, *>, kind: Int, sender: CommandSender, input: String): Pair { + val splitIdx = input.indexOf(':') + val ownerString: String + val index: Int? + + val speciallyParsedIndex = parcelAddress?.speciallyParsedIndex + + if (splitIdx == -1) { + + if (speciallyParsedIndex == null) { + // just the index. + index = input.toIntOrNull() + ownerString = if (index == null) input else "" + } else { + // just the owner. + index = speciallyParsedIndex + ownerString = input + } + + } else { + if (speciallyParsedIndex != null) { + invalidInput(parameter, "Duplicate home index") + } + + ownerString = input.substring(0, splitIdx) + + val indexString = input.substring(splitIdx + 1) + index = indexString.toIntOrNull() + ?: invalidInput(parameter, "The home index must be an integer, $indexString is not an integer") + } + + val owner = if (ownerString.isEmpty()) + PlayerProfile(requirePlayer(sender, parameter, "the player")) + else + PlayerProfile.byName(ownerString, allowReal = kind and OWNER_REAL != 0, allowFake = kind and OWNER_FAKE != 0) + + return owner to (index ?: 0) + } + + private fun requirePlayer(sender: CommandSender, parameter: Parameter<*, *>, objName: String): Player { + if (sender !is Player) invalidInput(parameter, "console cannot omit the $objName") + return sender + } + + override fun getDefaultValue(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): ParcelTarget? { + val kind = parameter.paramInfo ?: DEFAULT_KIND + val useLocation = when { + kind and REAL == REAL -> kind and PREFER_OWNED_FOR_DEFAULT == 0 + kind and ID != 0 -> true + kind and OWNER_REAL != 0 -> false + else -> return null + } + + val player = requirePlayer(sender, parameter, "the parcel") + val world = parcelProvider.getWorld(player.world) ?: invalidInput(parameter, "You must be in a parcel world to omit the parcel") + if (useLocation) { + val id = player.location.let { world.getParcelIdAt(it.x.floor(), it.z.floor())?.pos } + return ByID(world, id, kind, true) + } + + return ByOwner(world, PlayerProfile(player), parcelAddress?.speciallyParsedIndex ?: 0, kind, true) + } + } + +} diff --git a/src/main/kotlin/io/dico/parcels2/command/ParcelsChatHandler.kt b/src/main/kotlin/io/dico/parcels2/command/ParcelsChatHandler.kt index b616f77..52f1104 100644 --- a/src/main/kotlin/io/dico/parcels2/command/ParcelsChatHandler.kt +++ b/src/main/kotlin/io/dico/parcels2/command/ParcelsChatHandler.kt @@ -1,24 +1,24 @@ -package io.dico.parcels2.command - -import io.dico.dicore.Formatting -import io.dico.dicore.command.EMessageType -import io.dico.dicore.command.ExecutionContext -import io.dico.dicore.command.chat.AbstractChatHandler -import io.dico.parcels2.util.ext.plus - -class ParcelsChatHandler : AbstractChatHandler() { - - override fun getMessagePrefixForType(type: EMessageType?): String { - return Formatting.RED + "[Parcels] " - } - - override fun createMessage(context: ExecutionContext, type: EMessageType, message: String?): String? { - if (message.isNullOrEmpty()) return null - var result = getChatFormatForType(type) + message - if (context.address.mainKey != "info") { - result = getMessagePrefixForType(type) + result - } - return result - } - +package io.dico.parcels2.command + +import io.dico.dicore.Formatting +import io.dico.dicore.command.EMessageType +import io.dico.dicore.command.ExecutionContext +import io.dico.dicore.command.chat.AbstractChatHandler +import io.dico.parcels2.util.ext.plus + +class ParcelsChatHandler : AbstractChatHandler() { + + override fun getMessagePrefixForType(type: EMessageType?): String { + return Formatting.RED + "[Parcels] " + } + + override fun createMessage(context: ExecutionContext, type: EMessageType, message: String?): String? { + if (message.isNullOrEmpty()) return null + var result = getChatFormatForType(type) + message + if (context.address.mainKey != "info") { + result = getMessagePrefixForType(type) + result + } + return result + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelContainer.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelContainer.kt index 1193af3..b49cad4 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelContainer.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelContainer.kt @@ -1,73 +1,73 @@ -package io.dico.parcels2.defaultimpl - -import io.dico.parcels2.Parcel -import io.dico.parcels2.ParcelContainer -import io.dico.parcels2.ParcelId -import io.dico.parcels2.ParcelWorld - -class DefaultParcelContainer(val world: ParcelWorld) : ParcelContainer { - private var parcels: Array> - - init { - parcels = initArray(world.options.axisLimit, world) - } - - fun resizeIfSizeChanged() { - if (parcels.size != world.options.axisLimit * 2 + 1) { - resize(world.options.axisLimit) - } - } - - fun resize(axisLimit: Int) { - parcels = initArray(axisLimit, world, this) - } - - fun initArray(axisLimit: Int, world: ParcelWorld, cur: DefaultParcelContainer? = null): Array> { - val arraySize = 2 * axisLimit + 1 - return Array(arraySize) { - val x = it - axisLimit - Array(arraySize) { - val z = it - axisLimit - cur?.getParcelById(x, z) ?: ParcelImpl(world, x, z) - } - } - } - - override fun getParcelById(x: Int, z: Int): Parcel? { - return parcels.getOrNull(x + world.options.axisLimit)?.getOrNull(z + world.options.axisLimit) - } - - override fun getParcelById(id: ParcelId): Parcel? { - if (!world.id.equals(id.worldId)) throw IllegalArgumentException() - return when (id) { - is Parcel -> id - else -> getParcelById(id.x, id.z) - } - } - - override fun nextEmptyParcel(): Parcel? { - return walkInCircle().find { it.owner == null } - } - - private fun walkInCircle(): Iterable = Iterable { - iterator { - val center = world.options.axisLimit - yield(parcels[center][center]) - for (radius in 0..center) { - var x = center - radius; - var z = center - radius - repeat(radius * 2) { yield(parcels[x++][z]) } - repeat(radius * 2) { yield(parcels[x][z++]) } - repeat(radius * 2) { yield(parcels[x--][z]) } - repeat(radius * 2) { yield(parcels[x][z--]) } - } - } - } - - fun getAllParcels(): Iterator = iterator { - for (array in parcels) { - yieldAll(array.iterator()) - } - } - +package io.dico.parcels2.defaultimpl + +import io.dico.parcels2.Parcel +import io.dico.parcels2.ParcelContainer +import io.dico.parcels2.ParcelId +import io.dico.parcels2.ParcelWorld + +class DefaultParcelContainer(val world: ParcelWorld) : ParcelContainer { + private var parcels: Array> + + init { + parcels = initArray(world.options.axisLimit, world) + } + + fun resizeIfSizeChanged() { + if (parcels.size != world.options.axisLimit * 2 + 1) { + resize(world.options.axisLimit) + } + } + + fun resize(axisLimit: Int) { + parcels = initArray(axisLimit, world, this) + } + + fun initArray(axisLimit: Int, world: ParcelWorld, cur: DefaultParcelContainer? = null): Array> { + val arraySize = 2 * axisLimit + 1 + return Array(arraySize) { + val x = it - axisLimit + Array(arraySize) { + val z = it - axisLimit + cur?.getParcelById(x, z) ?: ParcelImpl(world, x, z) + } + } + } + + override fun getParcelById(x: Int, z: Int): Parcel? { + return parcels.getOrNull(x + world.options.axisLimit)?.getOrNull(z + world.options.axisLimit) + } + + override fun getParcelById(id: ParcelId): Parcel? { + if (!world.id.equals(id.worldId)) throw IllegalArgumentException() + return when (id) { + is Parcel -> id + else -> getParcelById(id.x, id.z) + } + } + + override fun nextEmptyParcel(): Parcel? { + return walkInCircle().find { it.owner == null } + } + + private fun walkInCircle(): Iterable = Iterable { + iterator { + val center = world.options.axisLimit + yield(parcels[center][center]) + for (radius in 0..center) { + var x = center - radius; + var z = center - radius + repeat(radius * 2) { yield(parcels[x++][z]) } + repeat(radius * 2) { yield(parcels[x][z++]) } + repeat(radius * 2) { yield(parcels[x--][z]) } + repeat(radius * 2) { yield(parcels[x][z--]) } + } + } + } + + fun getAllParcels(): Iterator = iterator { + for (array in parcels) { + yieldAll(array.iterator()) + } + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt index 9e43c05..caa3f1f 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt @@ -1,378 +1,378 @@ -package io.dico.parcels2.defaultimpl - -import io.dico.parcels2.* -import io.dico.parcels2.blockvisitor.RegionTraverser -import io.dico.parcels2.options.DefaultGeneratorOptions -import io.dico.parcels2.util.math.* -import kotlinx.coroutines.CoroutineScope -import org.bukkit.* -import org.bukkit.block.Biome -import org.bukkit.block.BlockFace -import org.bukkit.block.Skull -import org.bukkit.block.data.type.Slab -import org.bukkit.block.data.type.WallSign -import java.util.Random - -private val airType = Bukkit.createBlockData(Material.AIR) - -private const val chunkSize = 16 - -class DefaultParcelGenerator( - override val worldName: String, - private val o: DefaultGeneratorOptions -) : ParcelGenerator() { - private var _world: World? = null - override val world: World - get() { - if (_world == null) { - val world = Bukkit.getWorld(worldName) - maxHeight = world.maxHeight - _world = world - return world - } - return _world!! - } - - private var maxHeight = 0 - 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 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?) { - // do nothing - } - - 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 makeParcelLocatorAndBlockManager( - parcelProvider: ParcelProvider, - container: ParcelContainer, - coroutineScope: CoroutineScope, - jobDispatcher: JobDispatcher - ): Pair { - val impl = ParcelLocatorAndBlockManagerImpl(parcelProvider, container, coroutineScope, jobDispatcher) - return impl to impl - } - - private inline fun convertBlockLocationToId(x: Int, z: Int, mapper: (Int, Int) -> T): T? { - 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 (modX in 0 until parcelSize && modZ in 0 until parcelSize) { - return mapper((absX - modX) / sectionSize + 1, (absZ - modZ) / sectionSize + 1) - } - return null - } - - @Suppress("DEPRECATION") - private inner class ParcelLocatorAndBlockManagerImpl( - val parcelProvider: ParcelProvider, - val container: ParcelContainer, - coroutineScope: CoroutineScope, - override val jobDispatcher: JobDispatcher - ) : ParcelBlockManagerBase(), ParcelLocator, CoroutineScope by coroutineScope { - - override val world: World get() = this@DefaultParcelGenerator.world - val worldId = parcelProvider.getWorld(world)?.id ?: ParcelWorldId(world) - override val parcelTraverser: RegionTraverser = RegionTraverser.convergingTo(o.floorHeight) - - private val cornerWallType = when { - o.wallType is Slab -> (o.wallType.clone() as Slab).apply { type = Slab.Type.DOUBLE } - o.wallType.material.name.endsWith("CARPET") -> { - Bukkit.createBlockData(Material.getMaterial(o.wallType.material.name.substringBefore("CARPET") + "WOOL")) - } - else -> null - } - - override fun getParcelAt(x: Int, z: Int): Parcel? { - return convertBlockLocationToId(x, z, container::getParcelById) - } - - override fun getParcelIdAt(x: Int, z: Int): ParcelId? { - return convertBlockLocationToId(x, z) { idx, idz -> ParcelId(worldId, idx, idz) } - } - - - private fun checkParcelId(parcel: ParcelId): ParcelId { - if (!parcel.worldId.equals(worldId)) { - throw IllegalArgumentException() - } - return parcel - } - - override fun getRegionOrigin(parcel: ParcelId): Vec2i { - checkParcelId(parcel) - return Vec2i( - sectionSize * (parcel.x - 1) + pathOffset + o.offsetX, - sectionSize * (parcel.z - 1) + pathOffset + o.offsetZ - ) - } - - override fun getRegion(parcel: ParcelId): Region { - val origin = getRegionOrigin(parcel) - return Region( - Vec3i(origin.x, 0, origin.z), - Vec3i(o.parcelSize, maxHeight, o.parcelSize) - ) - } - - override fun getHomeLocation(parcel: ParcelId): Location { - val origin = getRegionOrigin(parcel) - val x = origin.x + (o.parcelSize - 1) / 2.0 - val z = origin.z - 2 - return Location(world, x + 0.5, o.floorHeight + 1.0, z + 0.5, 0F, 0F) - } - - override fun getParcelForInfoBlockInteraction(block: Vec3i, type: Material, face: BlockFace): Parcel? { - if (block.y != o.floorHeight + 1) return null - - val expectedParcelOrigin = when (type) { - Material.WALL_SIGN -> Vec2i(block.x + 1, block.z + 2) - o.wallType.material, cornerWallType?.material -> { - if (face != BlockFace.NORTH || world[block + Vec3i.convert(BlockFace.NORTH)].type == Material.WALL_SIGN) { - return null - } - - Vec2i(block.x + 1, block.z + 1) - } - else -> return null - } - - return getParcelAt(expectedParcelOrigin.x, expectedParcelOrigin.z) - ?.takeIf { expectedParcelOrigin == getRegionOrigin(it.id) } - ?.also { parcel -> - if (type != Material.WALL_SIGN && parcel.owner != null) { - updateParcelInfo(parcel.id, parcel.owner) - parcel.isOwnerSignOutdated = false - } - } - } - - override fun isParcelInfoSectionLoaded(parcel: ParcelId): Boolean { - val wallBlockChunk = getRegionOrigin(parcel).add(-1, -1).toChunk() - return world.isChunkLoaded(wallBlockChunk.x, wallBlockChunk.z) - } - - override fun updateParcelInfo(parcel: ParcelId, owner: PlayerProfile?) { - val b = getRegionOrigin(parcel) - - val wallBlock = world.getBlockAt(b.x - 1, o.floorHeight + 1, b.z - 1) - val signBlock = world.getBlockAt(b.x - 1, o.floorHeight + 1, b.z - 2) - val skullBlock = world.getBlockAt(b.x - 1, o.floorHeight + 2, b.z - 1) - - if (owner == null) { - wallBlock.blockData = o.wallType - signBlock.type = Material.AIR - skullBlock.type = Material.AIR - - } else { - cornerWallType?.let { wallBlock.blockData = it } - signBlock.blockData = (Bukkit.createBlockData(Material.WALL_SIGN) as WallSign).apply { facing = BlockFace.NORTH } - - val sign = signBlock.state as org.bukkit.block.Sign - sign.setLine(0, "${parcel.x},${parcel.z}") - sign.setLine(2, owner.name ?: "") - sign.update() - - skullBlock.type = Material.AIR - skullBlock.type = Material.PLAYER_HEAD - val skull = skullBlock.state as Skull - if (owner is PlayerProfile.Real) { - skull.owningPlayer = Bukkit.getOfflinePlayer(owner.uuid) - - } else if (!skull.setOwner(owner.name)) { - skullBlock.type = Material.AIR - return - } - - skull.rotation = BlockFace.SOUTH - skull.update() - } - } - - private fun trySubmitBlockVisitor(vararg parcels: ParcelId, function: JobFunction): Job? { - parcels.forEach { checkParcelId(it) } - return parcelProvider.trySubmitBlockVisitor(Permit(), parcels, function) - } - - override fun setBiome(parcel: ParcelId, biome: Biome) = trySubmitBlockVisitor(checkParcelId(parcel)) { - val world = world - val b = getRegionOrigin(parcel) - val parcelSize = o.parcelSize - for (x in b.x until b.x + parcelSize) { - for (z in b.z until b.z + parcelSize) { - markSuspensionPoint() - world.setBiome(x, z, biome) - } - } - } - - override fun clearParcel(parcel: ParcelId) = trySubmitBlockVisitor(checkParcelId(parcel)) { - val region = getRegion(parcel) - val blocks = parcelTraverser.traverseRegion(region) - val blockCount = region.blockCount.toDouble() - val world = world - val floorHeight = o.floorHeight - val airType = airType - val floorType = o.floorType - val fillType = o.fillType - - for ((index, vec) in blocks.withIndex()) { - markSuspensionPoint() - val y = vec.y - val blockType = when { - y > floorHeight -> airType - y == floorHeight -> floorType - else -> fillType - } - world[vec].blockData = blockType - setProgress((index + 1) / blockCount) - } - } - - override fun getParcelsWithOwnerBlockIn(chunk: Chunk): Collection { - /* - * Get the offsets for the world out of the way - * to simplify the calculation that follows. - */ - - val x = chunk.x.shl(4) - (o.offsetX + pathOffset) - val z = chunk.z.shl(4) - (o.offsetZ + pathOffset) - - /* Locations of wall corners (where owner blocks are placed) are defined as: - * - * x umod sectionSize == sectionSize-1 - * - * This check needs to be made for all 16 slices of the chunk in 2 dimensions - * How to optimize this? - * Let's take the expression - * - * x umod sectionSize - * - * And call it modX - * x can be shifted (chunkSize -1) times to attempt to get a modX of 0. - * This means that if the modX is 1, and sectionSize == (chunkSize-1), there would be a match at the last shift. - * To check that there are any matches, we can see if the following holds: - * - * modX >= ((sectionSize-1) - (chunkSize-1)) - * - * Which can be simplified to: - * modX >= sectionSize - chunkSize - * - * if sectionSize == chunkSize, this expression can be simplified to - * modX >= 0 - * which is always true. This is expected. - * To get the total number of matches on a dimension, we can evaluate the following: - * - * (modX - (sectionSize - chunkSize) + sectionSize) / sectionSize - * - * We add sectionSize to the lhs because, if the other part of the lhs is 0, we need at least 1. - * This can be simplified to: - * - * (modX + chunkSize) / sectionSize - */ - - val sectionSize = sectionSize - - val modX = x umod sectionSize - val matchesOnDimensionX = (modX + chunkSize) / sectionSize - if (matchesOnDimensionX <= 0) return emptyList() - - val modZ = z umod sectionSize - val matchesOnDimensionZ = (modZ + chunkSize) / sectionSize - if (matchesOnDimensionZ <= 0) return emptyList() - - /* - * Now we need to find the first id within the matches, - * and then return the subsequent matches in a rectangle following it. - * - * On each dimension, get the distance to the first match, which is equal to (sectionSize-1 - modX) - * and add it to the coordinate value - */ - val firstX = x + (sectionSize - 1 - modX) - val firstZ = z + (sectionSize - 1 - modZ) - - val firstIdX = (firstX + 1) / sectionSize + 1 - val firstIdZ = (firstZ + 1) / sectionSize + 1 - - if (matchesOnDimensionX == 1 && matchesOnDimensionZ == 1) { - // fast-path optimization - return listOf(Vec2i(firstIdX, firstIdZ)) - } - - return (0 until matchesOnDimensionX).flatMap { idOffsetX -> - (0 until matchesOnDimensionZ).map { idOffsetZ -> Vec2i(firstIdX + idOffsetX, firstIdZ + idOffsetZ) } - } - } - - } - +package io.dico.parcels2.defaultimpl + +import io.dico.parcels2.* +import io.dico.parcels2.blockvisitor.RegionTraverser +import io.dico.parcels2.options.DefaultGeneratorOptions +import io.dico.parcels2.util.math.* +import kotlinx.coroutines.CoroutineScope +import org.bukkit.* +import org.bukkit.block.Biome +import org.bukkit.block.BlockFace +import org.bukkit.block.Skull +import org.bukkit.block.data.type.Slab +import org.bukkit.block.data.type.WallSign +import java.util.Random + +private val airType = Bukkit.createBlockData(Material.AIR) + +private const val chunkSize = 16 + +class DefaultParcelGenerator( + override val worldName: String, + private val o: DefaultGeneratorOptions +) : ParcelGenerator() { + private var _world: World? = null + override val world: World + get() { + if (_world == null) { + val world = Bukkit.getWorld(worldName) + maxHeight = world.maxHeight + _world = world + return world + } + return _world!! + } + + private var maxHeight = 0 + 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 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?) { + // do nothing + } + + 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 makeParcelLocatorAndBlockManager( + parcelProvider: ParcelProvider, + container: ParcelContainer, + coroutineScope: CoroutineScope, + jobDispatcher: JobDispatcher + ): Pair { + val impl = ParcelLocatorAndBlockManagerImpl(parcelProvider, container, coroutineScope, jobDispatcher) + return impl to impl + } + + private inline fun convertBlockLocationToId(x: Int, z: Int, mapper: (Int, Int) -> T): T? { + 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 (modX in 0 until parcelSize && modZ in 0 until parcelSize) { + return mapper((absX - modX) / sectionSize + 1, (absZ - modZ) / sectionSize + 1) + } + return null + } + + @Suppress("DEPRECATION") + private inner class ParcelLocatorAndBlockManagerImpl( + val parcelProvider: ParcelProvider, + val container: ParcelContainer, + coroutineScope: CoroutineScope, + override val jobDispatcher: JobDispatcher + ) : ParcelBlockManagerBase(), ParcelLocator, CoroutineScope by coroutineScope { + + override val world: World get() = this@DefaultParcelGenerator.world + val worldId = parcelProvider.getWorld(world)?.id ?: ParcelWorldId(world) + override val parcelTraverser: RegionTraverser = RegionTraverser.convergingTo(o.floorHeight) + + private val cornerWallType = when { + o.wallType is Slab -> (o.wallType.clone() as Slab).apply { type = Slab.Type.DOUBLE } + o.wallType.material.name.endsWith("CARPET") -> { + Bukkit.createBlockData(Material.getMaterial(o.wallType.material.name.substringBefore("CARPET") + "WOOL")) + } + else -> null + } + + override fun getParcelAt(x: Int, z: Int): Parcel? { + return convertBlockLocationToId(x, z, container::getParcelById) + } + + override fun getParcelIdAt(x: Int, z: Int): ParcelId? { + return convertBlockLocationToId(x, z) { idx, idz -> ParcelId(worldId, idx, idz) } + } + + + private fun checkParcelId(parcel: ParcelId): ParcelId { + if (!parcel.worldId.equals(worldId)) { + throw IllegalArgumentException() + } + return parcel + } + + override fun getRegionOrigin(parcel: ParcelId): Vec2i { + checkParcelId(parcel) + return Vec2i( + sectionSize * (parcel.x - 1) + pathOffset + o.offsetX, + sectionSize * (parcel.z - 1) + pathOffset + o.offsetZ + ) + } + + override fun getRegion(parcel: ParcelId): Region { + val origin = getRegionOrigin(parcel) + return Region( + Vec3i(origin.x, 0, origin.z), + Vec3i(o.parcelSize, maxHeight, o.parcelSize) + ) + } + + override fun getHomeLocation(parcel: ParcelId): Location { + val origin = getRegionOrigin(parcel) + val x = origin.x + (o.parcelSize - 1) / 2.0 + val z = origin.z - 2 + return Location(world, x + 0.5, o.floorHeight + 1.0, z + 0.5, 0F, 0F) + } + + override fun getParcelForInfoBlockInteraction(block: Vec3i, type: Material, face: BlockFace): Parcel? { + if (block.y != o.floorHeight + 1) return null + + val expectedParcelOrigin = when (type) { + Material.WALL_SIGN -> Vec2i(block.x + 1, block.z + 2) + o.wallType.material, cornerWallType?.material -> { + if (face != BlockFace.NORTH || world[block + Vec3i.convert(BlockFace.NORTH)].type == Material.WALL_SIGN) { + return null + } + + Vec2i(block.x + 1, block.z + 1) + } + else -> return null + } + + return getParcelAt(expectedParcelOrigin.x, expectedParcelOrigin.z) + ?.takeIf { expectedParcelOrigin == getRegionOrigin(it.id) } + ?.also { parcel -> + if (type != Material.WALL_SIGN && parcel.owner != null) { + updateParcelInfo(parcel.id, parcel.owner) + parcel.isOwnerSignOutdated = false + } + } + } + + override fun isParcelInfoSectionLoaded(parcel: ParcelId): Boolean { + val wallBlockChunk = getRegionOrigin(parcel).add(-1, -1).toChunk() + return world.isChunkLoaded(wallBlockChunk.x, wallBlockChunk.z) + } + + override fun updateParcelInfo(parcel: ParcelId, owner: PlayerProfile?) { + val b = getRegionOrigin(parcel) + + val wallBlock = world.getBlockAt(b.x - 1, o.floorHeight + 1, b.z - 1) + val signBlock = world.getBlockAt(b.x - 1, o.floorHeight + 1, b.z - 2) + val skullBlock = world.getBlockAt(b.x - 1, o.floorHeight + 2, b.z - 1) + + if (owner == null) { + wallBlock.blockData = o.wallType + signBlock.type = Material.AIR + skullBlock.type = Material.AIR + + } else { + cornerWallType?.let { wallBlock.blockData = it } + signBlock.blockData = (Bukkit.createBlockData(Material.WALL_SIGN) as WallSign).apply { facing = BlockFace.NORTH } + + val sign = signBlock.state as org.bukkit.block.Sign + sign.setLine(0, "${parcel.x},${parcel.z}") + sign.setLine(2, owner.name ?: "") + sign.update() + + skullBlock.type = Material.AIR + skullBlock.type = Material.PLAYER_HEAD + val skull = skullBlock.state as Skull + if (owner is PlayerProfile.Real) { + skull.owningPlayer = Bukkit.getOfflinePlayer(owner.uuid) + + } else if (!skull.setOwner(owner.name)) { + skullBlock.type = Material.AIR + return + } + + skull.rotation = BlockFace.SOUTH + skull.update() + } + } + + private fun trySubmitBlockVisitor(vararg parcels: ParcelId, function: JobFunction): Job? { + parcels.forEach { checkParcelId(it) } + return parcelProvider.trySubmitBlockVisitor(Permit(), parcels, function) + } + + override fun setBiome(parcel: ParcelId, biome: Biome) = trySubmitBlockVisitor(checkParcelId(parcel)) { + val world = world + val b = getRegionOrigin(parcel) + val parcelSize = o.parcelSize + for (x in b.x until b.x + parcelSize) { + for (z in b.z until b.z + parcelSize) { + markSuspensionPoint() + world.setBiome(x, z, biome) + } + } + } + + override fun clearParcel(parcel: ParcelId) = trySubmitBlockVisitor(checkParcelId(parcel)) { + val region = getRegion(parcel) + val blocks = parcelTraverser.traverseRegion(region) + val blockCount = region.blockCount.toDouble() + val world = world + val floorHeight = o.floorHeight + val airType = airType + val floorType = o.floorType + val fillType = o.fillType + + for ((index, vec) in blocks.withIndex()) { + markSuspensionPoint() + val y = vec.y + val blockType = when { + y > floorHeight -> airType + y == floorHeight -> floorType + else -> fillType + } + world[vec].blockData = blockType + setProgress((index + 1) / blockCount) + } + } + + override fun getParcelsWithOwnerBlockIn(chunk: Chunk): Collection { + /* + * Get the offsets for the world out of the way + * to simplify the calculation that follows. + */ + + val x = chunk.x.shl(4) - (o.offsetX + pathOffset) + val z = chunk.z.shl(4) - (o.offsetZ + pathOffset) + + /* Locations of wall corners (where owner blocks are placed) are defined as: + * + * x umod sectionSize == sectionSize-1 + * + * This check needs to be made for all 16 slices of the chunk in 2 dimensions + * How to optimize this? + * Let's take the expression + * + * x umod sectionSize + * + * And call it modX + * x can be shifted (chunkSize -1) times to attempt to get a modX of 0. + * This means that if the modX is 1, and sectionSize == (chunkSize-1), there would be a match at the last shift. + * To check that there are any matches, we can see if the following holds: + * + * modX >= ((sectionSize-1) - (chunkSize-1)) + * + * Which can be simplified to: + * modX >= sectionSize - chunkSize + * + * if sectionSize == chunkSize, this expression can be simplified to + * modX >= 0 + * which is always true. This is expected. + * To get the total number of matches on a dimension, we can evaluate the following: + * + * (modX - (sectionSize - chunkSize) + sectionSize) / sectionSize + * + * We add sectionSize to the lhs because, if the other part of the lhs is 0, we need at least 1. + * This can be simplified to: + * + * (modX + chunkSize) / sectionSize + */ + + val sectionSize = sectionSize + + val modX = x umod sectionSize + val matchesOnDimensionX = (modX + chunkSize) / sectionSize + if (matchesOnDimensionX <= 0) return emptyList() + + val modZ = z umod sectionSize + val matchesOnDimensionZ = (modZ + chunkSize) / sectionSize + if (matchesOnDimensionZ <= 0) return emptyList() + + /* + * Now we need to find the first id within the matches, + * and then return the subsequent matches in a rectangle following it. + * + * On each dimension, get the distance to the first match, which is equal to (sectionSize-1 - modX) + * and add it to the coordinate value + */ + val firstX = x + (sectionSize - 1 - modX) + val firstZ = z + (sectionSize - 1 - modZ) + + val firstIdX = (firstX + 1) / sectionSize + 1 + val firstIdZ = (firstZ + 1) / sectionSize + 1 + + if (matchesOnDimensionX == 1 && matchesOnDimensionZ == 1) { + // fast-path optimization + return listOf(Vec2i(firstIdX, firstIdZ)) + } + + return (0 until matchesOnDimensionX).flatMap { idOffsetX -> + (0 until matchesOnDimensionZ).map { idOffsetZ -> Vec2i(firstIdX + idOffsetX, firstIdZ + idOffsetZ) } + } + } + + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/GlobalPrivilegesManagerImpl.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/GlobalPrivilegesManagerImpl.kt index 769cee6..670dd94 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/GlobalPrivilegesManagerImpl.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/GlobalPrivilegesManagerImpl.kt @@ -1,28 +1,28 @@ -@file:Suppress("UNCHECKED_CAST") - -package io.dico.parcels2.defaultimpl - -import io.dico.parcels2.* -import io.dico.parcels2.util.ext.alsoIfTrue -import java.util.Collections - -class GlobalPrivilegesManagerImpl(val plugin: ParcelsPlugin) : GlobalPrivilegesManager { - private val map = mutableMapOf() - - override fun get(owner: PlayerProfile.Real): GlobalPrivileges { - return map[owner] ?: GlobalPrivilegesImpl(owner).also { map[owner] = it } - } - - private inner class GlobalPrivilegesImpl(override val keyOfOwner: PlayerProfile.Real) : PrivilegesHolder(), GlobalPrivileges { - override var privilegeOfStar: Privilege - get() = super.privilegeOfStar - set(value) = run { super.privilegeOfStar = value } - - override fun setRawStoredPrivilege(key: PrivilegeKey, privilege: Privilege): Boolean { - return super.setRawStoredPrivilege(key, privilege).alsoIfTrue { - plugin.storage.setGlobalPrivilege(keyOfOwner, key, privilege) - } - } - } - +@file:Suppress("UNCHECKED_CAST") + +package io.dico.parcels2.defaultimpl + +import io.dico.parcels2.* +import io.dico.parcels2.util.ext.alsoIfTrue +import java.util.Collections + +class GlobalPrivilegesManagerImpl(val plugin: ParcelsPlugin) : GlobalPrivilegesManager { + private val map = mutableMapOf() + + override fun get(owner: PlayerProfile.Real): GlobalPrivileges { + return map[owner] ?: GlobalPrivilegesImpl(owner).also { map[owner] = it } + } + + private inner class GlobalPrivilegesImpl(override val keyOfOwner: PlayerProfile.Real) : PrivilegesHolder(), GlobalPrivileges { + override var privilegeOfStar: Privilege + get() = super.privilegeOfStar + set(value) = run { super.privilegeOfStar = value } + + override fun setRawStoredPrivilege(key: PrivilegeKey, privilege: Privilege): Boolean { + return super.setRawStoredPrivilege(key, privilege).alsoIfTrue { + plugin.storage.setGlobalPrivilege(keyOfOwner, key, privilege) + } + } + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/InfoBuilder.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/InfoBuilder.kt index a3704c7..99df82a 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/InfoBuilder.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/InfoBuilder.kt @@ -1,88 +1,88 @@ -package io.dico.parcels2.defaultimpl - -import io.dico.dicore.Formatting -import io.dico.parcels2.Privilege -import io.dico.parcels2.PrivilegeKey -import io.dico.parcels2.RawPrivileges -import io.dico.parcels2.filterProfilesWithPrivilegeTo - -object InfoBuilder { - val infoStringColor1 = Formatting.GREEN - val infoStringColor2 = Formatting.AQUA - - inline fun StringBuilder.appendField(field: StringBuilder.() -> Unit, value: StringBuilder.() -> Unit) { - append(infoStringColor1) - field() - append(": ") - append(infoStringColor2) - value() - append(' ') - } - - inline fun StringBuilder.appendField(name: String, value: StringBuilder.() -> Unit) { - appendField({ append(name) }, value) - } - - inline fun StringBuilder.appendFieldWithCount(name: String, count: Int, value: StringBuilder.() -> Unit) { - appendField({ - append(name) - append('(') - append(infoStringColor2) - append(count) - append(infoStringColor1) - append(')') - }, value) - } - - fun StringBuilder.appendProfilesWithPrivilege(fieldName: String, local: RawPrivileges, global: RawPrivileges?, privilege: Privilege) { - val map = linkedMapOf() - local.filterProfilesWithPrivilegeTo(map, privilege) - val localCount = map.size - global?.filterProfilesWithPrivilegeTo(map, privilege) - appendPrivilegeProfiles(fieldName, map, localCount) - } - - fun StringBuilder.appendPrivilegeProfiles(fieldName: String, map: LinkedHashMap, localCount: Int) { - if (map.isEmpty()) return - - appendFieldWithCount(fieldName, map.size) { - // first [localCount] entries are local - val separator = "$infoStringColor1, $infoStringColor2" - val iterator = map.iterator() - - if (localCount != 0) { - appendPrivilegeEntry(false, iterator.next().toPair()) - repeat(localCount - 1) { - append(separator) - appendPrivilegeEntry(false, iterator.next().toPair()) - } - - } else if (iterator.hasNext()) { - // ensure there is never a leading or trailing separator - appendPrivilegeEntry(true, iterator.next().toPair()) - } - - iterator.forEach { next -> - append(separator) - appendPrivilegeEntry(true, next.toPair()) - } - } - } - - fun StringBuilder.appendPrivilegeEntry(global: Boolean, pair: Pair) { - val (key, priv) = pair - - append(key.notNullName) - - // suffix. Maybe T should be M for mod or something. T means they have CAN_MANAGE privilege. - append( - when { - global && priv == Privilege.CAN_MANAGE -> " (G) (T)" - global -> " (G)" - priv == Privilege.CAN_MANAGE -> " (T)" - else -> "" - } - ) - } - +package io.dico.parcels2.defaultimpl + +import io.dico.dicore.Formatting +import io.dico.parcels2.Privilege +import io.dico.parcels2.PrivilegeKey +import io.dico.parcels2.RawPrivileges +import io.dico.parcels2.filterProfilesWithPrivilegeTo + +object InfoBuilder { + val infoStringColor1 = Formatting.GREEN + val infoStringColor2 = Formatting.AQUA + + inline fun StringBuilder.appendField(field: StringBuilder.() -> Unit, value: StringBuilder.() -> Unit) { + append(infoStringColor1) + field() + append(": ") + append(infoStringColor2) + value() + append(' ') + } + + inline fun StringBuilder.appendField(name: String, value: StringBuilder.() -> Unit) { + appendField({ append(name) }, value) + } + + inline fun StringBuilder.appendFieldWithCount(name: String, count: Int, value: StringBuilder.() -> Unit) { + appendField({ + append(name) + append('(') + append(infoStringColor2) + append(count) + append(infoStringColor1) + append(')') + }, value) + } + + fun StringBuilder.appendProfilesWithPrivilege(fieldName: String, local: RawPrivileges, global: RawPrivileges?, privilege: Privilege) { + val map = linkedMapOf() + local.filterProfilesWithPrivilegeTo(map, privilege) + val localCount = map.size + global?.filterProfilesWithPrivilegeTo(map, privilege) + appendPrivilegeProfiles(fieldName, map, localCount) + } + + fun StringBuilder.appendPrivilegeProfiles(fieldName: String, map: LinkedHashMap, localCount: Int) { + if (map.isEmpty()) return + + appendFieldWithCount(fieldName, map.size) { + // first [localCount] entries are local + val separator = "$infoStringColor1, $infoStringColor2" + val iterator = map.iterator() + + if (localCount != 0) { + appendPrivilegeEntry(false, iterator.next().toPair()) + repeat(localCount - 1) { + append(separator) + appendPrivilegeEntry(false, iterator.next().toPair()) + } + + } else if (iterator.hasNext()) { + // ensure there is never a leading or trailing separator + appendPrivilegeEntry(true, iterator.next().toPair()) + } + + iterator.forEach { next -> + append(separator) + appendPrivilegeEntry(true, next.toPair()) + } + } + } + + fun StringBuilder.appendPrivilegeEntry(global: Boolean, pair: Pair) { + val (key, priv) = pair + + append(key.notNullName) + + // suffix. Maybe T should be M for mod or something. T means they have CAN_MANAGE privilege. + append( + when { + global && priv == Privilege.CAN_MANAGE -> " (G) (T)" + global -> " (G)" + priv == Privilege.CAN_MANAGE -> " (T)" + else -> "" + } + ) + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt index 48a7fee..da004d6 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt @@ -1,223 +1,223 @@ -package io.dico.parcels2.defaultimpl - -import io.dico.parcels2.* -import io.dico.parcels2.blockvisitor.Schematic -import io.dico.parcels2.util.schedule -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import org.bukkit.Bukkit -import org.bukkit.WorldCreator -import org.joda.time.DateTime - -class ParcelProviderImpl(val plugin: ParcelsPlugin) : ParcelProvider { - inline val options get() = plugin.options - override val worlds: Map get() = _worlds - private val _worlds: MutableMap = hashMapOf() - private val _generators: MutableMap = hashMapOf() - private var _worldsLoaded = false - private var _dataIsLoaded = false - - // disabled while !_dataIsLoaded. getParcelById() will work though for data loading. - override fun getWorld(name: String): ParcelWorld? = _worlds[name]?.takeIf { _dataIsLoaded } - - override fun getWorldById(id: ParcelWorldId): ParcelWorld? { - if (id is ParcelWorld) return id - return _worlds[id.name] ?: id.bukkitWorld?.let { getWorld(it) } - } - - override fun getParcelById(id: ParcelId): Parcel? { - if (id is Parcel) return id - return getWorldById(id.worldId)?.container?.getParcelById(id.x, id.z) - } - - override fun getWorldGenerator(worldName: String): ParcelGenerator? { - return _worlds[worldName]?.generator - ?: _generators[worldName] - ?: options.worlds[worldName]?.generator?.newInstance(worldName)?.also { _generators[worldName] = it } - } - - override fun loadWorlds() { - if (_worldsLoaded) throw IllegalStateException() - _worldsLoaded = true - loadWorlds0() - } - - private fun loadWorlds0() { - if (Bukkit.getWorlds().isEmpty()) { - plugin.schedule(::loadWorlds0) - plugin.logger.warning("Scheduling to load worlds in the next tick because no bukkit worlds are loaded yet") - return - } - - val newlyCreatedWorlds = mutableListOf() - for ((worldName, worldOptions) in options.worlds.entries) { - var parcelWorld = _worlds[worldName] - if (parcelWorld != null) continue - - val generator: ParcelGenerator = getWorldGenerator(worldName)!! - val worldExists = Bukkit.getWorld(worldName) != null - val bukkitWorld = - if (worldExists) Bukkit.getWorld(worldName)!! - else { - logger.info("Creating world $worldName") - WorldCreator(worldName).generator(generator).createWorld() - } - - parcelWorld = ParcelWorldImpl(plugin, bukkitWorld, generator, worldOptions.runtime,::DefaultParcelContainer) - - if (!worldExists) { - val time = DateTime.now() - plugin.storage.setWorldCreationTime(parcelWorld.id, time) - parcelWorld.creationTime = time - newlyCreatedWorlds.add(parcelWorld) - } else { - GlobalScope.launch(context = Dispatchers.Unconfined) { - parcelWorld.creationTime = plugin.storage.getWorldCreationTime(parcelWorld.id).await() ?: DateTime.now() - } - } - - _worlds[worldName] = parcelWorld - } - - loadStoredData(newlyCreatedWorlds.toSet()) - } - - private fun loadStoredData(newlyCreatedWorlds: Collection = emptyList()) { - plugin.launch(Dispatchers.Default) { - 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 job1 = launch { - val channel = plugin.storage.transmitAllParcelData() - while (true) { - val (id, data) = channel.receiveOrNull() ?: break - val parcel = getParcelById(id) ?: continue - data?.let { parcel.copyData(it, callerIsDatabase = true) } - } - } - - val channel2 = plugin.storage.transmitAllGlobalPrivileges() - while (true) { - val (profile, data) = channel2.receiveOrNull() ?: break - if (profile !is PrivilegeKey) { - logger.error("Received profile that is not a privilege key: ${profile.javaClass}, $profile") - continue - } - (plugin.globalPrivileges[profile] as PrivilegesHolder).copyPrivilegesFrom(data) - } - - job1.join() - - logger.info("Loading data completed") - _dataIsLoaded = true - } - } - - override fun acquireBlockVisitorPermit(parcelId: ParcelId, with: Permit): Boolean { - val parcel = getParcelById(parcelId) as? ParcelImpl ?: return true - return parcel.acquireBlockVisitorPermit(with) - } - - override fun releaseBlockVisitorPermit(parcelId: ParcelId, with: Permit) { - val parcel = getParcelById(parcelId) as? ParcelImpl ?: return - parcel.releaseBlockVisitorPermit(with) - } - - override fun trySubmitBlockVisitor(permit: Permit, vararg parcelIds: ParcelId, function: JobFunction): Job? { - val withPermit = parcelIds.filter { acquireBlockVisitorPermit(it, permit) } - if (withPermit.size != parcelIds.size) { - withPermit.forEach { releaseBlockVisitorPermit(it, permit) } - return null - } - - val job = plugin.jobDispatcher.dispatch(function) - - plugin.launch { - job.awaitCompletion() - withPermit.forEach { releaseBlockVisitorPermit(it, permit) } - } - - return job - } - - override fun swapParcels(parcelId1: ParcelId, parcelId2: ParcelId): Job? { - val blockManager1 = getWorldById(parcelId1.worldId)?.blockManager ?: return null - val blockManager2 = getWorldById(parcelId2.worldId)?.blockManager ?: return null - - return trySubmitBlockVisitor(Permit(), parcelId1, parcelId2) { - var region1 = blockManager1.getRegion(parcelId1) - var region2 = blockManager2.getRegion(parcelId2) - - val size = region1.size.clampMax(region2.size) - if (size != region1.size) { - region1 = region1.withSize(size) - region2 = region2.withSize(size) - } - - val schematicOf1 = delegateWork(0.25) { Schematic().apply { load(blockManager1.world, region1) } } - val schematicOf2 = delegateWork(0.25) { Schematic().apply { load(blockManager2.world, region2) } } - delegateWork(0.25) { with(schematicOf1) { paste(blockManager2.world, region2.origin) } } - delegateWork(0.25) { with(schematicOf2) { paste(blockManager1.world, region1.origin) } } - } - } - - /* - fun loadWorlds(options: Options) { - for ((worldName, worldOptions) in options.worlds.entries) { - val world: ParcelWorld - try { - - world = ParcelWorldImpl( - worldName, - worldOptions, - worldOptions.generator.newGenerator(this, worldName), - plugin.storage, - plugin.globalPrivileges, - ::DefaultParcelContainer) - - } catch (ex: Exception) { - ex.printStackTrace() - continue - } - - _worlds[worldName] = world - } - - plugin.functionHelper.schedule(10) { - println("Parcels generating parcelProvider now") - for ((name, world) in _worlds) { - if (Bukkit.getWorld(name) == null) { - val bworld = WorldCreator(name).generator(world.generator).createWorld() - val spawn = world.generator.getFixedSpawnLocation(bworld, null) - bworld.setSpawnLocation(spawn.x.floor(), spawn.y.floor(), spawn.z.floor()) - } - } - - val channel = plugin.storage.transmitAllParcelData() - val job = plugin.functionHelper.launchLazilyOnMainThread { - do { - val pair = channel.receiveOrNull() ?: break - val parcel = getParcelById(pair.first) ?: continue - pair.second?.let { parcel.copyDataIgnoringDatabase(it) } - } while (true) - } - job.start() - } - - } - */ +package io.dico.parcels2.defaultimpl + +import io.dico.parcels2.* +import io.dico.parcels2.blockvisitor.Schematic +import io.dico.parcels2.util.schedule +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.bukkit.Bukkit +import org.bukkit.WorldCreator +import org.joda.time.DateTime + +class ParcelProviderImpl(val plugin: ParcelsPlugin) : ParcelProvider { + inline val options get() = plugin.options + override val worlds: Map get() = _worlds + private val _worlds: MutableMap = hashMapOf() + private val _generators: MutableMap = hashMapOf() + private var _worldsLoaded = false + private var _dataIsLoaded = false + + // disabled while !_dataIsLoaded. getParcelById() will work though for data loading. + override fun getWorld(name: String): ParcelWorld? = _worlds[name]?.takeIf { _dataIsLoaded } + + override fun getWorldById(id: ParcelWorldId): ParcelWorld? { + if (id is ParcelWorld) return id + return _worlds[id.name] ?: id.bukkitWorld?.let { getWorld(it) } + } + + override fun getParcelById(id: ParcelId): Parcel? { + if (id is Parcel) return id + return getWorldById(id.worldId)?.container?.getParcelById(id.x, id.z) + } + + override fun getWorldGenerator(worldName: String): ParcelGenerator? { + return _worlds[worldName]?.generator + ?: _generators[worldName] + ?: options.worlds[worldName]?.generator?.newInstance(worldName)?.also { _generators[worldName] = it } + } + + override fun loadWorlds() { + if (_worldsLoaded) throw IllegalStateException() + _worldsLoaded = true + loadWorlds0() + } + + private fun loadWorlds0() { + if (Bukkit.getWorlds().isEmpty()) { + plugin.schedule(::loadWorlds0) + plugin.logger.warning("Scheduling to load worlds in the next tick because no bukkit worlds are loaded yet") + return + } + + val newlyCreatedWorlds = mutableListOf() + for ((worldName, worldOptions) in options.worlds.entries) { + var parcelWorld = _worlds[worldName] + if (parcelWorld != null) continue + + val generator: ParcelGenerator = getWorldGenerator(worldName)!! + val worldExists = Bukkit.getWorld(worldName) != null + val bukkitWorld = + if (worldExists) Bukkit.getWorld(worldName)!! + else { + logger.info("Creating world $worldName") + WorldCreator(worldName).generator(generator).createWorld() + } + + parcelWorld = ParcelWorldImpl(plugin, bukkitWorld, generator, worldOptions.runtime,::DefaultParcelContainer) + + if (!worldExists) { + val time = DateTime.now() + plugin.storage.setWorldCreationTime(parcelWorld.id, time) + parcelWorld.creationTime = time + newlyCreatedWorlds.add(parcelWorld) + } else { + GlobalScope.launch(context = Dispatchers.Unconfined) { + parcelWorld.creationTime = plugin.storage.getWorldCreationTime(parcelWorld.id).await() ?: DateTime.now() + } + } + + _worlds[worldName] = parcelWorld + } + + loadStoredData(newlyCreatedWorlds.toSet()) + } + + private fun loadStoredData(newlyCreatedWorlds: Collection = emptyList()) { + plugin.launch(Dispatchers.Default) { + 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 job1 = launch { + val channel = plugin.storage.transmitAllParcelData() + while (true) { + val (id, data) = channel.receiveOrNull() ?: break + val parcel = getParcelById(id) ?: continue + data?.let { parcel.copyData(it, callerIsDatabase = true) } + } + } + + val channel2 = plugin.storage.transmitAllGlobalPrivileges() + while (true) { + val (profile, data) = channel2.receiveOrNull() ?: break + if (profile !is PrivilegeKey) { + logger.error("Received profile that is not a privilege key: ${profile.javaClass}, $profile") + continue + } + (plugin.globalPrivileges[profile] as PrivilegesHolder).copyPrivilegesFrom(data) + } + + job1.join() + + logger.info("Loading data completed") + _dataIsLoaded = true + } + } + + override fun acquireBlockVisitorPermit(parcelId: ParcelId, with: Permit): Boolean { + val parcel = getParcelById(parcelId) as? ParcelImpl ?: return true + return parcel.acquireBlockVisitorPermit(with) + } + + override fun releaseBlockVisitorPermit(parcelId: ParcelId, with: Permit) { + val parcel = getParcelById(parcelId) as? ParcelImpl ?: return + parcel.releaseBlockVisitorPermit(with) + } + + override fun trySubmitBlockVisitor(permit: Permit, vararg parcelIds: ParcelId, function: JobFunction): Job? { + val withPermit = parcelIds.filter { acquireBlockVisitorPermit(it, permit) } + if (withPermit.size != parcelIds.size) { + withPermit.forEach { releaseBlockVisitorPermit(it, permit) } + return null + } + + val job = plugin.jobDispatcher.dispatch(function) + + plugin.launch { + job.awaitCompletion() + withPermit.forEach { releaseBlockVisitorPermit(it, permit) } + } + + return job + } + + override fun swapParcels(parcelId1: ParcelId, parcelId2: ParcelId): Job? { + val blockManager1 = getWorldById(parcelId1.worldId)?.blockManager ?: return null + val blockManager2 = getWorldById(parcelId2.worldId)?.blockManager ?: return null + + return trySubmitBlockVisitor(Permit(), parcelId1, parcelId2) { + var region1 = blockManager1.getRegion(parcelId1) + var region2 = blockManager2.getRegion(parcelId2) + + val size = region1.size.clampMax(region2.size) + if (size != region1.size) { + region1 = region1.withSize(size) + region2 = region2.withSize(size) + } + + val schematicOf1 = delegateWork(0.25) { Schematic().apply { load(blockManager1.world, region1) } } + val schematicOf2 = delegateWork(0.25) { Schematic().apply { load(blockManager2.world, region2) } } + delegateWork(0.25) { with(schematicOf1) { paste(blockManager2.world, region2.origin) } } + delegateWork(0.25) { with(schematicOf2) { paste(blockManager1.world, region1.origin) } } + } + } + + /* + fun loadWorlds(options: Options) { + for ((worldName, worldOptions) in options.worlds.entries) { + val world: ParcelWorld + try { + + world = ParcelWorldImpl( + worldName, + worldOptions, + worldOptions.generator.newGenerator(this, worldName), + plugin.storage, + plugin.globalPrivileges, + ::DefaultParcelContainer) + + } catch (ex: Exception) { + ex.printStackTrace() + continue + } + + _worlds[worldName] = world + } + + plugin.functionHelper.schedule(10) { + println("Parcels generating parcelProvider now") + for ((name, world) in _worlds) { + if (Bukkit.getWorld(name) == null) { + val bworld = WorldCreator(name).generator(world.generator).createWorld() + val spawn = world.generator.getFixedSpawnLocation(bworld, null) + bworld.setSpawnLocation(spawn.x.floor(), spawn.y.floor(), spawn.z.floor()) + } + } + + val channel = plugin.storage.transmitAllParcelData() + val job = plugin.functionHelper.launchLazilyOnMainThread { + do { + val pair = channel.receiveOrNull() ?: break + val parcel = getParcelById(pair.first) ?: continue + pair.second?.let { parcel.copyDataIgnoringDatabase(it) } + } while (true) + } + job.start() + } + + } + */ } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelWorldImpl.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelWorldImpl.kt index 531a25f..6ce4f26 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelWorldImpl.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelWorldImpl.kt @@ -1,75 +1,75 @@ -@file:Suppress("CanBePrimaryConstructorProperty", "UsePropertyAccessSyntax") - -package io.dico.parcels2.defaultimpl - -import io.dico.parcels2.* -import io.dico.parcels2.options.RuntimeWorldOptions -import io.dico.parcels2.storage.Storage -import kotlinx.coroutines.CoroutineScope -import org.bukkit.GameRule -import org.bukkit.World -import org.joda.time.DateTime -import java.util.UUID - -class ParcelWorldImpl( - val plugin: ParcelsPlugin, - override val world: World, - override val generator: ParcelGenerator, - override var options: RuntimeWorldOptions, - containerFactory: ParcelContainerFactory -) : ParcelWorld, ParcelWorldId, ParcelContainer, ParcelLocator { - override val id: ParcelWorldId get() = this - override val uid: UUID? get() = world.uid - - override val storage get() = plugin.storage - override val globalPrivileges get() = plugin.globalPrivileges - - init { - if (generator.world != world) { - throw IllegalArgumentException() - } - } - - override val name: String = world.name!! - override val container: ParcelContainer = containerFactory(this) - override val locator: ParcelLocator - override val blockManager: ParcelBlockManager - - init { - val (locator, blockManager) = generator.makeParcelLocatorAndBlockManager(plugin.parcelProvider, container, plugin, plugin.jobDispatcher) - this.locator = locator - this.blockManager = blockManager - enforceOptions() - } - - fun enforceOptions() { - if (options.dayTime) { - world.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false) - world.setTime(6000) - } - - if (options.noWeather) { - world.setStorm(false) - world.setThundering(false) - world.weatherDuration = Int.MAX_VALUE - } - - world.setGameRule(GameRule.DO_TILE_DROPS, options.doTileDrops) - } - - // Accessed by ParcelProviderImpl - override var creationTime: DateTime? = null - - - override fun getParcelAt(x: Int, z: Int): Parcel? = locator.getParcelAt(x, z) - - override fun getParcelIdAt(x: Int, z: Int): ParcelId? = locator.getParcelIdAt(x, z) - - override fun getParcelById(x: Int, z: Int): Parcel? = container.getParcelById(x, z) - - override fun getParcelById(id: ParcelId): Parcel? = container.getParcelById(id) - - override fun nextEmptyParcel(): Parcel? = container.nextEmptyParcel() - - override fun toString() = parcelWorldIdToString() -} +@file:Suppress("CanBePrimaryConstructorProperty", "UsePropertyAccessSyntax") + +package io.dico.parcels2.defaultimpl + +import io.dico.parcels2.* +import io.dico.parcels2.options.RuntimeWorldOptions +import io.dico.parcels2.storage.Storage +import kotlinx.coroutines.CoroutineScope +import org.bukkit.GameRule +import org.bukkit.World +import org.joda.time.DateTime +import java.util.UUID + +class ParcelWorldImpl( + val plugin: ParcelsPlugin, + override val world: World, + override val generator: ParcelGenerator, + override var options: RuntimeWorldOptions, + containerFactory: ParcelContainerFactory +) : ParcelWorld, ParcelWorldId, ParcelContainer, ParcelLocator { + override val id: ParcelWorldId get() = this + override val uid: UUID? get() = world.uid + + override val storage get() = plugin.storage + override val globalPrivileges get() = plugin.globalPrivileges + + init { + if (generator.world != world) { + throw IllegalArgumentException() + } + } + + override val name: String = world.name!! + override val container: ParcelContainer = containerFactory(this) + override val locator: ParcelLocator + override val blockManager: ParcelBlockManager + + init { + val (locator, blockManager) = generator.makeParcelLocatorAndBlockManager(plugin.parcelProvider, container, plugin, plugin.jobDispatcher) + this.locator = locator + this.blockManager = blockManager + enforceOptions() + } + + fun enforceOptions() { + if (options.dayTime) { + world.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false) + world.setTime(6000) + } + + if (options.noWeather) { + world.setStorm(false) + world.setThundering(false) + world.weatherDuration = Int.MAX_VALUE + } + + world.setGameRule(GameRule.DO_TILE_DROPS, options.doTileDrops) + } + + // Accessed by ParcelProviderImpl + override var creationTime: DateTime? = null + + + override fun getParcelAt(x: Int, z: Int): Parcel? = locator.getParcelAt(x, z) + + override fun getParcelIdAt(x: Int, z: Int): ParcelId? = locator.getParcelIdAt(x, z) + + override fun getParcelById(x: Int, z: Int): Parcel? = container.getParcelById(x, z) + + override fun getParcelById(id: ParcelId): Parcel? = container.getParcelById(id) + + override fun nextEmptyParcel(): Parcel? = container.nextEmptyParcel() + + override fun toString() = parcelWorldIdToString() +} diff --git a/src/main/kotlin/io/dico/parcels2/listener/ParcelEntityTracker.kt b/src/main/kotlin/io/dico/parcels2/listener/ParcelEntityTracker.kt index 198e0e7..78b8b03 100644 --- a/src/main/kotlin/io/dico/parcels2/listener/ParcelEntityTracker.kt +++ b/src/main/kotlin/io/dico/parcels2/listener/ParcelEntityTracker.kt @@ -1,61 +1,61 @@ -package io.dico.parcels2.listener - -import io.dico.parcels2.Parcel -import io.dico.parcels2.ParcelProvider -import io.dico.parcels2.util.ext.editLoop -import org.bukkit.entity.Entity - -class ParcelEntityTracker(val parcelProvider: ParcelProvider) { - val map = mutableMapOf() - - fun untrack(entity: Entity) { - map.remove(entity) - } - - fun track(entity: Entity, parcel: Parcel?) { - map[entity] = parcel - } - - /* - * Tracks entities. If the entity is dead, they are removed from the collection. - * If the entity is found to have left the parcel it was created in, it will be removed from the world and from the list. - * If it is still in the parcel it was created in, and it is on the ground, it is removed from the list. - * - * Start after 5 seconds, run every 0.25 seconds - */ - fun tick() { - map.editLoop { entity, parcel -> - if (entity.isDead) { - remove(); return@editLoop - } - - if (parcel != null && parcel.hasBlockVisitors) { - remove() - - val newParcel = parcelProvider.getParcelAt(entity.location) - if (newParcel !== parcel && (newParcel == null || !newParcel.hasBlockVisitors)) { - entity.remove() - } - - return@editLoop - } - - val newParcel = parcelProvider.getParcelAt(entity.location) - if (newParcel !== parcel && (newParcel == null || !newParcel.hasBlockVisitors)) { - remove() - entity.remove() - } - } - } - - fun swapParcels(parcel1: Parcel, parcel2: Parcel) { - map.editLoop { -> - if (value === parcel1) { - value = parcel2 - } else if (value === parcel2) { - value = parcel1 - } - } - } - +package io.dico.parcels2.listener + +import io.dico.parcels2.Parcel +import io.dico.parcels2.ParcelProvider +import io.dico.parcels2.util.ext.editLoop +import org.bukkit.entity.Entity + +class ParcelEntityTracker(val parcelProvider: ParcelProvider) { + val map = mutableMapOf() + + fun untrack(entity: Entity) { + map.remove(entity) + } + + fun track(entity: Entity, parcel: Parcel?) { + map[entity] = parcel + } + + /* + * Tracks entities. If the entity is dead, they are removed from the collection. + * If the entity is found to have left the parcel it was created in, it will be removed from the world and from the list. + * If it is still in the parcel it was created in, and it is on the ground, it is removed from the list. + * + * Start after 5 seconds, run every 0.25 seconds + */ + fun tick() { + map.editLoop { entity, parcel -> + if (entity.isDead) { + remove(); return@editLoop + } + + if (parcel != null && parcel.hasBlockVisitors) { + remove() + + val newParcel = parcelProvider.getParcelAt(entity.location) + if (newParcel !== parcel && (newParcel == null || !newParcel.hasBlockVisitors)) { + entity.remove() + } + + return@editLoop + } + + val newParcel = parcelProvider.getParcelAt(entity.location) + if (newParcel !== parcel && (newParcel == null || !newParcel.hasBlockVisitors)) { + remove() + entity.remove() + } + } + } + + fun swapParcels(parcel1: Parcel, parcel2: Parcel) { + map.editLoop { -> + if (value === parcel1) { + value = parcel2 + } else if (value === parcel2) { + value = parcel1 + } + } + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt b/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt index 9c9bdc2..8bef7d1 100644 --- a/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt +++ b/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt @@ -1,661 +1,661 @@ -package io.dico.parcels2.listener - -import gnu.trove.TLongCollection -import gnu.trove.set.hash.TLongHashSet -import io.dico.dicore.Formatting -import io.dico.dicore.ListenerMarker -import io.dico.dicore.RegistratorListener -import io.dico.parcels2.* -import io.dico.parcels2.storage.Storage -import io.dico.parcels2.util.ext.* -import io.dico.parcels2.util.math.* -import org.bukkit.Location -import org.bukkit.Material.* -import org.bukkit.World -import org.bukkit.block.Biome -import org.bukkit.block.Block -import org.bukkit.block.data.Directional -import org.bukkit.block.data.type.Bed -import org.bukkit.entity.* -import org.bukkit.entity.minecart.ExplosiveMinecart -import org.bukkit.event.EventPriority -import org.bukkit.event.EventPriority.NORMAL -import org.bukkit.event.block.* -import org.bukkit.event.entity.* -import org.bukkit.event.hanging.HangingBreakByEntityEvent -import org.bukkit.event.hanging.HangingBreakEvent -import org.bukkit.event.hanging.HangingPlaceEvent -import org.bukkit.event.inventory.InventoryInteractEvent -import org.bukkit.event.player.* -import org.bukkit.event.vehicle.VehicleMoveEvent -import org.bukkit.event.weather.WeatherChangeEvent -import org.bukkit.event.world.ChunkLoadEvent -import org.bukkit.event.world.StructureGrowEvent -import org.bukkit.inventory.InventoryHolder -import java.util.EnumSet - -class ParcelListeners( - val parcelProvider: ParcelProvider, - val entityTracker: ParcelEntityTracker, - val storage: Storage -) { - private fun canBuildOnArea(user: Player, area: Parcel?) = - if (area == null) user.hasPermBuildAnywhere else area.canBuild(user) - - private fun canInteract(user: Player, area: Parcel?, interactClass: String) = - canBuildOnArea(user, area) || (area != null && area.interactableConfig(interactClass)) - - /** - * Get the world and parcel that the block resides in - * the parcel is nullable, and often named area because that means path. - * returns null if not in a registered parcel world - should always return in that case to not affect other worlds. - */ - private fun getWorldAndArea(block: Block): Pair? { - val world = parcelProvider.getWorld(block.world) ?: return null - return world to world.getParcelAt(block) - } - - - /* - * Prevents players from entering plots they are banned from - */ - @field:ListenerMarker(priority = NORMAL) - val onPlayerMoveEvent = RegistratorListener l@{ event -> - val user = event.player - if (user.hasPermBanBypass) return@l - val toLoc = event.to - val parcel = parcelProvider.getParcelAt(toLoc) ?: return@l - - if (!parcel.canEnterFast(user)) { - val region = parcel.world.blockManager.getRegion(parcel.id) - val dimension = region.getFirstUncontainedDimensionOf(Vec3i(event.from)) - - if (dimension == null) { - user.teleport(parcel.homeLocation) - user.sendParcelMessage(nopermit = true, message = "You are banned from this parcel") - - } else { - val speed = getPlayerSpeed(user) - val from = Vec3d(event.from) - val to = Vec3d(toLoc).with(dimension, from[dimension]) - - var newTo = to - dimension.otherDimensions.forEach { - val delta = to[it] - from[it] - newTo = newTo.add(it, delta * 100 * if (it == Dimension.Y) 0.5 else speed) - } - - event.to = Location( - toLoc.world, - newTo.x, newTo.y.clampMin(0.0).clampMax(255.0), newTo.z, - toLoc.yaw, toLoc.pitch - ) - } - } - } - - /* - * Prevents players from breaking blocks outside of their parcels - * Prevents containers from dropping their contents when broken, if configured - */ - @field:ListenerMarker(priority = NORMAL) - val onBlockBreakEvent = RegistratorListener l@{ event -> - val (world, area) = getWorldAndArea(event.block) ?: return@l - if (!canBuildOnArea(event.player, area)) { - event.isCancelled = true; return@l - } - - if (!world.options.dropEntityItems) { - val state = event.block.state - if (state is InventoryHolder) { - state.inventory.clear() - state.update() - } - } - } - - /* - * Prevents players from placing blocks outside of their parcels - */ - @field:ListenerMarker(priority = NORMAL) - val onBlockPlaceEvent = RegistratorListener l@{ event -> - val (_, area) = getWorldAndArea(event.block) ?: return@l - if (!canBuildOnArea(event.player, area)) { - event.isCancelled = true - } - - area?.updateOwnerSign() - } - - /* - * Control pistons - */ - @field:ListenerMarker(priority = NORMAL) - val onBlockPistonExtendEvent = RegistratorListener l@{ event -> - checkPistonMovement(event, event.blocks) - } - - @field:ListenerMarker(priority = NORMAL) - val onBlockPistonRetractEvent = RegistratorListener l@{ event -> - checkPistonMovement(event, event.blocks) - } - - // Doing some unnecessary optimizations here.. - //@formatter:off - private inline fun Column(x: Int, z: Int): Long = x.toLong() or (z.toLong().shl(32)) - - private inline val Long.columnX get() = and(0xFFFF_FFFFL).toInt() - private inline val Long.columnZ get() = ushr(32).and(0xFFFF_FFFFL).toInt() - private inline fun TLongCollection.troveForEach(block: (Long) -> Unit) = iterator().let { while (it.hasNext()) block(it.next()) } - //@formatter:on - private fun checkPistonMovement(event: BlockPistonEvent, blocks: List) { - val world = parcelProvider.getWorld(event.block.world) ?: return - val direction = event.direction - val columns = TLongHashSet(blocks.size * 2) - - blocks.forEach { - columns.add(Column(it.x, it.z)) - it.getRelative(direction).let { columns.add(Column(it.x, it.z)) } - } - - columns.troveForEach { - val area = world.getParcelAt(it.columnX, it.columnZ) - if (area == null || area.hasBlockVisitors) { - event.isCancelled = true - return - } - } - } - - /* - * Prevents explosions if enabled by the configs for that world - */ - @field:ListenerMarker(priority = NORMAL) - val onExplosionPrimeEvent = RegistratorListener l@{ event -> - val (world, area) = getWorldAndArea(event.entity.location.block) ?: return@l - if (area != null && area.hasBlockVisitors) { - event.radius = 0F; event.isCancelled = true - } else if (world.options.disableExplosions) { - event.radius = 0F - } - } - - /* - * Prevents creepers and tnt minecarts from exploding if explosions are disabled - */ - @field:ListenerMarker(priority = NORMAL) - val onEntityExplodeEvent = RegistratorListener l@{ event -> - entityTracker.untrack(event.entity) - val world = parcelProvider.getWorld(event.entity.world) ?: return@l - if (world.options.disableExplosions || world.getParcelAt(event.entity).let { it != null && it.hasBlockVisitors }) { - event.isCancelled = true - } - } - - /* - * Prevents liquids from flowing out of plots - */ - @field:ListenerMarker(priority = NORMAL) - val onBlockFromToEvent = RegistratorListener l@{ event -> - val (_, area) = getWorldAndArea(event.toBlock) ?: return@l - if (area == null || area.hasBlockVisitors) event.isCancelled = true - } - - private val bedTypes = EnumSet.copyOf(getMaterialsWithWoolColorPrefix("BED").toList()) - /* - * Prevents players from placing liquids, using flint and steel, changing redstone components, - * using inputs (unless allowed by the plot), - * and using items disabled in the configuration for that world. - * Prevents player from using beds in HELL or SKY biomes if explosions are disabled. - */ - @Suppress("NON_EXHAUSTIVE_WHEN") - @field:ListenerMarker(priority = NORMAL) - val onPlayerInteractEvent = RegistratorListener l@{ event -> - val user = event.player - val world = parcelProvider.getWorld(user.world) ?: return@l - val clickedBlock = event.clickedBlock - val parcel = clickedBlock?.let { world.getParcelAt(it) } - - if (!user.hasPermBuildAnywhere && parcel != null && !parcel.canEnter(user)) { - user.sendParcelMessage(nopermit = true, message = "You cannot interact with parcels you're banned from") - event.isCancelled = true; return@l - } - - when (event.action) { - Action.RIGHT_CLICK_BLOCK -> run { - val type = clickedBlock.type - - val interactableClass = Interactables[type] - if (interactableClass != null && !parcel.effectiveInteractableConfig.isInteractable(type) && (parcel == null || !parcel.canBuild(user))) { - user.sendParcelMessage(nopermit = true, message = "You cannot interact with ${interactableClass.name} here") - event.isCancelled = true - return@l - } - - if (bedTypes.contains(type)) { - val bed = clickedBlock.blockData as Bed - val head = if (bed.part == Bed.Part.FOOT) clickedBlock.getRelative(bed.facing) else clickedBlock - when (head.biome) { - Biome.NETHER, Biome.THE_END -> { - if (world.options.disableExplosions) { - user.sendParcelMessage(nopermit = true, message = "You cannot use this bed because it would explode") - event.isCancelled = true; return@l - } - } - } - - if (!canBuildOnArea(user, parcel)) { - user.sendParcelMessage(nopermit = true, message = "You may not sleep here") - event.isCancelled = true; return@l - } - } - - onPlayerRightClick(event, world, parcel) - - if (!event.isCancelled && parcel == null) { - world.blockManager.getParcelForInfoBlockInteraction(Vec3i(clickedBlock), type, event.blockFace) - ?.apply { user.sendMessage(Formatting.GREEN + infoString) } - } - } - - Action.RIGHT_CLICK_AIR -> onPlayerRightClick(event, world, parcel) - Action.PHYSICAL -> if (!canBuildOnArea(user, parcel) && !(parcel != null && parcel.interactableConfig("pressure_plates"))) { - user.sendParcelMessage(nopermit = true, message = "You cannot use inputs in this parcel") - event.isCancelled = true; return@l - } - } - } - - // private val blockPlaceInteractItems = EnumSet.of(LAVA_BUCKET, WATER_BUCKET, BUCKET, FLINT_AND_STEEL) - - @Suppress("NON_EXHAUSTIVE_WHEN") - private fun onPlayerRightClick(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 - } - - when (item) { - LAVA_BUCKET, WATER_BUCKET, BUCKET, FLINT_AND_STEEL -> { - val block = event.clickedBlock.getRelative(event.blockFace) - val otherParcel = world.getParcelAt(block) - if (!canBuildOnArea(event.player, otherParcel)) { - event.isCancelled = true - } - } - } - } - } - - /* - * Prevents players from breeding mobs, entering or opening boats/minecarts, - * rotating item frames, doing stuff with leashes, and putting stuff on armor stands. - */ - @Suppress("NON_EXHAUSTIVE_WHEN") - @field:ListenerMarker(priority = NORMAL) - val onPlayerInteractEntityEvent = RegistratorListener l@{ event -> - val (_, area) = getWorldAndArea(event.rightClicked.location.block) ?: return@l - if (canBuildOnArea(event.player, area)) return@l - when (event.rightClicked.type) { - EntityType.BOAT, - EntityType.MINECART, - EntityType.MINECART_CHEST, - EntityType.MINECART_COMMAND, - EntityType.MINECART_FURNACE, - EntityType.MINECART_HOPPER, - EntityType.MINECART_MOB_SPAWNER, - EntityType.MINECART_TNT, - - EntityType.ARMOR_STAND, - EntityType.PAINTING, - EntityType.ITEM_FRAME, - EntityType.LEASH_HITCH, - - EntityType.CHICKEN, - EntityType.COW, - EntityType.HORSE, - EntityType.SHEEP, - EntityType.VILLAGER, - EntityType.WOLF -> event.isCancelled = true - } - } - - /* - * Prevents endermen from griefing. - * Prevents sand blocks from exiting the parcel in which they became an entity. - */ - @field:ListenerMarker(priority = NORMAL) - val onEntityChangeBlockEvent = RegistratorListener l@{ event -> - val (_, area) = getWorldAndArea(event.block) ?: return@l - if (event.entity.type == EntityType.ENDERMAN || area == null || area.hasBlockVisitors) { - event.isCancelled = true; return@l - } - - if (event.entity.type == EntityType.FALLING_BLOCK) { - // a sand block started falling. Track it and delete it if it gets out of this parcel. - entityTracker.track(event.entity, area) - } - } - - /* - * Prevents portals from being created if set so in the configs for that world - */ - @field:ListenerMarker(priority = NORMAL) - val onEntityCreatePortalEvent = RegistratorListener l@{ event -> - val world = parcelProvider.getWorld(event.entity.world) ?: return@l - if (world.options.blockPortalCreation) event.isCancelled = true - } - - /* - * Prevents players from dropping items - */ - @field:ListenerMarker(priority = NORMAL) - val onPlayerDropItemEvent = RegistratorListener l@{ event -> - val (_, area) = getWorldAndArea(event.itemDrop.location.block) ?: return@l - if (!canInteract(event.player, area, "containers")) event.isCancelled = true - } - - /* - * Prevents players from picking up items - */ - @field:ListenerMarker(priority = NORMAL) - val onEntityPickupItemEvent = RegistratorListener l@{ event -> - val user = event.entity as? Player ?: return@l - val (_, area) = getWorldAndArea(event.item.location.block) ?: return@l - if (!canInteract(user, area, "containers")) event.isCancelled = true - } - - /* - * Prevents players from editing inventories - */ - @field:ListenerMarker(priority = NORMAL, events = ["inventory.InventoryClickEvent", "inventory.InventoryDragEvent"]) - val onInventoryClickEvent = RegistratorListener l@{ event -> - val user = event.whoClicked as? Player ?: return@l - if ((event.inventory ?: return@l).holder === user) return@l // inventory null: hotbar - val (_, area) = getWorldAndArea(event.inventory.location.block) ?: return@l - if (!canInteract(user, area, "containers")) { - event.isCancelled = true - } - } - - /* - * Cancels weather changes and sets the weather to sunny if requested by the config for that world. - */ - @field:ListenerMarker(priority = NORMAL) - val onWeatherChangeEvent = RegistratorListener l@{ event -> - val world = parcelProvider.getWorld(event.world) ?: return@l - if (world.options.noWeather && event.toWeatherState()) { - event.isCancelled = true - } - } - - private fun resetWeather(world: World) { - world.setStorm(false) - world.isThundering = false - world.weatherDuration = Int.MAX_VALUE - } - -// TODO: BlockFormEvent, BlockSpreadEvent, BlockFadeEvent, Fireworks - - /* - * Prevents natural blocks forming - */ - @ListenerMarker(priority = NORMAL) - val onBlockFormEvent = RegistratorListener l@{ event -> - val block = event.block - val (world, area) = getWorldAndArea(block) ?: return@l - - // prevent any generation whatsoever on paths - if (area == null) { - event.isCancelled = true; return@l - } - - val hasEntity = event is EntityBlockFormEvent - val player = (event as? EntityBlockFormEvent)?.entity as? Player - - val cancel: Boolean = when (event.newState.type) { - - // prevent ice generation from Frost Walkers enchantment - FROSTED_ICE -> player != null && !area.canBuild(player) - - // prevent snow generation from weather - SNOW -> !hasEntity && world.options.preventWeatherBlockChanges - - else -> false - } - - if (cancel) { - event.isCancelled = true - } - } - - /* - * Prevents mobs (living entities) from spawning if that is disabled for that world in the config. - */ - @field:ListenerMarker(priority = NORMAL) - val onEntitySpawnEvent = RegistratorListener l@{ event -> - val world = parcelProvider.getWorld(event.entity.world) ?: return@l - if (event.entity is Creature && world.options.blockMobSpawning) { - event.isCancelled = true - } else if (world.getParcelAt(event.entity).let { it != null && it.hasBlockVisitors }) { - event.isCancelled = true - } - } - - /* - * Prevents minecarts/boats from moving outside a plot - */ - @field:ListenerMarker(priority = NORMAL) - val onVehicleMoveEvent = RegistratorListener l@{ event -> - val (_, area) = getWorldAndArea(event.to.block) ?: return@l - if (area == null) { - 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 (area.hasBlockVisitors) { - event.to.subtract(event.to).add(event.from) - } - } - - /* - * Prevents players from removing items from item frames - * Prevents TNT Minecarts and creepers from destroying entities (This event is called BEFORE EntityExplodeEvent GG) - * Actually doesn't prevent this because the entities are destroyed anyway, even though the code works? - */ - @field:ListenerMarker(priority = NORMAL) - val onEntityDamageByEntityEvent = RegistratorListener l@{ event -> - val world = parcelProvider.getWorld(event.entity.world) ?: return@l - if (world.options.disableExplosions && event.damager is ExplosiveMinecart || event.damager is Creeper) { - event.isCancelled = true; return@l - } - - val user = event.damager as? Player - ?: (event.damager as? Projectile)?.let { it.shooter as? Player } - ?: return@l - - if (!canBuildOnArea(user, world.getParcelAt(event.entity))) { - event.isCancelled = true - } - } - - @field:ListenerMarker(priority = NORMAL) - val onHangingBreakEvent = RegistratorListener l@{ event -> - val world = parcelProvider.getWorld(event.entity.world) ?: return@l - if (event.cause == HangingBreakEvent.RemoveCause.EXPLOSION && world.options.disableExplosions) { - event.isCancelled = true; return@l - } - - if (world.getParcelAt(event.entity).let { it != null && it.hasBlockVisitors }) { - event.isCancelled = true - } - } - - /* - * Prevents players from deleting paintings and item frames - * This appears to take care of shooting with a bow, throwing snowballs or throwing ender pearls. - */ - @field:ListenerMarker(priority = NORMAL) - val onHangingBreakByEntityEvent = RegistratorListener l@{ event -> - val world = parcelProvider.getWorld(event.entity.world) ?: return@l - val user = event.remover as? Player ?: return@l - if (!canBuildOnArea(user, world.getParcelAt(event.entity))) { - event.isCancelled = true - } - } - - /* - * Prevents players from placing paintings and item frames - */ - @field:ListenerMarker(priority = NORMAL) - val onHangingPlaceEvent = RegistratorListener l@{ event -> - val world = parcelProvider.getWorld(event.entity.world) ?: return@l - val block = event.block.getRelative(event.blockFace) - if (!canBuildOnArea(event.player, world.getParcelAt(block))) { - event.isCancelled = true - } - } - - /* - * Prevents stuff from growing outside of plots - */ - @field:ListenerMarker(priority = NORMAL) - val onStructureGrowEvent = RegistratorListener l@{ event -> - val (world, area) = getWorldAndArea(event.location.block) ?: return@l - if (area == null) { - event.isCancelled = true; return@l - } - - if (!event.player.hasPermBuildAnywhere && !area.canBuild(event.player)) { - event.isCancelled = true; return@l - } - - event.blocks.removeIf { world.getParcelAt(it.block) !== area } - } - - /* - * Prevents dispensers/droppers from dispensing out of parcels - */ - @field:ListenerMarker(priority = NORMAL) - val onBlockDispenseEvent = RegistratorListener l@{ event -> - val block = event.block - if (!block.type.let { it == DISPENSER || it == DROPPER }) return@l - val world = parcelProvider.getWorld(block.world) ?: return@l - val data = block.blockData as Directional - val targetBlock = block.getRelative(data.facing) - if (world.getParcelAt(targetBlock) == null) { - event.isCancelled = true - } - } - - /* - * Track spawned items, making sure they don't leave the parcel. - */ - @field:ListenerMarker(priority = NORMAL) - val onItemSpawnEvent = RegistratorListener l@{ event -> - val (_, area) = getWorldAndArea(event.location.block) ?: return@l - if (area == null) event.isCancelled = true - else entityTracker.track(event.entity, area) - } - - /* - * Prevents endermen and endermite from teleporting outside their parcel - */ - @field:ListenerMarker(priority = NORMAL) - val onEntityTeleportEvent = RegistratorListener l@{ event -> - val (world, area) = getWorldAndArea(event.from.block) ?: return@l - if (area !== world.getParcelAt(event.to)) { - event.isCancelled = true - } - } - - /* - * Prevents projectiles from flying out of parcels - * Prevents players from firing projectiles if they cannot build - */ - @field:ListenerMarker(priority = NORMAL) - val onProjectileLaunchEvent = RegistratorListener l@{ event -> - val (_, area) = getWorldAndArea(event.entity.location.block) ?: return@l - if (area == null || (event.entity.shooter as? Player)?.let { !canBuildOnArea(it, area) } == true) { - event.isCancelled = true - } else { - entityTracker.track(event.entity, area) - } - } - - /* - * Prevents entities from dropping items upon death, if configured that way - */ - @field:ListenerMarker(priority = NORMAL) - val onEntityDeathEvent = RegistratorListener l@{ event -> - entityTracker.untrack(event.entity) - val world = parcelProvider.getWorld(event.entity.world) ?: return@l - if (!world.options.dropEntityItems) { - event.drops.clear() - event.droppedExp = 0 - } - } - - /* - * Assigns players their default game mode upon entering the world - */ - @field:ListenerMarker(priority = NORMAL) - val onPlayerChangedWorldEvent = RegistratorListener l@{ event -> - val world = parcelProvider.getWorld(event.player.world) ?: return@l - if (world.options.gameMode != null && !event.player.hasPermGamemodeBypass) { - event.player.gameMode = world.options.gameMode - } - } - - /** - * Updates owner signs of parcels that get loaded if it is marked outdated - */ - @ListenerMarker(priority = EventPriority.NORMAL) - val onChunkLoadEvent = RegistratorListener l@{ event -> - val world = parcelProvider.getWorld(event.chunk.world) ?: return@l - val parcels = world.blockManager.getParcelsWithOwnerBlockIn(event.chunk) - if (parcels.isEmpty()) return@l - - parcels.forEach { id -> - val parcel = world.getParcelById(id)?.takeIf { it.isOwnerSignOutdated } ?: return@forEach - world.blockManager.updateParcelInfo(parcel.id, parcel.owner) - parcel.isOwnerSignOutdated = false - } - - } - - @ListenerMarker - val onPlayerJoinEvent = RegistratorListener l@{ event -> - storage.updatePlayerName(event.player.uuid, event.player.name) - } - - /** - * Attempts to prevent redstone contraptions from breaking while they are being swapped - * Might remove if it causes lag - */ - @ListenerMarker - val onBlockRedstoneEvent = RegistratorListener l@{ event -> - val (_, area) = getWorldAndArea(event.block) ?: return@l - if (area == null || area.hasBlockVisitors) { - event.newCurrent = event.oldCurrent - } - } - - - private fun getPlayerSpeed(player: Player): Double = - if (player.isFlying) { - player.flySpeed * if (player.isSprinting) 21.6 else 10.92 - } else { - player.walkSpeed * when { - player.isSprinting -> 5.612 - player.isSneaking -> 1.31 - else -> 4.317 - } / 1.5 //? - } / 20.0 - +package io.dico.parcels2.listener + +import gnu.trove.TLongCollection +import gnu.trove.set.hash.TLongHashSet +import io.dico.dicore.Formatting +import io.dico.dicore.ListenerMarker +import io.dico.dicore.RegistratorListener +import io.dico.parcels2.* +import io.dico.parcels2.storage.Storage +import io.dico.parcels2.util.ext.* +import io.dico.parcels2.util.math.* +import org.bukkit.Location +import org.bukkit.Material.* +import org.bukkit.World +import org.bukkit.block.Biome +import org.bukkit.block.Block +import org.bukkit.block.data.Directional +import org.bukkit.block.data.type.Bed +import org.bukkit.entity.* +import org.bukkit.entity.minecart.ExplosiveMinecart +import org.bukkit.event.EventPriority +import org.bukkit.event.EventPriority.NORMAL +import org.bukkit.event.block.* +import org.bukkit.event.entity.* +import org.bukkit.event.hanging.HangingBreakByEntityEvent +import org.bukkit.event.hanging.HangingBreakEvent +import org.bukkit.event.hanging.HangingPlaceEvent +import org.bukkit.event.inventory.InventoryInteractEvent +import org.bukkit.event.player.* +import org.bukkit.event.vehicle.VehicleMoveEvent +import org.bukkit.event.weather.WeatherChangeEvent +import org.bukkit.event.world.ChunkLoadEvent +import org.bukkit.event.world.StructureGrowEvent +import org.bukkit.inventory.InventoryHolder +import java.util.EnumSet + +class ParcelListeners( + val parcelProvider: ParcelProvider, + val entityTracker: ParcelEntityTracker, + val storage: Storage +) { + private fun canBuildOnArea(user: Player, area: Parcel?) = + if (area == null) user.hasPermBuildAnywhere else area.canBuild(user) + + private fun canInteract(user: Player, area: Parcel?, interactClass: String) = + canBuildOnArea(user, area) || (area != null && area.interactableConfig(interactClass)) + + /** + * Get the world and parcel that the block resides in + * the parcel is nullable, and often named area because that means path. + * returns null if not in a registered parcel world - should always return in that case to not affect other worlds. + */ + private fun getWorldAndArea(block: Block): Pair? { + val world = parcelProvider.getWorld(block.world) ?: return null + return world to world.getParcelAt(block) + } + + + /* + * Prevents players from entering plots they are banned from + */ + @field:ListenerMarker(priority = NORMAL) + val onPlayerMoveEvent = RegistratorListener l@{ event -> + val user = event.player + if (user.hasPermBanBypass) return@l + val toLoc = event.to + val parcel = parcelProvider.getParcelAt(toLoc) ?: return@l + + if (!parcel.canEnterFast(user)) { + val region = parcel.world.blockManager.getRegion(parcel.id) + val dimension = region.getFirstUncontainedDimensionOf(Vec3i(event.from)) + + if (dimension == null) { + user.teleport(parcel.homeLocation) + user.sendParcelMessage(nopermit = true, message = "You are banned from this parcel") + + } else { + val speed = getPlayerSpeed(user) + val from = Vec3d(event.from) + val to = Vec3d(toLoc).with(dimension, from[dimension]) + + var newTo = to + dimension.otherDimensions.forEach { + val delta = to[it] - from[it] + newTo = newTo.add(it, delta * 100 * if (it == Dimension.Y) 0.5 else speed) + } + + event.to = Location( + toLoc.world, + newTo.x, newTo.y.clampMin(0.0).clampMax(255.0), newTo.z, + toLoc.yaw, toLoc.pitch + ) + } + } + } + + /* + * Prevents players from breaking blocks outside of their parcels + * Prevents containers from dropping their contents when broken, if configured + */ + @field:ListenerMarker(priority = NORMAL) + val onBlockBreakEvent = RegistratorListener l@{ event -> + val (world, area) = getWorldAndArea(event.block) ?: return@l + if (!canBuildOnArea(event.player, area)) { + event.isCancelled = true; return@l + } + + if (!world.options.dropEntityItems) { + val state = event.block.state + if (state is InventoryHolder) { + state.inventory.clear() + state.update() + } + } + } + + /* + * Prevents players from placing blocks outside of their parcels + */ + @field:ListenerMarker(priority = NORMAL) + val onBlockPlaceEvent = RegistratorListener l@{ event -> + val (_, area) = getWorldAndArea(event.block) ?: return@l + if (!canBuildOnArea(event.player, area)) { + event.isCancelled = true + } + + area?.updateOwnerSign() + } + + /* + * Control pistons + */ + @field:ListenerMarker(priority = NORMAL) + val onBlockPistonExtendEvent = RegistratorListener l@{ event -> + checkPistonMovement(event, event.blocks) + } + + @field:ListenerMarker(priority = NORMAL) + val onBlockPistonRetractEvent = RegistratorListener l@{ event -> + checkPistonMovement(event, event.blocks) + } + + // Doing some unnecessary optimizations here.. + //@formatter:off + private inline fun Column(x: Int, z: Int): Long = x.toLong() or (z.toLong().shl(32)) + + private inline val Long.columnX get() = and(0xFFFF_FFFFL).toInt() + private inline val Long.columnZ get() = ushr(32).and(0xFFFF_FFFFL).toInt() + private inline fun TLongCollection.troveForEach(block: (Long) -> Unit) = iterator().let { while (it.hasNext()) block(it.next()) } + //@formatter:on + private fun checkPistonMovement(event: BlockPistonEvent, blocks: List) { + val world = parcelProvider.getWorld(event.block.world) ?: return + val direction = event.direction + val columns = TLongHashSet(blocks.size * 2) + + blocks.forEach { + columns.add(Column(it.x, it.z)) + it.getRelative(direction).let { columns.add(Column(it.x, it.z)) } + } + + columns.troveForEach { + val area = world.getParcelAt(it.columnX, it.columnZ) + if (area == null || area.hasBlockVisitors) { + event.isCancelled = true + return + } + } + } + + /* + * Prevents explosions if enabled by the configs for that world + */ + @field:ListenerMarker(priority = NORMAL) + val onExplosionPrimeEvent = RegistratorListener l@{ event -> + val (world, area) = getWorldAndArea(event.entity.location.block) ?: return@l + if (area != null && area.hasBlockVisitors) { + event.radius = 0F; event.isCancelled = true + } else if (world.options.disableExplosions) { + event.radius = 0F + } + } + + /* + * Prevents creepers and tnt minecarts from exploding if explosions are disabled + */ + @field:ListenerMarker(priority = NORMAL) + val onEntityExplodeEvent = RegistratorListener l@{ event -> + entityTracker.untrack(event.entity) + val world = parcelProvider.getWorld(event.entity.world) ?: return@l + if (world.options.disableExplosions || world.getParcelAt(event.entity).let { it != null && it.hasBlockVisitors }) { + event.isCancelled = true + } + } + + /* + * Prevents liquids from flowing out of plots + */ + @field:ListenerMarker(priority = NORMAL) + val onBlockFromToEvent = RegistratorListener l@{ event -> + val (_, area) = getWorldAndArea(event.toBlock) ?: return@l + if (area == null || area.hasBlockVisitors) event.isCancelled = true + } + + private val bedTypes = EnumSet.copyOf(getMaterialsWithWoolColorPrefix("BED").toList()) + /* + * Prevents players from placing liquids, using flint and steel, changing redstone components, + * using inputs (unless allowed by the plot), + * and using items disabled in the configuration for that world. + * Prevents player from using beds in HELL or SKY biomes if explosions are disabled. + */ + @Suppress("NON_EXHAUSTIVE_WHEN") + @field:ListenerMarker(priority = NORMAL) + val onPlayerInteractEvent = RegistratorListener l@{ event -> + val user = event.player + val world = parcelProvider.getWorld(user.world) ?: return@l + val clickedBlock = event.clickedBlock + val parcel = clickedBlock?.let { world.getParcelAt(it) } + + if (!user.hasPermBuildAnywhere && parcel != null && !parcel.canEnter(user)) { + user.sendParcelMessage(nopermit = true, message = "You cannot interact with parcels you're banned from") + event.isCancelled = true; return@l + } + + when (event.action) { + Action.RIGHT_CLICK_BLOCK -> run { + val type = clickedBlock.type + + val interactableClass = Interactables[type] + if (interactableClass != null && !parcel.effectiveInteractableConfig.isInteractable(type) && (parcel == null || !parcel.canBuild(user))) { + user.sendParcelMessage(nopermit = true, message = "You cannot interact with ${interactableClass.name} here") + event.isCancelled = true + return@l + } + + if (bedTypes.contains(type)) { + val bed = clickedBlock.blockData as Bed + val head = if (bed.part == Bed.Part.FOOT) clickedBlock.getRelative(bed.facing) else clickedBlock + when (head.biome) { + Biome.NETHER, Biome.THE_END -> { + if (world.options.disableExplosions) { + user.sendParcelMessage(nopermit = true, message = "You cannot use this bed because it would explode") + event.isCancelled = true; return@l + } + } + } + + if (!canBuildOnArea(user, parcel)) { + user.sendParcelMessage(nopermit = true, message = "You may not sleep here") + event.isCancelled = true; return@l + } + } + + onPlayerRightClick(event, world, parcel) + + if (!event.isCancelled && parcel == null) { + world.blockManager.getParcelForInfoBlockInteraction(Vec3i(clickedBlock), type, event.blockFace) + ?.apply { user.sendMessage(Formatting.GREEN + infoString) } + } + } + + Action.RIGHT_CLICK_AIR -> onPlayerRightClick(event, world, parcel) + Action.PHYSICAL -> if (!canBuildOnArea(user, parcel) && !(parcel != null && parcel.interactableConfig("pressure_plates"))) { + user.sendParcelMessage(nopermit = true, message = "You cannot use inputs in this parcel") + event.isCancelled = true; return@l + } + } + } + + // private val blockPlaceInteractItems = EnumSet.of(LAVA_BUCKET, WATER_BUCKET, BUCKET, FLINT_AND_STEEL) + + @Suppress("NON_EXHAUSTIVE_WHEN") + private fun onPlayerRightClick(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 + } + + when (item) { + LAVA_BUCKET, WATER_BUCKET, BUCKET, FLINT_AND_STEEL -> { + val block = event.clickedBlock.getRelative(event.blockFace) + val otherParcel = world.getParcelAt(block) + if (!canBuildOnArea(event.player, otherParcel)) { + event.isCancelled = true + } + } + } + } + } + + /* + * Prevents players from breeding mobs, entering or opening boats/minecarts, + * rotating item frames, doing stuff with leashes, and putting stuff on armor stands. + */ + @Suppress("NON_EXHAUSTIVE_WHEN") + @field:ListenerMarker(priority = NORMAL) + val onPlayerInteractEntityEvent = RegistratorListener l@{ event -> + val (_, area) = getWorldAndArea(event.rightClicked.location.block) ?: return@l + if (canBuildOnArea(event.player, area)) return@l + when (event.rightClicked.type) { + EntityType.BOAT, + EntityType.MINECART, + EntityType.MINECART_CHEST, + EntityType.MINECART_COMMAND, + EntityType.MINECART_FURNACE, + EntityType.MINECART_HOPPER, + EntityType.MINECART_MOB_SPAWNER, + EntityType.MINECART_TNT, + + EntityType.ARMOR_STAND, + EntityType.PAINTING, + EntityType.ITEM_FRAME, + EntityType.LEASH_HITCH, + + EntityType.CHICKEN, + EntityType.COW, + EntityType.HORSE, + EntityType.SHEEP, + EntityType.VILLAGER, + EntityType.WOLF -> event.isCancelled = true + } + } + + /* + * Prevents endermen from griefing. + * Prevents sand blocks from exiting the parcel in which they became an entity. + */ + @field:ListenerMarker(priority = NORMAL) + val onEntityChangeBlockEvent = RegistratorListener l@{ event -> + val (_, area) = getWorldAndArea(event.block) ?: return@l + if (event.entity.type == EntityType.ENDERMAN || area == null || area.hasBlockVisitors) { + event.isCancelled = true; return@l + } + + if (event.entity.type == EntityType.FALLING_BLOCK) { + // a sand block started falling. Track it and delete it if it gets out of this parcel. + entityTracker.track(event.entity, area) + } + } + + /* + * Prevents portals from being created if set so in the configs for that world + */ + @field:ListenerMarker(priority = NORMAL) + val onEntityCreatePortalEvent = RegistratorListener l@{ event -> + val world = parcelProvider.getWorld(event.entity.world) ?: return@l + if (world.options.blockPortalCreation) event.isCancelled = true + } + + /* + * Prevents players from dropping items + */ + @field:ListenerMarker(priority = NORMAL) + val onPlayerDropItemEvent = RegistratorListener l@{ event -> + val (_, area) = getWorldAndArea(event.itemDrop.location.block) ?: return@l + if (!canInteract(event.player, area, "containers")) event.isCancelled = true + } + + /* + * Prevents players from picking up items + */ + @field:ListenerMarker(priority = NORMAL) + val onEntityPickupItemEvent = RegistratorListener l@{ event -> + val user = event.entity as? Player ?: return@l + val (_, area) = getWorldAndArea(event.item.location.block) ?: return@l + if (!canInteract(user, area, "containers")) event.isCancelled = true + } + + /* + * Prevents players from editing inventories + */ + @field:ListenerMarker(priority = NORMAL, events = ["inventory.InventoryClickEvent", "inventory.InventoryDragEvent"]) + val onInventoryClickEvent = RegistratorListener l@{ event -> + val user = event.whoClicked as? Player ?: return@l + if ((event.inventory ?: return@l).holder === user) return@l // inventory null: hotbar + val (_, area) = getWorldAndArea(event.inventory.location.block) ?: return@l + if (!canInteract(user, area, "containers")) { + event.isCancelled = true + } + } + + /* + * Cancels weather changes and sets the weather to sunny if requested by the config for that world. + */ + @field:ListenerMarker(priority = NORMAL) + val onWeatherChangeEvent = RegistratorListener l@{ event -> + val world = parcelProvider.getWorld(event.world) ?: return@l + if (world.options.noWeather && event.toWeatherState()) { + event.isCancelled = true + } + } + + private fun resetWeather(world: World) { + world.setStorm(false) + world.isThundering = false + world.weatherDuration = Int.MAX_VALUE + } + +// TODO: BlockFormEvent, BlockSpreadEvent, BlockFadeEvent, Fireworks + + /* + * Prevents natural blocks forming + */ + @ListenerMarker(priority = NORMAL) + val onBlockFormEvent = RegistratorListener l@{ event -> + val block = event.block + val (world, area) = getWorldAndArea(block) ?: return@l + + // prevent any generation whatsoever on paths + if (area == null) { + event.isCancelled = true; return@l + } + + val hasEntity = event is EntityBlockFormEvent + val player = (event as? EntityBlockFormEvent)?.entity as? Player + + val cancel: Boolean = when (event.newState.type) { + + // prevent ice generation from Frost Walkers enchantment + FROSTED_ICE -> player != null && !area.canBuild(player) + + // prevent snow generation from weather + SNOW -> !hasEntity && world.options.preventWeatherBlockChanges + + else -> false + } + + if (cancel) { + event.isCancelled = true + } + } + + /* + * Prevents mobs (living entities) from spawning if that is disabled for that world in the config. + */ + @field:ListenerMarker(priority = NORMAL) + val onEntitySpawnEvent = RegistratorListener l@{ event -> + val world = parcelProvider.getWorld(event.entity.world) ?: return@l + if (event.entity is Creature && world.options.blockMobSpawning) { + event.isCancelled = true + } else if (world.getParcelAt(event.entity).let { it != null && it.hasBlockVisitors }) { + event.isCancelled = true + } + } + + /* + * Prevents minecarts/boats from moving outside a plot + */ + @field:ListenerMarker(priority = NORMAL) + val onVehicleMoveEvent = RegistratorListener l@{ event -> + val (_, area) = getWorldAndArea(event.to.block) ?: return@l + if (area == null) { + 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 (area.hasBlockVisitors) { + event.to.subtract(event.to).add(event.from) + } + } + + /* + * Prevents players from removing items from item frames + * Prevents TNT Minecarts and creepers from destroying entities (This event is called BEFORE EntityExplodeEvent GG) + * Actually doesn't prevent this because the entities are destroyed anyway, even though the code works? + */ + @field:ListenerMarker(priority = NORMAL) + val onEntityDamageByEntityEvent = RegistratorListener l@{ event -> + val world = parcelProvider.getWorld(event.entity.world) ?: return@l + if (world.options.disableExplosions && event.damager is ExplosiveMinecart || event.damager is Creeper) { + event.isCancelled = true; return@l + } + + val user = event.damager as? Player + ?: (event.damager as? Projectile)?.let { it.shooter as? Player } + ?: return@l + + if (!canBuildOnArea(user, world.getParcelAt(event.entity))) { + event.isCancelled = true + } + } + + @field:ListenerMarker(priority = NORMAL) + val onHangingBreakEvent = RegistratorListener l@{ event -> + val world = parcelProvider.getWorld(event.entity.world) ?: return@l + if (event.cause == HangingBreakEvent.RemoveCause.EXPLOSION && world.options.disableExplosions) { + event.isCancelled = true; return@l + } + + if (world.getParcelAt(event.entity).let { it != null && it.hasBlockVisitors }) { + event.isCancelled = true + } + } + + /* + * Prevents players from deleting paintings and item frames + * This appears to take care of shooting with a bow, throwing snowballs or throwing ender pearls. + */ + @field:ListenerMarker(priority = NORMAL) + val onHangingBreakByEntityEvent = RegistratorListener l@{ event -> + val world = parcelProvider.getWorld(event.entity.world) ?: return@l + val user = event.remover as? Player ?: return@l + if (!canBuildOnArea(user, world.getParcelAt(event.entity))) { + event.isCancelled = true + } + } + + /* + * Prevents players from placing paintings and item frames + */ + @field:ListenerMarker(priority = NORMAL) + val onHangingPlaceEvent = RegistratorListener l@{ event -> + val world = parcelProvider.getWorld(event.entity.world) ?: return@l + val block = event.block.getRelative(event.blockFace) + if (!canBuildOnArea(event.player, world.getParcelAt(block))) { + event.isCancelled = true + } + } + + /* + * Prevents stuff from growing outside of plots + */ + @field:ListenerMarker(priority = NORMAL) + val onStructureGrowEvent = RegistratorListener l@{ event -> + val (world, area) = getWorldAndArea(event.location.block) ?: return@l + if (area == null) { + event.isCancelled = true; return@l + } + + if (!event.player.hasPermBuildAnywhere && !area.canBuild(event.player)) { + event.isCancelled = true; return@l + } + + event.blocks.removeIf { world.getParcelAt(it.block) !== area } + } + + /* + * Prevents dispensers/droppers from dispensing out of parcels + */ + @field:ListenerMarker(priority = NORMAL) + val onBlockDispenseEvent = RegistratorListener l@{ event -> + val block = event.block + if (!block.type.let { it == DISPENSER || it == DROPPER }) return@l + val world = parcelProvider.getWorld(block.world) ?: return@l + val data = block.blockData as Directional + val targetBlock = block.getRelative(data.facing) + if (world.getParcelAt(targetBlock) == null) { + event.isCancelled = true + } + } + + /* + * Track spawned items, making sure they don't leave the parcel. + */ + @field:ListenerMarker(priority = NORMAL) + val onItemSpawnEvent = RegistratorListener l@{ event -> + val (_, area) = getWorldAndArea(event.location.block) ?: return@l + if (area == null) event.isCancelled = true + else entityTracker.track(event.entity, area) + } + + /* + * Prevents endermen and endermite from teleporting outside their parcel + */ + @field:ListenerMarker(priority = NORMAL) + val onEntityTeleportEvent = RegistratorListener l@{ event -> + val (world, area) = getWorldAndArea(event.from.block) ?: return@l + if (area !== world.getParcelAt(event.to)) { + event.isCancelled = true + } + } + + /* + * Prevents projectiles from flying out of parcels + * Prevents players from firing projectiles if they cannot build + */ + @field:ListenerMarker(priority = NORMAL) + val onProjectileLaunchEvent = RegistratorListener l@{ event -> + val (_, area) = getWorldAndArea(event.entity.location.block) ?: return@l + if (area == null || (event.entity.shooter as? Player)?.let { !canBuildOnArea(it, area) } == true) { + event.isCancelled = true + } else { + entityTracker.track(event.entity, area) + } + } + + /* + * Prevents entities from dropping items upon death, if configured that way + */ + @field:ListenerMarker(priority = NORMAL) + val onEntityDeathEvent = RegistratorListener l@{ event -> + entityTracker.untrack(event.entity) + val world = parcelProvider.getWorld(event.entity.world) ?: return@l + if (!world.options.dropEntityItems) { + event.drops.clear() + event.droppedExp = 0 + } + } + + /* + * Assigns players their default game mode upon entering the world + */ + @field:ListenerMarker(priority = NORMAL) + val onPlayerChangedWorldEvent = RegistratorListener l@{ event -> + val world = parcelProvider.getWorld(event.player.world) ?: return@l + if (world.options.gameMode != null && !event.player.hasPermGamemodeBypass) { + event.player.gameMode = world.options.gameMode + } + } + + /** + * Updates owner signs of parcels that get loaded if it is marked outdated + */ + @ListenerMarker(priority = EventPriority.NORMAL) + val onChunkLoadEvent = RegistratorListener l@{ event -> + val world = parcelProvider.getWorld(event.chunk.world) ?: return@l + val parcels = world.blockManager.getParcelsWithOwnerBlockIn(event.chunk) + if (parcels.isEmpty()) return@l + + parcels.forEach { id -> + val parcel = world.getParcelById(id)?.takeIf { it.isOwnerSignOutdated } ?: return@forEach + world.blockManager.updateParcelInfo(parcel.id, parcel.owner) + parcel.isOwnerSignOutdated = false + } + + } + + @ListenerMarker + val onPlayerJoinEvent = RegistratorListener l@{ event -> + storage.updatePlayerName(event.player.uuid, event.player.name) + } + + /** + * Attempts to prevent redstone contraptions from breaking while they are being swapped + * Might remove if it causes lag + */ + @ListenerMarker + val onBlockRedstoneEvent = RegistratorListener l@{ event -> + val (_, area) = getWorldAndArea(event.block) ?: return@l + if (area == null || area.hasBlockVisitors) { + event.newCurrent = event.oldCurrent + } + } + + + private fun getPlayerSpeed(player: Player): Double = + if (player.isFlying) { + player.flySpeed * if (player.isSprinting) 21.6 else 10.92 + } else { + player.walkSpeed * when { + player.isSprinting -> 5.612 + player.isSneaking -> 1.31 + else -> 4.317 + } / 1.5 //? + } / 20.0 + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/listener/WorldEditListener.kt b/src/main/kotlin/io/dico/parcels2/listener/WorldEditListener.kt index 4d35a53..fc31305 100644 --- a/src/main/kotlin/io/dico/parcels2/listener/WorldEditListener.kt +++ b/src/main/kotlin/io/dico/parcels2/listener/WorldEditListener.kt @@ -1,79 +1,79 @@ -package io.dico.parcels2.listener - -import com.sk89q.worldedit.EditSession.Stage.BEFORE_REORDER -import com.sk89q.worldedit.Vector -import com.sk89q.worldedit.Vector2D -import com.sk89q.worldedit.WorldEdit -import com.sk89q.worldedit.bukkit.WorldEditPlugin -import com.sk89q.worldedit.event.extent.EditSessionEvent -import com.sk89q.worldedit.extent.AbstractDelegateExtent -import com.sk89q.worldedit.extent.Extent -import com.sk89q.worldedit.util.eventbus.EventHandler.Priority.VERY_EARLY -import com.sk89q.worldedit.util.eventbus.Subscribe -import com.sk89q.worldedit.world.biome.BaseBiome -import com.sk89q.worldedit.world.block.BlockStateHolder -import io.dico.parcels2.ParcelWorld -import io.dico.parcels2.ParcelsPlugin -import io.dico.parcels2.canBuildFast -import io.dico.parcels2.util.ext.hasPermBuildAnywhere -import io.dico.parcels2.util.ext.sendParcelMessage -import org.bukkit.entity.Player -import org.bukkit.plugin.Plugin - -class WorldEditListener(val parcels: ParcelsPlugin, val worldEdit: WorldEdit) { - - @Subscribe(priority = VERY_EARLY) - fun onEditSession(event: EditSessionEvent) { - val worldName = event.world?.name ?: return - val world = parcels.parcelProvider.getWorld(worldName) ?: return - if (event.stage == BEFORE_REORDER) return - - val actor = event.actor - if (actor == null || !actor.isPlayer) return - - val player = parcels.server.getPlayer(actor.uniqueId) - if (player.hasPermBuildAnywhere) return - - event.extent = ParcelsExtent(event.extent, world, player) - } - - private class ParcelsExtent(extent: Extent, - val world: ParcelWorld, - val player: Player) : AbstractDelegateExtent(extent) { - private var messageSent = false - - private fun canBuild(x: Int, z: Int): Boolean { - world.getParcelAt(x, z)?.let { parcel -> - if (parcel.canBuildFast(player)) { - return true - } - } - - if (!messageSent) { - messageSent = true - player.sendParcelMessage(except = true, message = "You can't use WorldEdit there") - } - - return false - } - - override fun setBlock(location: Vector, block: BlockStateHolder<*>): Boolean { - return canBuild(location.blockX, location.blockZ) && super.setBlock(location, block) - } - - override fun setBiome(coord: Vector2D, biome: BaseBiome): Boolean { - return canBuild(coord.blockX, coord.blockZ) && super.setBiome(coord, biome) - } - - } - - companion object { - fun register(parcels: ParcelsPlugin, worldEditPlugin: Plugin) { - if (worldEditPlugin !is WorldEditPlugin) return - val worldEdit = worldEditPlugin.worldEdit - val listener = WorldEditListener(parcels, worldEdit) - worldEdit.eventBus.register(listener) - } - } - +package io.dico.parcels2.listener + +import com.sk89q.worldedit.EditSession.Stage.BEFORE_REORDER +import com.sk89q.worldedit.Vector +import com.sk89q.worldedit.Vector2D +import com.sk89q.worldedit.WorldEdit +import com.sk89q.worldedit.bukkit.WorldEditPlugin +import com.sk89q.worldedit.event.extent.EditSessionEvent +import com.sk89q.worldedit.extent.AbstractDelegateExtent +import com.sk89q.worldedit.extent.Extent +import com.sk89q.worldedit.util.eventbus.EventHandler.Priority.VERY_EARLY +import com.sk89q.worldedit.util.eventbus.Subscribe +import com.sk89q.worldedit.world.biome.BaseBiome +import com.sk89q.worldedit.world.block.BlockStateHolder +import io.dico.parcels2.ParcelWorld +import io.dico.parcels2.ParcelsPlugin +import io.dico.parcels2.canBuildFast +import io.dico.parcels2.util.ext.hasPermBuildAnywhere +import io.dico.parcels2.util.ext.sendParcelMessage +import org.bukkit.entity.Player +import org.bukkit.plugin.Plugin + +class WorldEditListener(val parcels: ParcelsPlugin, val worldEdit: WorldEdit) { + + @Subscribe(priority = VERY_EARLY) + fun onEditSession(event: EditSessionEvent) { + val worldName = event.world?.name ?: return + val world = parcels.parcelProvider.getWorld(worldName) ?: return + if (event.stage == BEFORE_REORDER) return + + val actor = event.actor + if (actor == null || !actor.isPlayer) return + + val player = parcels.server.getPlayer(actor.uniqueId) + if (player.hasPermBuildAnywhere) return + + event.extent = ParcelsExtent(event.extent, world, player) + } + + private class ParcelsExtent(extent: Extent, + val world: ParcelWorld, + val player: Player) : AbstractDelegateExtent(extent) { + private var messageSent = false + + private fun canBuild(x: Int, z: Int): Boolean { + world.getParcelAt(x, z)?.let { parcel -> + if (parcel.canBuildFast(player)) { + return true + } + } + + if (!messageSent) { + messageSent = true + player.sendParcelMessage(except = true, message = "You can't use WorldEdit there") + } + + return false + } + + override fun setBlock(location: Vector, block: BlockStateHolder<*>): Boolean { + return canBuild(location.blockX, location.blockZ) && super.setBlock(location, block) + } + + override fun setBiome(coord: Vector2D, biome: BaseBiome): Boolean { + return canBuild(coord.blockX, coord.blockZ) && super.setBiome(coord, biome) + } + + } + + companion object { + fun register(parcels: ParcelsPlugin, worldEditPlugin: Plugin) { + if (worldEditPlugin !is WorldEditPlugin) return + val worldEdit = worldEditPlugin.worldEdit + val listener = WorldEditListener(parcels, worldEdit) + worldEdit.eventBus.register(listener) + } + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/options/GeneratorOptions.kt b/src/main/kotlin/io/dico/parcels2/options/GeneratorOptions.kt index d0626dc..a6a57e5 100644 --- a/src/main/kotlin/io/dico/parcels2/options/GeneratorOptions.kt +++ b/src/main/kotlin/io/dico/parcels2/options/GeneratorOptions.kt @@ -1,36 +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 = "default", options: Any = DefaultGeneratorOptions()) : 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, +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 = "default", options: Any = DefaultGeneratorOptions()) : 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 index 5e36099..7dd752e 100644 --- a/src/main/kotlin/io/dico/parcels2/options/MigrationOptions.kt +++ b/src/main/kotlin/io/dico/parcels2/options/MigrationOptions.kt @@ -1,22 +1,22 @@ -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 = "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() = PlotmeMigrationOptions::class - - override fun newInstance(key: String, options: Any, vararg extra: Any?): Migration { - return PlotmeMigration(options as PlotmeMigrationOptions) - } -} - -class PlotmeMigrationOptions(val worldsFromTo: Map = mapOf("plotworld" to "parcels"), - val storage: StorageOptions = StorageOptions(options = DataConnectionOptions(database = "plotme")), +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 = "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() = PlotmeMigrationOptions::class + + override fun newInstance(key: String, options: Any, vararg extra: Any?): Migration { + return PlotmeMigration(options as PlotmeMigrationOptions) + } +} + +class PlotmeMigrationOptions(val worldsFromTo: Map = mapOf("plotworld" to "parcels"), + val storage: StorageOptions = StorageOptions(options = DataConnectionOptions(database = "plotme")), val tableNamesUppercase: Boolean = false) \ 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 35d48ba..412c783 100644 --- a/src/main/kotlin/io/dico/parcels2/options/Options.kt +++ b/src/main/kotlin/io/dico/parcels2/options/Options.kt @@ -1,58 +1,58 @@ -package io.dico.parcels2.options - -import io.dico.parcels2.TickJobtimeOptions -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() - var tickJobtime: TickJobtimeOptions = TickJobtimeOptions(20, 1) - var migration = MigrationOptionsHolder() - - fun addWorld(name: String, - generatorOptions: GeneratorOptions? = null, - worldOptions: RuntimeWorldOptions? = null) { - val optionsHolder = WorldOptions( - generatorOptions ?: GeneratorOptions(), - 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/") - -class MigrationOptionsHolder { - var enabled = false - var disableWhenComplete = true - var instance: MigrationOptions? = MigrationOptions() +package io.dico.parcels2.options + +import io.dico.parcels2.TickJobtimeOptions +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() + var tickJobtime: TickJobtimeOptions = TickJobtimeOptions(20, 1) + var migration = MigrationOptionsHolder() + + fun addWorld(name: String, + generatorOptions: GeneratorOptions? = null, + worldOptions: RuntimeWorldOptions? = null) { + val optionsHolder = WorldOptions( + generatorOptions ?: GeneratorOptions(), + 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/") + +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/OptionsMapper.kt b/src/main/kotlin/io/dico/parcels2/options/OptionsMapper.kt index 671a25f..d741617 100644 --- a/src/main/kotlin/io/dico/parcels2/options/OptionsMapper.kt +++ b/src/main/kotlin/io/dico/parcels2/options/OptionsMapper.kt @@ -1,66 +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) - } - } - -} +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 index aa60f39..f65efa1 100644 --- a/src/main/kotlin/io/dico/parcels2/options/PolymorphicOptions.kt +++ b/src/main/kotlin/io/dico/parcels2/options/PolymorphicOptions.kt @@ -1,89 +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() - } - } +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 index 3d68701..29aab7a 100644 --- a/src/main/kotlin/io/dico/parcels2/options/StorageOptions.kt +++ b/src/main/kotlin/io/dico/parcels2/options/StorageOptions.kt @@ -1,59 +1,59 @@ -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.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 = "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") - - fun getDataSourceFactory(key: String, options: Any): DataSourceFactory { - val hikariConfig = getHikariConfig(key, options as DataConnectionOptions) - return { HikariDataSource(hikariConfig) } - } - - override fun newInstance(key: String, options: Any, vararg extra: Any?): Storage { - return BackedStorage(ExposedBacking(getDataSourceFactory(key, options), (options as DataConnectionOptions).poolSize)) - } -} - -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) - } - +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.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 = "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") + + fun getDataSourceFactory(key: String, options: Any): DataSourceFactory { + val hikariConfig = getHikariConfig(key, options as DataConnectionOptions) + return { HikariDataSource(hikariConfig) } + } + + override fun newInstance(key: String, options: Any, vararg extra: Any?): Storage { + return BackedStorage(ExposedBacking(getDataSourceFactory(key, options), (options as DataConnectionOptions).poolSize)) + } +} + +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/Backing.kt b/src/main/kotlin/io/dico/parcels2/storage/Backing.kt index 12d5bc7..63a5db6 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/Backing.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/Backing.kt @@ -1,69 +1,69 @@ -package io.dico.parcels2.storage - -import io.dico.parcels2.* -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.coroutines.channels.SendChannel -import org.joda.time.DateTime -import java.util.UUID -import kotlin.coroutines.CoroutineContext - -interface Backing { - - val name: String - - val isConnected: Boolean - - val coroutineContext: CoroutineContext - - 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() - - - fun getWorldCreationTime(worldId: ParcelWorldId): DateTime? - - fun setWorldCreationTime(worldId: ParcelWorldId, time: DateTime) - - fun getPlayerUuidForName(name: String): UUID? - - fun updatePlayerName(uuid: UUID, name: String) - - fun transmitParcelData(channel: SendChannel, parcels: Sequence) - - fun transmitAllParcelData(channel: SendChannel) - - fun readParcelData(parcel: ParcelId): ParcelDataHolder? - - fun getOwnedParcels(user: PlayerProfile): List - - fun getNumParcels(user: PlayerProfile): Int = getOwnedParcels(user).size - - - fun setParcelData(parcel: ParcelId, data: ParcelDataHolder?) - - fun setParcelOwner(parcel: ParcelId, owner: PlayerProfile?) - - fun setParcelOwnerSignOutdated(parcel: ParcelId, outdated: Boolean) - - fun setLocalPrivilege(parcel: ParcelId, player: PlayerProfile, privilege: Privilege) - - fun setParcelOptionsInteractConfig(parcel: ParcelId, config: InteractableConfiguration) - - - fun transmitAllGlobalPrivileges(channel: SendChannel>) - - fun readGlobalPrivileges(owner: PlayerProfile): PrivilegesHolder? - - fun setGlobalPrivilege(owner: PlayerProfile, player: PlayerProfile, privilege: Privilege) -} +package io.dico.parcels2.storage + +import io.dico.parcels2.* +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel +import org.joda.time.DateTime +import java.util.UUID +import kotlin.coroutines.CoroutineContext + +interface Backing { + + val name: String + + val isConnected: Boolean + + val coroutineContext: CoroutineContext + + 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() + + + fun getWorldCreationTime(worldId: ParcelWorldId): DateTime? + + fun setWorldCreationTime(worldId: ParcelWorldId, time: DateTime) + + fun getPlayerUuidForName(name: String): UUID? + + fun updatePlayerName(uuid: UUID, name: String) + + fun transmitParcelData(channel: SendChannel, parcels: Sequence) + + fun transmitAllParcelData(channel: SendChannel) + + fun readParcelData(parcel: ParcelId): ParcelDataHolder? + + fun getOwnedParcels(user: PlayerProfile): List + + fun getNumParcels(user: PlayerProfile): Int = getOwnedParcels(user).size + + + fun setParcelData(parcel: ParcelId, data: ParcelDataHolder?) + + fun setParcelOwner(parcel: ParcelId, owner: PlayerProfile?) + + fun setParcelOwnerSignOutdated(parcel: ParcelId, outdated: Boolean) + + fun setLocalPrivilege(parcel: ParcelId, player: PlayerProfile, privilege: Privilege) + + fun setParcelOptionsInteractConfig(parcel: ParcelId, config: InteractableConfiguration) + + + fun transmitAllGlobalPrivileges(channel: SendChannel>) + + fun readGlobalPrivileges(owner: PlayerProfile): PrivilegesHolder? + + fun setGlobalPrivilege(owner: PlayerProfile, player: PlayerProfile, privilege: Privilege) +} diff --git a/src/main/kotlin/io/dico/parcels2/storage/DataConverters.kt b/src/main/kotlin/io/dico/parcels2/storage/DataConverters.kt index 80f41b2..54f7677 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/DataConverters.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/DataConverters.kt @@ -1,38 +1,38 @@ -package io.dico.parcels2.storage - -import java.lang.IllegalArgumentException -import java.nio.ByteBuffer -import java.util.UUID - -/* For putting it into the database */ -fun UUID.toByteArray(): ByteArray = - ByteBuffer.allocate(16).apply { - putLong(mostSignificantBits) - putLong(leastSignificantBits) - }.array() - -/* For getting it out of the database */ -fun ByteArray.toUUID(): UUID = - ByteBuffer.wrap(this).run { - val mostSignificantBits = getLong() - val leastSignificantBits = getLong() - UUID(mostSignificantBits, leastSignificantBits) - } - -/* For putting it into the database */ -fun IntArray.toByteArray(): ByteArray = - ByteBuffer.allocate(size * Int.SIZE_BYTES).also { buf -> - buf.asIntBuffer().put(this) - }.array() - -/* For getting it out of the database */ -fun ByteArray.toIntArray(): IntArray { - if (this.size % Int.SIZE_BYTES != 0) - throw IllegalArgumentException("Size must be divisible by ${Int.SIZE_BYTES}") - - return ByteBuffer.wrap(this).run { - IntArray(remaining() / 4).also { array -> - asIntBuffer().get(array) - } - } +package io.dico.parcels2.storage + +import java.lang.IllegalArgumentException +import java.nio.ByteBuffer +import java.util.UUID + +/* For putting it into the database */ +fun UUID.toByteArray(): ByteArray = + ByteBuffer.allocate(16).apply { + putLong(mostSignificantBits) + putLong(leastSignificantBits) + }.array() + +/* For getting it out of the database */ +fun ByteArray.toUUID(): UUID = + ByteBuffer.wrap(this).run { + val mostSignificantBits = getLong() + val leastSignificantBits = getLong() + UUID(mostSignificantBits, leastSignificantBits) + } + +/* For putting it into the database */ +fun IntArray.toByteArray(): ByteArray = + ByteBuffer.allocate(size * Int.SIZE_BYTES).also { buf -> + buf.asIntBuffer().put(this) + }.array() + +/* For getting it out of the database */ +fun ByteArray.toIntArray(): IntArray { + if (this.size % Int.SIZE_BYTES != 0) + throw IllegalArgumentException("Size must be divisible by ${Int.SIZE_BYTES}") + + return ByteBuffer.wrap(this).run { + IntArray(remaining() / 4).also { array -> + asIntBuffer().get(array) + } + } } \ 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 480d533..f3030f6 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/Hikari.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/Hikari.kt @@ -1,73 +1,73 @@ -package io.dico.parcels2.storage - -import com.zaxxer.hikari.HikariConfig -import io.dico.parcels2.options.DataConnectionOptions - -fun getHikariConfig(dialectName: String, - dco: DataConnectionOptions): HikariConfig = HikariConfig().apply { - - val (address, port) = dco.splitAddressAndPort() ?: throw IllegalArgumentException("Invalid address: ${dco.address}") - - when (dialectName) { - "postgresql" -> run { - dataSourceClassName = "org.postgresql.ds.PGSimpleDataSource" - dataSourceProperties["serverName"] = address - dataSourceProperties["portNumber"] = port.toString() - dataSourceProperties["databaseName"] = dco.database - } - - "mariadb" -> run { - dataSourceClassName = "org.mariadb.jdbc.MariaDbDataSource" - dataSourceProperties["serverName"] = address - dataSourceProperties["port"] = port.toString() - dataSourceProperties["databaseName"] = dco.database - dataSourceProperties["properties"] = "useUnicode=true;characterEncoding=utf8" - } - - else -> throw IllegalArgumentException("Unsupported dialect: $dialectName") - } - - poolName = "parcels" - maximumPoolSize = dco.poolSize - 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 if (dialectName.toLowerCase() == "h2") { - dataSourceProperties.remove("serverName") - dataSourceProperties.remove("port") - dataSourceProperties.remove("databaseName") - addDataSourceProperty("url", "jdbc:h2:${if (address.isBlank()) "" else "tcp://$address/"}~/${dco.database}") - } else if (dialectName.toLowerCase() == "mysql") { - // 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") - } else { - - }*/ -} +package io.dico.parcels2.storage + +import com.zaxxer.hikari.HikariConfig +import io.dico.parcels2.options.DataConnectionOptions + +fun getHikariConfig(dialectName: String, + dco: DataConnectionOptions): HikariConfig = HikariConfig().apply { + + val (address, port) = dco.splitAddressAndPort() ?: throw IllegalArgumentException("Invalid address: ${dco.address}") + + when (dialectName) { + "postgresql" -> run { + dataSourceClassName = "org.postgresql.ds.PGSimpleDataSource" + dataSourceProperties["serverName"] = address + dataSourceProperties["portNumber"] = port.toString() + dataSourceProperties["databaseName"] = dco.database + } + + "mariadb" -> run { + dataSourceClassName = "org.mariadb.jdbc.MariaDbDataSource" + dataSourceProperties["serverName"] = address + dataSourceProperties["port"] = port.toString() + dataSourceProperties["databaseName"] = dco.database + dataSourceProperties["properties"] = "useUnicode=true;characterEncoding=utf8" + } + + else -> throw IllegalArgumentException("Unsupported dialect: $dialectName") + } + + poolName = "parcels" + maximumPoolSize = dco.poolSize + 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 if (dialectName.toLowerCase() == "h2") { + dataSourceProperties.remove("serverName") + dataSourceProperties.remove("port") + dataSourceProperties.remove("databaseName") + addDataSourceProperty("url", "jdbc:h2:${if (address.isBlank()) "" else "tcp://$address/"}~/${dco.database}") + } else if (dialectName.toLowerCase() == "mysql") { + // 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") + } else { + + }*/ +} diff --git a/src/main/kotlin/io/dico/parcels2/storage/Storage.kt b/src/main/kotlin/io/dico/parcels2/storage/Storage.kt index d718f20..f76aad6 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/Storage.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/Storage.kt @@ -1,114 +1,114 @@ -@file:Suppress("NOTHING_TO_INLINE") - -package io.dico.parcels2.storage - -import io.dico.parcels2.* -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.coroutines.channels.SendChannel -import kotlinx.coroutines.launch -import org.joda.time.DateTime -import java.util.UUID -import kotlin.coroutines.CoroutineContext - -typealias DataPair = Pair -typealias PrivilegePair = Pair - -interface Storage { - val name: String - val isConnected: Boolean - - fun init(): Job - - fun shutdown(): Job - - - fun getWorldCreationTime(worldId: ParcelWorldId): Deferred - - fun setWorldCreationTime(worldId: ParcelWorldId, time: DateTime): Job - - fun getPlayerUuidForName(name: String): Deferred - - fun updatePlayerName(uuid: UUID, name: String): Job - - fun readParcelData(parcel: ParcelId): Deferred - - fun transmitParcelData(parcels: Sequence): ReceiveChannel - - fun transmitAllParcelData(): ReceiveChannel - - fun getOwnedParcels(user: PlayerProfile): Deferred> - - fun getNumParcels(user: PlayerProfile): Deferred - - - fun setParcelData(parcel: ParcelId, data: ParcelDataHolder?): Job - - fun setParcelOwner(parcel: ParcelId, owner: PlayerProfile?): Job - - fun setParcelOwnerSignOutdated(parcel: ParcelId, outdated: Boolean): Job - - fun setLocalPrivilege(parcel: ParcelId, player: PlayerProfile, privilege: Privilege): Job - - fun setParcelOptionsInteractConfig(parcel: ParcelId, config: InteractableConfiguration): Job - - - fun transmitAllGlobalPrivileges(): ReceiveChannel> - - fun readGlobalPrivileges(owner: PlayerProfile): Deferred - - fun setGlobalPrivilege(owner: PlayerProfile, player: PlayerProfile, privilege: Privilege): Job - - - fun getChannelToUpdateParcelData(): SendChannel> -} - -class BackedStorage internal constructor(val b: Backing) : Storage, CoroutineScope { - override val name get() = b.name - override val isConnected get() = b.isConnected - override val coroutineContext: CoroutineContext get() = b.coroutineContext - - override fun init() = launch { b.init() } - - override fun shutdown() = launch { b.shutdown() } - - - override fun getWorldCreationTime(worldId: ParcelWorldId): Deferred = b.launchFuture { b.getWorldCreationTime(worldId) } - - override fun setWorldCreationTime(worldId: ParcelWorldId, time: DateTime): Job = b.launchJob { b.setWorldCreationTime(worldId, time) } - - override fun getPlayerUuidForName(name: String): Deferred = b.launchFuture { b.getPlayerUuidForName(name) } - - override fun updatePlayerName(uuid: UUID, name: String): Job = b.launchJob { b.updatePlayerName(uuid, name) } - - override fun readParcelData(parcel: ParcelId) = b.launchFuture { b.readParcelData(parcel) } - - override fun transmitParcelData(parcels: Sequence) = b.openChannel { b.transmitParcelData(it, parcels) } - - override fun transmitAllParcelData() = b.openChannel { b.transmitAllParcelData(it) } - - override fun getOwnedParcels(user: PlayerProfile) = b.launchFuture { b.getOwnedParcels(user) } - - override fun getNumParcels(user: PlayerProfile) = b.launchFuture { b.getNumParcels(user) } - - override fun setParcelData(parcel: ParcelId, data: ParcelDataHolder?) = b.launchJob { b.setParcelData(parcel, data) } - - override fun setParcelOwner(parcel: ParcelId, owner: PlayerProfile?) = b.launchJob { b.setParcelOwner(parcel, owner) } - - override fun setParcelOwnerSignOutdated(parcel: ParcelId, outdated: Boolean): Job = b.launchJob { b.setParcelOwnerSignOutdated(parcel, outdated) } - - override fun setLocalPrivilege(parcel: ParcelId, player: PlayerProfile, privilege: Privilege) = b.launchJob { b.setLocalPrivilege(parcel, player, privilege) } - - override fun setParcelOptionsInteractConfig(parcel: ParcelId, config: InteractableConfiguration) = b.launchJob { b.setParcelOptionsInteractConfig(parcel, config) } - - - override fun transmitAllGlobalPrivileges(): ReceiveChannel> = b.openChannel { b.transmitAllGlobalPrivileges(it) } - - override fun readGlobalPrivileges(owner: PlayerProfile): Deferred = b.launchFuture { b.readGlobalPrivileges(owner) } - - override fun setGlobalPrivilege(owner: PlayerProfile, player: PlayerProfile, privilege: Privilege) = b.launchJob { b.setGlobalPrivilege(owner, player, privilege) } - - override fun getChannelToUpdateParcelData(): SendChannel> = b.openChannelForWriting { b.setParcelData(it.first, it.second) } -} +@file:Suppress("NOTHING_TO_INLINE") + +package io.dico.parcels2.storage + +import io.dico.parcels2.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.launch +import org.joda.time.DateTime +import java.util.UUID +import kotlin.coroutines.CoroutineContext + +typealias DataPair = Pair +typealias PrivilegePair = Pair + +interface Storage { + val name: String + val isConnected: Boolean + + fun init(): Job + + fun shutdown(): Job + + + fun getWorldCreationTime(worldId: ParcelWorldId): Deferred + + fun setWorldCreationTime(worldId: ParcelWorldId, time: DateTime): Job + + fun getPlayerUuidForName(name: String): Deferred + + fun updatePlayerName(uuid: UUID, name: String): Job + + fun readParcelData(parcel: ParcelId): Deferred + + fun transmitParcelData(parcels: Sequence): ReceiveChannel + + fun transmitAllParcelData(): ReceiveChannel + + fun getOwnedParcels(user: PlayerProfile): Deferred> + + fun getNumParcels(user: PlayerProfile): Deferred + + + fun setParcelData(parcel: ParcelId, data: ParcelDataHolder?): Job + + fun setParcelOwner(parcel: ParcelId, owner: PlayerProfile?): Job + + fun setParcelOwnerSignOutdated(parcel: ParcelId, outdated: Boolean): Job + + fun setLocalPrivilege(parcel: ParcelId, player: PlayerProfile, privilege: Privilege): Job + + fun setParcelOptionsInteractConfig(parcel: ParcelId, config: InteractableConfiguration): Job + + + fun transmitAllGlobalPrivileges(): ReceiveChannel> + + fun readGlobalPrivileges(owner: PlayerProfile): Deferred + + fun setGlobalPrivilege(owner: PlayerProfile, player: PlayerProfile, privilege: Privilege): Job + + + fun getChannelToUpdateParcelData(): SendChannel> +} + +class BackedStorage internal constructor(val b: Backing) : Storage, CoroutineScope { + override val name get() = b.name + override val isConnected get() = b.isConnected + override val coroutineContext: CoroutineContext get() = b.coroutineContext + + override fun init() = launch { b.init() } + + override fun shutdown() = launch { b.shutdown() } + + + override fun getWorldCreationTime(worldId: ParcelWorldId): Deferred = b.launchFuture { b.getWorldCreationTime(worldId) } + + override fun setWorldCreationTime(worldId: ParcelWorldId, time: DateTime): Job = b.launchJob { b.setWorldCreationTime(worldId, time) } + + override fun getPlayerUuidForName(name: String): Deferred = b.launchFuture { b.getPlayerUuidForName(name) } + + override fun updatePlayerName(uuid: UUID, name: String): Job = b.launchJob { b.updatePlayerName(uuid, name) } + + override fun readParcelData(parcel: ParcelId) = b.launchFuture { b.readParcelData(parcel) } + + override fun transmitParcelData(parcels: Sequence) = b.openChannel { b.transmitParcelData(it, parcels) } + + override fun transmitAllParcelData() = b.openChannel { b.transmitAllParcelData(it) } + + override fun getOwnedParcels(user: PlayerProfile) = b.launchFuture { b.getOwnedParcels(user) } + + override fun getNumParcels(user: PlayerProfile) = b.launchFuture { b.getNumParcels(user) } + + override fun setParcelData(parcel: ParcelId, data: ParcelDataHolder?) = b.launchJob { b.setParcelData(parcel, data) } + + override fun setParcelOwner(parcel: ParcelId, owner: PlayerProfile?) = b.launchJob { b.setParcelOwner(parcel, owner) } + + override fun setParcelOwnerSignOutdated(parcel: ParcelId, outdated: Boolean): Job = b.launchJob { b.setParcelOwnerSignOutdated(parcel, outdated) } + + override fun setLocalPrivilege(parcel: ParcelId, player: PlayerProfile, privilege: Privilege) = b.launchJob { b.setLocalPrivilege(parcel, player, privilege) } + + override fun setParcelOptionsInteractConfig(parcel: ParcelId, config: InteractableConfiguration) = b.launchJob { b.setParcelOptionsInteractConfig(parcel, config) } + + + override fun transmitAllGlobalPrivileges(): ReceiveChannel> = b.openChannel { b.transmitAllGlobalPrivileges(it) } + + override fun readGlobalPrivileges(owner: PlayerProfile): Deferred = b.launchFuture { b.readGlobalPrivileges(owner) } + + override fun setGlobalPrivilege(owner: PlayerProfile, player: PlayerProfile, privilege: Privilege) = b.launchJob { b.setGlobalPrivilege(owner, player, privilege) } + + override fun getChannelToUpdateParcelData(): SendChannel> = b.openChannelForWriting { b.setParcelData(it.first, it.second) } +} 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 e49be79..32065bc 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedBacking.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedBacking.kt @@ -1,282 +1,282 @@ -@file:Suppress("NOTHING_TO_INLINE", "PARAMETER_NAME_CHANGED_ON_OVERRIDE", "LocalVariableName", "UNUSED_EXPRESSION") - -package io.dico.parcels2.storage.exposed - -import com.zaxxer.hikari.HikariDataSource -import io.dico.parcels2.* -import io.dico.parcels2.PlayerProfile.Star.name -import io.dico.parcels2.storage.* -import io.dico.parcels2.util.math.clampMax -import io.dico.parcels2.util.ext.synchronized -import kotlinx.coroutines.* -import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.ArrayChannel -import kotlinx.coroutines.channels.LinkedListChannel -import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.coroutines.channels.SendChannel -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.SchemaUtils.create -import org.jetbrains.exposed.sql.transactions.transaction -import org.jetbrains.exposed.sql.vendors.DatabaseDialect -import org.joda.time.DateTime -import java.util.UUID -import javax.sql.DataSource - -class ExposedDatabaseException(message: String? = null) : Exception(message) - -class ExposedBacking(private val dataSourceFactory: () -> DataSource, val poolSize: Int) : Backing, CoroutineScope { - override val name get() = "Exposed" - override val coroutineContext = Job() + newFixedThreadPoolContext(poolSize, "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 { transaction { job() } } - override fun launchFuture(future: Backing.() -> T): Deferred = async { transaction { future() } } - - override fun openChannel(future: Backing.(SendChannel) -> Unit): ReceiveChannel { - val channel = LinkedListChannel() - launchJob { future(channel) } - return channel - } - - override fun openChannelForWriting(action: Backing.(T) -> Unit): SendChannel { - val channel = ArrayChannel(poolSize * 2) - - repeat(poolSize.clampMax(3)) { - launch { - try { - while (true) { - action(channel.receive()) - } - } catch (ex: Exception) { - // channel closed - } - } - } - - return channel - } - - private fun transaction(statement: Transaction.() -> T) = transaction(database!!, statement) - - companion object { - init { - Database.registerDialect("mariadb") { - Class.forName("org.jetbrains.exposed.sql.vendors.MysqlDialect").newInstance() as DatabaseDialect - } - } - } - - override fun init() { - synchronized { - if (isShutdown || isConnected) throw IllegalStateException() - dataSource = dataSourceFactory() - database = Database.connect(dataSource!!) - transaction(database!!) { - create(WorldsT, ProfilesT, ParcelsT, ParcelOptionsT, PrivilegesLocalT, PrivilegesGlobalT) - } - } - } - - override fun shutdown() { - synchronized { - if (isShutdown) throw IllegalStateException() - isShutdown = true - coroutineContext[Job]!!.cancel(CancellationException("ExposedBacking shutdown")) - dataSource?.let { - (it as? HikariDataSource)?.close() - } - database = null - } - } - - @Suppress("RedundantObjectTypeCheck") - private fun PlayerProfile.toOwnerProfile(): PlayerProfile { - if (this is PlayerProfile.Star) return PlayerProfile.Fake(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 getWorldCreationTime(worldId: ParcelWorldId): DateTime? { - return WorldsT.getWorldCreationTime(worldId) - } - - override fun setWorldCreationTime(worldId: ParcelWorldId, time: DateTime) { - WorldsT.setWorldCreationTime(worldId, time) - } - - 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 updatePlayerName(uuid: UUID, name: String) { - val binaryUuid = uuid.toByteArray() - ProfilesT.upsert(ProfilesT.uuid) { - it[ProfilesT.uuid] = binaryUuid - it[ProfilesT.name] = name - } - } - - override fun transmitParcelData(channel: SendChannel, parcels: Sequence) { - for (parcel in parcels) { - val data = readParcelData(parcel) - channel.offer(parcel to data) - } - channel.close() - } - - override fun transmitAllParcelData(channel: SendChannel) { - ParcelsT.selectAll().forEach { row -> - val parcel = ParcelsT.getItem(row) ?: return@forEach - val data = rowToParcelData(row) - channel.offer(parcel to data) - } - channel.close() - } - - override fun readParcelData(parcel: ParcelId): ParcelDataHolder? { - val row = ParcelsT.getRow(parcel) ?: return null - return rowToParcelData(row) - } - - 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::getItem) - .toList() - } - - override fun setParcelData(parcel: ParcelId, data: ParcelDataHolder?) { - if (data == null) { - transaction { - ParcelsT.getId(parcel)?.let { id -> - ParcelsT.deleteIgnoreWhere { ParcelsT.id eq id } - - // Below should cascade automatically - /* - PrivilegesLocalT.deleteIgnoreWhere { PrivilegesLocalT.parcel_id eq id } - ParcelOptionsT.deleteIgnoreWhere(limit = 1) { ParcelOptionsT.parcel_id eq id } - */ - } - - } - return - } - - transaction { - val id = ParcelsT.getOrInitId(parcel) - PrivilegesLocalT.deleteIgnoreWhere { PrivilegesLocalT.attach_id eq id } - } - - setParcelOwner(parcel, data.owner) - - for ((profile, privilege) in data.privilegeMap) { - PrivilegesLocalT.setPrivilege(parcel, profile, privilege) - } - - data.privilegeOfStar.takeIf { it != Privilege.DEFAULT }?.let { privilege -> - PrivilegesLocalT.setPrivilege(parcel, PlayerProfile.Star, privilege) - } - - setParcelOptionsInteractConfig(parcel, data.interactableConfig) - } - - override fun setParcelOwner(parcel: ParcelId, owner: PlayerProfile?) { - val id = if (owner == null) - ParcelsT.getId(parcel) ?: return - else - ParcelsT.getOrInitId(parcel) - - val owner_id = owner?.let { ProfilesT.getOrInitId(it.toOwnerProfile()) } - val time = owner?.let { DateTime.now() } - - ParcelsT.update({ ParcelsT.id eq id }) { - it[ParcelsT.owner_id] = owner_id - it[claim_time] = time - it[sign_oudated] = false - } - } - - override fun setParcelOwnerSignOutdated(parcel: ParcelId, outdated: Boolean) { - val id = ParcelsT.getId(parcel) ?: return - ParcelsT.update({ ParcelsT.id eq id }) { - it[sign_oudated] = outdated - } - } - - override fun setLocalPrivilege(parcel: ParcelId, player: PlayerProfile, privilege: Privilege) { - PrivilegesLocalT.setPrivilege(parcel, player.toRealProfile(), privilege) - } - - override fun setParcelOptionsInteractConfig(parcel: ParcelId, config: InteractableConfiguration) { - val bitmaskArray = (config as? BitmaskInteractableConfiguration ?: return).bitmaskArray - val isAllZero = !bitmaskArray.fold(false) { cur, elem -> cur || elem != 0 } - - if (isAllZero) { - val id = ParcelsT.getId(parcel) ?: return - ParcelOptionsT.deleteWhere { ParcelOptionsT.parcel_id eq id } - return - } - - if (bitmaskArray.size != 1) throw IllegalArgumentException() - val array = bitmaskArray.toByteArray() - val id = ParcelsT.getOrInitId(parcel) - ParcelOptionsT.upsert(ParcelOptionsT.parcel_id) { - it[parcel_id] = id - it[interact_bitmask] = array - } - } - - override fun transmitAllGlobalPrivileges(channel: SendChannel>) { - PrivilegesGlobalT.sendAllPrivilegesH(channel) - channel.close() - } - - override fun readGlobalPrivileges(owner: PlayerProfile): PrivilegesHolder? { - return PrivilegesGlobalT.readPrivileges(ProfilesT.getId(owner.toOwnerProfile()) ?: return null) - } - - override fun setGlobalPrivilege(owner: PlayerProfile, player: PlayerProfile, privilege: Privilege) { - PrivilegesGlobalT.setPrivilege(owner, player.toRealProfile(), privilege) - } - - private fun rowToParcelData(row: ResultRow) = ParcelDataHolder().apply { - owner = row[ParcelsT.owner_id]?.let { ProfilesT.getItem(it) } - lastClaimTime = row[ParcelsT.claim_time] - isOwnerSignOutdated = row[ParcelsT.sign_oudated] - - val id = row[ParcelsT.id] - ParcelOptionsT.select { ParcelOptionsT.parcel_id eq id }.firstOrNull()?.let { optrow -> - val source = optrow[ParcelOptionsT.interact_bitmask].toIntArray() - val target = (interactableConfig as? BitmaskInteractableConfiguration ?: return@let).bitmaskArray - System.arraycopy(source, 0, target, 0, source.size.clampMax(target.size)) - } - - val privileges = PrivilegesLocalT.readPrivileges(id) - if (privileges != null) { - copyPrivilegesFrom(privileges) - } - } - -} - +@file:Suppress("NOTHING_TO_INLINE", "PARAMETER_NAME_CHANGED_ON_OVERRIDE", "LocalVariableName", "UNUSED_EXPRESSION") + +package io.dico.parcels2.storage.exposed + +import com.zaxxer.hikari.HikariDataSource +import io.dico.parcels2.* +import io.dico.parcels2.PlayerProfile.Star.name +import io.dico.parcels2.storage.* +import io.dico.parcels2.util.math.clampMax +import io.dico.parcels2.util.ext.synchronized +import kotlinx.coroutines.* +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.ArrayChannel +import kotlinx.coroutines.channels.LinkedListChannel +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SchemaUtils.create +import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.vendors.DatabaseDialect +import org.joda.time.DateTime +import java.util.UUID +import javax.sql.DataSource + +class ExposedDatabaseException(message: String? = null) : Exception(message) + +class ExposedBacking(private val dataSourceFactory: () -> DataSource, val poolSize: Int) : Backing, CoroutineScope { + override val name get() = "Exposed" + override val coroutineContext = Job() + newFixedThreadPoolContext(poolSize, "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 { transaction { job() } } + override fun launchFuture(future: Backing.() -> T): Deferred = async { transaction { future() } } + + override fun openChannel(future: Backing.(SendChannel) -> Unit): ReceiveChannel { + val channel = LinkedListChannel() + launchJob { future(channel) } + return channel + } + + override fun openChannelForWriting(action: Backing.(T) -> Unit): SendChannel { + val channel = ArrayChannel(poolSize * 2) + + repeat(poolSize.clampMax(3)) { + launch { + try { + while (true) { + action(channel.receive()) + } + } catch (ex: Exception) { + // channel closed + } + } + } + + return channel + } + + private fun transaction(statement: Transaction.() -> T) = transaction(database!!, statement) + + companion object { + init { + Database.registerDialect("mariadb") { + Class.forName("org.jetbrains.exposed.sql.vendors.MysqlDialect").newInstance() as DatabaseDialect + } + } + } + + override fun init() { + synchronized { + if (isShutdown || isConnected) throw IllegalStateException() + dataSource = dataSourceFactory() + database = Database.connect(dataSource!!) + transaction(database!!) { + create(WorldsT, ProfilesT, ParcelsT, ParcelOptionsT, PrivilegesLocalT, PrivilegesGlobalT) + } + } + } + + override fun shutdown() { + synchronized { + if (isShutdown) throw IllegalStateException() + isShutdown = true + coroutineContext[Job]!!.cancel(CancellationException("ExposedBacking shutdown")) + dataSource?.let { + (it as? HikariDataSource)?.close() + } + database = null + } + } + + @Suppress("RedundantObjectTypeCheck") + private fun PlayerProfile.toOwnerProfile(): PlayerProfile { + if (this is PlayerProfile.Star) return PlayerProfile.Fake(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 getWorldCreationTime(worldId: ParcelWorldId): DateTime? { + return WorldsT.getWorldCreationTime(worldId) + } + + override fun setWorldCreationTime(worldId: ParcelWorldId, time: DateTime) { + WorldsT.setWorldCreationTime(worldId, time) + } + + 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 updatePlayerName(uuid: UUID, name: String) { + val binaryUuid = uuid.toByteArray() + ProfilesT.upsert(ProfilesT.uuid) { + it[ProfilesT.uuid] = binaryUuid + it[ProfilesT.name] = name + } + } + + override fun transmitParcelData(channel: SendChannel, parcels: Sequence) { + for (parcel in parcels) { + val data = readParcelData(parcel) + channel.offer(parcel to data) + } + channel.close() + } + + override fun transmitAllParcelData(channel: SendChannel) { + ParcelsT.selectAll().forEach { row -> + val parcel = ParcelsT.getItem(row) ?: return@forEach + val data = rowToParcelData(row) + channel.offer(parcel to data) + } + channel.close() + } + + override fun readParcelData(parcel: ParcelId): ParcelDataHolder? { + val row = ParcelsT.getRow(parcel) ?: return null + return rowToParcelData(row) + } + + 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::getItem) + .toList() + } + + override fun setParcelData(parcel: ParcelId, data: ParcelDataHolder?) { + if (data == null) { + transaction { + ParcelsT.getId(parcel)?.let { id -> + ParcelsT.deleteIgnoreWhere { ParcelsT.id eq id } + + // Below should cascade automatically + /* + PrivilegesLocalT.deleteIgnoreWhere { PrivilegesLocalT.parcel_id eq id } + ParcelOptionsT.deleteIgnoreWhere(limit = 1) { ParcelOptionsT.parcel_id eq id } + */ + } + + } + return + } + + transaction { + val id = ParcelsT.getOrInitId(parcel) + PrivilegesLocalT.deleteIgnoreWhere { PrivilegesLocalT.attach_id eq id } + } + + setParcelOwner(parcel, data.owner) + + for ((profile, privilege) in data.privilegeMap) { + PrivilegesLocalT.setPrivilege(parcel, profile, privilege) + } + + data.privilegeOfStar.takeIf { it != Privilege.DEFAULT }?.let { privilege -> + PrivilegesLocalT.setPrivilege(parcel, PlayerProfile.Star, privilege) + } + + setParcelOptionsInteractConfig(parcel, data.interactableConfig) + } + + override fun setParcelOwner(parcel: ParcelId, owner: PlayerProfile?) { + val id = if (owner == null) + ParcelsT.getId(parcel) ?: return + else + ParcelsT.getOrInitId(parcel) + + val owner_id = owner?.let { ProfilesT.getOrInitId(it.toOwnerProfile()) } + val time = owner?.let { DateTime.now() } + + ParcelsT.update({ ParcelsT.id eq id }) { + it[ParcelsT.owner_id] = owner_id + it[claim_time] = time + it[sign_oudated] = false + } + } + + override fun setParcelOwnerSignOutdated(parcel: ParcelId, outdated: Boolean) { + val id = ParcelsT.getId(parcel) ?: return + ParcelsT.update({ ParcelsT.id eq id }) { + it[sign_oudated] = outdated + } + } + + override fun setLocalPrivilege(parcel: ParcelId, player: PlayerProfile, privilege: Privilege) { + PrivilegesLocalT.setPrivilege(parcel, player.toRealProfile(), privilege) + } + + override fun setParcelOptionsInteractConfig(parcel: ParcelId, config: InteractableConfiguration) { + val bitmaskArray = (config as? BitmaskInteractableConfiguration ?: return).bitmaskArray + val isAllZero = !bitmaskArray.fold(false) { cur, elem -> cur || elem != 0 } + + if (isAllZero) { + val id = ParcelsT.getId(parcel) ?: return + ParcelOptionsT.deleteWhere { ParcelOptionsT.parcel_id eq id } + return + } + + if (bitmaskArray.size != 1) throw IllegalArgumentException() + val array = bitmaskArray.toByteArray() + val id = ParcelsT.getOrInitId(parcel) + ParcelOptionsT.upsert(ParcelOptionsT.parcel_id) { + it[parcel_id] = id + it[interact_bitmask] = array + } + } + + override fun transmitAllGlobalPrivileges(channel: SendChannel>) { + PrivilegesGlobalT.sendAllPrivilegesH(channel) + channel.close() + } + + override fun readGlobalPrivileges(owner: PlayerProfile): PrivilegesHolder? { + return PrivilegesGlobalT.readPrivileges(ProfilesT.getId(owner.toOwnerProfile()) ?: return null) + } + + override fun setGlobalPrivilege(owner: PlayerProfile, player: PlayerProfile, privilege: Privilege) { + PrivilegesGlobalT.setPrivilege(owner, player.toRealProfile(), privilege) + } + + private fun rowToParcelData(row: ResultRow) = ParcelDataHolder().apply { + owner = row[ParcelsT.owner_id]?.let { ProfilesT.getItem(it) } + lastClaimTime = row[ParcelsT.claim_time] + isOwnerSignOutdated = row[ParcelsT.sign_oudated] + + val id = row[ParcelsT.id] + ParcelOptionsT.select { ParcelOptionsT.parcel_id eq id }.firstOrNull()?.let { optrow -> + val source = optrow[ParcelOptionsT.interact_bitmask].toIntArray() + val target = (interactableConfig as? BitmaskInteractableConfiguration ?: return@let).bitmaskArray + System.arraycopy(source, 0, target, 0, source.size.clampMax(target.size)) + } + + val privileges = PrivilegesLocalT.readPrivileges(id) + if (privileges != null) { + copyPrivilegesFrom(privileges) + } + } + +} + 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 0245625..7640c0c 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedExtensions.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedExtensions.kt @@ -1,74 +1,74 @@ -package io.dico.parcels2.storage.exposed - -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 - -class UpsertStatement(table: Table, conflictColumn: Column<*>? = null, conflictIndex: Index? = null) : InsertStatement(table, false) { - val indexName: String - val indexColumns: List> - - init { - when { - conflictIndex != null -> { - indexName = conflictIndex.indexName - indexColumns = conflictIndex.columns - } - conflictColumn != null -> { - indexName = conflictColumn.name - indexColumns = listOf(conflictColumn) - } - else -> throw IllegalArgumentException() - } - } - - override fun prepareSQL(transaction: Transaction) = buildString { - append(super.prepareSQL(transaction)) - - val dialect = transaction.db.vendor - if (dialect == "postgresql") { - - append(" ON CONFLICT(") - append(indexName) - append(") DO UPDATE SET ") - - values.keys.filter { it !in indexColumns }.joinTo(this) { "${transaction.identity(it)}=EXCLUDED.${transaction.identity(it)}" } - - } else { - - append(" ON DUPLICATE KEY UPDATE ") - values.keys.filter { it !in indexColumns }.joinTo(this) { "${transaction.identity(it)}=VALUES(${transaction.identity(it)})" } - - } - } - -} - -inline fun T.upsert(conflictColumn: Column<*>? = null, conflictIndex: Index? = null, body: T.(UpsertStatement) -> Unit) = - UpsertStatement(this, conflictColumn, conflictIndex).apply { - body(this) - execute(TransactionManager.current()) - } - -fun Table.indexR(customIndexName: String? = null, isUnique: Boolean = false, vararg columns: Column<*>): Index { - val index = Index(columns.toList(), isUnique, customIndexName) - indices.add(index) - return index -} - -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 > greaterOf(col1: ExpressionWithColumnType, col2: ExpressionWithColumnType): Expression = - with(SqlExpressionBuilder) { - case(col1) - .When(col1.greater(col2), col1) - .Else(col2) - } - +package io.dico.parcels2.storage.exposed + +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 + +class UpsertStatement(table: Table, conflictColumn: Column<*>? = null, conflictIndex: Index? = null) : InsertStatement(table, false) { + val indexName: String + val indexColumns: List> + + init { + when { + conflictIndex != null -> { + indexName = conflictIndex.indexName + indexColumns = conflictIndex.columns + } + conflictColumn != null -> { + indexName = conflictColumn.name + indexColumns = listOf(conflictColumn) + } + else -> throw IllegalArgumentException() + } + } + + override fun prepareSQL(transaction: Transaction) = buildString { + append(super.prepareSQL(transaction)) + + val dialect = transaction.db.vendor + if (dialect == "postgresql") { + + append(" ON CONFLICT(") + append(indexName) + append(") DO UPDATE SET ") + + values.keys.filter { it !in indexColumns }.joinTo(this) { "${transaction.identity(it)}=EXCLUDED.${transaction.identity(it)}" } + + } else { + + append(" ON DUPLICATE KEY UPDATE ") + values.keys.filter { it !in indexColumns }.joinTo(this) { "${transaction.identity(it)}=VALUES(${transaction.identity(it)})" } + + } + } + +} + +inline fun T.upsert(conflictColumn: Column<*>? = null, conflictIndex: Index? = null, body: T.(UpsertStatement) -> Unit) = + UpsertStatement(this, conflictColumn, conflictIndex).apply { + body(this) + execute(TransactionManager.current()) + } + +fun Table.indexR(customIndexName: String? = null, isUnique: Boolean = false, vararg columns: Column<*>): Index { + val index = Index(columns.toList(), isUnique, customIndexName) + indices.add(index) + return index +} + +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 > greaterOf(col1: ExpressionWithColumnType, col2: ExpressionWithColumnType): Expression = + with(SqlExpressionBuilder) { + 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 6f6ad6b..8428b3a 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/exposed/IdTables.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/exposed/IdTables.kt @@ -1,165 +1,165 @@ -@file:Suppress("NOTHING_TO_INLINE", "PARAMETER_NAME_CHANGED_ON_OVERRIDE", "unused", "MemberVisibilityCanBePrivate") - -package io.dico.parcels2.storage.exposed - -import io.dico.parcels2.ParcelId -import io.dico.parcels2.ParcelWorldId -import io.dico.parcels2.PlayerProfile -import io.dico.parcels2.storage.toByteArray -import io.dico.parcels2.storage.toUUID -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.statements.UpdateBuilder -import org.joda.time.DateTime -import java.util.UUID - -abstract class IdTransactionsTable, QueryObj>(tableName: String, columnName: String) - : Table(tableName) { - val id = integer(columnName).autoIncrement().primaryKey() - - @Suppress("UNCHECKED_CAST") - inline val table: TableT - get() = this as TableT - - internal inline fun getId(where: SqlExpressionBuilder.(TableT) -> Op): Int? { - return select { where(table) }.firstOrNull()?.let { it[id] } - } - - 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 number") - } - - abstract fun getId(obj: QueryObj): Int? - abstract fun getOrInitId(obj: QueryObj): Int - 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("parcels_worlds", "world_id") { - val name = varchar("name", 50) - val uid = binary("uid", 16).nullable() - val creation_time = datetime("creation_time").nullable() - val index_name = uniqueIndexR("index_name", name) - val index_uid = uniqueIndexR("index_uid", uid) - - 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 -> - 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 getItem(row: ResultRow): ParcelWorldId { - return ParcelWorldId(row[name], row[uid]?.toUUID()) - } - - fun getWorldCreationTime(worldId: ParcelWorldId): DateTime? { - val id = getId(worldId) ?: return null - return select { WorldsT.id eq id }.firstOrNull()?.let { it[WorldsT.creation_time] } - } - - fun setWorldCreationTime(worldId: ParcelWorldId, time: DateTime) { - val id = getOrInitId(worldId) - update({ WorldsT.id eq id }) { - it[WorldsT.creation_time] = time - } - } -} - -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(ProfilesT.id).nullable() - val sign_oudated = bool("sign_outdated").default(false) - val claim_time = datetime("claim_time").nullable() - val index_location = uniqueIndexR("index_location", world_id, px, pz) - - private inline fun getId(worldId: Int, parcelX: Int, parcelZ: Int): Int? = getId { world_id.eq(worldId) and px.eq(parcelX) and pz.eq(parcelZ) } - 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 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) - override fun getOrInitId(parcel: ParcelId): Int = getOrInitId(parcel.worldId.name, parcel.worldId.uid, parcel.x, parcel.z) - - 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 getItem(row: ResultRow): ParcelId? { - val worldId = row[world_id] - val world = WorldsT.getItem(worldId) ?: return null - return ParcelId(world, row[px], row[pz]) - } -} - -object ProfilesT : IdTransactionsTable("parcels_profiles", "owner_id") { - val uuid = binary("uuid", 16).nullable() - val name = varchar("name", 32).nullable() - - // MySQL dialect MUST permit multiple null values for this to work. Server SQL does not allow this. That dialect is shit anyway. - val uuid_constraint = uniqueIndexR("uuid_constraint", uuid) - 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.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 -> - getOrInitId( - { getId(binaryUuid) }, - { it[this@ProfilesT.uuid] = binaryUuid; it[this@ProfilesT.name] = name }, - { "profile(uuid = $uuid, name = $name)" }) - } - - private inline fun getOrInitId(name: String) = getOrInitId( - { getId(name) }, - { it[ProfilesT.name] = name }, - { "owner(name = $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.nameOrBukkitName) - is PlayerProfile.Fake -> getOrInitId(profile.name) - else -> throw IllegalArgumentException() // Unresolved profiles cannot be added to the database - } - - 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 - } - - /* - fun updatePlayerProfile(profile: PlayerProfile.Real) { - update({ uuid eq profile.uuid.toByteArray() }) { - it[name] = profile.nameOrBukkitName - } - }*/ - -} - +@file:Suppress("NOTHING_TO_INLINE", "PARAMETER_NAME_CHANGED_ON_OVERRIDE", "unused", "MemberVisibilityCanBePrivate") + +package io.dico.parcels2.storage.exposed + +import io.dico.parcels2.ParcelId +import io.dico.parcels2.ParcelWorldId +import io.dico.parcels2.PlayerProfile +import io.dico.parcels2.storage.toByteArray +import io.dico.parcels2.storage.toUUID +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.statements.UpdateBuilder +import org.joda.time.DateTime +import java.util.UUID + +abstract class IdTransactionsTable, QueryObj>(tableName: String, columnName: String) + : Table(tableName) { + val id = integer(columnName).autoIncrement().primaryKey() + + @Suppress("UNCHECKED_CAST") + inline val table: TableT + get() = this as TableT + + internal inline fun getId(where: SqlExpressionBuilder.(TableT) -> Op): Int? { + return select { where(table) }.firstOrNull()?.let { it[id] } + } + + 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 number") + } + + abstract fun getId(obj: QueryObj): Int? + abstract fun getOrInitId(obj: QueryObj): Int + 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("parcels_worlds", "world_id") { + val name = varchar("name", 50) + val uid = binary("uid", 16).nullable() + val creation_time = datetime("creation_time").nullable() + val index_name = uniqueIndexR("index_name", name) + val index_uid = uniqueIndexR("index_uid", uid) + + 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 -> + 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 getItem(row: ResultRow): ParcelWorldId { + return ParcelWorldId(row[name], row[uid]?.toUUID()) + } + + fun getWorldCreationTime(worldId: ParcelWorldId): DateTime? { + val id = getId(worldId) ?: return null + return select { WorldsT.id eq id }.firstOrNull()?.let { it[WorldsT.creation_time] } + } + + fun setWorldCreationTime(worldId: ParcelWorldId, time: DateTime) { + val id = getOrInitId(worldId) + update({ WorldsT.id eq id }) { + it[WorldsT.creation_time] = time + } + } +} + +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(ProfilesT.id).nullable() + val sign_oudated = bool("sign_outdated").default(false) + val claim_time = datetime("claim_time").nullable() + val index_location = uniqueIndexR("index_location", world_id, px, pz) + + private inline fun getId(worldId: Int, parcelX: Int, parcelZ: Int): Int? = getId { world_id.eq(worldId) and px.eq(parcelX) and pz.eq(parcelZ) } + 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 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) + override fun getOrInitId(parcel: ParcelId): Int = getOrInitId(parcel.worldId.name, parcel.worldId.uid, parcel.x, parcel.z) + + 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 getItem(row: ResultRow): ParcelId? { + val worldId = row[world_id] + val world = WorldsT.getItem(worldId) ?: return null + return ParcelId(world, row[px], row[pz]) + } +} + +object ProfilesT : IdTransactionsTable("parcels_profiles", "owner_id") { + val uuid = binary("uuid", 16).nullable() + val name = varchar("name", 32).nullable() + + // MySQL dialect MUST permit multiple null values for this to work. Server SQL does not allow this. That dialect is shit anyway. + val uuid_constraint = uniqueIndexR("uuid_constraint", uuid) + 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.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 -> + getOrInitId( + { getId(binaryUuid) }, + { it[this@ProfilesT.uuid] = binaryUuid; it[this@ProfilesT.name] = name }, + { "profile(uuid = $uuid, name = $name)" }) + } + + private inline fun getOrInitId(name: String) = getOrInitId( + { getId(name) }, + { it[ProfilesT.name] = name }, + { "owner(name = $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.nameOrBukkitName) + is PlayerProfile.Fake -> getOrInitId(profile.name) + else -> throw IllegalArgumentException() // Unresolved profiles cannot be added to the database + } + + 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 + } + + /* + fun updatePlayerProfile(profile: PlayerProfile.Real) { + update({ uuid eq profile.uuid.toByteArray() }) { + it[name] = profile.nameOrBukkitName + } + }*/ + +} + // 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 b9d16fc..26cfc7a 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/exposed/ListTables.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/exposed/ListTables.kt @@ -1,111 +1,111 @@ -@file:Suppress("PropertyName", "LocalVariableName", "NOTHING_TO_INLINE") - -package io.dico.parcels2.storage.exposed - -import io.dico.parcels2.* -import io.dico.parcels2.Privilege.DEFAULT -import io.dico.parcels2.util.ext.alsoIfTrue -import kotlinx.coroutines.channels.SendChannel -import org.jetbrains.exposed.sql.* - -object PrivilegesLocalT : PrivilegesTable("parcels_privilege_local", ParcelsT) -object PrivilegesGlobalT : PrivilegesTable("parcels_privilege_global", ProfilesT) - -object ParcelOptionsT : Table("parcels_options") { - val parcel_id = integer("parcel_id").primaryKey().references(ParcelsT.id, ReferenceOption.CASCADE) - val interact_bitmask = binary("interact_bitmask", 4) -} - -typealias PrivilegesSendChannel = SendChannel> - -sealed class PrivilegesTable(name: String, val idTable: IdTransactionsTable<*, AttachT>) : Table(name) { - val attach_id = integer("attach_id").references(idTable.id, ReferenceOption.CASCADE) - val profile_id = integer("profile_id").references(ProfilesT.id, ReferenceOption.CASCADE) - val privilege = integer("privilege") - val index_pair = uniqueIndexR("index_pair", attach_id, profile_id) - - fun setPrivilege(attachedOn: AttachT, player: PlayerProfile.Real, privilege: Privilege) { - privilege.requireNonTransient() - - if (privilege == DEFAULT) { - 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 holder = idTable.getOrInitId(attachedOn) - val player_id = ProfilesT.getOrInitId(player) - upsert(conflictIndex = index_pair) { - it[attach_id] = holder - it[profile_id] = player_id - it[this.privilege] = privilege.number - } - } - - fun readPrivileges(id: Int): PrivilegesHolder? { - val list = slice(profile_id, privilege).select { attach_id eq id } - val result = PrivilegesHolder() - for (row in list) { - val profile = ProfilesT.getRealItem(row[profile_id]) ?: continue - result.setRawStoredPrivilege(profile, Privilege.getByNumber(row[privilege]) ?: continue) - } - return result - } - - fun sendAllPrivilegesH(channel: PrivilegesSendChannel) { - val iterator = selectAll().orderBy(attach_id).iterator() - - if (iterator.hasNext()) { - var row = iterator.next() - var id: Int = row[attach_id] - var attach: AttachT? = null - var map: PrivilegesHolder? = null - - fun initAttachAndMap() { - attach = idTable.getItem(id) - map = attach?.let { PrivilegesHolder() } - } - - fun sendIfPresent() { - if (attach != null && map != null) { - channel.offer(attach!! to map!!) - } - attach = null - map = null - } - - initAttachAndMap() - - do { - val rowId = row[attach_id] - if (rowId != id) { - sendIfPresent() - id = rowId - initAttachAndMap() - } - - if (attach == null) { - continue // owner not found for this owner id - } - - val profile = ProfilesT.getRealItem(row[profile_id]) - if (profile == null) { - logger.error("Privilege from database is null, id ${row[profile_id]}") - continue - } - val privilege = Privilege.getByNumber(row[privilege]) - if (privilege == null) { - logger.error("Privilege from database is null, number ${row[this.privilege]}") - continue - } - map!!.setRawStoredPrivilege(profile, privilege) - - } while (iterator.hasNext().alsoIfTrue { row = iterator.next() }) - - sendIfPresent() - } - } - -} +@file:Suppress("PropertyName", "LocalVariableName", "NOTHING_TO_INLINE") + +package io.dico.parcels2.storage.exposed + +import io.dico.parcels2.* +import io.dico.parcels2.Privilege.DEFAULT +import io.dico.parcels2.util.ext.alsoIfTrue +import kotlinx.coroutines.channels.SendChannel +import org.jetbrains.exposed.sql.* + +object PrivilegesLocalT : PrivilegesTable("parcels_privilege_local", ParcelsT) +object PrivilegesGlobalT : PrivilegesTable("parcels_privilege_global", ProfilesT) + +object ParcelOptionsT : Table("parcels_options") { + val parcel_id = integer("parcel_id").primaryKey().references(ParcelsT.id, ReferenceOption.CASCADE) + val interact_bitmask = binary("interact_bitmask", 4) +} + +typealias PrivilegesSendChannel = SendChannel> + +sealed class PrivilegesTable(name: String, val idTable: IdTransactionsTable<*, AttachT>) : Table(name) { + val attach_id = integer("attach_id").references(idTable.id, ReferenceOption.CASCADE) + val profile_id = integer("profile_id").references(ProfilesT.id, ReferenceOption.CASCADE) + val privilege = integer("privilege") + val index_pair = uniqueIndexR("index_pair", attach_id, profile_id) + + fun setPrivilege(attachedOn: AttachT, player: PlayerProfile.Real, privilege: Privilege) { + privilege.requireNonTransient() + + if (privilege == DEFAULT) { + 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 holder = idTable.getOrInitId(attachedOn) + val player_id = ProfilesT.getOrInitId(player) + upsert(conflictIndex = index_pair) { + it[attach_id] = holder + it[profile_id] = player_id + it[this.privilege] = privilege.number + } + } + + fun readPrivileges(id: Int): PrivilegesHolder? { + val list = slice(profile_id, privilege).select { attach_id eq id } + val result = PrivilegesHolder() + for (row in list) { + val profile = ProfilesT.getRealItem(row[profile_id]) ?: continue + result.setRawStoredPrivilege(profile, Privilege.getByNumber(row[privilege]) ?: continue) + } + return result + } + + fun sendAllPrivilegesH(channel: PrivilegesSendChannel) { + val iterator = selectAll().orderBy(attach_id).iterator() + + if (iterator.hasNext()) { + var row = iterator.next() + var id: Int = row[attach_id] + var attach: AttachT? = null + var map: PrivilegesHolder? = null + + fun initAttachAndMap() { + attach = idTable.getItem(id) + map = attach?.let { PrivilegesHolder() } + } + + fun sendIfPresent() { + if (attach != null && map != null) { + channel.offer(attach!! to map!!) + } + attach = null + map = null + } + + initAttachAndMap() + + do { + val rowId = row[attach_id] + if (rowId != id) { + sendIfPresent() + id = rowId + initAttachAndMap() + } + + if (attach == null) { + continue // owner not found for this owner id + } + + val profile = ProfilesT.getRealItem(row[profile_id]) + if (profile == null) { + logger.error("Privilege from database is null, id ${row[profile_id]}") + continue + } + val privilege = Privilege.getByNumber(row[privilege]) + if (privilege == null) { + logger.error("Privilege from database is null, number ${row[this.privilege]}") + continue + } + map!!.setRawStoredPrivilege(profile, privilege) + + } while (iterator.hasNext().alsoIfTrue { row = iterator.next() }) + + sendIfPresent() + } + } + +} 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 acc7c5e..a512f2a 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/migration/Migration.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/migration/Migration.kt @@ -1,9 +1,9 @@ -package io.dico.parcels2.storage.migration - -import io.dico.parcels2.storage.Storage -import kotlinx.coroutines.Job - -interface Migration { - fun migrateTo(storage: Storage): Job -} - +package io.dico.parcels2.storage.migration + +import io.dico.parcels2.storage.Storage +import kotlinx.coroutines.Job + +interface Migration { + 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 831fe42..954da5d 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,118 +1,118 @@ -@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.exposed.abs -import io.dico.parcels2.storage.exposed.greaterOf -import io.dico.parcels2.storage.migration.Migration -import io.dico.parcels2.storage.migration.plotme.PlotmeTables.PlotmePlotPlayerMap -import io.dico.parcels2.storage.migration.plotme.PlotmeTables.PlotmeTable -import io.dico.parcels2.storage.toUUID -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import kotlinx.coroutines.newFixedThreadPoolContext -import org.jetbrains.exposed.sql.* -import org.slf4j.LoggerFactory -import java.sql.Blob -import java.util.UUID -import javax.sql.DataSource - -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") - private val tables = PlotmeTables(options.tableNamesUppercase) - 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(dispatcher) { - init() - doWork(storage) - shutdown() - } - } - - fun init() { - if (isShutdown || database != null) throw IllegalStateException() - dataSource = options.storage.getDataSourceFactory()!!() - database = Database.connect(dataSource!!) - } - - fun shutdown() { - if (isShutdown) throw IllegalStateException() - dataSource?.let { - (it as? HikariDataSource)?.close() - } - database = null - isShutdown = true - } - - suspend fun doWork(target: Storage) = with (tables) { - val exit = transaction { - (!PlotmePlots.exists()).also { - if (it) mlogger.warn("Plotme tables don't appear to exist. Exiting.") - } - } - if (exit) return - - val worldCache = options.worldsFromTo.mapValues { ParcelWorldId(it.value) } - - 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]) - } - - fun PlotmePlotPlayerMap.transmitPlotmeAddedTable(kind: Privilege) { - selectAll().forEach { row -> - val parcel = getParcelId(this, row) ?: return@forEach - val profile = PrivilegeKey.safe(row[player_uuid]?.toUUID(), row[player_name]) ?: return@forEach - target.setLocalPrivilege(parcel, profile, kind) - } - } - - mlogger.info("Transmitting data from plotmeplots table") - var count = 0 - transaction { - - PlotmePlots.selectAll() - .orderBy(PlotmePlots.world_name) - .orderBy(greaterOf(PlotmePlots.px.abs(), PlotmePlots.pz.abs())) - .forEach { row -> - val parcel = getParcelId(PlotmePlots, row) ?: return@forEach - val owner = PlayerProfile.safe(row[PlotmePlots.owner_uuid]?.toUUID(), row[PlotmePlots.owner_name]) - target.setParcelOwner(parcel, owner) - target.setParcelOwnerSignOutdated(parcel, true) - ++count - } - } - - mlogger.info("Transmitting data from plotmeallowed table") - transaction { - PlotmeAllowed.transmitPlotmeAddedTable(Privilege.CAN_BUILD) - } - - mlogger.info("Transmitting data from plotmedenied table") - transaction { - PlotmeDenied.transmitPlotmeAddedTable(Privilege.BANNED) - } - - mlogger.warn("Data has been **transmitted**. $count plots were migrated to the parcels database.") - mlogger.warn("Loading parcel data might take a while as enqueued transactions from this migration are completed.") - } - - private fun Blob.toUUID(): UUID? { - val ba = ByteArray(16) - val count = binaryStream.read(ba, 0, 16) - if (count < 16) return null - return ba.toUUID() - } - - +@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.exposed.abs +import io.dico.parcels2.storage.exposed.greaterOf +import io.dico.parcels2.storage.migration.Migration +import io.dico.parcels2.storage.migration.plotme.PlotmeTables.PlotmePlotPlayerMap +import io.dico.parcels2.storage.migration.plotme.PlotmeTables.PlotmeTable +import io.dico.parcels2.storage.toUUID +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.newFixedThreadPoolContext +import org.jetbrains.exposed.sql.* +import org.slf4j.LoggerFactory +import java.sql.Blob +import java.util.UUID +import javax.sql.DataSource + +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") + private val tables = PlotmeTables(options.tableNamesUppercase) + 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(dispatcher) { + init() + doWork(storage) + shutdown() + } + } + + fun init() { + if (isShutdown || database != null) throw IllegalStateException() + dataSource = options.storage.getDataSourceFactory()!!() + database = Database.connect(dataSource!!) + } + + fun shutdown() { + if (isShutdown) throw IllegalStateException() + dataSource?.let { + (it as? HikariDataSource)?.close() + } + database = null + isShutdown = true + } + + suspend fun doWork(target: Storage) = with (tables) { + val exit = transaction { + (!PlotmePlots.exists()).also { + if (it) mlogger.warn("Plotme tables don't appear to exist. Exiting.") + } + } + if (exit) return + + val worldCache = options.worldsFromTo.mapValues { ParcelWorldId(it.value) } + + 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]) + } + + fun PlotmePlotPlayerMap.transmitPlotmeAddedTable(kind: Privilege) { + selectAll().forEach { row -> + val parcel = getParcelId(this, row) ?: return@forEach + val profile = PrivilegeKey.safe(row[player_uuid]?.toUUID(), row[player_name]) ?: return@forEach + target.setLocalPrivilege(parcel, profile, kind) + } + } + + mlogger.info("Transmitting data from plotmeplots table") + var count = 0 + transaction { + + PlotmePlots.selectAll() + .orderBy(PlotmePlots.world_name) + .orderBy(greaterOf(PlotmePlots.px.abs(), PlotmePlots.pz.abs())) + .forEach { row -> + val parcel = getParcelId(PlotmePlots, row) ?: return@forEach + val owner = PlayerProfile.safe(row[PlotmePlots.owner_uuid]?.toUUID(), row[PlotmePlots.owner_name]) + target.setParcelOwner(parcel, owner) + target.setParcelOwnerSignOutdated(parcel, true) + ++count + } + } + + mlogger.info("Transmitting data from plotmeallowed table") + transaction { + PlotmeAllowed.transmitPlotmeAddedTable(Privilege.CAN_BUILD) + } + + mlogger.info("Transmitting data from plotmedenied table") + transaction { + PlotmeDenied.transmitPlotmeAddedTable(Privilege.BANNED) + } + + mlogger.warn("Data has been **transmitted**. $count plots were migrated to the parcels database.") + mlogger.warn("Loading parcel data might take a while as enqueued transactions from this migration are completed.") + } + + private fun Blob.toUUID(): UUID? { + val ba = ByteArray(16) + val count = binaryStream.read(ba, 0, 16) + if (count < 16) return null + return ba.toUUID() + } + + } \ No newline at end of file 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 dc788c8..b276e11 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 @@ -1,31 +1,31 @@ -package io.dico.parcels2.storage.migration.plotme - -import org.jetbrains.exposed.sql.Table - -class PlotmeTables(val uppercase: Boolean) { - fun String.toCorrectCase() = if (uppercase) this else toLowerCase() - - val PlotmePlots = PlotmePlotsT() - val PlotmeAllowed = PlotmeAllowedT() - val PlotmeDenied = PlotmeDeniedT() - - inner abstract class PlotmeTable(name: String) : Table(name) { - val px = integer("idX").primaryKey() - val pz = integer("idZ").primaryKey() - val world_name = varchar("world", 32).primaryKey() - } - - inner abstract class PlotmePlotPlayerMap(name: String) : PlotmeTable(name) { - val player_name = varchar("player", 32) - val player_uuid = blob("playerid").nullable() - } - - inner class PlotmePlotsT : PlotmeTable("plotmePlots".toCorrectCase()) { - val owner_name = varchar("owner", 32) - val owner_uuid = blob("ownerid").nullable() - } - - inner class PlotmeAllowedT : PlotmePlotPlayerMap("plotmeAllowed".toCorrectCase()) - inner class PlotmeDeniedT : PlotmePlotPlayerMap("plotmeDenied".toCorrectCase()) -} - +package io.dico.parcels2.storage.migration.plotme + +import org.jetbrains.exposed.sql.Table + +class PlotmeTables(val uppercase: Boolean) { + fun String.toCorrectCase() = if (uppercase) this else toLowerCase() + + val PlotmePlots = PlotmePlotsT() + val PlotmeAllowed = PlotmeAllowedT() + val PlotmeDenied = PlotmeDeniedT() + + inner abstract class PlotmeTable(name: String) : Table(name) { + val px = integer("idX").primaryKey() + val pz = integer("idZ").primaryKey() + val world_name = varchar("world", 32).primaryKey() + } + + inner abstract class PlotmePlotPlayerMap(name: String) : PlotmeTable(name) { + val player_name = varchar("player", 32) + val player_uuid = blob("playerid").nullable() + } + + inner class PlotmePlotsT : PlotmeTable("plotmePlots".toCorrectCase()) { + val owner_name = varchar("owner", 32) + val owner_uuid = blob("ownerid").nullable() + } + + inner class PlotmeAllowedT : PlotmePlotPlayerMap("plotmeAllowed".toCorrectCase()) + inner class PlotmeDeniedT : PlotmePlotPlayerMap("plotmeDenied".toCorrectCase()) +} + diff --git a/src/main/kotlin/io/dico/parcels2/util/BukkitUtil.kt b/src/main/kotlin/io/dico/parcels2/util/BukkitUtil.kt index 618eaed..a4a6da9 100644 --- a/src/main/kotlin/io/dico/parcels2/util/BukkitUtil.kt +++ b/src/main/kotlin/io/dico/parcels2/util/BukkitUtil.kt @@ -1,14 +1,14 @@ -package io.dico.parcels2.util - -import io.dico.parcels2.util.ext.isValid -import org.bukkit.Bukkit -import org.bukkit.OfflinePlayer -import java.util.UUID - -fun getPlayerName(uuid: UUID): String? = getOfflinePlayer(uuid)?.name - -fun getOfflinePlayer(uuid: UUID): OfflinePlayer? = Bukkit.getOfflinePlayer(uuid).takeIf { it.isValid } - -fun getOfflinePlayer(name: String): OfflinePlayer? = Bukkit.getOfflinePlayer(name).takeIf { it.isValid } - -fun isServerThread(): Boolean = Thread.currentThread().name == "Server thread" +package io.dico.parcels2.util + +import io.dico.parcels2.util.ext.isValid +import org.bukkit.Bukkit +import org.bukkit.OfflinePlayer +import java.util.UUID + +fun getPlayerName(uuid: UUID): String? = getOfflinePlayer(uuid)?.name + +fun getOfflinePlayer(uuid: UUID): OfflinePlayer? = Bukkit.getOfflinePlayer(uuid).takeIf { it.isValid } + +fun getOfflinePlayer(name: String): OfflinePlayer? = Bukkit.getOfflinePlayer(name).takeIf { it.isValid } + +fun isServerThread(): Boolean = Thread.currentThread().name == "Server thread" diff --git a/src/main/kotlin/io/dico/parcels2/util/MainThreadDispatcher.kt b/src/main/kotlin/io/dico/parcels2/util/MainThreadDispatcher.kt index 3eb2e81..3904026 100644 --- a/src/main/kotlin/io/dico/parcels2/util/MainThreadDispatcher.kt +++ b/src/main/kotlin/io/dico/parcels2/util/MainThreadDispatcher.kt @@ -1,43 +1,43 @@ -package io.dico.parcels2.util - -import kotlinx.coroutines.CancellableContinuation -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Delay -import kotlinx.coroutines.Runnable -import kotlinx.coroutines.timeunit.TimeUnit -import org.bukkit.plugin.Plugin -import kotlin.coroutines.CoroutineContext - -abstract class MainThreadDispatcher : CoroutineDispatcher(), Delay { - abstract val mainThread: Thread - abstract fun runOnMainThread(task: Runnable) -} - -@Suppress("FunctionName") -fun MainThreadDispatcher(plugin: Plugin): MainThreadDispatcher { - return object : MainThreadDispatcher() { - override val mainThread: Thread = Thread.currentThread() - - override fun dispatch(context: CoroutineContext, block: Runnable) { - doDispatch(block) - } - - override fun runOnMainThread(task: Runnable) { - doDispatch(task) - } - - private fun doDispatch(task: Runnable) { - if (Thread.currentThread() === mainThread) task.run() - else plugin.server.scheduler.runTaskLater(plugin, task, 0) - } - - override fun scheduleResumeAfterDelay(time: Long, unit: TimeUnit, continuation: CancellableContinuation) { - val task = Runnable { - with (continuation) { resumeUndispatched(Unit) } - } - - val millis = unit.toMillis(time) - plugin.server.scheduler.runTaskLater(plugin, task, (millis + 25) / 50 - 1) - } - } +package io.dico.parcels2.util + +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Delay +import kotlinx.coroutines.Runnable +import kotlinx.coroutines.timeunit.TimeUnit +import org.bukkit.plugin.Plugin +import kotlin.coroutines.CoroutineContext + +abstract class MainThreadDispatcher : CoroutineDispatcher(), Delay { + abstract val mainThread: Thread + abstract fun runOnMainThread(task: Runnable) +} + +@Suppress("FunctionName") +fun MainThreadDispatcher(plugin: Plugin): MainThreadDispatcher { + return object : MainThreadDispatcher() { + override val mainThread: Thread = Thread.currentThread() + + override fun dispatch(context: CoroutineContext, block: Runnable) { + doDispatch(block) + } + + override fun runOnMainThread(task: Runnable) { + doDispatch(task) + } + + private fun doDispatch(task: Runnable) { + if (Thread.currentThread() === mainThread) task.run() + else plugin.server.scheduler.runTaskLater(plugin, task, 0) + } + + override fun scheduleResumeAfterDelay(time: Long, unit: TimeUnit, continuation: CancellableContinuation) { + val task = Runnable { + with (continuation) { resumeUndispatched(Unit) } + } + + val millis = unit.toMillis(time) + plugin.server.scheduler.runTaskLater(plugin, task, (millis + 25) / 50 - 1) + } + } } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/util/PluginScheduler.kt b/src/main/kotlin/io/dico/parcels2/util/PluginScheduler.kt index f29ba2b..268a083 100644 --- a/src/main/kotlin/io/dico/parcels2/util/PluginScheduler.kt +++ b/src/main/kotlin/io/dico/parcels2/util/PluginScheduler.kt @@ -1,20 +1,20 @@ -package io.dico.parcels2.util - -import org.bukkit.plugin.Plugin -import org.bukkit.scheduler.BukkitTask - -interface PluginScheduler { - val plugin: Plugin - - fun schedule(delay: Int, task: () -> Unit): BukkitTask { - return plugin.server.scheduler.runTaskLater(plugin, task, delay.toLong()) - } - - fun scheduleRepeating(delay: Int, interval: Int, task: () -> Unit): BukkitTask { - return plugin.server.scheduler.runTaskTimer(plugin, task, delay.toLong(), interval.toLong()) - } -} - -@Suppress("NOTHING_TO_INLINE") -inline fun PluginScheduler.schedule(noinline task: () -> Unit) = schedule(0, task) - +package io.dico.parcels2.util + +import org.bukkit.plugin.Plugin +import org.bukkit.scheduler.BukkitTask + +interface PluginScheduler { + val plugin: Plugin + + fun schedule(delay: Int, task: () -> Unit): BukkitTask { + return plugin.server.scheduler.runTaskLater(plugin, task, delay.toLong()) + } + + fun scheduleRepeating(delay: Int, interval: Int, task: () -> Unit): BukkitTask { + return plugin.server.scheduler.runTaskTimer(plugin, task, delay.toLong(), interval.toLong()) + } +} + +@Suppress("NOTHING_TO_INLINE") +inline fun PluginScheduler.schedule(noinline task: () -> Unit) = schedule(0, task) + diff --git a/src/main/kotlin/io/dico/parcels2/util/ext/Material.kt b/src/main/kotlin/io/dico/parcels2/util/ext/Material.kt index e160e55..1351b5d 100644 --- a/src/main/kotlin/io/dico/parcels2/util/ext/Material.kt +++ b/src/main/kotlin/io/dico/parcels2/util/ext/Material.kt @@ -1,108 +1,108 @@ -package io.dico.parcels2.util.ext - -import org.bukkit.Material -import org.bukkit.Material.* - -/* -colors: -WHITE_$, ORANGE_$, MAGENTA_$, LIGHT_BLUE_$, YELLOW_$, LIME_$, PINK_$, GRAY_$, LIGHT_GRAY_$, CYAN_$, PURPLE_$, BLUE_$, BROWN_$, GREEN_$, RED_$, BLACK_$, -wood: -OAK_$, BIRCH_$, SPRUCE_$, JUNGLE_$, ACACIA_$, DARK_OAK_$, - */ - -val Material.isBed - get() = when (this) { - 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 -> true - else -> false - } - -val Material.isWoodDoor - get() = when (this) { - OAK_DOOR, - BIRCH_DOOR, - SPRUCE_DOOR, - JUNGLE_DOOR, - ACACIA_DOOR, - DARK_OAK_DOOR -> true - else -> false - } - -val Material.isWoodTrapdoor - get() = when (this) { - OAK_TRAPDOOR, - BIRCH_TRAPDOOR, - SPRUCE_TRAPDOOR, - JUNGLE_TRAPDOOR, - ACACIA_TRAPDOOR, - DARK_OAK_TRAPDOOR -> true - else -> false - } - -val Material.isWoodFenceGate - get() = when (this) { - OAK_FENCE_GATE, - BIRCH_FENCE_GATE, - SPRUCE_FENCE_GATE, - JUNGLE_FENCE_GATE, - ACACIA_FENCE_GATE, - DARK_OAK_FENCE_GATE -> true - else -> false - } - -val Material.isWoodButton - get() = when (this) { - OAK_BUTTON, - BIRCH_BUTTON, - SPRUCE_BUTTON, - JUNGLE_BUTTON, - ACACIA_BUTTON, - DARK_OAK_BUTTON -> true - else -> false - } - -private fun getMaterialPrefixed(prefix: String, name: String): Material { - return Material.getMaterial("${prefix}_$name") ?: throw IllegalArgumentException("Material ${prefix}_$name doesn't exist") -} - -fun getMaterialsWithWoodTypePrefix(name: String) = arrayOf( - getMaterialPrefixed("OAK", name), - getMaterialPrefixed("BIRCH", name), - getMaterialPrefixed("SPRUCE", name), - getMaterialPrefixed("JUNGLE", name), - getMaterialPrefixed("ACACIA", name), - getMaterialPrefixed("DARK_OAK", name) -) - -fun getMaterialsWithWoolColorPrefix(name: String) = arrayOf( - getMaterialPrefixed("WHITE", name), - getMaterialPrefixed("ORANGE", name), - getMaterialPrefixed("MAGENTA", name), - getMaterialPrefixed("LIGHT_BLUE", name), - getMaterialPrefixed("YELLOW", name), - getMaterialPrefixed("LIME", name), - getMaterialPrefixed("PINK", name), - getMaterialPrefixed("GRAY", name), - getMaterialPrefixed("LIGHT_GRAY", name), - getMaterialPrefixed("CYAN", name), - getMaterialPrefixed("PURPLE", name), - getMaterialPrefixed("BLUE", name), - getMaterialPrefixed("BROWN", name), - getMaterialPrefixed("GREEN", name), - getMaterialPrefixed("RED", name), - getMaterialPrefixed("BLACK", name) +package io.dico.parcels2.util.ext + +import org.bukkit.Material +import org.bukkit.Material.* + +/* +colors: +WHITE_$, ORANGE_$, MAGENTA_$, LIGHT_BLUE_$, YELLOW_$, LIME_$, PINK_$, GRAY_$, LIGHT_GRAY_$, CYAN_$, PURPLE_$, BLUE_$, BROWN_$, GREEN_$, RED_$, BLACK_$, +wood: +OAK_$, BIRCH_$, SPRUCE_$, JUNGLE_$, ACACIA_$, DARK_OAK_$, + */ + +val Material.isBed + get() = when (this) { + 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 -> true + else -> false + } + +val Material.isWoodDoor + get() = when (this) { + OAK_DOOR, + BIRCH_DOOR, + SPRUCE_DOOR, + JUNGLE_DOOR, + ACACIA_DOOR, + DARK_OAK_DOOR -> true + else -> false + } + +val Material.isWoodTrapdoor + get() = when (this) { + OAK_TRAPDOOR, + BIRCH_TRAPDOOR, + SPRUCE_TRAPDOOR, + JUNGLE_TRAPDOOR, + ACACIA_TRAPDOOR, + DARK_OAK_TRAPDOOR -> true + else -> false + } + +val Material.isWoodFenceGate + get() = when (this) { + OAK_FENCE_GATE, + BIRCH_FENCE_GATE, + SPRUCE_FENCE_GATE, + JUNGLE_FENCE_GATE, + ACACIA_FENCE_GATE, + DARK_OAK_FENCE_GATE -> true + else -> false + } + +val Material.isWoodButton + get() = when (this) { + OAK_BUTTON, + BIRCH_BUTTON, + SPRUCE_BUTTON, + JUNGLE_BUTTON, + ACACIA_BUTTON, + DARK_OAK_BUTTON -> true + else -> false + } + +private fun getMaterialPrefixed(prefix: String, name: String): Material { + return Material.getMaterial("${prefix}_$name") ?: throw IllegalArgumentException("Material ${prefix}_$name doesn't exist") +} + +fun getMaterialsWithWoodTypePrefix(name: String) = arrayOf( + getMaterialPrefixed("OAK", name), + getMaterialPrefixed("BIRCH", name), + getMaterialPrefixed("SPRUCE", name), + getMaterialPrefixed("JUNGLE", name), + getMaterialPrefixed("ACACIA", name), + getMaterialPrefixed("DARK_OAK", name) +) + +fun getMaterialsWithWoolColorPrefix(name: String) = arrayOf( + getMaterialPrefixed("WHITE", name), + getMaterialPrefixed("ORANGE", name), + getMaterialPrefixed("MAGENTA", name), + getMaterialPrefixed("LIGHT_BLUE", name), + getMaterialPrefixed("YELLOW", name), + getMaterialPrefixed("LIME", name), + getMaterialPrefixed("PINK", name), + getMaterialPrefixed("GRAY", name), + getMaterialPrefixed("LIGHT_GRAY", name), + getMaterialPrefixed("CYAN", name), + getMaterialPrefixed("PURPLE", name), + getMaterialPrefixed("BLUE", name), + getMaterialPrefixed("BROWN", name), + getMaterialPrefixed("GREEN", name), + getMaterialPrefixed("RED", name), + getMaterialPrefixed("BLACK", name) ) \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/util/ext/Misc.kt b/src/main/kotlin/io/dico/parcels2/util/ext/Misc.kt index 75aba35..e5e8aa9 100644 --- a/src/main/kotlin/io/dico/parcels2/util/ext/Misc.kt +++ b/src/main/kotlin/io/dico/parcels2/util/ext/Misc.kt @@ -1,81 +1,81 @@ -package io.dico.parcels2.util.ext - -import io.dico.dicore.Formatting -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") - return false - } - 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() -//inline fun T?.isPresentAnd(condition: T.() -> Boolean): Boolean = this != null && condition() -inline fun T?.ifNullRun(block: () -> Unit): T? { - if (this == null) block() - return this -} - -inline fun MutableMap.editLoop(block: EditLoopScope.(T, U) -> Unit) { - return EditLoopScope(this).doEditLoop(block) -} - -inline fun MutableMap.editLoop(block: EditLoopScope.() -> Unit) { - return EditLoopScope(this).doEditLoop(block) -} - -class EditLoopScope(val _map: MutableMap) { - private var iterator: MutableIterator>? = null - lateinit var _entry: MutableMap.MutableEntry - - inline val key get() = _entry.key - inline var value - get() = _entry.value - set(target) = run { _entry.setValue(target) } - - inline fun doEditLoop(block: EditLoopScope.() -> Unit) { - val it = _initIterator() - while (it.hasNext()) { - _entry = it.next() - block() - } - } - - inline fun doEditLoop(block: EditLoopScope.(T, U) -> Unit) { - val it = _initIterator() - while (it.hasNext()) { - val entry = it.next().also { _entry = it } - block(entry.key, entry.value) - } - } - - fun remove() { - iterator!!.remove() - } - - fun _initIterator(): MutableIterator> { - iterator?.let { throw IllegalStateException() } - return _map.entries.iterator().also { iterator = it } - } - -} - -operator fun Formatting.plus(other: Formatting) = toString() + other -operator fun Formatting.plus(other: String) = toString() + other - -inline fun Pair.forEach(block: (T) -> Unit) { - block(first) - block(second) -} +package io.dico.parcels2.util.ext + +import io.dico.dicore.Formatting +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") + return false + } + 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() +//inline fun T?.isPresentAnd(condition: T.() -> Boolean): Boolean = this != null && condition() +inline fun T?.ifNullRun(block: () -> Unit): T? { + if (this == null) block() + return this +} + +inline fun MutableMap.editLoop(block: EditLoopScope.(T, U) -> Unit) { + return EditLoopScope(this).doEditLoop(block) +} + +inline fun MutableMap.editLoop(block: EditLoopScope.() -> Unit) { + return EditLoopScope(this).doEditLoop(block) +} + +class EditLoopScope(val _map: MutableMap) { + private var iterator: MutableIterator>? = null + lateinit var _entry: MutableMap.MutableEntry + + inline val key get() = _entry.key + inline var value + get() = _entry.value + set(target) = run { _entry.setValue(target) } + + inline fun doEditLoop(block: EditLoopScope.() -> Unit) { + val it = _initIterator() + while (it.hasNext()) { + _entry = it.next() + block() + } + } + + inline fun doEditLoop(block: EditLoopScope.(T, U) -> Unit) { + val it = _initIterator() + while (it.hasNext()) { + val entry = it.next().also { _entry = it } + block(entry.key, entry.value) + } + } + + fun remove() { + iterator!!.remove() + } + + fun _initIterator(): MutableIterator> { + iterator?.let { throw IllegalStateException() } + return _map.entries.iterator().also { iterator = it } + } + +} + +operator fun Formatting.plus(other: Formatting) = toString() + other +operator fun Formatting.plus(other: String) = toString() + other + +inline fun Pair.forEach(block: (T) -> Unit) { + block(first) + block(second) +} diff --git a/src/main/kotlin/io/dico/parcels2/util/ext/Player.kt b/src/main/kotlin/io/dico/parcels2/util/ext/Player.kt index a7f21c5..4c502a0 100644 --- a/src/main/kotlin/io/dico/parcels2/util/ext/Player.kt +++ b/src/main/kotlin/io/dico/parcels2/util/ext/Player.kt @@ -1,57 +1,57 @@ -package io.dico.parcels2.util.ext - -import io.dico.dicore.Formatting -import io.dico.parcels2.ParcelsPlugin -import io.dico.parcels2.logger -import org.bukkit.OfflinePlayer -import org.bukkit.entity.Player -import org.bukkit.permissions.Permissible -import org.bukkit.plugin.java.JavaPlugin - -inline val OfflinePlayer.uuid get() = uniqueId - -@Suppress("UsePropertyAccessSyntax") -inline val OfflinePlayer.isValid - get() = isOnline() || hasPlayedBefore() - -const val PERM_BAN_BYPASS = "parcels.admin.bypass.ban" -const val PERM_BUILD_ANYWHERE = "parcels.admin.bypass.build" -const val PERM_ADMIN_MANAGE = "parcels.admin.manage" - -inline val Permissible.hasPermBanBypass get() = hasPermission(PERM_BAN_BYPASS) -inline val Permissible.hasPermGamemodeBypass get() = hasPermission("parcels.admin.bypass.gamemode") -inline val Permissible.hasPermBuildAnywhere get() = hasPermission(PERM_BUILD_ANYWHERE) -inline val Permissible.hasPermAdminManage get() = hasPermission(PERM_ADMIN_MANAGE) -inline val Permissible.hasParcelHomeOthers get() = hasPermission("parcels.command.home.others") -inline val Permissible.hasPermRandomSpecific get() = hasPermission("parcels.command.random.specific") -val Player.parcelLimit: Int - get() { - for (info in effectivePermissions) { - val perm = info.permission - if (perm.startsWith("parcels.limit.")) { - val limitString = perm.substring("parcels.limit.".length) - if (limitString == "*") { - return Int.MAX_VALUE - } - return limitString.toIntOrNull() ?: DEFAULT_LIMIT.also { - logger.warn("$name has permission '$perm'. The suffix can not be parsed to an integer (or *).") - } - } - } - return DEFAULT_LIMIT - } - -private const val DEFAULT_LIMIT = 1 -private val prefix = Formatting.translateChars('&', "&4[&c${JavaPlugin.getPlugin(ParcelsPlugin::class.java).name}&4] &a") - -fun Player.sendParcelMessage(except: Boolean = false, nopermit: Boolean = false, message: String) { - if (except) { - sendMessage(prefix + Formatting.YELLOW + Formatting.translateChars('&', message)) - } else if (nopermit) { - sendMessage(prefix + Formatting.RED + Formatting.translateChars('&', message)) - } else { - sendMessage(prefix + Formatting.translateChars('&', message)) - } -} - -const val PLAYER_NAME_PLACEHOLDER = ":unknown_name:" +package io.dico.parcels2.util.ext + +import io.dico.dicore.Formatting +import io.dico.parcels2.ParcelsPlugin +import io.dico.parcels2.logger +import org.bukkit.OfflinePlayer +import org.bukkit.entity.Player +import org.bukkit.permissions.Permissible +import org.bukkit.plugin.java.JavaPlugin + +inline val OfflinePlayer.uuid get() = uniqueId + +@Suppress("UsePropertyAccessSyntax") +inline val OfflinePlayer.isValid + get() = isOnline() || hasPlayedBefore() + +const val PERM_BAN_BYPASS = "parcels.admin.bypass.ban" +const val PERM_BUILD_ANYWHERE = "parcels.admin.bypass.build" +const val PERM_ADMIN_MANAGE = "parcels.admin.manage" + +inline val Permissible.hasPermBanBypass get() = hasPermission(PERM_BAN_BYPASS) +inline val Permissible.hasPermGamemodeBypass get() = hasPermission("parcels.admin.bypass.gamemode") +inline val Permissible.hasPermBuildAnywhere get() = hasPermission(PERM_BUILD_ANYWHERE) +inline val Permissible.hasPermAdminManage get() = hasPermission(PERM_ADMIN_MANAGE) +inline val Permissible.hasParcelHomeOthers get() = hasPermission("parcels.command.home.others") +inline val Permissible.hasPermRandomSpecific get() = hasPermission("parcels.command.random.specific") +val Player.parcelLimit: Int + get() { + for (info in effectivePermissions) { + val perm = info.permission + if (perm.startsWith("parcels.limit.")) { + val limitString = perm.substring("parcels.limit.".length) + if (limitString == "*") { + return Int.MAX_VALUE + } + return limitString.toIntOrNull() ?: DEFAULT_LIMIT.also { + logger.warn("$name has permission '$perm'. The suffix can not be parsed to an integer (or *).") + } + } + } + return DEFAULT_LIMIT + } + +private const val DEFAULT_LIMIT = 1 +private val prefix = Formatting.translateChars('&', "&4[&c${JavaPlugin.getPlugin(ParcelsPlugin::class.java).name}&4] &a") + +fun Player.sendParcelMessage(except: Boolean = false, nopermit: Boolean = false, message: String) { + if (except) { + sendMessage(prefix + Formatting.YELLOW + Formatting.translateChars('&', message)) + } else if (nopermit) { + sendMessage(prefix + Formatting.RED + Formatting.translateChars('&', message)) + } else { + sendMessage(prefix + Formatting.translateChars('&', message)) + } +} + +const val PLAYER_NAME_PLACEHOLDER = ":unknown_name:" diff --git a/src/main/kotlin/io/dico/parcels2/util/math/Dimension.kt b/src/main/kotlin/io/dico/parcels2/util/math/Dimension.kt index cf67148..5b16860 100644 --- a/src/main/kotlin/io/dico/parcels2/util/math/Dimension.kt +++ b/src/main/kotlin/io/dico/parcels2/util/math/Dimension.kt @@ -1,19 +1,19 @@ -package io.dico.parcels2.util.math - -enum class Dimension { - X, - Y, - Z; - - val otherDimensions - get() = when (this) { - X -> Y to Z - Y -> X to Z - Z -> X to Y - } - - companion object { - private val values = values() - operator fun get(ordinal: Int) = values[ordinal] - } +package io.dico.parcels2.util.math + +enum class Dimension { + X, + Y, + Z; + + val otherDimensions + get() = when (this) { + X -> Y to Z + Y -> X to Z + Z -> X to Y + } + + companion object { + private val values = values() + operator fun get(ordinal: Int) = values[ordinal] + } } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/util/math/Math.kt b/src/main/kotlin/io/dico/parcels2/util/math/Math.kt index 12c3e9f..5f8deef 100644 --- a/src/main/kotlin/io/dico/parcels2/util/math/Math.kt +++ b/src/main/kotlin/io/dico/parcels2/util/math/Math.kt @@ -1,42 +1,42 @@ -package io.dico.parcels2.util.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 -} - -// the name coerceAtMost is bad -fun Int.clampMax(max: Int) = coerceAtMost(max) -fun Double.clampMin(min: Double) = coerceAtLeast(min) -fun Double.clampMax(max: Double) = coerceAtMost(max) - -// Why does this not exist? -infix fun Int.ceilDiv(divisor: Int): Int { - return -Math.floorDiv(-this, divisor) +package io.dico.parcels2.util.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 +} + +// the name coerceAtMost is bad +fun Int.clampMax(max: Int) = coerceAtMost(max) +fun Double.clampMin(min: Double) = coerceAtLeast(min) +fun Double.clampMax(max: Double) = coerceAtMost(max) + +// Why does this not exist? +infix fun Int.ceilDiv(divisor: Int): Int { + return -Math.floorDiv(-this, divisor) } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/util/math/Region.kt b/src/main/kotlin/io/dico/parcels2/util/math/Region.kt index cdbd497..cdcbe0e 100644 --- a/src/main/kotlin/io/dico/parcels2/util/math/Region.kt +++ b/src/main/kotlin/io/dico/parcels2/util/math/Region.kt @@ -1,37 +1,37 @@ -package io.dico.parcels2.util.math - -data class Region(val origin: Vec3i, val size: Vec3i) { - val blockCount: Int get() = size.x * size.y * size.z - - val center: Vec3d - get() { - val x = (origin.x + size.x) / 2.0 - val y = (origin.y + size.y) / 2.0 - val z = (origin.z + size.z) / 2.0 - return Vec3d(x, y, z) - } - - val end: Vec3i - get() = origin + size - - val max: Vec3i - get() = Vec3i(origin.x + size.x - 1, origin.y + size.y - 1, origin.z + size.z - 1) - - fun withSize(size: Vec3i): Region { - if (size == this.size) return this - return Region(origin, size) - } - - operator fun contains(loc: Vec3i): Boolean = getFirstUncontainedDimensionOf(loc) == null - - fun getFirstUncontainedDimensionOf(loc: Vec3i): Dimension? { - val max = max - return when { - loc.x !in origin.x..max.x -> Dimension.X - loc.z !in origin.z..max.z -> Dimension.Z - loc.y !in origin.y..max.y -> Dimension.Y - else -> null - } - } - +package io.dico.parcels2.util.math + +data class Region(val origin: Vec3i, val size: Vec3i) { + val blockCount: Int get() = size.x * size.y * size.z + + val center: Vec3d + get() { + val x = (origin.x + size.x) / 2.0 + val y = (origin.y + size.y) / 2.0 + val z = (origin.z + size.z) / 2.0 + return Vec3d(x, y, z) + } + + val end: Vec3i + get() = origin + size + + val max: Vec3i + get() = Vec3i(origin.x + size.x - 1, origin.y + size.y - 1, origin.z + size.z - 1) + + fun withSize(size: Vec3i): Region { + if (size == this.size) return this + return Region(origin, size) + } + + operator fun contains(loc: Vec3i): Boolean = getFirstUncontainedDimensionOf(loc) == null + + fun getFirstUncontainedDimensionOf(loc: Vec3i): Dimension? { + val max = max + return when { + loc.x !in origin.x..max.x -> Dimension.X + loc.z !in origin.z..max.z -> Dimension.Z + loc.y !in origin.y..max.y -> Dimension.Y + else -> null + } + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/util/math/Vec2i.kt b/src/main/kotlin/io/dico/parcels2/util/math/Vec2i.kt index 5945120..3b25526 100644 --- a/src/main/kotlin/io/dico/parcels2/util/math/Vec2i.kt +++ b/src/main/kotlin/io/dico/parcels2/util/math/Vec2i.kt @@ -1,9 +1,9 @@ -package io.dico.parcels2.util.math - -data class Vec2i( - val x: Int, - val z: Int -) { - fun add(ox: Int, oz: Int) = Vec2i(x + ox, z + oz) - fun toChunk() = Vec2i(x shr 4, z shr 4) -} +package io.dico.parcels2.util.math + +data class Vec2i( + val x: Int, + val z: Int +) { + fun add(ox: Int, oz: Int) = Vec2i(x + ox, z + oz) + fun toChunk() = Vec2i(x shr 4, z shr 4) +} diff --git a/src/main/kotlin/io/dico/parcels2/util/math/Vec3d.kt b/src/main/kotlin/io/dico/parcels2/util/math/Vec3d.kt index 787f46c..72b6dcd 100644 --- a/src/main/kotlin/io/dico/parcels2/util/math/Vec3d.kt +++ b/src/main/kotlin/io/dico/parcels2/util/math/Vec3d.kt @@ -1,53 +1,53 @@ -package io.dico.parcels2.util.math - -import org.bukkit.Location -import kotlin.math.sqrt - -data class Vec3d( - val x: Double, - val y: Double, - val z: Double -) { - constructor(loc: Location) : this(loc.x, loc.y, loc.z) - - operator fun plus(o: Vec3d) = Vec3d(x + o.x, y + o.y, z + o.z) - operator fun minus(o: Vec3i) = Vec3d(x - o.x, y - o.y, z - o.z) - infix fun addX(o: Double) = Vec3d(x + o, y, z) - infix fun addY(o: Double) = Vec3d(x, y + o, z) - infix fun addZ(o: Double) = Vec3d(x, y, z + o) - infix fun withX(o: Double) = Vec3d(o, y, z) - infix fun withY(o: Double) = Vec3d(x, o, z) - infix fun withZ(o: Double) = Vec3d(x, y, o) - fun add(ox: Double, oy: Double, oz: Double) = Vec3d(x + ox, y + oy, z + oz) - fun toVec3i() = Vec3i(x.floor(), y.floor(), z.floor()) - - fun distanceSquared(o: Vec3d): Double { - val dx = o.x - x - val dy = o.y - y - val dz = o.z - z - return dx * dx + dy * dy + dz * dz - } - - fun distance(o: Vec3d) = sqrt(distanceSquared(o)) - - operator fun get(dimension: Dimension) = - when (dimension) { - Dimension.X -> x - Dimension.Y -> y - Dimension.Z -> z - } - - fun with(dimension: Dimension, value: Double) = - when (dimension) { - Dimension.X -> withX(value) - Dimension.Y -> withY(value) - Dimension.Z -> withZ(value) - } - - fun add(dimension: Dimension, value: Double) = - when (dimension) { - Dimension.X -> addX(value) - Dimension.Y -> addY(value) - Dimension.Z -> addZ(value) - } +package io.dico.parcels2.util.math + +import org.bukkit.Location +import kotlin.math.sqrt + +data class Vec3d( + val x: Double, + val y: Double, + val z: Double +) { + constructor(loc: Location) : this(loc.x, loc.y, loc.z) + + operator fun plus(o: Vec3d) = Vec3d(x + o.x, y + o.y, z + o.z) + operator fun minus(o: Vec3i) = Vec3d(x - o.x, y - o.y, z - o.z) + infix fun addX(o: Double) = Vec3d(x + o, y, z) + infix fun addY(o: Double) = Vec3d(x, y + o, z) + infix fun addZ(o: Double) = Vec3d(x, y, z + o) + infix fun withX(o: Double) = Vec3d(o, y, z) + infix fun withY(o: Double) = Vec3d(x, o, z) + infix fun withZ(o: Double) = Vec3d(x, y, o) + fun add(ox: Double, oy: Double, oz: Double) = Vec3d(x + ox, y + oy, z + oz) + fun toVec3i() = Vec3i(x.floor(), y.floor(), z.floor()) + + fun distanceSquared(o: Vec3d): Double { + val dx = o.x - x + val dy = o.y - y + val dz = o.z - z + return dx * dx + dy * dy + dz * dz + } + + fun distance(o: Vec3d) = sqrt(distanceSquared(o)) + + operator fun get(dimension: Dimension) = + when (dimension) { + Dimension.X -> x + Dimension.Y -> y + Dimension.Z -> z + } + + fun with(dimension: Dimension, value: Double) = + when (dimension) { + Dimension.X -> withX(value) + Dimension.Y -> withY(value) + Dimension.Z -> withZ(value) + } + + fun add(dimension: Dimension, value: Double) = + when (dimension) { + Dimension.X -> addX(value) + Dimension.Y -> addY(value) + Dimension.Z -> addZ(value) + } } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/util/math/Vec3i.kt b/src/main/kotlin/io/dico/parcels2/util/math/Vec3i.kt index b25764e..484ad13 100644 --- a/src/main/kotlin/io/dico/parcels2/util/math/Vec3i.kt +++ b/src/main/kotlin/io/dico/parcels2/util/math/Vec3i.kt @@ -1,105 +1,105 @@ -package io.dico.parcels2.util.math - -import org.bukkit.Location -import org.bukkit.World -import org.bukkit.block.Block -import org.bukkit.block.BlockFace - -data class Vec3i( - val x: Int, - val y: Int, - val z: Int -) { - constructor(loc: Location) : this(loc.blockX, loc.blockY, loc.blockZ) - constructor(block: Block) : this(block.x, block.y, block.z) - - fun toVec2i() = Vec2i(x, z) - operator fun plus(o: Vec3i) = Vec3i(x + o.x, y + o.y, z + o.z) - operator fun minus(o: Vec3i) = Vec3i(x - o.x, y - o.y, z - o.z) - infix fun addX(o: Int) = Vec3i(x + o, y, z) - infix fun addY(o: Int) = Vec3i(x, y + o, z) - infix fun addZ(o: Int) = Vec3i(x, y, z + o) - infix fun withX(o: Int) = Vec3i(o, y, z) - infix fun withY(o: Int) = Vec3i(x, o, z) - infix fun withZ(o: Int) = Vec3i(x, y, o) - fun add(ox: Int, oy: Int, oz: Int) = Vec3i(x + ox, y + oy, z + oz) - fun neg() = Vec3i(-x, -y, -z) - fun clampMax(o: Vec3i) = Vec3i(x.clampMax(o.x), y.clampMax(o.y), z.clampMax(o.z)) - - operator fun get(dimension: Dimension) = - when (dimension) { - Dimension.X -> x - Dimension.Y -> y - Dimension.Z -> z - } - - fun with(dimension: Dimension, value: Int) = - when (dimension) { - Dimension.X -> withX(value) - Dimension.Y -> withY(value) - Dimension.Z -> withZ(value) - } - - fun add(dimension: Dimension, value: Int) = - when (dimension) { - Dimension.X -> addX(value) - Dimension.Y -> addY(value) - Dimension.Z -> addZ(value) - } - - companion object { - private operator fun invoke(face: BlockFace) = Vec3i(face.modX, face.modY, face.modZ) - val down = Vec3i(BlockFace.DOWN) - val up = Vec3i(BlockFace.UP) - val north = Vec3i(BlockFace.NORTH) - val east = Vec3i(BlockFace.EAST) - val south = Vec3i(BlockFace.SOUTH) - val west = Vec3i(BlockFace.WEST) - - fun convert(face: BlockFace) = when (face) { - BlockFace.DOWN -> down - BlockFace.UP -> up - BlockFace.NORTH -> north - BlockFace.EAST -> east - BlockFace.SOUTH -> south - BlockFace.WEST -> west - else -> Vec3i(face) - } - } -} - -@Suppress("NOTHING_TO_INLINE") -inline operator fun World.get(vec: Vec3i): Block = getBlockAt(vec.x, vec.y, vec.z) - -/* -private /*inline */class IVec3i(private val data: Long) { - - private companion object { - const val mask = 0x001F_FFFF - const val max: Int = 0x000F_FFFF // +1048575 - const val min: Int = -max - 1 // -1048575 // 0xFFF0_0000 - - @Suppress("NOTHING_TO_INLINE") - inline fun Int.compressIntoLong(offset: Int): Long { - if (this !in min..max) throw IllegalArgumentException() - return and(mask).toLong().shl(offset) - } - - @Suppress("NOTHING_TO_INLINE") - inline fun Long.extractInt(offset: Int): Int { - val result = ushr(offset).toInt().and(mask) - return if (result > max) result or mask.inv() else result - } - } - - constructor(x: Int, y: Int, z: Int) : this( - x.compressIntoLong(42) - or y.compressIntoLong(21) - or z.compressIntoLong(0)) - - val x: Int get() = data.extractInt(42) - val y: Int get() = data.extractInt(21) - val z: Int get() = data.extractInt(0) - -} -*/ +package io.dico.parcels2.util.math + +import org.bukkit.Location +import org.bukkit.World +import org.bukkit.block.Block +import org.bukkit.block.BlockFace + +data class Vec3i( + val x: Int, + val y: Int, + val z: Int +) { + constructor(loc: Location) : this(loc.blockX, loc.blockY, loc.blockZ) + constructor(block: Block) : this(block.x, block.y, block.z) + + fun toVec2i() = Vec2i(x, z) + operator fun plus(o: Vec3i) = Vec3i(x + o.x, y + o.y, z + o.z) + operator fun minus(o: Vec3i) = Vec3i(x - o.x, y - o.y, z - o.z) + infix fun addX(o: Int) = Vec3i(x + o, y, z) + infix fun addY(o: Int) = Vec3i(x, y + o, z) + infix fun addZ(o: Int) = Vec3i(x, y, z + o) + infix fun withX(o: Int) = Vec3i(o, y, z) + infix fun withY(o: Int) = Vec3i(x, o, z) + infix fun withZ(o: Int) = Vec3i(x, y, o) + fun add(ox: Int, oy: Int, oz: Int) = Vec3i(x + ox, y + oy, z + oz) + fun neg() = Vec3i(-x, -y, -z) + fun clampMax(o: Vec3i) = Vec3i(x.clampMax(o.x), y.clampMax(o.y), z.clampMax(o.z)) + + operator fun get(dimension: Dimension) = + when (dimension) { + Dimension.X -> x + Dimension.Y -> y + Dimension.Z -> z + } + + fun with(dimension: Dimension, value: Int) = + when (dimension) { + Dimension.X -> withX(value) + Dimension.Y -> withY(value) + Dimension.Z -> withZ(value) + } + + fun add(dimension: Dimension, value: Int) = + when (dimension) { + Dimension.X -> addX(value) + Dimension.Y -> addY(value) + Dimension.Z -> addZ(value) + } + + companion object { + private operator fun invoke(face: BlockFace) = Vec3i(face.modX, face.modY, face.modZ) + val down = Vec3i(BlockFace.DOWN) + val up = Vec3i(BlockFace.UP) + val north = Vec3i(BlockFace.NORTH) + val east = Vec3i(BlockFace.EAST) + val south = Vec3i(BlockFace.SOUTH) + val west = Vec3i(BlockFace.WEST) + + fun convert(face: BlockFace) = when (face) { + BlockFace.DOWN -> down + BlockFace.UP -> up + BlockFace.NORTH -> north + BlockFace.EAST -> east + BlockFace.SOUTH -> south + BlockFace.WEST -> west + else -> Vec3i(face) + } + } +} + +@Suppress("NOTHING_TO_INLINE") +inline operator fun World.get(vec: Vec3i): Block = getBlockAt(vec.x, vec.y, vec.z) + +/* +private /*inline */class IVec3i(private val data: Long) { + + private companion object { + const val mask = 0x001F_FFFF + const val max: Int = 0x000F_FFFF // +1048575 + const val min: Int = -max - 1 // -1048575 // 0xFFF0_0000 + + @Suppress("NOTHING_TO_INLINE") + inline fun Int.compressIntoLong(offset: Int): Long { + if (this !in min..max) throw IllegalArgumentException() + return and(mask).toLong().shl(offset) + } + + @Suppress("NOTHING_TO_INLINE") + inline fun Long.extractInt(offset: Int): Int { + val result = ushr(offset).toInt().and(mask) + return if (result > max) result or mask.inv() else result + } + } + + constructor(x: Int, y: Int, z: Int) : this( + x.compressIntoLong(42) + or y.compressIntoLong(21) + or z.compressIntoLong(0)) + + val x: Int get() = data.extractInt(42) + val y: Int get() = data.extractInt(21) + val z: Int get() = data.extractInt(0) + +} +*/ diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index eff524c..c2e272e 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,17 +1,17 @@ - - - - - %magenta(%-16.-16(%thread)) %highlight(%-5level) %boldCyan(%6.-32logger{32}) - %msg - - - - - - - - - - - + + + + + %magenta(%-16.-16(%thread)) %highlight(%-5level) %boldCyan(%6.-32logger{32}) - %msg + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 19c68da..8830377 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ -name: Parcels -author: Dico -main: io.dico.parcels2.ParcelsPlugin -version: 0.1 -api-version: 1.13 +name: Parcels +author: Dico +main: io.dico.parcels2.ParcelsPlugin +version: 0.1 +api-version: 1.13 load: STARTUP \ No newline at end of file -- cgit v1.2.3 From 5ef2584fdb6e4db482aa4c57e6ecf0202c67a48d Mon Sep 17 00:00:00 2001 From: Dico Karssiens Date: Sat, 17 Nov 2018 21:32:43 +0000 Subject: Tweak some command stuff, clear/swap entities --- src/main/kotlin/io/dico/parcels2/JobDispatcher.kt | 705 ++++++++++--------- .../kotlin/io/dico/parcels2/ParcelGenerator.kt | 205 +++--- src/main/kotlin/io/dico/parcels2/ParcelWorld.kt | 206 +++--- src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt | 305 ++++---- src/main/kotlin/io/dico/parcels2/PlayerProfile.kt | 390 ++++++----- .../io/dico/parcels2/blockvisitor/Entities.kt | 38 + .../dico/parcels2/command/ParcelParameterTypes.kt | 173 ++--- .../io/dico/parcels2/command/ParcelTarget.kt | 406 ++++++----- .../parcels2/defaultimpl/DefaultParcelGenerator.kt | 767 +++++++++++---------- .../parcels2/defaultimpl/ParcelProviderImpl.kt | 505 ++++++++------ .../parcels2/storage/exposed/ExposedBacking.kt | 566 +++++++-------- .../kotlin/io/dico/parcels2/util/BukkitUtil.kt | 37 +- .../kotlin/io/dico/parcels2/util/PluginAware.kt | 18 + .../io/dico/parcels2/util/PluginScheduler.kt | 20 - .../kotlin/io/dico/parcels2/util/math/Vec3d.kt | 112 +-- .../kotlin/io/dico/parcels2/util/math/Vec3i.kt | 212 +++--- src/main/kotlin/io/dico/parcels2/util/parallel.kt | 9 + 17 files changed, 2450 insertions(+), 2224 deletions(-) create mode 100644 src/main/kotlin/io/dico/parcels2/blockvisitor/Entities.kt create mode 100644 src/main/kotlin/io/dico/parcels2/util/PluginAware.kt delete mode 100644 src/main/kotlin/io/dico/parcels2/util/PluginScheduler.kt create mode 100644 src/main/kotlin/io/dico/parcels2/util/parallel.kt (limited to 'src') diff --git a/src/main/kotlin/io/dico/parcels2/JobDispatcher.kt b/src/main/kotlin/io/dico/parcels2/JobDispatcher.kt index 12be89a..ebbe334 100644 --- a/src/main/kotlin/io/dico/parcels2/JobDispatcher.kt +++ b/src/main/kotlin/io/dico/parcels2/JobDispatcher.kt @@ -1,337 +1,368 @@ -package io.dico.parcels2 - -import io.dico.parcels2.util.math.clampMin -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart.LAZY -import kotlinx.coroutines.Job as CoroutineJob -import kotlinx.coroutines.launch -import org.bukkit.scheduler.BukkitTask -import java.lang.System.currentTimeMillis -import java.util.LinkedList -import kotlin.coroutines.Continuation -import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED -import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn -import kotlin.coroutines.resume - -typealias JobFunction = suspend JobScope.() -> Unit -typealias JobUpdateLister = Job.(Double, Long) -> Unit - -data class TickJobtimeOptions(var jobTime: Int, var tickInterval: Int) - -interface JobDispatcher { - /** - * Submit a [function] that should be run synchronously, but limited such that it does not stall the server - */ - fun dispatch(function: JobFunction): Job - - /** - * Get a list of all jobs - */ - val jobs: List - - /** - * Attempts to complete any remaining tasks immediately, without suspension. - */ - fun completeAllTasks() -} - -interface JobAndScopeMembersUnion { - /** - * The time that elapsed since this job was dispatched, in milliseconds - */ - val elapsedTime: Long - - /** - * A value indicating the progress of this job, in the range 0.0 <= progress <= 1.0 - * with no guarantees to its accuracy. - */ - val progress: Double -} - -interface Job : JobAndScopeMembersUnion { - /** - * The coroutine associated with this job - */ - val coroutine: CoroutineJob - - /** - * true if this job has completed - */ - val isComplete: Boolean - - /** - * If an exception was thrown during the execution of this task, - * returns that exception. Returns null otherwise. - */ - val completionException: Throwable? - - /** - * Calls the given [block] whenever the progress of this job is updated, - * if [minInterval] milliseconds expired since the last call. - * The first call occurs after at least [minDelay] milliseconds in a likewise manner. - * Repeated invocations of this method result in an [IllegalStateException] - * - * if [asCompletionListener] is true, [onCompleted] is called with the same [block] - */ - fun onProgressUpdate(minDelay: Int, minInterval: Int, asCompletionListener: Boolean = true, block: JobUpdateLister): Job - - /** - * Calls the given [block] when this job completes, with the progress value 1.0. - * Multiple listeners may be registered to this function. - */ - fun onCompleted(block: JobUpdateLister): Job - - /** - * Await completion of this job - */ - suspend fun awaitCompletion() -} - -interface JobScope : JobAndScopeMembersUnion { - /** - * A task should call this frequently during its execution, such that the timer can suspend it when necessary. - */ - suspend fun markSuspensionPoint() - - /** - * A task should call this method to indicate its progress - */ - fun setProgress(progress: Double) - - /** - * Indicate that this job is complete - */ - fun markComplete() = setProgress(1.0) - - /** - * Get a [JobScope] that is responsible for [portion] part of the progress - * If [portion] is negative, the remaining progress is used - */ - fun delegateProgress(portion: Double = -1.0): JobScope -} - -inline fun JobScope.delegateWork(portion: Double = -1.0, block: JobScope.() -> T): T { - delegateProgress(portion).apply { - val result = block() - markComplete() - return result - } -} - -interface JobInternal : Job, JobScope { - /** - * Start or resumes the execution of this job - * and returns true if the job completed - * - * [worktime] is the maximum amount of time, in milliseconds, - * that this job may run for until suspension. - * - * If [worktime] is not positive, the job will complete - * without suspension and this method will always return true. - */ - fun resume(worktime: Long): Boolean -} - -/** - * An object that controls one or more jobs, ensuring that they don't stall the server too much. - * There is a configurable maxiumum amount of milliseconds that can be allocated to all jobs together in each server tick - * This object attempts to split that maximum amount of milliseconds equally between all jobs - */ -class BukkitJobDispatcher(private val plugin: ParcelsPlugin, var options: TickJobtimeOptions) : JobDispatcher { - // The currently registered bukkit scheduler task - private var bukkitTask: BukkitTask? = null - // The jobs. - private val _jobs = LinkedList() - override val jobs: List = _jobs - - override fun dispatch(function: JobFunction): Job { - val job: JobInternal = JobImpl(plugin, function) - - if (bukkitTask == null) { - val completed = job.resume(options.jobTime.toLong()) - if (completed) return job - bukkitTask = plugin.scheduleRepeating(0, options.tickInterval) { tickCoroutineJobs() } - } - _jobs.addFirst(job) - return job - } - - private fun tickCoroutineJobs() { - val jobs = _jobs - if (jobs.isEmpty()) return - val tickStartTime = System.currentTimeMillis() - - val iterator = jobs.listIterator(index = 0) - while (iterator.hasNext()) { - val time = System.currentTimeMillis() - val timeElapsed = time - tickStartTime - val timeLeft = options.jobTime - timeElapsed - if (timeLeft <= 0) return - - val count = jobs.size - iterator.nextIndex() - val timePerJob = (timeLeft + count - 1) / count - val job = iterator.next() - val completed = job.resume(timePerJob) - if (completed) { - iterator.remove() - } - } - - if (jobs.isEmpty()) { - bukkitTask?.cancel() - bukkitTask = null - } - } - - override fun completeAllTasks() { - _jobs.forEach { - it.resume(-1) - } - _jobs.clear() - bukkitTask?.cancel() - bukkitTask = null - } - -} - -private class JobImpl(scope: CoroutineScope, task: JobFunction) : JobInternal { - override val coroutine: CoroutineJob = scope.launch(start = LAZY) { task() } - - private var continuation: Continuation? = null - private var nextSuspensionTime: Long = 0L - private var completeForcefully = false - private var isStarted = false - - override val elapsedTime - get() = - if (coroutine.isCompleted) startTimeOrElapsedTime - else currentTimeMillis() - startTimeOrElapsedTime - - override val isComplete get() = coroutine.isCompleted - - private var _progress = 0.0 - override val progress get() = _progress - override var completionException: Throwable? = null; private set - - private var startTimeOrElapsedTime: Long = 0L // startTime before completed, elapsed time otherwise - private var onProgressUpdate: JobUpdateLister? = null - private var progressUpdateInterval: Int = 0 - private var lastUpdateTime: Long = 0L - private var onCompleted: JobUpdateLister? = null - - init { - coroutine.invokeOnCompletion { exception -> - // report any error that occurred - completionException = exception?.also { - if (it !is CancellationException) - logger.error("JobFunction generated an exception", it) - } - - // convert to elapsed time here - startTimeOrElapsedTime = System.currentTimeMillis() - startTimeOrElapsedTime - onCompleted?.let { it(1.0, elapsedTime) } - - onCompleted = null - onProgressUpdate = { prog, el -> } - } - } - - override fun onProgressUpdate(minDelay: Int, minInterval: Int, asCompletionListener: Boolean, block: JobUpdateLister): Job { - onProgressUpdate?.let { throw IllegalStateException() } - if (asCompletionListener) onCompleted(block) - if (isComplete) return this - onProgressUpdate = block - progressUpdateInterval = minInterval - lastUpdateTime = System.currentTimeMillis() + minDelay - minInterval - - return this - } - - override fun onCompleted(block: JobUpdateLister): Job { - if (isComplete) { - block(1.0, startTimeOrElapsedTime) - return this - } - - val cur = onCompleted - onCompleted = if (cur == null) { - block - } else { - fun Job.(prog: Double, el: Long) { - cur(prog, el) - block(prog, el) - } - } - return this - } - - override suspend fun markSuspensionPoint() { - if (System.currentTimeMillis() >= nextSuspensionTime && !completeForcefully) - suspendCoroutineUninterceptedOrReturn { cont: Continuation -> - continuation = cont - COROUTINE_SUSPENDED - } - } - - override fun setProgress(progress: Double) { - this._progress = progress - val onProgressUpdate = onProgressUpdate ?: return - val time = System.currentTimeMillis() - if (time > lastUpdateTime + progressUpdateInterval) { - onProgressUpdate(progress, elapsedTime) - lastUpdateTime = time - } - } - - override fun resume(worktime: Long): Boolean { - if (isComplete) return true - - if (worktime > 0) { - nextSuspensionTime = currentTimeMillis() + worktime - } else { - completeForcefully = true - } - - if (isStarted) { - continuation?.let { - continuation = null - it.resume(Unit) - return continuation == null - } - return true - } - - isStarted = true - startTimeOrElapsedTime = System.currentTimeMillis() - coroutine.start() - - return continuation == null - } - - override suspend fun awaitCompletion() { - coroutine.join() - } - - private fun delegateProgress(curPortion: Double, portion: Double): JobScope = - DelegateScope(progress, curPortion * (if (portion < 0) 1.0 - progress else portion).clampMin(0.0)) - - override fun delegateProgress(portion: Double): JobScope = delegateProgress(1.0, portion) - - private inner class DelegateScope(val progressStart: Double, val portion: Double) : JobScope { - override val elapsedTime: Long - get() = this@JobImpl.elapsedTime - - override suspend fun markSuspensionPoint() = - this@JobImpl.markSuspensionPoint() - - override val progress: Double - get() = (this@JobImpl.progress - progressStart) / portion - - override fun setProgress(progress: Double) = - this@JobImpl.setProgress(progressStart + progress * portion) - - override fun delegateProgress(portion: Double): JobScope = - this@JobImpl.delegateProgress(this.portion, portion) - } -} +package io.dico.parcels2 + +import io.dico.parcels2.util.PluginAware +import io.dico.parcels2.util.math.clampMin +import io.dico.parcels2.util.scheduleRepeating +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart.LAZY +import kotlinx.coroutines.Job as CoroutineJob +import kotlinx.coroutines.launch +import org.bukkit.scheduler.BukkitTask +import java.lang.System.currentTimeMillis +import java.util.LinkedList +import kotlin.coroutines.Continuation +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn +import kotlin.coroutines.resume + +typealias JobFunction = suspend JobScope.() -> Unit +typealias JobUpdateLister = Job.(Double, Long) -> Unit + +data class TickJobtimeOptions(var jobTime: Int, var tickInterval: Int) + +interface JobDispatcher { + /** + * Submit a [function] that should be run synchronously, but limited such that it does not stall the server + */ + fun dispatch(function: JobFunction): Job + + /** + * Get a list of all jobs + */ + val jobs: List + + /** + * Attempts to complete any remaining tasks immediately, without suspension. + */ + fun completeAllTasks() +} + +interface JobAndScopeMembersUnion { + /** + * The time that elapsed since this job was dispatched, in milliseconds + */ + val elapsedTime: Long + + /** + * A value indicating the progress of this job, in the range 0.0 <= progress <= 1.0 + * with no guarantees to its accuracy. + */ + val progress: Double +} + +interface Job : JobAndScopeMembersUnion { + /** + * The coroutine associated with this job + */ + val coroutine: CoroutineJob + + /** + * true if this job has completed + */ + val isComplete: Boolean + + /** + * If an exception was thrown during the execution of this task, + * returns that exception. Returns null otherwise. + */ + val completionException: Throwable? + + /** + * Calls the given [block] whenever the progress of this job is updated, + * if [minInterval] milliseconds expired since the last call. + * The first call occurs after at least [minDelay] milliseconds in a likewise manner. + * Repeated invocations of this method result in an [IllegalStateException] + * + * if [asCompletionListener] is true, [onCompleted] is called with the same [block] + */ + fun onProgressUpdate( + minDelay: Int, + minInterval: Int, + asCompletionListener: Boolean = true, + block: JobUpdateLister + ): Job + + /** + * Calls the given [block] when this job completes, with the progress value 1.0. + * Multiple listeners may be registered to this function. + */ + fun onCompleted(block: JobUpdateLister): Job + + /** + * Await completion of this job + */ + suspend fun awaitCompletion() +} + +interface JobScope : JobAndScopeMembersUnion { + /** + * A task should call this frequently during its execution, such that the timer can suspend it when necessary. + */ + suspend fun markSuspensionPoint() + + /** + * A task should call this method to indicate its progress + */ + fun setProgress(progress: Double) + + /** + * Indicate that this job is complete + */ + fun markComplete() = setProgress(1.0) + + /** + * Get a [JobScope] that is responsible for [portion] part of the progress + * If [portion] is negative, the remaining progress is used + */ + fun delegateProgress(portion: Double = -1.0): JobScope +} + +inline fun JobScope.delegateWork(portion: Double = -1.0, block: JobScope.() -> T): T { + delegateProgress(portion).apply { + val result = block() + markComplete() + return result + } +} + +interface JobInternal : Job, JobScope { + /** + * Start or resumes the execution of this job + * and returns true if the job completed + * + * [worktime] is the maximum amount of time, in milliseconds, + * that this job may run for until suspension. + * + * If [worktime] is not positive, the job will complete + * without suspension and this method will always return true. + */ + fun resume(worktime: Long): Boolean +} + +/** + * An object that controls one or more jobs, ensuring that they don't stall the server too much. + * There is a configurable maxiumum amount of milliseconds that can be allocated to all jobs together in each server tick + * This object attempts to split that maximum amount of milliseconds equally between all jobs + */ +class BukkitJobDispatcher( + private val plugin: PluginAware, + private val scope: CoroutineScope, + var options: TickJobtimeOptions +) : JobDispatcher { + // The currently registered bukkit scheduler task + private var bukkitTask: BukkitTask? = null + // The jobs. + private val _jobs = LinkedList() + override val jobs: List = _jobs + + override fun dispatch(function: JobFunction): Job { + val job: JobInternal = JobImpl(scope, function) + + if (bukkitTask == null) { + val completed = job.resume(options.jobTime.toLong()) + if (completed) return job + bukkitTask = plugin.scheduleRepeating(options.tickInterval) { tickJobs() } + } + _jobs.addFirst(job) + return job + } + + private fun tickJobs() { + val jobs = _jobs + if (jobs.isEmpty()) return + val tickStartTime = System.currentTimeMillis() + + val iterator = jobs.listIterator(index = 0) + while (iterator.hasNext()) { + val time = System.currentTimeMillis() + val timeElapsed = time - tickStartTime + val timeLeft = options.jobTime - timeElapsed + if (timeLeft <= 0) return + + val count = jobs.size - iterator.nextIndex() + val timePerJob = (timeLeft + count - 1) / count + val job = iterator.next() + val completed = job.resume(timePerJob) + if (completed) { + iterator.remove() + } + } + + if (jobs.isEmpty()) { + bukkitTask?.cancel() + bukkitTask = null + } + } + + override fun completeAllTasks() { + _jobs.forEach { + it.resume(-1) + } + _jobs.clear() + bukkitTask?.cancel() + bukkitTask = null + } + +} + +private class JobImpl(scope: CoroutineScope, task: JobFunction) : JobInternal { + override val coroutine: CoroutineJob = scope.launch(start = LAZY) { task() } + + private var continuation: Continuation? = null + private var nextSuspensionTime: Long = 0L + private var completeForcefully = false + private var isStarted = false + + override val elapsedTime + get() = + if (coroutine.isCompleted) startTimeOrElapsedTime + else currentTimeMillis() - startTimeOrElapsedTime + + override val isComplete get() = coroutine.isCompleted + + private var _progress = 0.0 + override val progress get() = _progress + override var completionException: Throwable? = null; private set + + private var startTimeOrElapsedTime: Long = 0L // startTime before completed, elapsed time otherwise + private var onProgressUpdate: JobUpdateLister? = null + private var progressUpdateInterval: Int = 0 + private var lastUpdateTime: Long = 0L + private var onCompleted: JobUpdateLister? = null + + init { + coroutine.invokeOnCompletion { exception -> + // report any error that occurred + completionException = exception?.also { + if (it !is CancellationException) + logger.error("JobFunction generated an exception", it) + } + + // convert to elapsed time here + startTimeOrElapsedTime = System.currentTimeMillis() - startTimeOrElapsedTime + onCompleted?.let { it(1.0, elapsedTime) } + + onCompleted = null + onProgressUpdate = { prog, el -> } + } + } + + override fun onProgressUpdate( + minDelay: Int, + minInterval: Int, + asCompletionListener: Boolean, + block: JobUpdateLister + ): Job { + onProgressUpdate?.let { throw IllegalStateException() } + if (asCompletionListener) onCompleted(block) + if (isComplete) return this + onProgressUpdate = block + progressUpdateInterval = minInterval + lastUpdateTime = System.currentTimeMillis() + minDelay - minInterval + + return this + } + + override fun onCompleted(block: JobUpdateLister): Job { + if (isComplete) { + block(1.0, startTimeOrElapsedTime) + return this + } + + val cur = onCompleted + onCompleted = if (cur == null) { + block + } else { + fun Job.(prog: Double, el: Long) { + cur(prog, el) + block(prog, el) + } + } + return this + } + + override suspend fun markSuspensionPoint() { + if (System.currentTimeMillis() >= nextSuspensionTime && !completeForcefully) + suspendCoroutineUninterceptedOrReturn { cont: Continuation -> + continuation = cont + COROUTINE_SUSPENDED + } + } + + override fun setProgress(progress: Double) { + this._progress = progress + val onProgressUpdate = onProgressUpdate ?: return + val time = System.currentTimeMillis() + if (time > lastUpdateTime + progressUpdateInterval) { + onProgressUpdate(progress, elapsedTime) + lastUpdateTime = time + } + } + + override fun resume(worktime: Long): Boolean { + if (isComplete) return true + + if (worktime > 0) { + nextSuspensionTime = currentTimeMillis() + worktime + } else { + completeForcefully = true + } + + if (isStarted) { + continuation?.let { + continuation = null + + wrapExternalCall { + it.resume(Unit) + } + + return continuation == null + } + return true + } + + isStarted = true + startTimeOrElapsedTime = System.currentTimeMillis() + + wrapExternalCall { + coroutine.start() + } + + return continuation == null + } + + private inline fun wrapExternalCall(block: () -> Unit) { + try { + block() + } catch (ex: Throwable) { + logger.error("Job $coroutine generated an exception", ex) + } + } + + override suspend fun awaitCompletion() { + coroutine.join() + } + + private fun delegateProgress(curPortion: Double, portion: Double): JobScope = + DelegateScope(this, progress, curPortion * (if (portion < 0) 1.0 - progress else portion).clampMin(0.0)) + + override fun delegateProgress(portion: Double): JobScope = delegateProgress(1.0, portion) + + private class DelegateScope(val parent: JobImpl, val progressStart: Double, val portion: Double) : JobScope { + override val elapsedTime: Long + get() = parent.elapsedTime + + override suspend fun markSuspensionPoint() = + parent.markSuspensionPoint() + + override val progress: Double + get() = (parent.progress - progressStart) / portion + + override fun setProgress(progress: Double) = + parent.setProgress(progressStart + progress * portion) + + override fun delegateProgress(portion: Double): JobScope = + parent.delegateProgress(this.portion, portion) + } +} diff --git a/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt b/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt index 63ec02c..ada6d12 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt @@ -1,103 +1,102 @@ -package io.dico.parcels2 - -import io.dico.parcels2.blockvisitor.RegionTraverser -import io.dico.parcels2.util.math.Region -import io.dico.parcels2.util.math.Vec2i -import io.dico.parcels2.util.math.Vec3i -import io.dico.parcels2.util.math.get -import kotlinx.coroutines.CoroutineScope -import org.bukkit.Chunk -import org.bukkit.Location -import org.bukkit.Material -import org.bukkit.World -import org.bukkit.block.Biome -import org.bukkit.block.Block -import org.bukkit.block.BlockFace -import org.bukkit.entity.Entity -import org.bukkit.generator.BlockPopulator -import org.bukkit.generator.ChunkGenerator -import java.util.Random - -abstract class ParcelGenerator : ChunkGenerator() { - abstract val worldName: String - - abstract val world: World - - 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 { - return mutableListOf(object : BlockPopulator() { - override fun populate(world: World?, random: Random?, chunk: Chunk?) { - this@ParcelGenerator.populate(world, random, chunk) - } - }) - } - - abstract fun makeParcelLocatorAndBlockManager( - parcelProvider: ParcelProvider, - container: ParcelContainer, - coroutineScope: CoroutineScope, - jobDispatcher: JobDispatcher - ): Pair -} - -interface ParcelBlockManager { - val world: World - val jobDispatcher: JobDispatcher - val parcelTraverser: RegionTraverser - - fun getRegionOrigin(parcel: ParcelId) = getRegion(parcel).origin.toVec2i() - - fun getHomeLocation(parcel: ParcelId): Location - - fun getRegion(parcel: ParcelId): Region - - fun getEntities(parcel: ParcelId): Collection - - fun isParcelInfoSectionLoaded(parcel: ParcelId): Boolean - - fun updateParcelInfo(parcel: ParcelId, owner: PlayerProfile?) - - fun getParcelForInfoBlockInteraction(block: Vec3i, type: Material, face: BlockFace): Parcel? - - fun setBiome(parcel: ParcelId, biome: Biome): Job? - - fun clearParcel(parcel: ParcelId): Job? - - /** - * Used to update owner blocks in the corner of the parcel - */ - fun getParcelsWithOwnerBlockIn(chunk: Chunk): Collection -} - -inline fun ParcelBlockManager.tryDoBlockOperation( - parcelProvider: ParcelProvider, - parcel: ParcelId, - traverser: RegionTraverser, - crossinline operation: suspend JobScope.(Block) -> Unit -) = parcelProvider.trySubmitBlockVisitor(Permit(), arrayOf(parcel)) { - val region = getRegion(parcel) - val blockCount = region.blockCount.toDouble() - val blocks = traverser.traverseRegion(region) - for ((index, vec) in blocks.withIndex()) { - markSuspensionPoint() - operation(world[vec]) - setProgress((index + 1) / blockCount) - } -} - -abstract class ParcelBlockManagerBase : ParcelBlockManager { - - override fun getEntities(parcel: ParcelId): Collection { - val region = getRegion(parcel) - val center = region.center - val centerLoc = Location(world, center.x, center.y, center.z) - val centerDist = (center - region.origin).add(0.2, 0.2, 0.2) - return world.getNearbyEntities(centerLoc, centerDist.x, centerDist.y, centerDist.z) - } - -} +package io.dico.parcels2 + +import io.dico.parcels2.blockvisitor.RegionTraverser +import io.dico.parcels2.util.math.Region +import io.dico.parcels2.util.math.Vec2i +import io.dico.parcels2.util.math.Vec3i +import io.dico.parcels2.util.math.get +import kotlinx.coroutines.CoroutineScope +import org.bukkit.Chunk +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.World +import org.bukkit.block.Biome +import org.bukkit.block.Block +import org.bukkit.block.BlockFace +import org.bukkit.entity.Entity +import org.bukkit.generator.BlockPopulator +import org.bukkit.generator.ChunkGenerator +import java.util.Random + +abstract class ParcelGenerator : ChunkGenerator() { + abstract val worldName: String + + abstract val world: World + + 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 { + return mutableListOf(object : BlockPopulator() { + override fun populate(world: World?, random: Random?, chunk: Chunk?) { + this@ParcelGenerator.populate(world, random, chunk) + } + }) + } + + abstract fun makeParcelLocatorAndBlockManager( + parcelProvider: ParcelProvider, + container: ParcelContainer, + coroutineScope: CoroutineScope, + jobDispatcher: JobDispatcher + ): Pair +} + +interface ParcelBlockManager { + val world: World + val jobDispatcher: JobDispatcher + val parcelTraverser: RegionTraverser + + fun getRegionOrigin(parcel: ParcelId) = getRegion(parcel).origin.toVec2i() + + fun getHomeLocation(parcel: ParcelId): Location + + fun getRegion(parcel: ParcelId): Region + + fun getEntities(region: Region): Collection + + fun isParcelInfoSectionLoaded(parcel: ParcelId): Boolean + + fun updateParcelInfo(parcel: ParcelId, owner: PlayerProfile?) + + fun getParcelForInfoBlockInteraction(block: Vec3i, type: Material, face: BlockFace): Parcel? + + fun setBiome(parcel: ParcelId, biome: Biome): Job? + + fun clearParcel(parcel: ParcelId): Job? + + /** + * Used to update owner blocks in the corner of the parcel + */ + fun getParcelsWithOwnerBlockIn(chunk: Chunk): Collection +} + +inline fun ParcelBlockManager.tryDoBlockOperation( + parcelProvider: ParcelProvider, + parcel: ParcelId, + traverser: RegionTraverser, + crossinline operation: suspend JobScope.(Block) -> Unit +) = parcelProvider.trySubmitBlockVisitor(Permit(), arrayOf(parcel)) { + val region = getRegion(parcel) + val blockCount = region.blockCount.toDouble() + val blocks = traverser.traverseRegion(region) + for ((index, vec) in blocks.withIndex()) { + markSuspensionPoint() + operation(world[vec]) + setProgress((index + 1) / blockCount) + } +} + +abstract class ParcelBlockManagerBase : ParcelBlockManager { + + override fun getEntities(region: Region): Collection { + val center = region.center + val centerLoc = Location(world, center.x, center.y, center.z) + val centerDist = (center - region.origin).add(0.2, 0.2, 0.2) + return world.getNearbyEntities(centerLoc, centerDist.x, centerDist.y, centerDist.z) + } + +} diff --git a/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt b/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt index 36dfe1c..054d9d6 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt @@ -1,103 +1,103 @@ -package io.dico.parcels2 - -import io.dico.parcels2.options.RuntimeWorldOptions -import io.dico.parcels2.storage.Storage -import io.dico.parcels2.util.math.Vec2i -import io.dico.parcels2.util.math.floor -import org.bukkit.Location -import org.bukkit.World -import org.bukkit.block.Block -import org.bukkit.entity.Entity -import org.joda.time.DateTime -import java.lang.IllegalStateException -import java.util.UUID - -class Permit - -interface ParcelProvider { - val worlds: Map - - fun getWorldById(id: ParcelWorldId): ParcelWorld? - - fun getParcelById(id: ParcelId): Parcel? - - fun getWorld(name: String): ParcelWorld? - - fun getWorld(world: World): ParcelWorld? = getWorld(world.name) - - fun getWorld(block: Block): ParcelWorld? = getWorld(block.world) - - fun getWorld(loc: Location): ParcelWorld? = getWorld(loc.world) - - fun getWorld(entity: Entity): ParcelWorld? = getWorld(entity.location) - - fun getParcelAt(worldName: String, x: Int, z: Int): Parcel? = getWorld(worldName)?.locator?.getParcelAt(x, z) - - fun getParcelAt(world: World, x: Int, z: Int): Parcel? = getParcelAt(world.name, x, z) - - fun getParcelAt(world: World, vec: Vec2i): Parcel? = getParcelAt(world, vec.x, vec.z) - - fun getParcelAt(loc: Location): Parcel? = getParcelAt(loc.world, loc.x.floor(), loc.z.floor()) - - fun getParcelAt(entity: Entity): Parcel? = getParcelAt(entity.location) - - fun getParcelAt(block: Block): Parcel? = getParcelAt(block.world, block.x, block.z) - - fun getWorldGenerator(worldName: String): ParcelGenerator? - - fun loadWorlds() - - fun acquireBlockVisitorPermit(parcelId: ParcelId, with: Permit): Boolean - - @Throws(IllegalStateException::class) - fun releaseBlockVisitorPermit(parcelId: ParcelId, with: Permit) - - fun trySubmitBlockVisitor(permit: Permit, parcelIds: Array, function: JobFunction): Job? - - fun swapParcels(parcelId1: ParcelId, parcelId2: ParcelId): Job? -} - -interface ParcelLocator { - val world: World - - fun getParcelIdAt(x: Int, z: Int): ParcelId? - - fun getParcelAt(x: Int, z: Int): Parcel? - - fun getParcelAt(vec: Vec2i): Parcel? = getParcelAt(vec.x, vec.z) - - fun getParcelAt(loc: Location): Parcel? = getParcelAt(loc.x.floor(), loc.z.floor()).takeIf { loc.world == world } - - fun getParcelAt(entity: Entity): Parcel? = getParcelAt(entity.location).takeIf { entity.world == world } - - fun getParcelAt(block: Block): Parcel? = getParcelAt(block.x, block.z).takeIf { block.world == world } -} - -typealias ParcelContainerFactory = (ParcelWorld) -> ParcelContainer - -interface ParcelContainer { - - fun getParcelById(x: Int, z: Int): Parcel? - - fun getParcelById(id: Vec2i): Parcel? = getParcelById(id.x, id.z) - - fun getParcelById(id: ParcelId): Parcel? - - fun nextEmptyParcel(): Parcel? - -} - -interface ParcelWorld : ParcelLocator, ParcelContainer { - val id: ParcelWorldId - val name: String - val uid: UUID? - val options: RuntimeWorldOptions - val generator: ParcelGenerator - val storage: Storage - val container: ParcelContainer - val locator: ParcelLocator - val blockManager: ParcelBlockManager - val globalPrivileges: GlobalPrivilegesManager - - val creationTime: DateTime? -} +package io.dico.parcels2 + +import io.dico.parcels2.options.RuntimeWorldOptions +import io.dico.parcels2.storage.Storage +import io.dico.parcels2.util.math.Vec2i +import io.dico.parcels2.util.math.floor +import org.bukkit.Location +import org.bukkit.World +import org.bukkit.block.Block +import org.bukkit.entity.Entity +import org.joda.time.DateTime +import java.lang.IllegalStateException +import java.util.UUID + +class Permit + +interface ParcelProvider { + val worlds: Map + + fun getWorldById(id: ParcelWorldId): ParcelWorld? + + fun getParcelById(id: ParcelId): Parcel? + + fun getWorld(name: String): ParcelWorld? + + fun getWorld(world: World): ParcelWorld? = getWorld(world.name) + + fun getWorld(block: Block): ParcelWorld? = getWorld(block.world) + + fun getWorld(loc: Location): ParcelWorld? = getWorld(loc.world) + + fun getWorld(entity: Entity): ParcelWorld? = getWorld(entity.location) + + fun getParcelAt(worldName: String, x: Int, z: Int): Parcel? = getWorld(worldName)?.locator?.getParcelAt(x, z) + + fun getParcelAt(world: World, x: Int, z: Int): Parcel? = getParcelAt(world.name, x, z) + + fun getParcelAt(world: World, vec: Vec2i): Parcel? = getParcelAt(world, vec.x, vec.z) + + fun getParcelAt(loc: Location): Parcel? = getParcelAt(loc.world, loc.x.floor(), loc.z.floor()) + + fun getParcelAt(entity: Entity): Parcel? = getParcelAt(entity.location) + + fun getParcelAt(block: Block): Parcel? = getParcelAt(block.world, block.x, block.z) + + fun getWorldGenerator(worldName: String): ParcelGenerator? + + fun loadWorlds() + + fun acquireBlockVisitorPermit(parcelId: ParcelId, with: Permit): Boolean + + @Throws(IllegalStateException::class) + fun releaseBlockVisitorPermit(parcelId: ParcelId, with: Permit) + + fun trySubmitBlockVisitor(permit: Permit, parcelIds: Array, function: JobFunction): Job? + + fun swapParcels(parcelId1: ParcelId, parcelId2: ParcelId): Job? +} + +interface ParcelLocator { + val world: World + + fun getParcelIdAt(x: Int, z: Int): ParcelId? + + fun getParcelAt(x: Int, z: Int): Parcel? + + fun getParcelAt(vec: Vec2i): Parcel? = getParcelAt(vec.x, vec.z) + + fun getParcelAt(loc: Location): Parcel? = getParcelAt(loc.x.floor(), loc.z.floor()).takeIf { loc.world == world } + + fun getParcelAt(entity: Entity): Parcel? = getParcelAt(entity.location).takeIf { entity.world == world } + + fun getParcelAt(block: Block): Parcel? = getParcelAt(block.x, block.z).takeIf { block.world == world } +} + +typealias ParcelContainerFactory = (ParcelWorld) -> ParcelContainer + +interface ParcelContainer { + + fun getParcelById(x: Int, z: Int): Parcel? + + fun getParcelById(id: Vec2i): Parcel? = getParcelById(id.x, id.z) + + fun getParcelById(id: ParcelId): Parcel? + + suspend fun nextEmptyParcel(): Parcel? + +} + +interface ParcelWorld : ParcelLocator, ParcelContainer { + val id: ParcelWorldId + val name: String + val uid: UUID? + val options: RuntimeWorldOptions + val generator: ParcelGenerator + val storage: Storage + val container: ParcelContainer + val locator: ParcelLocator + val blockManager: ParcelBlockManager + val globalPrivileges: GlobalPrivilegesManager + + val creationTime: DateTime? +} diff --git a/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt b/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt index b2d52a9..2ffef06 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt @@ -1,153 +1,154 @@ -package io.dico.parcels2 - -import io.dico.dicore.Registrator -import io.dico.dicore.command.EOverridePolicy -import io.dico.dicore.command.ICommandDispatcher -import io.dico.parcels2.command.getParcelCommands -import io.dico.parcels2.defaultimpl.GlobalPrivilegesManagerImpl -import io.dico.parcels2.defaultimpl.ParcelProviderImpl -import io.dico.parcels2.listener.ParcelEntityTracker -import io.dico.parcels2.listener.ParcelListeners -import io.dico.parcels2.listener.WorldEditListener -import io.dico.parcels2.options.Options -import io.dico.parcels2.options.optionsMapper -import io.dico.parcels2.storage.Storage -import io.dico.parcels2.util.MainThreadDispatcher -import io.dico.parcels2.util.PluginScheduler -import io.dico.parcels2.util.ext.tryCreate -import io.dico.parcels2.util.isServerThread -import kotlinx.coroutines.CoroutineScope -import org.bukkit.Bukkit -import org.bukkit.generator.ChunkGenerator -import org.bukkit.plugin.Plugin -import org.bukkit.plugin.java.JavaPlugin -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.io.File -import kotlin.coroutines.CoroutineContext - -val logger: Logger = LoggerFactory.getLogger("ParcelsPlugin") -private inline val plogger get() = logger - -class ParcelsPlugin : JavaPlugin(), CoroutineScope, PluginScheduler { - lateinit var optionsFile: File; private set - lateinit var options: Options; private set - lateinit var parcelProvider: ParcelProvider; private set - lateinit var storage: Storage; private set - lateinit var globalPrivileges: GlobalPrivilegesManager; private set - - val registrator = Registrator(this) - lateinit var entityTracker: ParcelEntityTracker; private set - private var listeners: ParcelListeners? = null - private var cmdDispatcher: ICommandDispatcher? = null - - override val coroutineContext: CoroutineContext = MainThreadDispatcher(this) - override val plugin: Plugin get() = this - val jobDispatcher: JobDispatcher by lazy { BukkitJobDispatcher(this, options.tickJobtime) } - - override fun onEnable() { - plogger.info("Is server thread: ${isServerThread()}") - plogger.info("Debug enabled: ${plogger.isDebugEnabled}") - plogger.debug(System.getProperty("user.dir")) - if (!init()) { - Bukkit.getPluginManager().disablePlugin(this) - } - } - - override fun onDisable() { - val hasWorkers = jobDispatcher.jobs.isNotEmpty() - if (hasWorkers) { - plogger.warn("Parcels is attempting to complete all ${jobDispatcher.jobs.size} remaining jobs before shutdown...") - } - jobDispatcher.completeAllTasks() - if (hasWorkers) { - plogger.info("Parcels has completed the remaining jobs.") - } - - cmdDispatcher?.unregisterFromCommandMap() - } - - private fun init(): Boolean { - optionsFile = File(dataFolder, "options.yml") - options = Options() - parcelProvider = ParcelProviderImpl(this) - - try { - if (!loadOptions()) return false - - try { - storage = options.storage.newInstance() - storage.init() - } catch (ex: Exception) { - plogger.error("Failed to connect to database", ex) - return false - } - - globalPrivileges = GlobalPrivilegesManagerImpl(this) - entityTracker = ParcelEntityTracker(parcelProvider) - } catch (ex: Exception) { - plogger.error("Error loading options", ex) - return false - } - - registerListeners() - registerCommands() - - parcelProvider.loadWorlds() - return true - } - - fun loadOptions(): Boolean { - when { - optionsFile.exists() -> optionsMapper.readerForUpdating(options).readValue(optionsFile) - else -> run { - options.addWorld("parcels") - 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}") - } - 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) - } - - private fun registerCommands() { - cmdDispatcher = getParcelCommands(this).apply { - registerToCommandMap("parcels:", EOverridePolicy.FALLBACK_ONLY) - } - } - - private fun registerListeners() { - if (listeners == null) { - listeners = ParcelListeners(parcelProvider, entityTracker, storage) - registrator.registerListeners(listeners!!) - - val worldEditPlugin = server.pluginManager.getPlugin("WorldEdit") - if (worldEditPlugin != null) { - WorldEditListener.register(this, worldEditPlugin) - } - } - - scheduleRepeating(100, 5, entityTracker::tick) - } - +package io.dico.parcels2 + +import io.dico.dicore.Registrator +import io.dico.dicore.command.EOverridePolicy +import io.dico.dicore.command.ICommandDispatcher +import io.dico.parcels2.command.getParcelCommands +import io.dico.parcels2.defaultimpl.GlobalPrivilegesManagerImpl +import io.dico.parcels2.defaultimpl.ParcelProviderImpl +import io.dico.parcels2.listener.ParcelEntityTracker +import io.dico.parcels2.listener.ParcelListeners +import io.dico.parcels2.listener.WorldEditListener +import io.dico.parcels2.options.Options +import io.dico.parcels2.options.optionsMapper +import io.dico.parcels2.storage.Storage +import io.dico.parcels2.util.MainThreadDispatcher +import io.dico.parcels2.util.PluginAware +import io.dico.parcels2.util.ext.tryCreate +import io.dico.parcels2.util.isServerThread +import io.dico.parcels2.util.scheduleRepeating +import kotlinx.coroutines.CoroutineScope +import org.bukkit.Bukkit +import org.bukkit.generator.ChunkGenerator +import org.bukkit.plugin.Plugin +import org.bukkit.plugin.java.JavaPlugin +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.File +import kotlin.coroutines.CoroutineContext + +val logger: Logger = LoggerFactory.getLogger("ParcelsPlugin") +private inline val plogger get() = logger + +class ParcelsPlugin : JavaPlugin(), CoroutineScope, PluginAware { + lateinit var optionsFile: File; private set + lateinit var options: Options; private set + lateinit var parcelProvider: ParcelProvider; private set + lateinit var storage: Storage; private set + lateinit var globalPrivileges: GlobalPrivilegesManager; private set + + val registrator = Registrator(this) + lateinit var entityTracker: ParcelEntityTracker; private set + private var listeners: ParcelListeners? = null + private var cmdDispatcher: ICommandDispatcher? = null + + override val coroutineContext: CoroutineContext = MainThreadDispatcher(this) + override val plugin: Plugin get() = this + val jobDispatcher: JobDispatcher by lazy { BukkitJobDispatcher(this, this, options.tickJobtime) } + + override fun onEnable() { + plogger.info("Is server thread: ${isServerThread()}") + plogger.info("Debug enabled: ${plogger.isDebugEnabled}") + plogger.debug(System.getProperty("user.dir")) + if (!init()) { + Bukkit.getPluginManager().disablePlugin(this) + } + } + + override fun onDisable() { + val hasWorkers = jobDispatcher.jobs.isNotEmpty() + if (hasWorkers) { + plogger.warn("Parcels is attempting to complete all ${jobDispatcher.jobs.size} remaining jobs before shutdown...") + } + jobDispatcher.completeAllTasks() + if (hasWorkers) { + plogger.info("Parcels has completed the remaining jobs.") + } + + cmdDispatcher?.unregisterFromCommandMap() + } + + private fun init(): Boolean { + optionsFile = File(dataFolder, "options.yml") + options = Options() + parcelProvider = ParcelProviderImpl(this) + + try { + if (!loadOptions()) return false + + try { + storage = options.storage.newInstance() + storage.init() + } catch (ex: Exception) { + plogger.error("Failed to connect to database", ex) + return false + } + + globalPrivileges = GlobalPrivilegesManagerImpl(this) + entityTracker = ParcelEntityTracker(parcelProvider) + } catch (ex: Exception) { + plogger.error("Error loading options", ex) + return false + } + + registerListeners() + registerCommands() + + parcelProvider.loadWorlds() + return true + } + + fun loadOptions(): Boolean { + when { + optionsFile.exists() -> optionsMapper.readerForUpdating(options).readValue(optionsFile) + else -> run { + options.addWorld("parcels") + 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}") + } + 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) + } + + private fun registerCommands() { + cmdDispatcher = getParcelCommands(this).apply { + registerToCommandMap("parcels:", EOverridePolicy.FALLBACK_ONLY) + } + } + + private fun registerListeners() { + if (listeners == null) { + listeners = ParcelListeners(parcelProvider, entityTracker, storage) + registrator.registerListeners(listeners!!) + + val worldEditPlugin = server.pluginManager.getPlugin("WorldEdit") + if (worldEditPlugin != null) { + WorldEditListener.register(this, worldEditPlugin) + } + } + + scheduleRepeating(5, delay = 100, task = entityTracker::tick) + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/PlayerProfile.kt b/src/main/kotlin/io/dico/parcels2/PlayerProfile.kt index 6c30c27..b73f7ba 100644 --- a/src/main/kotlin/io/dico/parcels2/PlayerProfile.kt +++ b/src/main/kotlin/io/dico/parcels2/PlayerProfile.kt @@ -1,184 +1,208 @@ -@file:Suppress("unused", "UsePropertyAccessSyntax", "DEPRECATION") - -package io.dico.parcels2 - -import io.dico.parcels2.storage.Storage -import io.dico.parcels2.util.ext.PLAYER_NAME_PLACEHOLDER -import io.dico.parcels2.util.ext.isValid -import io.dico.parcels2.util.ext.uuid -import io.dico.parcels2.util.getOfflinePlayer -import io.dico.parcels2.util.getPlayerName -import org.bukkit.Bukkit -import org.bukkit.OfflinePlayer -import java.util.UUID - -interface PlayerProfile { - val uuid: UUID? get() = null - val name: String? - val nameOrBukkitName: String? - val notNullName: String - val isStar: Boolean get() = this is Star - val exists: Boolean get() = this is RealImpl - - 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 getOfflinePlayer(input)?.let { PlayerProfile(it) } ?: Unresolved(input) - } - } - - interface Real : PlayerProfile { - override val uuid: UUID - override val nameOrBukkitName: String? - // If a player is online, their name is prioritized to get name changes right immediately - get() = Bukkit.getPlayer(uuid)?.name ?: name ?: getPlayerName(uuid) - override val notNullName: String - get() = nameOrBukkitName ?: PLAYER_NAME_PLACEHOLDER - - val player: OfflinePlayer? get() = 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 get() = "*" - override val nameOrBukkitName get() = name - override val notNullName get() = name - - // hopefully nobody will have this random UUID :) - override val uuid: UUID = UUID.fromString("7d09c4c6-117d-4f36-9778-c4d24618cee1") - - override fun matches(player: OfflinePlayer, allowNameMatch: Boolean): Boolean { - return true - } - - override fun toString() = "Star" - } - - abstract class NameOnly(override val name: String) : BaseImpl() { - override val notNullName get() = name - override val nameOrBukkitName: String get() = name - - override fun matches(player: OfflinePlayer, allowNameMatch: Boolean): Boolean { - return allowNameMatch && player.name == name - } - - override fun toString() = "${javaClass.simpleName}($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 - } - - suspend fun tryResolveSuspendedly(storage: Storage): Real? { - return storage.getPlayerUuidForName(name).await()?.let { resolve(it) } - } - - 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 { - override fun toString() = "Real($notNullName)" - } - -} - -suspend fun PlayerProfile.resolved(storage: Storage, resolveToFake: Boolean = false): PlayerProfile? = - when (this) { - is PlayerProfile.Unresolved -> tryResolveSuspendedly(storage) - ?: if (resolveToFake) PlayerProfile.Fake(name) else null - else -> this +@file:Suppress("unused", "UsePropertyAccessSyntax", "DEPRECATION") + +package io.dico.parcels2 + +import io.dico.parcels2.storage.Storage +import io.dico.parcels2.util.checkPlayerNameValid +import io.dico.parcels2.util.ext.PLAYER_NAME_PLACEHOLDER +import io.dico.parcels2.util.ext.isValid +import io.dico.parcels2.util.ext.uuid +import io.dico.parcels2.util.getOfflinePlayer +import io.dico.parcels2.util.getPlayerName +import io.dico.parcels2.util.isPlayerNameValid +import org.bukkit.Bukkit +import org.bukkit.OfflinePlayer +import java.util.UUID + +interface PlayerProfile { + val uuid: UUID? get() = null + val name: String? + val nameOrBukkitName: String? + val notNullName: String + val isStar: Boolean get() = this is Star + val exists: Boolean get() = this is RealImpl + + 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, if (name != null && !isPlayerNameValid(name)) null else 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 equalsIgnoreCase 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 (!isPlayerNameValid(input)) { + if (!allowFake) return null + return Fake(input) + } + + if (input == Star.name) return Star + + return getOfflinePlayer(input)?.let { PlayerProfile(it) } ?: Unresolved(input) + } + } + + interface Real : PlayerProfile { + override val uuid: UUID + override val nameOrBukkitName: String? + // If a player is online, their name is prioritized to get name changes right immediately + get() = Bukkit.getPlayer(uuid)?.name ?: name ?: getPlayerName(uuid) + override val notNullName: String + get() = nameOrBukkitName ?: PLAYER_NAME_PLACEHOLDER + + val player: OfflinePlayer? get() = 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 equalsIgnoreCase Star.name) return Star + return Unresolved(name) + } + + operator fun invoke(uuid: UUID, name: String?): Real { + if (name equalsIgnoreCase Star.name || uuid == Star.uuid) return Star + return RealImpl(uuid, name) + } + + fun safe(uuid: UUID?, name: String?): Real? { + if (name equalsIgnoreCase Star.name || uuid == Star.uuid) return Star + if (uuid == null) return null + return RealImpl(uuid, if (name != null && !isPlayerNameValid(name)) null else name) + } + + } + } + + object Star : BaseImpl(), Real { + override val name get() = "*" + override val nameOrBukkitName get() = name + override val notNullName get() = name + + // hopefully nobody will have this random UUID :) + override val uuid: UUID = UUID.fromString("7d09c4c6-117d-4f36-9778-c4d24618cee1") + + override fun matches(player: OfflinePlayer, allowNameMatch: Boolean): Boolean { + return true + } + + override fun toString() = "Star" + } + + abstract class NameOnly(override val name: String) : BaseImpl() { + override val notNullName get() = name + override val nameOrBukkitName: String get() = name + + override fun matches(player: OfflinePlayer, allowNameMatch: Boolean): Boolean { + return allowNameMatch && player.name equalsIgnoreCase name + } + + override fun toString() = "${javaClass.simpleName}($name)" + } + + class Fake(name: String) : NameOnly(name) { + override fun equals(other: PlayerProfile): Boolean { + return other is Fake && other.name equalsIgnoreCase name + } + } + + class Unresolved(name: String) : NameOnly(name) { + init { + checkPlayerNameValid(name) + } + + override fun equals(other: PlayerProfile): Boolean { + return other is Unresolved && name equalsIgnoreCase other.name + } + + suspend fun tryResolveSuspendedly(storage: Storage): Real? { + return storage.getPlayerUuidForName(name).await()?.let { resolve(it) } + } + + 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 { + init { + name?.let { checkPlayerNameValid(it) } + } + + override fun toString() = "Real($notNullName)" + } + +} + +private infix fun String?.equalsIgnoreCase(other: String): Boolean { + if (this == null) return false + if (length != other.length) return false + repeat(length) { i -> + if (this[i].toLowerCase() != other[i].toLowerCase()) return false + } + return true +} + +suspend fun PlayerProfile.resolved(storage: Storage, resolveToFake: Boolean = false): PlayerProfile? = + when (this) { + is PlayerProfile.Unresolved -> tryResolveSuspendedly(storage) + ?: if (resolveToFake) PlayerProfile.Fake(name) else null + else -> this } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/blockvisitor/Entities.kt b/src/main/kotlin/io/dico/parcels2/blockvisitor/Entities.kt new file mode 100644 index 0000000..d9ea09f --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/blockvisitor/Entities.kt @@ -0,0 +1,38 @@ +package io.dico.parcels2.blockvisitor + +import io.dico.parcels2.util.math.Vec3d +import org.bukkit.Location +import org.bukkit.World +import org.bukkit.entity.Entity +import org.bukkit.entity.Minecart + +/* +open class EntityCopy(entity: T) { + val type = entity.type + + @Suppress("UNCHECKED_CAST") + fun spawn(world: World, position: Vec3d): T { + val entity = world.spawnEntity(Location(null, position.x, position.y, position.z), type) as T + setAttributes(entity) + return entity + } + + open fun setAttributes(entity: T) {} +} + +open class MinecartCopy(entity: T) : EntityCopy(entity) { + val damage = entity.damage + val maxSpeed = entity.maxSpeed + val isSlowWhenEmpty = entity.isSlowWhenEmpty + val flyingVelocityMod = entity.flyingVelocityMod + val derailedVelocityMod = entity.derailedVelocityMod + val displayBlockData = entity.displayBlockData + val displayBlockOffset = entity.displayBlockOffset + + override fun setAttributes(entity: T) { + super.setAttributes(entity) + entity.damage = damage + entity.displayBlockData = displayBlockData + entity.displayBlockOffset = displayBlockOffset + } +}*/ \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/command/ParcelParameterTypes.kt b/src/main/kotlin/io/dico/parcels2/command/ParcelParameterTypes.kt index 730625e..1b20f72 100644 --- a/src/main/kotlin/io/dico/parcels2/command/ParcelParameterTypes.kt +++ b/src/main/kotlin/io/dico/parcels2/command/ParcelParameterTypes.kt @@ -1,83 +1,90 @@ -package io.dico.parcels2.command - -import io.dico.dicore.command.CommandException -import io.dico.dicore.command.parameter.ArgumentBuffer -import io.dico.dicore.command.parameter.Parameter -import io.dico.dicore.command.parameter.type.ParameterConfig -import io.dico.dicore.command.parameter.type.ParameterType -import io.dico.parcels2.* -import io.dico.parcels2.command.ProfileKind.Companion.ANY -import io.dico.parcels2.command.ProfileKind.Companion.FAKE -import io.dico.parcels2.command.ProfileKind.Companion.REAL -import org.bukkit.Location -import org.bukkit.command.CommandSender -import org.bukkit.entity.Player - -fun invalidInput(parameter: Parameter<*, *>, message: String): Nothing { - throw CommandException("invalid input for ${parameter.name}: $message") -} - -fun ParcelProvider.getTargetWorld(input: String?, sender: CommandSender, parameter: Parameter<*, *>): ParcelWorld { - val worldName = input - ?.takeUnless { it.isEmpty() } - ?: (sender as? Player)?.world?.name - ?: invalidInput(parameter, "console cannot omit the world name") - - return getWorld(worldName) - ?: invalidInput(parameter, "$worldName is not a parcel world") -} - -class ParcelParameterType(val parcelProvider: ParcelProvider) : ParameterType(Parcel::class.java) { - val regex = Regex.fromLiteral("((.+)->)?([0-9]+):([0-9]+)") - - override fun parse(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): Parcel { - val matchResult = regex.matchEntire(buffer.next()!!) - ?: invalidInput(parameter, "must match (w->)?a:b (/${regex.pattern}/)") - - val world = parcelProvider.getTargetWorld(matchResult.groupValues[2], sender, parameter) - - val x = matchResult.groupValues[3].toIntOrNull() - ?: invalidInput(parameter, "couldn't parse int") - - val z = matchResult.groupValues[4].toIntOrNull() - ?: invalidInput(parameter, "couldn't parse int") - - return world.getParcelById(x, z) - ?: invalidInput(parameter, "parcel id is out of range") - } - -} - -annotation class ProfileKind(val kind: Int) { - companion object : ParameterConfig(ProfileKind::class.java) { - const val REAL = 1 - const val FAKE = 2 - const val ANY = REAL or FAKE - - override fun toParameterInfo(annotation: ProfileKind): Int { - return annotation.kind - } - } -} - -class ProfileParameterType : ParameterType(PlayerProfile::class.java, ProfileKind) { - - override fun parse(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): PlayerProfile { - val info = parameter.paramInfo ?: REAL - val allowReal = (info and REAL) != 0 - val allowFake = (info and FAKE) != 0 - - val input = buffer.next()!! - return PlayerProfile.byName(input, allowReal, allowFake) - } - - override fun complete( - parameter: Parameter, - sender: CommandSender, - location: Location?, - buffer: ArgumentBuffer - ): MutableList { - logger.info("Completing PlayerProfile: ${buffer.next()}") - return super.complete(parameter, sender, location, buffer) - } -} +package io.dico.parcels2.command + +import io.dico.dicore.command.CommandException +import io.dico.dicore.command.parameter.ArgumentBuffer +import io.dico.dicore.command.parameter.Parameter +import io.dico.dicore.command.parameter.type.ParameterConfig +import io.dico.dicore.command.parameter.type.ParameterType +import io.dico.parcels2.* +import io.dico.parcels2.command.ProfileKind.Companion.FAKE +import io.dico.parcels2.command.ProfileKind.Companion.REAL +import org.bukkit.Location +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + +fun invalidInput(parameter: Parameter<*, *>, message: String): Nothing { + throw CommandException("invalid input for ${parameter.name}: $message") +} + +fun ParcelProvider.getTargetWorld(input: String?, sender: CommandSender, parameter: Parameter<*, *>): ParcelWorld { + val worldName = input + ?.takeUnless { it.isEmpty() } + ?: (sender as? Player)?.world?.name + ?: invalidInput(parameter, "console cannot omit the world name") + + return getWorld(worldName) + ?: invalidInput(parameter, "$worldName is not a parcel world") +} + +class ParcelParameterType(val parcelProvider: ParcelProvider) : ParameterType(Parcel::class.java) { + val regex = Regex.fromLiteral("((.+)->)?([0-9]+):([0-9]+)") + + override fun parse(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): Parcel { + val matchResult = regex.matchEntire(buffer.next()!!) + ?: invalidInput(parameter, "must match (w->)?a:b (/${regex.pattern}/)") + + val world = parcelProvider.getTargetWorld(matchResult.groupValues[2], sender, parameter) + + val x = matchResult.groupValues[3].toIntOrNull() + ?: invalidInput(parameter, "couldn't parse int") + + val z = matchResult.groupValues[4].toIntOrNull() + ?: invalidInput(parameter, "couldn't parse int") + + return world.getParcelById(x, z) + ?: invalidInput(parameter, "parcel id is out of range") + } + +} + +annotation class ProfileKind(val kind: Int) { + companion object : ParameterConfig(ProfileKind::class.java) { + const val REAL = 1 + const val FAKE = 2 + const val ANY = REAL or FAKE + const val ALLOW_INVALID = 4 + + override fun toParameterInfo(annotation: ProfileKind): Int { + return annotation.kind + } + } +} + +class ProfileParameterType : ParameterType(PlayerProfile::class.java, ProfileKind) { + + override fun parse(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): PlayerProfile? { + val info = parameter.paramInfo ?: REAL + val allowReal = (info and REAL) != 0 + val allowFake = (info and FAKE) != 0 + + val input = buffer.next()!! + + val profile = PlayerProfile.byName(input, allowReal, allowFake) + + if (profile == null && (info and ProfileKind.ALLOW_INVALID) == 0) { + invalidInput(parameter, "\'$input\' is not a valid player name") + } + + return profile + } + + override fun complete( + parameter: Parameter, + sender: CommandSender, + location: Location?, + buffer: ArgumentBuffer + ): MutableList { + logger.info("Completing PlayerProfile: ${buffer.next()}") + return super.complete(parameter, sender, location, buffer) + } +} diff --git a/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt b/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt index c39c4b6..934a993 100644 --- a/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt +++ b/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt @@ -1,191 +1,215 @@ -package io.dico.parcels2.command - -import io.dico.dicore.command.parameter.ArgumentBuffer -import io.dico.dicore.command.parameter.Parameter -import io.dico.dicore.command.parameter.type.ParameterConfig -import io.dico.dicore.command.parameter.type.ParameterType -import io.dico.parcels2.Parcel -import io.dico.parcels2.ParcelProvider -import io.dico.parcels2.ParcelWorld -import io.dico.parcels2.PlayerProfile -import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.DEFAULT_KIND -import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.ID -import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.OWNER -import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.OWNER_FAKE -import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.OWNER_REAL -import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.PREFER_OWNED_FOR_DEFAULT -import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.REAL -import io.dico.parcels2.storage.Storage -import io.dico.parcels2.util.math.Vec2i -import io.dico.parcels2.util.math.floor -import org.bukkit.command.CommandSender -import org.bukkit.entity.Player - -sealed class ParcelTarget(val world: ParcelWorld, val parsedKind: Int, val isDefault: Boolean) { - - abstract suspend fun getParcelSuspend(storage: Storage): Parcel? - - class ByID(world: ParcelWorld, val id: Vec2i?, parsedKind: Int, isDefault: Boolean) : ParcelTarget(world, parsedKind, isDefault) { - override suspend fun getParcelSuspend(storage: Storage): Parcel? = getParcel() - fun getParcel() = id?.let { world.getParcelById(it) } - val isPath: Boolean get() = id == null - } - - class ByOwner( - world: ParcelWorld, - owner: PlayerProfile, - val index: Int, - parsedKind: Int, - isDefault: Boolean, - val onResolveFailure: (() -> Unit)? = null - ) : ParcelTarget(world, parsedKind, isDefault) { - init { - if (index < 0) throw IllegalArgumentException("Invalid parcel home index: $index") - } - - var owner = owner; private set - - suspend fun resolveOwner(storage: Storage): Boolean { - val owner = owner - if (owner is PlayerProfile.Unresolved) { - this.owner = owner.tryResolveSuspendedly(storage) ?: if (parsedKind and OWNER_FAKE != 0) PlayerProfile.Fake(owner.name) - else run { onResolveFailure?.invoke(); return false } - } - return true - } - - override suspend fun getParcelSuspend(storage: Storage): Parcel? { - onResolveFailure?.let { resolveOwner(storage) } - - val ownedParcelsSerialized = storage.getOwnedParcels(owner).await() - val ownedParcels = ownedParcelsSerialized - .filter { it.worldId.equals(world.id) } - .map { world.getParcelById(it.x, it.z) } - - return ownedParcels.getOrNull(index) - } - } - - annotation class TargetKind(val kind: Int) { - companion object : ParameterConfig(TargetKind::class.java) { - const val ID = 1 // ID - const val OWNER_REAL = 2 // an owner backed by a UUID - const val OWNER_FAKE = 4 // an owner not backed by a UUID - - const val OWNER = OWNER_REAL or OWNER_FAKE // any owner - const val ANY = ID or OWNER_REAL or OWNER_FAKE // any - const val REAL = ID or OWNER_REAL // no owner not backed by a UUID - - const val DEFAULT_KIND = REAL - - const val PREFER_OWNED_FOR_DEFAULT = 8 // if the kind can be ID and OWNER_REAL, prefer OWNER_REAL for default - // instead of parcel that the player is in - - override fun toParameterInfo(annotation: TargetKind): Int { - return annotation.kind - } - } - } - - class PType(val parcelProvider: ParcelProvider, val parcelAddress: SpecialCommandAddress? = null) : - ParameterType(ParcelTarget::class.java, TargetKind) { - - override fun parse(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): ParcelTarget { - var input = buffer.next()!! - val worldString = input.substringBefore("/", missingDelimiterValue = "") - input = input.substringAfter("/") - - val world = if (worldString.isEmpty()) { - val player = requirePlayer(sender, parameter, "the world") - parcelProvider.getWorld(player.world) - ?: invalidInput(parameter, "You cannot omit the world if you're not in a parcel world") - } else { - parcelProvider.getWorld(worldString) ?: invalidInput(parameter, "$worldString is not a parcel world") - } - - val kind = parameter.paramInfo ?: DEFAULT_KIND - if (input.contains(',')) { - if (kind and ID == 0) invalidInput(parameter, "You must specify a parcel by OWNER, that is, an owner and index") - return ByID(world, getId(parameter, input), kind, false) - } - - 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, kind, sender, input) - return ByOwner(world, owner, index, kind, false, onResolveFailure = { invalidInput(parameter, "The player $input does not exist") }) - } - - private fun getId(parameter: Parameter<*, *>, input: String): Vec2i { - val x = input.substringBefore(',').run { - toIntOrNull() ?: invalidInput(parameter, "ID(x) must be an integer, $this is not an integer") - } - val z = input.substringAfter(',').run { - toIntOrNull() ?: invalidInput(parameter, "ID(z) must be an integer, $this is not an integer") - } - return Vec2i(x, z) - } - - private fun getHomeIndex(parameter: Parameter<*, *>, kind: Int, sender: CommandSender, input: String): Pair { - val splitIdx = input.indexOf(':') - val ownerString: String - val index: Int? - - val speciallyParsedIndex = parcelAddress?.speciallyParsedIndex - - if (splitIdx == -1) { - - if (speciallyParsedIndex == null) { - // just the index. - index = input.toIntOrNull() - ownerString = if (index == null) input else "" - } else { - // just the owner. - index = speciallyParsedIndex - ownerString = input - } - - } else { - if (speciallyParsedIndex != null) { - invalidInput(parameter, "Duplicate home index") - } - - ownerString = input.substring(0, splitIdx) - - val indexString = input.substring(splitIdx + 1) - index = indexString.toIntOrNull() - ?: invalidInput(parameter, "The home index must be an integer, $indexString is not an integer") - } - - val owner = if (ownerString.isEmpty()) - PlayerProfile(requirePlayer(sender, parameter, "the player")) - else - PlayerProfile.byName(ownerString, allowReal = kind and OWNER_REAL != 0, allowFake = kind and OWNER_FAKE != 0) - - return owner to (index ?: 0) - } - - private fun requirePlayer(sender: CommandSender, parameter: Parameter<*, *>, objName: String): Player { - if (sender !is Player) invalidInput(parameter, "console cannot omit the $objName") - return sender - } - - override fun getDefaultValue(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): ParcelTarget? { - val kind = parameter.paramInfo ?: DEFAULT_KIND - val useLocation = when { - kind and REAL == REAL -> kind and PREFER_OWNED_FOR_DEFAULT == 0 - kind and ID != 0 -> true - kind and OWNER_REAL != 0 -> false - else -> return null - } - - val player = requirePlayer(sender, parameter, "the parcel") - val world = parcelProvider.getWorld(player.world) ?: invalidInput(parameter, "You must be in a parcel world to omit the parcel") - if (useLocation) { - val id = player.location.let { world.getParcelIdAt(it.x.floor(), it.z.floor())?.pos } - return ByID(world, id, kind, true) - } - - return ByOwner(world, PlayerProfile(player), parcelAddress?.speciallyParsedIndex ?: 0, kind, true) - } - } - -} +package io.dico.parcels2.command + +import io.dico.dicore.command.parameter.ArgumentBuffer +import io.dico.dicore.command.parameter.Parameter +import io.dico.dicore.command.parameter.type.ParameterConfig +import io.dico.dicore.command.parameter.type.ParameterType +import io.dico.parcels2.Parcel +import io.dico.parcels2.ParcelProvider +import io.dico.parcels2.ParcelWorld +import io.dico.parcels2.PlayerProfile +import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.DEFAULT_KIND +import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.ID +import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.OWNER +import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.OWNER_FAKE +import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.OWNER_REAL +import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.PREFER_OWNED_FOR_DEFAULT +import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.REAL +import io.dico.parcels2.storage.Storage +import io.dico.parcels2.util.math.Vec2i +import io.dico.parcels2.util.math.floor +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + +sealed class ParcelTarget(val world: ParcelWorld, val parsedKind: Int, val isDefault: Boolean) { + + abstract suspend fun getParcelSuspend(storage: Storage): Parcel? + + class ByID(world: ParcelWorld, val id: Vec2i?, parsedKind: Int, isDefault: Boolean) : + ParcelTarget(world, parsedKind, isDefault) { + override suspend fun getParcelSuspend(storage: Storage): Parcel? = getParcel() + fun getParcel() = id?.let { world.getParcelById(it) } + val isPath: Boolean get() = id == null + } + + class ByOwner( + world: ParcelWorld, + owner: PlayerProfile, + val index: Int, + parsedKind: Int, + isDefault: Boolean, + val onResolveFailure: (() -> Unit)? = null + ) : ParcelTarget(world, parsedKind, isDefault) { + init { + if (index < 0) throw IllegalArgumentException("Invalid parcel home index: $index") + } + + var owner = owner; private set + + suspend fun resolveOwner(storage: Storage): Boolean { + val owner = owner + if (owner is PlayerProfile.Unresolved) { + this.owner = owner.tryResolveSuspendedly(storage) ?: if (parsedKind and OWNER_FAKE != 0) PlayerProfile.Fake(owner.name) + else run { onResolveFailure?.invoke(); return false } + } + return true + } + + override suspend fun getParcelSuspend(storage: Storage): Parcel? { + onResolveFailure?.let { resolveOwner(storage) } + + val ownedParcelsSerialized = storage.getOwnedParcels(owner).await() + val ownedParcels = ownedParcelsSerialized + .filter { it.worldId.equals(world.id) } + .map { world.getParcelById(it.x, it.z) } + + return ownedParcels.getOrNull(index) + } + } + + annotation class TargetKind(val kind: Int) { + companion object : ParameterConfig(TargetKind::class.java) { + const val ID = 1 // ID + const val OWNER_REAL = 2 // an owner backed by a UUID + const val OWNER_FAKE = 4 // an owner not backed by a UUID + + const val OWNER = OWNER_REAL or OWNER_FAKE // any owner + const val ANY = ID or OWNER_REAL or OWNER_FAKE // any + const val REAL = ID or OWNER_REAL // no owner not backed by a UUID + + const val DEFAULT_KIND = REAL + + const val PREFER_OWNED_FOR_DEFAULT = + 8 // if the kind can be ID and OWNER_REAL, prefer OWNER_REAL for default + // instead of parcel that the player is in + + override fun toParameterInfo(annotation: TargetKind): Int { + return annotation.kind + } + } + } + + class PType(val parcelProvider: ParcelProvider, val parcelAddress: SpecialCommandAddress? = null) : + ParameterType(ParcelTarget::class.java, TargetKind) { + + override fun parse( + parameter: Parameter, + sender: CommandSender, + buffer: ArgumentBuffer + ): ParcelTarget { + var input = buffer.next()!! + val worldString = input.substringBefore("/", missingDelimiterValue = "") + input = input.substringAfter("/") + + val world = if (worldString.isEmpty()) { + val player = requirePlayer(sender, parameter, "the world") + parcelProvider.getWorld(player.world) + ?: invalidInput(parameter, "You cannot omit the world if you're not in a parcel world") + } else { + parcelProvider.getWorld(worldString) ?: invalidInput(parameter, "$worldString is not a parcel world") + } + + val kind = parameter.paramInfo ?: DEFAULT_KIND + if (input.contains(',')) { + if (kind and ID == 0) invalidInput(parameter, + "You must specify a parcel by OWNER, that is, an owner and index") + return ByID(world, getId(parameter, input), kind, false) + } + + 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, kind, sender, input) + return ByOwner(world, + owner, + index, + kind, + false, + onResolveFailure = { invalidInput(parameter, "The player $input does not exist") }) + } + + private fun getId(parameter: Parameter<*, *>, input: String): Vec2i { + val x = input.substringBefore(',').run { + toIntOrNull() ?: invalidInput(parameter, "ID(x) must be an integer, $this is not an integer") + } + val z = input.substringAfter(',').run { + toIntOrNull() ?: invalidInput(parameter, "ID(z) must be an integer, $this is not an integer") + } + return Vec2i(x, z) + } + + private fun getHomeIndex( + parameter: Parameter<*, *>, + kind: Int, + sender: CommandSender, + input: String + ): Pair { + val splitIdx = input.indexOf(':') + val ownerString: String + val index: Int? + + val speciallyParsedIndex = parcelAddress?.speciallyParsedIndex + + if (splitIdx == -1) { + + if (speciallyParsedIndex == null) { + // just the index. + index = input.toIntOrNull() + ownerString = if (index == null) input else "" + } else { + // just the owner. + index = speciallyParsedIndex + ownerString = input + } + + } else { + if (speciallyParsedIndex != null) { + invalidInput(parameter, "Duplicate home index") + } + + ownerString = input.substring(0, splitIdx) + + val indexString = input.substring(splitIdx + 1) + index = indexString.toIntOrNull() + ?: invalidInput(parameter, "The home index must be an integer, $indexString is not an integer") + } + + val owner = (if (ownerString.isEmpty()) + PlayerProfile(requirePlayer(sender, parameter, "the player")) + else + PlayerProfile.byName(ownerString, allowReal = kind and OWNER_REAL != 0, allowFake = kind and OWNER_FAKE != 0)) + ?: invalidInput(parameter, "\'$ownerString\' is not a valid player name") + + return owner to (index ?: 0) + } + + private fun requirePlayer(sender: CommandSender, parameter: Parameter<*, *>, objName: String): Player { + if (sender !is Player) invalidInput(parameter, "console cannot omit the $objName") + return sender + } + + override fun getDefaultValue( + parameter: Parameter, + sender: CommandSender, + buffer: ArgumentBuffer + ): ParcelTarget? { + val kind = parameter.paramInfo ?: DEFAULT_KIND + val useLocation = when { + kind and REAL == REAL -> kind and PREFER_OWNED_FOR_DEFAULT == 0 + kind and ID != 0 -> true + kind and OWNER_REAL != 0 -> false + else -> return null + } + + val player = requirePlayer(sender, parameter, "the parcel") + val world = parcelProvider.getWorld(player.world) ?: invalidInput(parameter, + "You must be in a parcel world to omit the parcel") + if (useLocation) { + val id = player.location.let { world.getParcelIdAt(it.x.floor(), it.z.floor())?.pos } + return ByID(world, id, kind, true) + } + + return ByOwner(world, PlayerProfile(player), parcelAddress?.speciallyParsedIndex ?: 0, kind, true) + } + } + +} diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt index caa3f1f..73b6b4d 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt @@ -1,378 +1,391 @@ -package io.dico.parcels2.defaultimpl - -import io.dico.parcels2.* -import io.dico.parcels2.blockvisitor.RegionTraverser -import io.dico.parcels2.options.DefaultGeneratorOptions -import io.dico.parcels2.util.math.* -import kotlinx.coroutines.CoroutineScope -import org.bukkit.* -import org.bukkit.block.Biome -import org.bukkit.block.BlockFace -import org.bukkit.block.Skull -import org.bukkit.block.data.type.Slab -import org.bukkit.block.data.type.WallSign -import java.util.Random - -private val airType = Bukkit.createBlockData(Material.AIR) - -private const val chunkSize = 16 - -class DefaultParcelGenerator( - override val worldName: String, - private val o: DefaultGeneratorOptions -) : ParcelGenerator() { - private var _world: World? = null - override val world: World - get() { - if (_world == null) { - val world = Bukkit.getWorld(worldName) - maxHeight = world.maxHeight - _world = world - return world - } - return _world!! - } - - private var maxHeight = 0 - 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 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?) { - // do nothing - } - - 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 makeParcelLocatorAndBlockManager( - parcelProvider: ParcelProvider, - container: ParcelContainer, - coroutineScope: CoroutineScope, - jobDispatcher: JobDispatcher - ): Pair { - val impl = ParcelLocatorAndBlockManagerImpl(parcelProvider, container, coroutineScope, jobDispatcher) - return impl to impl - } - - private inline fun convertBlockLocationToId(x: Int, z: Int, mapper: (Int, Int) -> T): T? { - 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 (modX in 0 until parcelSize && modZ in 0 until parcelSize) { - return mapper((absX - modX) / sectionSize + 1, (absZ - modZ) / sectionSize + 1) - } - return null - } - - @Suppress("DEPRECATION") - private inner class ParcelLocatorAndBlockManagerImpl( - val parcelProvider: ParcelProvider, - val container: ParcelContainer, - coroutineScope: CoroutineScope, - override val jobDispatcher: JobDispatcher - ) : ParcelBlockManagerBase(), ParcelLocator, CoroutineScope by coroutineScope { - - override val world: World get() = this@DefaultParcelGenerator.world - val worldId = parcelProvider.getWorld(world)?.id ?: ParcelWorldId(world) - override val parcelTraverser: RegionTraverser = RegionTraverser.convergingTo(o.floorHeight) - - private val cornerWallType = when { - o.wallType is Slab -> (o.wallType.clone() as Slab).apply { type = Slab.Type.DOUBLE } - o.wallType.material.name.endsWith("CARPET") -> { - Bukkit.createBlockData(Material.getMaterial(o.wallType.material.name.substringBefore("CARPET") + "WOOL")) - } - else -> null - } - - override fun getParcelAt(x: Int, z: Int): Parcel? { - return convertBlockLocationToId(x, z, container::getParcelById) - } - - override fun getParcelIdAt(x: Int, z: Int): ParcelId? { - return convertBlockLocationToId(x, z) { idx, idz -> ParcelId(worldId, idx, idz) } - } - - - private fun checkParcelId(parcel: ParcelId): ParcelId { - if (!parcel.worldId.equals(worldId)) { - throw IllegalArgumentException() - } - return parcel - } - - override fun getRegionOrigin(parcel: ParcelId): Vec2i { - checkParcelId(parcel) - return Vec2i( - sectionSize * (parcel.x - 1) + pathOffset + o.offsetX, - sectionSize * (parcel.z - 1) + pathOffset + o.offsetZ - ) - } - - override fun getRegion(parcel: ParcelId): Region { - val origin = getRegionOrigin(parcel) - return Region( - Vec3i(origin.x, 0, origin.z), - Vec3i(o.parcelSize, maxHeight, o.parcelSize) - ) - } - - override fun getHomeLocation(parcel: ParcelId): Location { - val origin = getRegionOrigin(parcel) - val x = origin.x + (o.parcelSize - 1) / 2.0 - val z = origin.z - 2 - return Location(world, x + 0.5, o.floorHeight + 1.0, z + 0.5, 0F, 0F) - } - - override fun getParcelForInfoBlockInteraction(block: Vec3i, type: Material, face: BlockFace): Parcel? { - if (block.y != o.floorHeight + 1) return null - - val expectedParcelOrigin = when (type) { - Material.WALL_SIGN -> Vec2i(block.x + 1, block.z + 2) - o.wallType.material, cornerWallType?.material -> { - if (face != BlockFace.NORTH || world[block + Vec3i.convert(BlockFace.NORTH)].type == Material.WALL_SIGN) { - return null - } - - Vec2i(block.x + 1, block.z + 1) - } - else -> return null - } - - return getParcelAt(expectedParcelOrigin.x, expectedParcelOrigin.z) - ?.takeIf { expectedParcelOrigin == getRegionOrigin(it.id) } - ?.also { parcel -> - if (type != Material.WALL_SIGN && parcel.owner != null) { - updateParcelInfo(parcel.id, parcel.owner) - parcel.isOwnerSignOutdated = false - } - } - } - - override fun isParcelInfoSectionLoaded(parcel: ParcelId): Boolean { - val wallBlockChunk = getRegionOrigin(parcel).add(-1, -1).toChunk() - return world.isChunkLoaded(wallBlockChunk.x, wallBlockChunk.z) - } - - override fun updateParcelInfo(parcel: ParcelId, owner: PlayerProfile?) { - val b = getRegionOrigin(parcel) - - val wallBlock = world.getBlockAt(b.x - 1, o.floorHeight + 1, b.z - 1) - val signBlock = world.getBlockAt(b.x - 1, o.floorHeight + 1, b.z - 2) - val skullBlock = world.getBlockAt(b.x - 1, o.floorHeight + 2, b.z - 1) - - if (owner == null) { - wallBlock.blockData = o.wallType - signBlock.type = Material.AIR - skullBlock.type = Material.AIR - - } else { - cornerWallType?.let { wallBlock.blockData = it } - signBlock.blockData = (Bukkit.createBlockData(Material.WALL_SIGN) as WallSign).apply { facing = BlockFace.NORTH } - - val sign = signBlock.state as org.bukkit.block.Sign - sign.setLine(0, "${parcel.x},${parcel.z}") - sign.setLine(2, owner.name ?: "") - sign.update() - - skullBlock.type = Material.AIR - skullBlock.type = Material.PLAYER_HEAD - val skull = skullBlock.state as Skull - if (owner is PlayerProfile.Real) { - skull.owningPlayer = Bukkit.getOfflinePlayer(owner.uuid) - - } else if (!skull.setOwner(owner.name)) { - skullBlock.type = Material.AIR - return - } - - skull.rotation = BlockFace.SOUTH - skull.update() - } - } - - private fun trySubmitBlockVisitor(vararg parcels: ParcelId, function: JobFunction): Job? { - parcels.forEach { checkParcelId(it) } - return parcelProvider.trySubmitBlockVisitor(Permit(), parcels, function) - } - - override fun setBiome(parcel: ParcelId, biome: Biome) = trySubmitBlockVisitor(checkParcelId(parcel)) { - val world = world - val b = getRegionOrigin(parcel) - val parcelSize = o.parcelSize - for (x in b.x until b.x + parcelSize) { - for (z in b.z until b.z + parcelSize) { - markSuspensionPoint() - world.setBiome(x, z, biome) - } - } - } - - override fun clearParcel(parcel: ParcelId) = trySubmitBlockVisitor(checkParcelId(parcel)) { - val region = getRegion(parcel) - val blocks = parcelTraverser.traverseRegion(region) - val blockCount = region.blockCount.toDouble() - val world = world - val floorHeight = o.floorHeight - val airType = airType - val floorType = o.floorType - val fillType = o.fillType - - for ((index, vec) in blocks.withIndex()) { - markSuspensionPoint() - val y = vec.y - val blockType = when { - y > floorHeight -> airType - y == floorHeight -> floorType - else -> fillType - } - world[vec].blockData = blockType - setProgress((index + 1) / blockCount) - } - } - - override fun getParcelsWithOwnerBlockIn(chunk: Chunk): Collection { - /* - * Get the offsets for the world out of the way - * to simplify the calculation that follows. - */ - - val x = chunk.x.shl(4) - (o.offsetX + pathOffset) - val z = chunk.z.shl(4) - (o.offsetZ + pathOffset) - - /* Locations of wall corners (where owner blocks are placed) are defined as: - * - * x umod sectionSize == sectionSize-1 - * - * This check needs to be made for all 16 slices of the chunk in 2 dimensions - * How to optimize this? - * Let's take the expression - * - * x umod sectionSize - * - * And call it modX - * x can be shifted (chunkSize -1) times to attempt to get a modX of 0. - * This means that if the modX is 1, and sectionSize == (chunkSize-1), there would be a match at the last shift. - * To check that there are any matches, we can see if the following holds: - * - * modX >= ((sectionSize-1) - (chunkSize-1)) - * - * Which can be simplified to: - * modX >= sectionSize - chunkSize - * - * if sectionSize == chunkSize, this expression can be simplified to - * modX >= 0 - * which is always true. This is expected. - * To get the total number of matches on a dimension, we can evaluate the following: - * - * (modX - (sectionSize - chunkSize) + sectionSize) / sectionSize - * - * We add sectionSize to the lhs because, if the other part of the lhs is 0, we need at least 1. - * This can be simplified to: - * - * (modX + chunkSize) / sectionSize - */ - - val sectionSize = sectionSize - - val modX = x umod sectionSize - val matchesOnDimensionX = (modX + chunkSize) / sectionSize - if (matchesOnDimensionX <= 0) return emptyList() - - val modZ = z umod sectionSize - val matchesOnDimensionZ = (modZ + chunkSize) / sectionSize - if (matchesOnDimensionZ <= 0) return emptyList() - - /* - * Now we need to find the first id within the matches, - * and then return the subsequent matches in a rectangle following it. - * - * On each dimension, get the distance to the first match, which is equal to (sectionSize-1 - modX) - * and add it to the coordinate value - */ - val firstX = x + (sectionSize - 1 - modX) - val firstZ = z + (sectionSize - 1 - modZ) - - val firstIdX = (firstX + 1) / sectionSize + 1 - val firstIdZ = (firstZ + 1) / sectionSize + 1 - - if (matchesOnDimensionX == 1 && matchesOnDimensionZ == 1) { - // fast-path optimization - return listOf(Vec2i(firstIdX, firstIdZ)) - } - - return (0 until matchesOnDimensionX).flatMap { idOffsetX -> - (0 until matchesOnDimensionZ).map { idOffsetZ -> Vec2i(firstIdX + idOffsetX, firstIdZ + idOffsetZ) } - } - } - - } - +package io.dico.parcels2.defaultimpl + +import io.dico.parcels2.* +import io.dico.parcels2.blockvisitor.RegionTraverser +import io.dico.parcels2.options.DefaultGeneratorOptions +import io.dico.parcels2.util.math.* +import kotlinx.coroutines.CoroutineScope +import org.bukkit.* +import org.bukkit.block.Biome +import org.bukkit.block.BlockFace +import org.bukkit.block.Skull +import org.bukkit.block.data.type.Slab +import org.bukkit.block.data.type.WallSign +import org.bukkit.entity.Player +import java.util.Random + +private val airType = Bukkit.createBlockData(Material.AIR) + +private const val chunkSize = 16 + +class DefaultParcelGenerator( + override val worldName: String, + private val o: DefaultGeneratorOptions +) : ParcelGenerator() { + private var _world: World? = null + override val world: World + get() { + if (_world == null) { + val world = Bukkit.getWorld(worldName) + maxHeight = world.maxHeight + _world = world + return world + } + return _world!! + } + + private var maxHeight = 0 + 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 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?) { + // do nothing + } + + 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 makeParcelLocatorAndBlockManager( + parcelProvider: ParcelProvider, + container: ParcelContainer, + coroutineScope: CoroutineScope, + jobDispatcher: JobDispatcher + ): Pair { + val impl = ParcelLocatorAndBlockManagerImpl(parcelProvider, container, coroutineScope, jobDispatcher) + return impl to impl + } + + private inline fun convertBlockLocationToId(x: Int, z: Int, mapper: (Int, Int) -> T): T? { + 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 (modX in 0 until parcelSize && modZ in 0 until parcelSize) { + return mapper((absX - modX) / sectionSize + 1, (absZ - modZ) / sectionSize + 1) + } + return null + } + + @Suppress("DEPRECATION") + private inner class ParcelLocatorAndBlockManagerImpl( + val parcelProvider: ParcelProvider, + val container: ParcelContainer, + coroutineScope: CoroutineScope, + override val jobDispatcher: JobDispatcher + ) : ParcelBlockManagerBase(), ParcelLocator, CoroutineScope by coroutineScope { + + override val world: World get() = this@DefaultParcelGenerator.world + val worldId = parcelProvider.getWorld(world)?.id ?: ParcelWorldId(world) + override val parcelTraverser: RegionTraverser = RegionTraverser.convergingTo(o.floorHeight) + + private val cornerWallType = when { + o.wallType is Slab -> (o.wallType.clone() as Slab).apply { type = Slab.Type.DOUBLE } + o.wallType.material.name.endsWith("CARPET") -> { + Bukkit.createBlockData(Material.getMaterial(o.wallType.material.name.substringBefore("CARPET") + "WOOL")) + } + else -> null + } + + override fun getParcelAt(x: Int, z: Int): Parcel? { + return convertBlockLocationToId(x, z, container::getParcelById) + } + + override fun getParcelIdAt(x: Int, z: Int): ParcelId? { + return convertBlockLocationToId(x, z) { idx, idz -> ParcelId(worldId, idx, idz) } + } + + + private fun checkParcelId(parcel: ParcelId): ParcelId { + if (!parcel.worldId.equals(worldId)) { + throw IllegalArgumentException() + } + return parcel + } + + override fun getRegionOrigin(parcel: ParcelId): Vec2i { + checkParcelId(parcel) + return Vec2i( + sectionSize * (parcel.x - 1) + pathOffset + o.offsetX, + sectionSize * (parcel.z - 1) + pathOffset + o.offsetZ + ) + } + + override fun getRegion(parcel: ParcelId): Region { + val origin = getRegionOrigin(parcel) + return Region( + Vec3i(origin.x, 0, origin.z), + Vec3i(o.parcelSize, maxHeight, o.parcelSize) + ) + } + + override fun getHomeLocation(parcel: ParcelId): Location { + val origin = getRegionOrigin(parcel) + val x = origin.x + (o.parcelSize - 1) / 2.0 + val z = origin.z - 2 + return Location(world, x + 0.5, o.floorHeight + 1.0, z + 0.5, 0F, 0F) + } + + override fun getParcelForInfoBlockInteraction(block: Vec3i, type: Material, face: BlockFace): Parcel? { + if (block.y != o.floorHeight + 1) return null + + val expectedParcelOrigin = when (type) { + Material.WALL_SIGN -> Vec2i(block.x + 1, block.z + 2) + o.wallType.material, cornerWallType?.material -> { + if (face != BlockFace.NORTH || world[block + Vec3i.convert(BlockFace.NORTH)].type == Material.WALL_SIGN) { + return null + } + + Vec2i(block.x + 1, block.z + 1) + } + else -> return null + } + + return getParcelAt(expectedParcelOrigin.x, expectedParcelOrigin.z) + ?.takeIf { expectedParcelOrigin == getRegionOrigin(it.id) } + ?.also { parcel -> + if (type != Material.WALL_SIGN && parcel.owner != null) { + updateParcelInfo(parcel.id, parcel.owner) + parcel.isOwnerSignOutdated = false + } + } + } + + override fun isParcelInfoSectionLoaded(parcel: ParcelId): Boolean { + val wallBlockChunk = getRegionOrigin(parcel).add(-1, -1).toChunk() + return world.isChunkLoaded(wallBlockChunk.x, wallBlockChunk.z) + } + + override fun updateParcelInfo(parcel: ParcelId, owner: PlayerProfile?) { + val b = getRegionOrigin(parcel) + + val wallBlock = world.getBlockAt(b.x - 1, o.floorHeight + 1, b.z - 1) + val signBlock = world.getBlockAt(b.x - 1, o.floorHeight + 1, b.z - 2) + val skullBlock = world.getBlockAt(b.x - 1, o.floorHeight + 2, b.z - 1) + + if (owner == null) { + wallBlock.blockData = o.wallType + signBlock.type = Material.AIR + skullBlock.type = Material.AIR + + } else { + cornerWallType?.let { wallBlock.blockData = it } + signBlock.blockData = (Bukkit.createBlockData(Material.WALL_SIGN) as WallSign).apply { facing = BlockFace.NORTH } + + val sign = signBlock.state as org.bukkit.block.Sign + sign.setLine(0, "${parcel.x},${parcel.z}") + sign.setLine(2, owner.name ?: "") + sign.update() + + skullBlock.type = Material.AIR + skullBlock.type = Material.PLAYER_HEAD + val skull = skullBlock.state as Skull + if (owner is PlayerProfile.Real) { + skull.owningPlayer = Bukkit.getOfflinePlayer(owner.uuid) + + } else if (!skull.setOwner(owner.name)) { + skullBlock.type = Material.AIR + return + } + + skull.rotation = BlockFace.SOUTH + skull.update() + } + } + + private fun trySubmitBlockVisitor(vararg parcels: ParcelId, function: JobFunction): Job? { + parcels.forEach { checkParcelId(it) } + return parcelProvider.trySubmitBlockVisitor(Permit(), parcels, function) + } + + override fun setBiome(parcel: ParcelId, biome: Biome) = trySubmitBlockVisitor(checkParcelId(parcel)) { + val world = world + val b = getRegionOrigin(parcel) + val parcelSize = o.parcelSize + for (x in b.x until b.x + parcelSize) { + for (z in b.z until b.z + parcelSize) { + markSuspensionPoint() + world.setBiome(x, z, biome) + } + } + } + + override fun clearParcel(parcel: ParcelId) = trySubmitBlockVisitor(checkParcelId(parcel)) { + val region = getRegion(parcel) + val blocks = parcelTraverser.traverseRegion(region) + val blockCount = region.blockCount.toDouble() + val world = world + val floorHeight = o.floorHeight + val airType = airType + val floorType = o.floorType + val fillType = o.fillType + + delegateWork(0.95) { + for ((index, vec) in blocks.withIndex()) { + markSuspensionPoint() + val y = vec.y + val blockType = when { + y > floorHeight -> airType + y == floorHeight -> floorType + else -> fillType + } + world[vec].blockData = blockType + setProgress((index + 1) / blockCount) + } + } + + delegateWork { + val entities = getEntities(region) + for ((index, entity) in entities.withIndex()) { + if (entity is Player) continue + entity.remove() + setProgress((index + 1) / entities.size.toDouble()) + } + } + + } + + override fun getParcelsWithOwnerBlockIn(chunk: Chunk): Collection { + /* + * Get the offsets for the world out of the way + * to simplify the calculation that follows. + */ + + val x = chunk.x.shl(4) - (o.offsetX + pathOffset) + val z = chunk.z.shl(4) - (o.offsetZ + pathOffset) + + /* Locations of wall corners (where owner blocks are placed) are defined as: + * + * x umod sectionSize == sectionSize-1 + * + * This check needs to be made for all 16 slices of the chunk in 2 dimensions + * How to optimize this? + * Let's take the expression + * + * x umod sectionSize + * + * And call it modX + * x can be shifted (chunkSize -1) times to attempt to get a modX of 0. + * This means that if the modX is 1, and sectionSize == (chunkSize-1), there would be a match at the last shift. + * To check that there are any matches, we can see if the following holds: + * + * modX >= ((sectionSize-1) - (chunkSize-1)) + * + * Which can be simplified to: + * modX >= sectionSize - chunkSize + * + * if sectionSize == chunkSize, this expression can be simplified to + * modX >= 0 + * which is always true. This is expected. + * To get the total number of matches on a dimension, we can evaluate the following: + * + * (modX - (sectionSize - chunkSize) + sectionSize) / sectionSize + * + * We add sectionSize to the lhs because, if the other part of the lhs is 0, we need at least 1. + * This can be simplified to: + * + * (modX + chunkSize) / sectionSize + */ + + val sectionSize = sectionSize + + val modX = x umod sectionSize + val matchesOnDimensionX = (modX + chunkSize) / sectionSize + if (matchesOnDimensionX <= 0) return emptyList() + + val modZ = z umod sectionSize + val matchesOnDimensionZ = (modZ + chunkSize) / sectionSize + if (matchesOnDimensionZ <= 0) return emptyList() + + /* + * Now we need to find the first id within the matches, + * and then return the subsequent matches in a rectangle following it. + * + * On each dimension, get the distance to the first match, which is equal to (sectionSize-1 - modX) + * and add it to the coordinate value + */ + val firstX = x + (sectionSize - 1 - modX) + val firstZ = z + (sectionSize - 1 - modZ) + + val firstIdX = (firstX + 1) / sectionSize + 1 + val firstIdZ = (firstZ + 1) / sectionSize + 1 + + if (matchesOnDimensionX == 1 && matchesOnDimensionZ == 1) { + // fast-path optimization + return listOf(Vec2i(firstIdX, firstIdZ)) + } + + return (0 until matchesOnDimensionX).flatMap { idOffsetX -> + (0 until matchesOnDimensionZ).map { idOffsetZ -> Vec2i(firstIdX + idOffsetX, firstIdZ + idOffsetZ) } + } + } + + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt index da004d6..7748fc7 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt @@ -1,223 +1,284 @@ -package io.dico.parcels2.defaultimpl - -import io.dico.parcels2.* -import io.dico.parcels2.blockvisitor.Schematic -import io.dico.parcels2.util.schedule -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import org.bukkit.Bukkit -import org.bukkit.WorldCreator -import org.joda.time.DateTime - -class ParcelProviderImpl(val plugin: ParcelsPlugin) : ParcelProvider { - inline val options get() = plugin.options - override val worlds: Map get() = _worlds - private val _worlds: MutableMap = hashMapOf() - private val _generators: MutableMap = hashMapOf() - private var _worldsLoaded = false - private var _dataIsLoaded = false - - // disabled while !_dataIsLoaded. getParcelById() will work though for data loading. - override fun getWorld(name: String): ParcelWorld? = _worlds[name]?.takeIf { _dataIsLoaded } - - override fun getWorldById(id: ParcelWorldId): ParcelWorld? { - if (id is ParcelWorld) return id - return _worlds[id.name] ?: id.bukkitWorld?.let { getWorld(it) } - } - - override fun getParcelById(id: ParcelId): Parcel? { - if (id is Parcel) return id - return getWorldById(id.worldId)?.container?.getParcelById(id.x, id.z) - } - - override fun getWorldGenerator(worldName: String): ParcelGenerator? { - return _worlds[worldName]?.generator - ?: _generators[worldName] - ?: options.worlds[worldName]?.generator?.newInstance(worldName)?.also { _generators[worldName] = it } - } - - override fun loadWorlds() { - if (_worldsLoaded) throw IllegalStateException() - _worldsLoaded = true - loadWorlds0() - } - - private fun loadWorlds0() { - if (Bukkit.getWorlds().isEmpty()) { - plugin.schedule(::loadWorlds0) - plugin.logger.warning("Scheduling to load worlds in the next tick because no bukkit worlds are loaded yet") - return - } - - val newlyCreatedWorlds = mutableListOf() - for ((worldName, worldOptions) in options.worlds.entries) { - var parcelWorld = _worlds[worldName] - if (parcelWorld != null) continue - - val generator: ParcelGenerator = getWorldGenerator(worldName)!! - val worldExists = Bukkit.getWorld(worldName) != null - val bukkitWorld = - if (worldExists) Bukkit.getWorld(worldName)!! - else { - logger.info("Creating world $worldName") - WorldCreator(worldName).generator(generator).createWorld() - } - - parcelWorld = ParcelWorldImpl(plugin, bukkitWorld, generator, worldOptions.runtime,::DefaultParcelContainer) - - if (!worldExists) { - val time = DateTime.now() - plugin.storage.setWorldCreationTime(parcelWorld.id, time) - parcelWorld.creationTime = time - newlyCreatedWorlds.add(parcelWorld) - } else { - GlobalScope.launch(context = Dispatchers.Unconfined) { - parcelWorld.creationTime = plugin.storage.getWorldCreationTime(parcelWorld.id).await() ?: DateTime.now() - } - } - - _worlds[worldName] = parcelWorld - } - - loadStoredData(newlyCreatedWorlds.toSet()) - } - - private fun loadStoredData(newlyCreatedWorlds: Collection = emptyList()) { - plugin.launch(Dispatchers.Default) { - 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 job1 = launch { - val channel = plugin.storage.transmitAllParcelData() - while (true) { - val (id, data) = channel.receiveOrNull() ?: break - val parcel = getParcelById(id) ?: continue - data?.let { parcel.copyData(it, callerIsDatabase = true) } - } - } - - val channel2 = plugin.storage.transmitAllGlobalPrivileges() - while (true) { - val (profile, data) = channel2.receiveOrNull() ?: break - if (profile !is PrivilegeKey) { - logger.error("Received profile that is not a privilege key: ${profile.javaClass}, $profile") - continue - } - (plugin.globalPrivileges[profile] as PrivilegesHolder).copyPrivilegesFrom(data) - } - - job1.join() - - logger.info("Loading data completed") - _dataIsLoaded = true - } - } - - override fun acquireBlockVisitorPermit(parcelId: ParcelId, with: Permit): Boolean { - val parcel = getParcelById(parcelId) as? ParcelImpl ?: return true - return parcel.acquireBlockVisitorPermit(with) - } - - override fun releaseBlockVisitorPermit(parcelId: ParcelId, with: Permit) { - val parcel = getParcelById(parcelId) as? ParcelImpl ?: return - parcel.releaseBlockVisitorPermit(with) - } - - override fun trySubmitBlockVisitor(permit: Permit, vararg parcelIds: ParcelId, function: JobFunction): Job? { - val withPermit = parcelIds.filter { acquireBlockVisitorPermit(it, permit) } - if (withPermit.size != parcelIds.size) { - withPermit.forEach { releaseBlockVisitorPermit(it, permit) } - return null - } - - val job = plugin.jobDispatcher.dispatch(function) - - plugin.launch { - job.awaitCompletion() - withPermit.forEach { releaseBlockVisitorPermit(it, permit) } - } - - return job - } - - override fun swapParcels(parcelId1: ParcelId, parcelId2: ParcelId): Job? { - val blockManager1 = getWorldById(parcelId1.worldId)?.blockManager ?: return null - val blockManager2 = getWorldById(parcelId2.worldId)?.blockManager ?: return null - - return trySubmitBlockVisitor(Permit(), parcelId1, parcelId2) { - var region1 = blockManager1.getRegion(parcelId1) - var region2 = blockManager2.getRegion(parcelId2) - - val size = region1.size.clampMax(region2.size) - if (size != region1.size) { - region1 = region1.withSize(size) - region2 = region2.withSize(size) - } - - val schematicOf1 = delegateWork(0.25) { Schematic().apply { load(blockManager1.world, region1) } } - val schematicOf2 = delegateWork(0.25) { Schematic().apply { load(blockManager2.world, region2) } } - delegateWork(0.25) { with(schematicOf1) { paste(blockManager2.world, region2.origin) } } - delegateWork(0.25) { with(schematicOf2) { paste(blockManager1.world, region1.origin) } } - } - } - - /* - fun loadWorlds(options: Options) { - for ((worldName, worldOptions) in options.worlds.entries) { - val world: ParcelWorld - try { - - world = ParcelWorldImpl( - worldName, - worldOptions, - worldOptions.generator.newGenerator(this, worldName), - plugin.storage, - plugin.globalPrivileges, - ::DefaultParcelContainer) - - } catch (ex: Exception) { - ex.printStackTrace() - continue - } - - _worlds[worldName] = world - } - - plugin.functionHelper.schedule(10) { - println("Parcels generating parcelProvider now") - for ((name, world) in _worlds) { - if (Bukkit.getWorld(name) == null) { - val bworld = WorldCreator(name).generator(world.generator).createWorld() - val spawn = world.generator.getFixedSpawnLocation(bworld, null) - bworld.setSpawnLocation(spawn.x.floor(), spawn.y.floor(), spawn.z.floor()) - } - } - - val channel = plugin.storage.transmitAllParcelData() - val job = plugin.functionHelper.launchLazilyOnMainThread { - do { - val pair = channel.receiveOrNull() ?: break - val parcel = getParcelById(pair.first) ?: continue - pair.second?.let { parcel.copyDataIgnoringDatabase(it) } - } while (true) - } - job.start() - } - - } - */ +package io.dico.parcels2.defaultimpl + +import io.dico.parcels2.* +import io.dico.parcels2.blockvisitor.Schematic +import io.dico.parcels2.util.math.Region +import io.dico.parcels2.util.math.Vec3d +import io.dico.parcels2.util.math.Vec3i +import io.dico.parcels2.util.schedule +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.bukkit.Bukkit +import org.bukkit.World +import org.bukkit.WorldCreator +import org.bukkit.entity.Entity +import org.bukkit.util.Vector +import org.joda.time.DateTime + +class ParcelProviderImpl(val plugin: ParcelsPlugin) : ParcelProvider { + inline val options get() = plugin.options + override val worlds: Map get() = _worlds + private val _worlds: MutableMap = hashMapOf() + private val _generators: MutableMap = hashMapOf() + private var _worldsLoaded = false + private var _dataIsLoaded = false + + // disabled while !_dataIsLoaded. getParcelById() will work though for data loading. + override fun getWorld(name: String): ParcelWorld? = _worlds[name]?.takeIf { _dataIsLoaded } + + override fun getWorldById(id: ParcelWorldId): ParcelWorld? { + if (id is ParcelWorld) return id + return _worlds[id.name] ?: id.bukkitWorld?.let { getWorld(it) } + } + + override fun getParcelById(id: ParcelId): Parcel? { + if (id is Parcel) return id + return getWorldById(id.worldId)?.container?.getParcelById(id.x, id.z) + } + + override fun getWorldGenerator(worldName: String): ParcelGenerator? { + return _worlds[worldName]?.generator + ?: _generators[worldName] + ?: options.worlds[worldName]?.generator?.newInstance(worldName)?.also { _generators[worldName] = it } + } + + override fun loadWorlds() { + if (_worldsLoaded) throw IllegalStateException() + _worldsLoaded = true + loadWorlds0() + } + + private fun loadWorlds0() { + if (Bukkit.getWorlds().isEmpty()) { + plugin.schedule { loadWorlds0() } + plugin.logger.warning("Scheduling to load worlds in the next tick because no bukkit worlds are loaded yet") + return + } + + val newlyCreatedWorlds = mutableListOf() + for ((worldName, worldOptions) in options.worlds.entries) { + var parcelWorld = _worlds[worldName] + if (parcelWorld != null) continue + + val generator: ParcelGenerator = getWorldGenerator(worldName)!! + val worldExists = Bukkit.getWorld(worldName) != null + val bukkitWorld = + if (worldExists) Bukkit.getWorld(worldName)!! + else { + logger.info("Creating world $worldName") + WorldCreator(worldName).generator(generator).createWorld() + } + + parcelWorld = + ParcelWorldImpl(plugin, bukkitWorld, generator, worldOptions.runtime, ::DefaultParcelContainer) + + if (!worldExists) { + val time = DateTime.now() + plugin.storage.setWorldCreationTime(parcelWorld.id, time) + parcelWorld.creationTime = time + newlyCreatedWorlds.add(parcelWorld) + } else { + GlobalScope.launch(context = Dispatchers.Unconfined) { + parcelWorld.creationTime = plugin.storage.getWorldCreationTime(parcelWorld.id).await() ?: + DateTime.now() + } + } + + _worlds[worldName] = parcelWorld + } + + loadStoredData(newlyCreatedWorlds.toSet()) + } + + private fun loadStoredData(newlyCreatedWorlds: Collection = emptyList()) { + plugin.launch { + 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 job1 = launch { + val channel = plugin.storage.transmitAllParcelData() + while (true) { + val (id, data) = channel.receiveOrNull() ?: break + val parcel = getParcelById(id) ?: continue + data?.let { parcel.copyData(it, callerIsDatabase = true) } + } + } + + val channel2 = plugin.storage.transmitAllGlobalPrivileges() + while (true) { + val (profile, data) = channel2.receiveOrNull() ?: break + if (profile !is PrivilegeKey) { + logger.error("Received profile that is not a privilege key: ${profile.javaClass}, $profile") + continue + } + (plugin.globalPrivileges[profile] as PrivilegesHolder).copyPrivilegesFrom(data) + } + + job1.join() + + logger.info("Loading data completed") + _dataIsLoaded = true + } + } + + override fun acquireBlockVisitorPermit(parcelId: ParcelId, with: Permit): Boolean { + val parcel = getParcelById(parcelId) as? ParcelImpl ?: return true + return parcel.acquireBlockVisitorPermit(with) + } + + override fun releaseBlockVisitorPermit(parcelId: ParcelId, with: Permit) { + val parcel = getParcelById(parcelId) as? ParcelImpl ?: return + parcel.releaseBlockVisitorPermit(with) + } + + override fun trySubmitBlockVisitor(permit: Permit, vararg parcelIds: ParcelId, function: JobFunction): Job? { + val withPermit = parcelIds.filter { acquireBlockVisitorPermit(it, permit) } + if (withPermit.size != parcelIds.size) { + withPermit.forEach { releaseBlockVisitorPermit(it, permit) } + return null + } + + val job = plugin.jobDispatcher.dispatch(function) + + plugin.launch { + job.awaitCompletion() + withPermit.forEach { releaseBlockVisitorPermit(it, permit) } + } + + return job + } + + override fun swapParcels(parcelId1: ParcelId, parcelId2: ParcelId): Job? { + val world1 = getWorldById(parcelId1.worldId) ?: return null + val world2 = getWorldById(parcelId2.worldId) ?: return null + val blockManager1 = world1.blockManager + val blockManager2 = world2.blockManager + + class CopyTarget(val world: World, val region: Region) + class CopySource(val origin: Vec3i, val schematic: Schematic, val entities: Collection) + + suspend fun JobScope.copy(source: CopySource, target: CopyTarget) { + with(source.schematic) { paste(target.world, target.region.origin) } + + for (entity in source.entities) { + entity.velocity = Vector(0, 0, 0) + val location = entity.location + location.world = target.world + val coords = target.region.origin + (Vec3d(entity.location) - source.origin) + coords.copyInto(location) + entity.teleport(location) + } + } + + return trySubmitBlockVisitor(Permit(), parcelId1, parcelId2) { + val temporaryParcel = world1.nextEmptyParcel() + ?: world2.nextEmptyParcel() + ?: return@trySubmitBlockVisitor + + var region1 = blockManager1.getRegion(parcelId1) + var region2 = blockManager2.getRegion(parcelId2) + + val size = region1.size.clampMax(region2.size) + if (size != region1.size) { + region1 = region1.withSize(size) + region2 = region2.withSize(size) + } + + // Teleporting entities safely requires a different approach: + // * Copy schematic1 into temporary location + // * Teleport entities1 into temporary location + // * Copy schematic2 into parcel1 + // * Teleport entities2 into parcel1 + // * Copy schematic1 into parcel2 + // * Teleport entities1 into parcel2 + // * Clear temporary location + + lateinit var source1: CopySource + lateinit var source2: CopySource + + delegateWork(0.30) { + val schematicOf1 = delegateWork(0.50) { Schematic().apply { load(blockManager1.world, region1) } } + val schematicOf2 = delegateWork(0.50) { Schematic().apply { load(blockManager2.world, region2) } } + + source1 = CopySource(region1.origin, schematicOf1, blockManager1.getEntities(region1)) + source2 = CopySource(region2.origin, schematicOf2, blockManager2.getEntities(region2)) + } + + val target1 = CopyTarget(blockManager1.world, region1) + val target2 = CopyTarget(blockManager2.world, region2) + val targetTemp = CopyTarget( + temporaryParcel.world.world, + temporaryParcel.world.blockManager.getRegion(temporaryParcel.id) + ) + + delegateWork { + delegateWork(1.0 / 3.0) { copy(source1, targetTemp) } + delegateWork(1.0 / 3.0) { copy(source2, target1) } + delegateWork(1.0 / 3.0) { copy(source1, target2) } + } + + // Separate job. Whatever + temporaryParcel.world.blockManager.clearParcel(temporaryParcel.id) + } + } + + /* + fun loadWorlds(options: Options) { + for ((worldName, worldOptions) in options.worlds.entries) { + val world: ParcelWorld + try { + + world = ParcelWorldImpl( + worldName, + worldOptions, + worldOptions.generator.newGenerator(this, worldName), + plugin.storage, + plugin.globalPrivileges, + ::DefaultParcelContainer) + + } catch (ex: Exception) { + ex.printStackTrace() + continue + } + + _worlds[worldName] = world + } + + plugin.functionHelper.schedule(10) { + println("Parcels generating parcelProvider now") + for ((name, world) in _worlds) { + if (Bukkit.getWorld(name) == null) { + val bworld = WorldCreator(name).generator(world.generator).createWorld() + val spawn = world.generator.getFixedSpawnLocation(bworld, null) + bworld.setSpawnLocation(spawn.x.floor(), spawn.y.floor(), spawn.z.floor()) + } + } + + val channel = plugin.storage.transmitAllParcelData() + val job = plugin.functionHelper.launchLazilyOnMainThread { + do { + val pair = channel.receiveOrNull() ?: break + val parcel = getParcelById(pair.first) ?: continue + pair.second?.let { parcel.copyDataIgnoringDatabase(it) } + } while (true) + } + job.start() + } + + } + */ } \ No newline at end of file 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 32065bc..d9e3071 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedBacking.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedBacking.kt @@ -1,282 +1,284 @@ -@file:Suppress("NOTHING_TO_INLINE", "PARAMETER_NAME_CHANGED_ON_OVERRIDE", "LocalVariableName", "UNUSED_EXPRESSION") - -package io.dico.parcels2.storage.exposed - -import com.zaxxer.hikari.HikariDataSource -import io.dico.parcels2.* -import io.dico.parcels2.PlayerProfile.Star.name -import io.dico.parcels2.storage.* -import io.dico.parcels2.util.math.clampMax -import io.dico.parcels2.util.ext.synchronized -import kotlinx.coroutines.* -import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.ArrayChannel -import kotlinx.coroutines.channels.LinkedListChannel -import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.coroutines.channels.SendChannel -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.SchemaUtils.create -import org.jetbrains.exposed.sql.transactions.transaction -import org.jetbrains.exposed.sql.vendors.DatabaseDialect -import org.joda.time.DateTime -import java.util.UUID -import javax.sql.DataSource - -class ExposedDatabaseException(message: String? = null) : Exception(message) - -class ExposedBacking(private val dataSourceFactory: () -> DataSource, val poolSize: Int) : Backing, CoroutineScope { - override val name get() = "Exposed" - override val coroutineContext = Job() + newFixedThreadPoolContext(poolSize, "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 { transaction { job() } } - override fun launchFuture(future: Backing.() -> T): Deferred = async { transaction { future() } } - - override fun openChannel(future: Backing.(SendChannel) -> Unit): ReceiveChannel { - val channel = LinkedListChannel() - launchJob { future(channel) } - return channel - } - - override fun openChannelForWriting(action: Backing.(T) -> Unit): SendChannel { - val channel = ArrayChannel(poolSize * 2) - - repeat(poolSize.clampMax(3)) { - launch { - try { - while (true) { - action(channel.receive()) - } - } catch (ex: Exception) { - // channel closed - } - } - } - - return channel - } - - private fun transaction(statement: Transaction.() -> T) = transaction(database!!, statement) - - companion object { - init { - Database.registerDialect("mariadb") { - Class.forName("org.jetbrains.exposed.sql.vendors.MysqlDialect").newInstance() as DatabaseDialect - } - } - } - - override fun init() { - synchronized { - if (isShutdown || isConnected) throw IllegalStateException() - dataSource = dataSourceFactory() - database = Database.connect(dataSource!!) - transaction(database!!) { - create(WorldsT, ProfilesT, ParcelsT, ParcelOptionsT, PrivilegesLocalT, PrivilegesGlobalT) - } - } - } - - override fun shutdown() { - synchronized { - if (isShutdown) throw IllegalStateException() - isShutdown = true - coroutineContext[Job]!!.cancel(CancellationException("ExposedBacking shutdown")) - dataSource?.let { - (it as? HikariDataSource)?.close() - } - database = null - } - } - - @Suppress("RedundantObjectTypeCheck") - private fun PlayerProfile.toOwnerProfile(): PlayerProfile { - if (this is PlayerProfile.Star) return PlayerProfile.Fake(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 getWorldCreationTime(worldId: ParcelWorldId): DateTime? { - return WorldsT.getWorldCreationTime(worldId) - } - - override fun setWorldCreationTime(worldId: ParcelWorldId, time: DateTime) { - WorldsT.setWorldCreationTime(worldId, time) - } - - 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 updatePlayerName(uuid: UUID, name: String) { - val binaryUuid = uuid.toByteArray() - ProfilesT.upsert(ProfilesT.uuid) { - it[ProfilesT.uuid] = binaryUuid - it[ProfilesT.name] = name - } - } - - override fun transmitParcelData(channel: SendChannel, parcels: Sequence) { - for (parcel in parcels) { - val data = readParcelData(parcel) - channel.offer(parcel to data) - } - channel.close() - } - - override fun transmitAllParcelData(channel: SendChannel) { - ParcelsT.selectAll().forEach { row -> - val parcel = ParcelsT.getItem(row) ?: return@forEach - val data = rowToParcelData(row) - channel.offer(parcel to data) - } - channel.close() - } - - override fun readParcelData(parcel: ParcelId): ParcelDataHolder? { - val row = ParcelsT.getRow(parcel) ?: return null - return rowToParcelData(row) - } - - 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::getItem) - .toList() - } - - override fun setParcelData(parcel: ParcelId, data: ParcelDataHolder?) { - if (data == null) { - transaction { - ParcelsT.getId(parcel)?.let { id -> - ParcelsT.deleteIgnoreWhere { ParcelsT.id eq id } - - // Below should cascade automatically - /* - PrivilegesLocalT.deleteIgnoreWhere { PrivilegesLocalT.parcel_id eq id } - ParcelOptionsT.deleteIgnoreWhere(limit = 1) { ParcelOptionsT.parcel_id eq id } - */ - } - - } - return - } - - transaction { - val id = ParcelsT.getOrInitId(parcel) - PrivilegesLocalT.deleteIgnoreWhere { PrivilegesLocalT.attach_id eq id } - } - - setParcelOwner(parcel, data.owner) - - for ((profile, privilege) in data.privilegeMap) { - PrivilegesLocalT.setPrivilege(parcel, profile, privilege) - } - - data.privilegeOfStar.takeIf { it != Privilege.DEFAULT }?.let { privilege -> - PrivilegesLocalT.setPrivilege(parcel, PlayerProfile.Star, privilege) - } - - setParcelOptionsInteractConfig(parcel, data.interactableConfig) - } - - override fun setParcelOwner(parcel: ParcelId, owner: PlayerProfile?) { - val id = if (owner == null) - ParcelsT.getId(parcel) ?: return - else - ParcelsT.getOrInitId(parcel) - - val owner_id = owner?.let { ProfilesT.getOrInitId(it.toOwnerProfile()) } - val time = owner?.let { DateTime.now() } - - ParcelsT.update({ ParcelsT.id eq id }) { - it[ParcelsT.owner_id] = owner_id - it[claim_time] = time - it[sign_oudated] = false - } - } - - override fun setParcelOwnerSignOutdated(parcel: ParcelId, outdated: Boolean) { - val id = ParcelsT.getId(parcel) ?: return - ParcelsT.update({ ParcelsT.id eq id }) { - it[sign_oudated] = outdated - } - } - - override fun setLocalPrivilege(parcel: ParcelId, player: PlayerProfile, privilege: Privilege) { - PrivilegesLocalT.setPrivilege(parcel, player.toRealProfile(), privilege) - } - - override fun setParcelOptionsInteractConfig(parcel: ParcelId, config: InteractableConfiguration) { - val bitmaskArray = (config as? BitmaskInteractableConfiguration ?: return).bitmaskArray - val isAllZero = !bitmaskArray.fold(false) { cur, elem -> cur || elem != 0 } - - if (isAllZero) { - val id = ParcelsT.getId(parcel) ?: return - ParcelOptionsT.deleteWhere { ParcelOptionsT.parcel_id eq id } - return - } - - if (bitmaskArray.size != 1) throw IllegalArgumentException() - val array = bitmaskArray.toByteArray() - val id = ParcelsT.getOrInitId(parcel) - ParcelOptionsT.upsert(ParcelOptionsT.parcel_id) { - it[parcel_id] = id - it[interact_bitmask] = array - } - } - - override fun transmitAllGlobalPrivileges(channel: SendChannel>) { - PrivilegesGlobalT.sendAllPrivilegesH(channel) - channel.close() - } - - override fun readGlobalPrivileges(owner: PlayerProfile): PrivilegesHolder? { - return PrivilegesGlobalT.readPrivileges(ProfilesT.getId(owner.toOwnerProfile()) ?: return null) - } - - override fun setGlobalPrivilege(owner: PlayerProfile, player: PlayerProfile, privilege: Privilege) { - PrivilegesGlobalT.setPrivilege(owner, player.toRealProfile(), privilege) - } - - private fun rowToParcelData(row: ResultRow) = ParcelDataHolder().apply { - owner = row[ParcelsT.owner_id]?.let { ProfilesT.getItem(it) } - lastClaimTime = row[ParcelsT.claim_time] - isOwnerSignOutdated = row[ParcelsT.sign_oudated] - - val id = row[ParcelsT.id] - ParcelOptionsT.select { ParcelOptionsT.parcel_id eq id }.firstOrNull()?.let { optrow -> - val source = optrow[ParcelOptionsT.interact_bitmask].toIntArray() - val target = (interactableConfig as? BitmaskInteractableConfiguration ?: return@let).bitmaskArray - System.arraycopy(source, 0, target, 0, source.size.clampMax(target.size)) - } - - val privileges = PrivilegesLocalT.readPrivileges(id) - if (privileges != null) { - copyPrivilegesFrom(privileges) - } - } - -} - +@file:Suppress("NOTHING_TO_INLINE", "PARAMETER_NAME_CHANGED_ON_OVERRIDE", "LocalVariableName", "UNUSED_EXPRESSION") + +package io.dico.parcels2.storage.exposed + +import com.zaxxer.hikari.HikariDataSource +import io.dico.parcels2.* +import io.dico.parcels2.PlayerProfile.Star.name +import io.dico.parcels2.storage.* +import io.dico.parcels2.util.math.clampMax +import io.dico.parcels2.util.ext.synchronized +import kotlinx.coroutines.* +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.ArrayChannel +import kotlinx.coroutines.channels.LinkedListChannel +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SchemaUtils.create +import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.vendors.DatabaseDialect +import org.joda.time.DateTime +import java.util.UUID +import javax.sql.DataSource + +class ExposedDatabaseException(message: String? = null) : Exception(message) + +class ExposedBacking(private val dataSourceFactory: () -> DataSource, val poolSize: Int) : Backing, CoroutineScope { + override val name get() = "Exposed" + override val coroutineContext = Job() + newFixedThreadPoolContext(poolSize, "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 { transaction { job() } } + override fun launchFuture(future: Backing.() -> T): Deferred = async { transaction { future() } } + + override fun openChannel(future: Backing.(SendChannel) -> Unit): ReceiveChannel { + val channel = LinkedListChannel() + launchJob { future(channel) } + return channel + } + + override fun openChannelForWriting(action: Backing.(T) -> Unit): SendChannel { + val channel = ArrayChannel(poolSize * 2) + + repeat(poolSize.clampMax(3)) { + launch { + try { + while (true) { + action(channel.receive()) + } + } catch (ex: Exception) { + // channel closed + } + } + } + + return channel + } + + private fun transaction(statement: Transaction.() -> T) = transaction(database!!, statement) + + companion object { + init { + Database.registerDialect("mariadb") { + Class.forName("org.jetbrains.exposed.sql.vendors.MysqlDialect").newInstance() as DatabaseDialect + } + } + } + + override fun init() { + synchronized { + if (isShutdown || isConnected) throw IllegalStateException() + val dataSource = dataSourceFactory() + this.dataSource = dataSource + val database = Database.connect(dataSource) + this.database = database + transaction(database) { + create(WorldsT, ProfilesT, ParcelsT, ParcelOptionsT, PrivilegesLocalT, PrivilegesGlobalT) + } + } + } + + override fun shutdown() { + synchronized { + if (isShutdown) throw IllegalStateException() + isShutdown = true + coroutineContext.cancel(CancellationException("ExposedBacking shutdown")) + dataSource?.let { + (it as? HikariDataSource)?.close() + } + database = null + } + } + + @Suppress("RedundantObjectTypeCheck") + private fun PlayerProfile.toOwnerProfile(): PlayerProfile { + if (this is PlayerProfile.Star) return PlayerProfile.Fake(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 getWorldCreationTime(worldId: ParcelWorldId): DateTime? { + return WorldsT.getWorldCreationTime(worldId) + } + + override fun setWorldCreationTime(worldId: ParcelWorldId, time: DateTime) { + WorldsT.setWorldCreationTime(worldId, time) + } + + 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 updatePlayerName(uuid: UUID, name: String) { + val binaryUuid = uuid.toByteArray() + ProfilesT.upsert(ProfilesT.uuid) { + it[ProfilesT.uuid] = binaryUuid + it[ProfilesT.name] = name + } + } + + override fun transmitParcelData(channel: SendChannel, parcels: Sequence) { + for (parcel in parcels) { + val data = readParcelData(parcel) + channel.offer(parcel to data) + } + channel.close() + } + + override fun transmitAllParcelData(channel: SendChannel) { + ParcelsT.selectAll().forEach { row -> + val parcel = ParcelsT.getItem(row) ?: return@forEach + val data = rowToParcelData(row) + channel.offer(parcel to data) + } + channel.close() + } + + override fun readParcelData(parcel: ParcelId): ParcelDataHolder? { + val row = ParcelsT.getRow(parcel) ?: return null + return rowToParcelData(row) + } + + 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::getItem) + .toList() + } + + override fun setParcelData(parcel: ParcelId, data: ParcelDataHolder?) { + if (data == null) { + transaction { + ParcelsT.getId(parcel)?.let { id -> + ParcelsT.deleteIgnoreWhere { ParcelsT.id eq id } + + // Below should cascade automatically + /* + PrivilegesLocalT.deleteIgnoreWhere { PrivilegesLocalT.parcel_id eq id } + ParcelOptionsT.deleteIgnoreWhere(limit = 1) { ParcelOptionsT.parcel_id eq id } + */ + } + + } + return + } + + transaction { + val id = ParcelsT.getOrInitId(parcel) + PrivilegesLocalT.deleteIgnoreWhere { PrivilegesLocalT.attach_id eq id } + } + + setParcelOwner(parcel, data.owner) + + for ((profile, privilege) in data.privilegeMap) { + PrivilegesLocalT.setPrivilege(parcel, profile, privilege) + } + + data.privilegeOfStar.takeIf { it != Privilege.DEFAULT }?.let { privilege -> + PrivilegesLocalT.setPrivilege(parcel, PlayerProfile.Star, privilege) + } + + setParcelOptionsInteractConfig(parcel, data.interactableConfig) + } + + override fun setParcelOwner(parcel: ParcelId, owner: PlayerProfile?) { + val id = if (owner == null) + ParcelsT.getId(parcel) ?: return + else + ParcelsT.getOrInitId(parcel) + + val owner_id = owner?.let { ProfilesT.getOrInitId(it.toOwnerProfile()) } + val time = owner?.let { DateTime.now() } + + ParcelsT.update({ ParcelsT.id eq id }) { + it[ParcelsT.owner_id] = owner_id + it[claim_time] = time + it[sign_oudated] = false + } + } + + override fun setParcelOwnerSignOutdated(parcel: ParcelId, outdated: Boolean) { + val id = ParcelsT.getId(parcel) ?: return + ParcelsT.update({ ParcelsT.id eq id }) { + it[sign_oudated] = outdated + } + } + + override fun setLocalPrivilege(parcel: ParcelId, player: PlayerProfile, privilege: Privilege) { + PrivilegesLocalT.setPrivilege(parcel, player.toRealProfile(), privilege) + } + + override fun setParcelOptionsInteractConfig(parcel: ParcelId, config: InteractableConfiguration) { + val bitmaskArray = (config as? BitmaskInteractableConfiguration ?: return).bitmaskArray + val isAllZero = !bitmaskArray.fold(false) { cur, elem -> cur || elem != 0 } + + if (isAllZero) { + val id = ParcelsT.getId(parcel) ?: return + ParcelOptionsT.deleteWhere { ParcelOptionsT.parcel_id eq id } + return + } + + if (bitmaskArray.size != 1) throw IllegalArgumentException() + val array = bitmaskArray.toByteArray() + val id = ParcelsT.getOrInitId(parcel) + ParcelOptionsT.upsert(ParcelOptionsT.parcel_id) { + it[parcel_id] = id + it[interact_bitmask] = array + } + } + + override fun transmitAllGlobalPrivileges(channel: SendChannel>) { + PrivilegesGlobalT.sendAllPrivilegesH(channel) + channel.close() + } + + override fun readGlobalPrivileges(owner: PlayerProfile): PrivilegesHolder? { + return PrivilegesGlobalT.readPrivileges(ProfilesT.getId(owner.toOwnerProfile()) ?: return null) + } + + override fun setGlobalPrivilege(owner: PlayerProfile, player: PlayerProfile, privilege: Privilege) { + PrivilegesGlobalT.setPrivilege(owner, player.toRealProfile(), privilege) + } + + private fun rowToParcelData(row: ResultRow) = ParcelDataHolder().apply { + owner = row[ParcelsT.owner_id]?.let { ProfilesT.getItem(it) } + lastClaimTime = row[ParcelsT.claim_time] + isOwnerSignOutdated = row[ParcelsT.sign_oudated] + + val id = row[ParcelsT.id] + ParcelOptionsT.select { ParcelOptionsT.parcel_id eq id }.firstOrNull()?.let { optrow -> + val source = optrow[ParcelOptionsT.interact_bitmask].toIntArray() + val target = (interactableConfig as? BitmaskInteractableConfiguration ?: return@let).bitmaskArray + System.arraycopy(source, 0, target, 0, source.size.clampMax(target.size)) + } + + val privileges = PrivilegesLocalT.readPrivileges(id) + if (privileges != null) { + copyPrivilegesFrom(privileges) + } + } + +} + diff --git a/src/main/kotlin/io/dico/parcels2/util/BukkitUtil.kt b/src/main/kotlin/io/dico/parcels2/util/BukkitUtil.kt index a4a6da9..c7d813b 100644 --- a/src/main/kotlin/io/dico/parcels2/util/BukkitUtil.kt +++ b/src/main/kotlin/io/dico/parcels2/util/BukkitUtil.kt @@ -1,14 +1,23 @@ -package io.dico.parcels2.util - -import io.dico.parcels2.util.ext.isValid -import org.bukkit.Bukkit -import org.bukkit.OfflinePlayer -import java.util.UUID - -fun getPlayerName(uuid: UUID): String? = getOfflinePlayer(uuid)?.name - -fun getOfflinePlayer(uuid: UUID): OfflinePlayer? = Bukkit.getOfflinePlayer(uuid).takeIf { it.isValid } - -fun getOfflinePlayer(name: String): OfflinePlayer? = Bukkit.getOfflinePlayer(name).takeIf { it.isValid } - -fun isServerThread(): Boolean = Thread.currentThread().name == "Server thread" +package io.dico.parcels2.util + +import io.dico.parcels2.util.ext.isValid +import org.bukkit.Bukkit +import org.bukkit.OfflinePlayer +import java.lang.IllegalArgumentException +import java.util.UUID + +fun getPlayerName(uuid: UUID): String? = getOfflinePlayer(uuid)?.name + +fun getOfflinePlayer(uuid: UUID): OfflinePlayer? = Bukkit.getOfflinePlayer(uuid).takeIf { it.isValid } + +fun getOfflinePlayer(name: String): OfflinePlayer? = Bukkit.getOfflinePlayer(name).takeIf { it.isValid } + +fun isServerThread(): Boolean = Thread.currentThread().name == "Server thread" + +fun isPlayerNameValid(name: String): Boolean = + name.length in 3..16 + && name.find { it !in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" } == null + +fun checkPlayerNameValid(name: String) { + if (!isPlayerNameValid(name)) throw IllegalArgumentException("Invalid player name: $name") +} diff --git a/src/main/kotlin/io/dico/parcels2/util/PluginAware.kt b/src/main/kotlin/io/dico/parcels2/util/PluginAware.kt new file mode 100644 index 0000000..de75519 --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/util/PluginAware.kt @@ -0,0 +1,18 @@ +package io.dico.parcels2.util + +import org.bukkit.plugin.Plugin +import org.bukkit.scheduler.BukkitTask + +interface PluginAware { + val plugin: Plugin +} + +inline fun PluginAware.schedule(delay: Int = 0, crossinline task: () -> Unit): BukkitTask { + return plugin.server.scheduler.runTaskLater(plugin, { task() }, delay.toLong()) +} + +inline fun PluginAware.scheduleRepeating(interval: Int, delay: Int = 0, crossinline task: () -> Unit): BukkitTask { + return plugin.server.scheduler.runTaskTimer(plugin, { task() }, delay.toLong(), interval.toLong()) +} + + diff --git a/src/main/kotlin/io/dico/parcels2/util/PluginScheduler.kt b/src/main/kotlin/io/dico/parcels2/util/PluginScheduler.kt deleted file mode 100644 index 268a083..0000000 --- a/src/main/kotlin/io/dico/parcels2/util/PluginScheduler.kt +++ /dev/null @@ -1,20 +0,0 @@ -package io.dico.parcels2.util - -import org.bukkit.plugin.Plugin -import org.bukkit.scheduler.BukkitTask - -interface PluginScheduler { - val plugin: Plugin - - fun schedule(delay: Int, task: () -> Unit): BukkitTask { - return plugin.server.scheduler.runTaskLater(plugin, task, delay.toLong()) - } - - fun scheduleRepeating(delay: Int, interval: Int, task: () -> Unit): BukkitTask { - return plugin.server.scheduler.runTaskTimer(plugin, task, delay.toLong(), interval.toLong()) - } -} - -@Suppress("NOTHING_TO_INLINE") -inline fun PluginScheduler.schedule(noinline task: () -> Unit) = schedule(0, task) - diff --git a/src/main/kotlin/io/dico/parcels2/util/math/Vec3d.kt b/src/main/kotlin/io/dico/parcels2/util/math/Vec3d.kt index 72b6dcd..2c3512f 100644 --- a/src/main/kotlin/io/dico/parcels2/util/math/Vec3d.kt +++ b/src/main/kotlin/io/dico/parcels2/util/math/Vec3d.kt @@ -1,53 +1,61 @@ -package io.dico.parcels2.util.math - -import org.bukkit.Location -import kotlin.math.sqrt - -data class Vec3d( - val x: Double, - val y: Double, - val z: Double -) { - constructor(loc: Location) : this(loc.x, loc.y, loc.z) - - operator fun plus(o: Vec3d) = Vec3d(x + o.x, y + o.y, z + o.z) - operator fun minus(o: Vec3i) = Vec3d(x - o.x, y - o.y, z - o.z) - infix fun addX(o: Double) = Vec3d(x + o, y, z) - infix fun addY(o: Double) = Vec3d(x, y + o, z) - infix fun addZ(o: Double) = Vec3d(x, y, z + o) - infix fun withX(o: Double) = Vec3d(o, y, z) - infix fun withY(o: Double) = Vec3d(x, o, z) - infix fun withZ(o: Double) = Vec3d(x, y, o) - fun add(ox: Double, oy: Double, oz: Double) = Vec3d(x + ox, y + oy, z + oz) - fun toVec3i() = Vec3i(x.floor(), y.floor(), z.floor()) - - fun distanceSquared(o: Vec3d): Double { - val dx = o.x - x - val dy = o.y - y - val dz = o.z - z - return dx * dx + dy * dy + dz * dz - } - - fun distance(o: Vec3d) = sqrt(distanceSquared(o)) - - operator fun get(dimension: Dimension) = - when (dimension) { - Dimension.X -> x - Dimension.Y -> y - Dimension.Z -> z - } - - fun with(dimension: Dimension, value: Double) = - when (dimension) { - Dimension.X -> withX(value) - Dimension.Y -> withY(value) - Dimension.Z -> withZ(value) - } - - fun add(dimension: Dimension, value: Double) = - when (dimension) { - Dimension.X -> addX(value) - Dimension.Y -> addY(value) - Dimension.Z -> addZ(value) - } +package io.dico.parcels2.util.math + +import org.bukkit.Location +import kotlin.math.sqrt + +data class Vec3d( + val x: Double, + val y: Double, + val z: Double +) { + constructor(loc: Location) : this(loc.x, loc.y, loc.z) + + operator fun plus(o: Vec3d) = Vec3d(x + o.x, y + o.y, z + o.z) + operator fun plus(o: Vec3i) = Vec3d(x + o.x, y + o.y, z + o.z) + operator fun minus(o: Vec3d) = Vec3d(x - o.x, y - o.y, z - o.z) + operator fun minus(o: Vec3i) = Vec3d(x - o.x, y - o.y, z - o.z) + infix fun addX(o: Double) = Vec3d(x + o, y, z) + infix fun addY(o: Double) = Vec3d(x, y + o, z) + infix fun addZ(o: Double) = Vec3d(x, y, z + o) + infix fun withX(o: Double) = Vec3d(o, y, z) + infix fun withY(o: Double) = Vec3d(x, o, z) + infix fun withZ(o: Double) = Vec3d(x, y, o) + fun add(ox: Double, oy: Double, oz: Double) = Vec3d(x + ox, y + oy, z + oz) + fun toVec3i() = Vec3i(x.floor(), y.floor(), z.floor()) + + fun distanceSquared(o: Vec3d): Double { + val dx = o.x - x + val dy = o.y - y + val dz = o.z - z + return dx * dx + dy * dy + dz * dz + } + + fun distance(o: Vec3d) = sqrt(distanceSquared(o)) + + operator fun get(dimension: Dimension) = + when (dimension) { + Dimension.X -> x + Dimension.Y -> y + Dimension.Z -> z + } + + fun with(dimension: Dimension, value: Double) = + when (dimension) { + Dimension.X -> withX(value) + Dimension.Y -> withY(value) + Dimension.Z -> withZ(value) + } + + fun add(dimension: Dimension, value: Double) = + when (dimension) { + Dimension.X -> addX(value) + Dimension.Y -> addY(value) + Dimension.Z -> addZ(value) + } + + fun copyInto(loc: Location) { + loc.x = x + loc.y = y + loc.z = z + } } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/util/math/Vec3i.kt b/src/main/kotlin/io/dico/parcels2/util/math/Vec3i.kt index 484ad13..b3ba169 100644 --- a/src/main/kotlin/io/dico/parcels2/util/math/Vec3i.kt +++ b/src/main/kotlin/io/dico/parcels2/util/math/Vec3i.kt @@ -1,105 +1,107 @@ -package io.dico.parcels2.util.math - -import org.bukkit.Location -import org.bukkit.World -import org.bukkit.block.Block -import org.bukkit.block.BlockFace - -data class Vec3i( - val x: Int, - val y: Int, - val z: Int -) { - constructor(loc: Location) : this(loc.blockX, loc.blockY, loc.blockZ) - constructor(block: Block) : this(block.x, block.y, block.z) - - fun toVec2i() = Vec2i(x, z) - operator fun plus(o: Vec3i) = Vec3i(x + o.x, y + o.y, z + o.z) - operator fun minus(o: Vec3i) = Vec3i(x - o.x, y - o.y, z - o.z) - infix fun addX(o: Int) = Vec3i(x + o, y, z) - infix fun addY(o: Int) = Vec3i(x, y + o, z) - infix fun addZ(o: Int) = Vec3i(x, y, z + o) - infix fun withX(o: Int) = Vec3i(o, y, z) - infix fun withY(o: Int) = Vec3i(x, o, z) - infix fun withZ(o: Int) = Vec3i(x, y, o) - fun add(ox: Int, oy: Int, oz: Int) = Vec3i(x + ox, y + oy, z + oz) - fun neg() = Vec3i(-x, -y, -z) - fun clampMax(o: Vec3i) = Vec3i(x.clampMax(o.x), y.clampMax(o.y), z.clampMax(o.z)) - - operator fun get(dimension: Dimension) = - when (dimension) { - Dimension.X -> x - Dimension.Y -> y - Dimension.Z -> z - } - - fun with(dimension: Dimension, value: Int) = - when (dimension) { - Dimension.X -> withX(value) - Dimension.Y -> withY(value) - Dimension.Z -> withZ(value) - } - - fun add(dimension: Dimension, value: Int) = - when (dimension) { - Dimension.X -> addX(value) - Dimension.Y -> addY(value) - Dimension.Z -> addZ(value) - } - - companion object { - private operator fun invoke(face: BlockFace) = Vec3i(face.modX, face.modY, face.modZ) - val down = Vec3i(BlockFace.DOWN) - val up = Vec3i(BlockFace.UP) - val north = Vec3i(BlockFace.NORTH) - val east = Vec3i(BlockFace.EAST) - val south = Vec3i(BlockFace.SOUTH) - val west = Vec3i(BlockFace.WEST) - - fun convert(face: BlockFace) = when (face) { - BlockFace.DOWN -> down - BlockFace.UP -> up - BlockFace.NORTH -> north - BlockFace.EAST -> east - BlockFace.SOUTH -> south - BlockFace.WEST -> west - else -> Vec3i(face) - } - } -} - -@Suppress("NOTHING_TO_INLINE") -inline operator fun World.get(vec: Vec3i): Block = getBlockAt(vec.x, vec.y, vec.z) - -/* -private /*inline */class IVec3i(private val data: Long) { - - private companion object { - const val mask = 0x001F_FFFF - const val max: Int = 0x000F_FFFF // +1048575 - const val min: Int = -max - 1 // -1048575 // 0xFFF0_0000 - - @Suppress("NOTHING_TO_INLINE") - inline fun Int.compressIntoLong(offset: Int): Long { - if (this !in min..max) throw IllegalArgumentException() - return and(mask).toLong().shl(offset) - } - - @Suppress("NOTHING_TO_INLINE") - inline fun Long.extractInt(offset: Int): Int { - val result = ushr(offset).toInt().and(mask) - return if (result > max) result or mask.inv() else result - } - } - - constructor(x: Int, y: Int, z: Int) : this( - x.compressIntoLong(42) - or y.compressIntoLong(21) - or z.compressIntoLong(0)) - - val x: Int get() = data.extractInt(42) - val y: Int get() = data.extractInt(21) - val z: Int get() = data.extractInt(0) - -} -*/ +package io.dico.parcels2.util.math + +import org.bukkit.Location +import org.bukkit.World +import org.bukkit.block.Block +import org.bukkit.block.BlockFace + +data class Vec3i( + val x: Int, + val y: Int, + val z: Int +) { + constructor(loc: Location) : this(loc.blockX, loc.blockY, loc.blockZ) + constructor(block: Block) : this(block.x, block.y, block.z) + + fun toVec2i() = Vec2i(x, z) + operator fun plus(o: Vec3i) = Vec3i(x + o.x, y + o.y, z + o.z) + operator fun plus(o: Vec3d) = Vec3d(x + o.x, y + o.y, z + o.z) + operator fun minus(o: Vec3i) = Vec3i(x - o.x, y - o.y, z - o.z) + operator fun minus(o: Vec3d) = Vec3d(x - o.x, y - o.y, z - o.z) + infix fun addX(o: Int) = Vec3i(x + o, y, z) + infix fun addY(o: Int) = Vec3i(x, y + o, z) + infix fun addZ(o: Int) = Vec3i(x, y, z + o) + infix fun withX(o: Int) = Vec3i(o, y, z) + infix fun withY(o: Int) = Vec3i(x, o, z) + infix fun withZ(o: Int) = Vec3i(x, y, o) + fun add(ox: Int, oy: Int, oz: Int) = Vec3i(x + ox, y + oy, z + oz) + fun neg() = Vec3i(-x, -y, -z) + fun clampMax(o: Vec3i) = Vec3i(x.clampMax(o.x), y.clampMax(o.y), z.clampMax(o.z)) + + operator fun get(dimension: Dimension) = + when (dimension) { + Dimension.X -> x + Dimension.Y -> y + Dimension.Z -> z + } + + fun with(dimension: Dimension, value: Int) = + when (dimension) { + Dimension.X -> withX(value) + Dimension.Y -> withY(value) + Dimension.Z -> withZ(value) + } + + fun add(dimension: Dimension, value: Int) = + when (dimension) { + Dimension.X -> addX(value) + Dimension.Y -> addY(value) + Dimension.Z -> addZ(value) + } + + companion object { + private operator fun invoke(face: BlockFace) = Vec3i(face.modX, face.modY, face.modZ) + val down = Vec3i(BlockFace.DOWN) + val up = Vec3i(BlockFace.UP) + val north = Vec3i(BlockFace.NORTH) + val east = Vec3i(BlockFace.EAST) + val south = Vec3i(BlockFace.SOUTH) + val west = Vec3i(BlockFace.WEST) + + fun convert(face: BlockFace) = when (face) { + BlockFace.DOWN -> down + BlockFace.UP -> up + BlockFace.NORTH -> north + BlockFace.EAST -> east + BlockFace.SOUTH -> south + BlockFace.WEST -> west + else -> Vec3i(face) + } + } +} + +@Suppress("NOTHING_TO_INLINE") +inline operator fun World.get(vec: Vec3i): Block = getBlockAt(vec.x, vec.y, vec.z) + +/* +private /*inline */class IVec3i(private val data: Long) { + + private companion object { + const val mask = 0x001F_FFFF + const val max: Int = 0x000F_FFFF // +1048575 + const val min: Int = -max - 1 // -1048575 // 0xFFF0_0000 + + @Suppress("NOTHING_TO_INLINE") + inline fun Int.compressIntoLong(offset: Int): Long { + if (this !in min..max) throw IllegalArgumentException() + return and(mask).toLong().shl(offset) + } + + @Suppress("NOTHING_TO_INLINE") + inline fun Long.extractInt(offset: Int): Int { + val result = ushr(offset).toInt().and(mask) + return if (result > max) result or mask.inv() else result + } + } + + constructor(x: Int, y: Int, z: Int) : this( + x.compressIntoLong(42) + or y.compressIntoLong(21) + or z.compressIntoLong(0)) + + val x: Int get() = data.extractInt(42) + val y: Int get() = data.extractInt(21) + val z: Int get() = data.extractInt(0) + +} +*/ diff --git a/src/main/kotlin/io/dico/parcels2/util/parallel.kt b/src/main/kotlin/io/dico/parcels2/util/parallel.kt new file mode 100644 index 0000000..a4edc3c --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/util/parallel.kt @@ -0,0 +1,9 @@ +package io.dico.parcels2.util + +fun doParallel() { + + val array = IntArray(1000) + IntRange(0, 1000).chunked() + + +} \ No newline at end of file -- cgit v1.2.3 From a475226ffcf17a7327b78e5c1e6ba6ac0dfd10c7 Mon Sep 17 00:00:00 2001 From: Dico Karssiens Date: Sun, 6 Jan 2019 12:02:34 +0000 Subject: Perform some fixes --- .../io/dico/parcels2/command/CommandsGeneral.kt | 283 ++--- .../io/dico/parcels2/command/ParcelTarget.kt | 3 +- .../parcels2/defaultimpl/DefaultParcelContainer.kt | 144 +-- .../dico/parcels2/defaultimpl/ParcelWorldImpl.kt | 150 +-- .../io/dico/parcels2/listener/ParcelListeners.kt | 1338 ++++++++++---------- .../io/dico/parcels2/listener/WorldEditListener.kt | 155 ++- .../kotlin/io/dico/parcels2/util/PluginAware.kt | 7 +- src/main/kotlin/io/dico/parcels2/util/parallel.kt | 9 - 8 files changed, 1050 insertions(+), 1039 deletions(-) delete mode 100644 src/main/kotlin/io/dico/parcels2/util/parallel.kt (limited to 'src') diff --git a/src/main/kotlin/io/dico/parcels2/command/CommandsGeneral.kt b/src/main/kotlin/io/dico/parcels2/command/CommandsGeneral.kt index 61e8d9e..9929a4c 100644 --- a/src/main/kotlin/io/dico/parcels2/command/CommandsGeneral.kt +++ b/src/main/kotlin/io/dico/parcels2/command/CommandsGeneral.kt @@ -1,142 +1,143 @@ -package io.dico.parcels2.command - -import io.dico.dicore.command.ExecutionContext -import io.dico.dicore.command.Validate -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.ParcelsPlugin -import io.dico.parcels2.PlayerProfile -import io.dico.parcels2.Privilege -import io.dico.parcels2.command.ParcelTarget.TargetKind -import io.dico.parcels2.util.ext.hasParcelHomeOthers -import io.dico.parcels2.util.ext.hasPermAdminManage -import io.dico.parcels2.util.ext.uuid -import org.bukkit.block.Biome -import org.bukkit.entity.Player - -class CommandsGeneral(plugin: ParcelsPlugin, parent: SpecialCommandAddress) : AbstractParcelCommands(plugin) { - - @Cmd("auto") - @Desc( - "Finds the unclaimed parcel nearest to origin,", - "and gives it to you", - shortVersion = "sets you up with a fresh, unclaimed parcel" - ) - suspend fun WorldScope.cmdAuto(player: Player): Any? { - checkConnected("be claimed") - checkParcelLimit(player, world) - - val parcel = world.nextEmptyParcel() - ?: err("This world is full, please ask an admin to upsize it") - parcel.owner = PlayerProfile(uuid = player.uuid) - player.teleport(parcel.homeLocation) - return "Enjoy your new parcel!" - } - - @Cmd("info", aliases = ["i"]) - @Desc( - "Displays general information", - "about the parcel you're on", - shortVersion = "displays information about this parcel" - ) - fun ParcelScope.cmdInfo(player: Player) = parcel.infoString - - init { - parent.addSpeciallyTreatedKeys("home", "h") - } - - @Cmd("home", aliases = ["h"]) - @Desc( - "Teleports you to your parcels,", - "unless another player was specified.", - "You can specify an index number if you have", - "more than one parcel", - shortVersion = "teleports you to parcels" - ) - @RequireParameters(0) - suspend fun cmdHome( - player: Player, - @TargetKind(TargetKind.OWNER_REAL) target: ParcelTarget - ): Any? { - return cmdGoto(player, target) - } - - @Cmd("tp", aliases = ["teleport"]) - suspend fun cmdTp( - player: Player, - @TargetKind(TargetKind.ID) target: ParcelTarget - ): Any? { - return cmdGoto(player, target) - } - - @Cmd("goto") - suspend fun cmdGoto( - player: Player, - @TargetKind(TargetKind.ANY) target: ParcelTarget - ): Any? { - if (target is ParcelTarget.ByOwner) { - target.resolveOwner(plugin.storage) - if (!target.owner.matches(player) && !player.hasParcelHomeOthers) { - err("You do not have permission to teleport to other people's parcels") - } - } - - val match = target.getParcelSuspend(plugin.storage) - ?: err("The specified parcel could not be matched") - player.teleport(match.homeLocation) - return null - } - - @Cmd("goto_fake") - suspend fun cmdGotoFake( - player: Player, - @TargetKind(TargetKind.OWNER_FAKE) target: ParcelTarget - ): Any? { - return cmdGoto(player, target) - } - - @Cmd("claim") - @Desc( - "If this parcel is unowned, makes you the owner", - shortVersion = "claims this parcel" - ) - suspend fun ParcelScope.cmdClaim(player: Player): Any? { - checkConnected("be claimed") - parcel.owner.takeIf { !player.hasPermAdminManage }?.let { - err(if (it.matches(player)) "You already own this parcel" else "This parcel is not available") - } - - checkParcelLimit(player, world) - parcel.owner = PlayerProfile(player) - return "Enjoy your new parcel!" - } - - @Cmd("unclaim") - @Desc("Unclaims this parcel") - @RequireParcelPrivilege(Privilege.OWNER) - fun ParcelScope.cmdUnclaim(player: Player): Any? { - checkConnected("be unclaimed") - parcel.dispose() - return "Your parcel has been disposed" - } - - @Cmd("clear") - @RequireParcelPrivilege(Privilege.OWNER) - fun ParcelScope.cmdClear(context: ExecutionContext, @Flag sure: Boolean): Any? { - Validate.isTrue(!parcel.hasBlockVisitors, "A process is already running in this parcel") - if (!sure) return areYouSureMessage(context) - world.blockManager.clearParcel(parcel.id)?.reportProgressUpdates(context, "Clear") - return null - } - - @Cmd("setbiome") - @RequireParcelPrivilege(Privilege.OWNER) - fun ParcelScope.cmdSetbiome(context: ExecutionContext, biome: Biome): Any? { - Validate.isTrue(!parcel.hasBlockVisitors, "A process is already running in this parcel") - world.blockManager.setBiome(parcel.id, biome)?.reportProgressUpdates(context, "Biome change") - return null - } - +package io.dico.parcels2.command + +import io.dico.dicore.command.ExecutionContext +import io.dico.dicore.command.Validate +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.ParcelsPlugin +import io.dico.parcels2.PlayerProfile +import io.dico.parcels2.Privilege +import io.dico.parcels2.command.ParcelTarget.TargetKind +import io.dico.parcels2.util.ext.hasParcelHomeOthers +import io.dico.parcels2.util.ext.hasPermAdminManage +import io.dico.parcels2.util.ext.uuid +import org.bukkit.block.Biome +import org.bukkit.entity.Player + +class CommandsGeneral(plugin: ParcelsPlugin, parent: SpecialCommandAddress) : AbstractParcelCommands(plugin) { + + @Cmd("auto") + @Desc( + "Finds the unclaimed parcel nearest to origin,", + "and gives it to you", + shortVersion = "sets you up with a fresh, unclaimed parcel" + ) + suspend fun WorldScope.cmdAuto(player: Player): Any? { + checkConnected("be claimed") + checkParcelLimit(player, world) + + val parcel = world.nextEmptyParcel() + ?: err("This world is full, please ask an admin to upsize it") + parcel.owner = PlayerProfile(uuid = player.uuid) + player.teleport(parcel.homeLocation) + return "Enjoy your new parcel!" + } + + @Cmd("info", aliases = ["i"]) + @Desc( + "Displays general information", + "about the parcel you're on", + shortVersion = "displays information about this parcel" + ) + fun ParcelScope.cmdInfo(player: Player) = parcel.infoString + + init { + parent.addSpeciallyTreatedKeys("home", "h") + } + + @Cmd("home", aliases = ["h"]) + @Desc( + "Teleports you to your parcels,", + "unless another player was specified.", + "You can specify an index number if you have", + "more than one parcel", + shortVersion = "teleports you to parcels" + ) + @RequireParameters(0) + suspend fun cmdHome( + player: Player, + @TargetKind(TargetKind.OWNER_REAL) target: ParcelTarget + ): Any? { + return cmdGoto(player, target) + } + + @Cmd("tp", aliases = ["teleport"]) + suspend fun cmdTp( + player: Player, + @TargetKind(TargetKind.ID) target: ParcelTarget + ): Any? { + return cmdGoto(player, target) + } + + @Cmd("goto") + suspend fun cmdGoto( + player: Player, + @TargetKind(TargetKind.ANY) target: ParcelTarget + ): Any? { + if (target is ParcelTarget.ByOwner) { + target.resolveOwner(plugin.storage) + if (!target.owner.matches(player) && !player.hasParcelHomeOthers) { + err("You do not have permission to teleport to other people's parcels") + } + } + + val match = target.getParcelSuspend(plugin.storage) + ?: err("The specified parcel could not be matched") + player.teleport(match.homeLocation) + return null + } + + @Cmd("goto_fake") + suspend fun cmdGotoFake( + player: Player, + @TargetKind(TargetKind.OWNER_FAKE) target: ParcelTarget + ): Any? { + return cmdGoto(player, target) + } + + @Cmd("claim") + @Desc( + "If this parcel is unowned, makes you the owner", + shortVersion = "claims this parcel" + ) + suspend fun ParcelScope.cmdClaim(player: Player): Any? { + checkConnected("be claimed") + parcel.owner.takeIf { !player.hasPermAdminManage }?.let { + err(if (it.matches(player)) "You already own this parcel" else "This parcel is not available") + } + + checkParcelLimit(player, world) + parcel.owner = PlayerProfile(player) + return "Enjoy your new parcel!" + } + + /* + @Cmd("unclaim") + @Desc("Unclaims this parcel") + @RequireParcelPrivilege(Privilege.OWNER) + fun ParcelScope.cmdUnclaim(player: Player): Any? { + checkConnected("be unclaimed") + parcel.dispose() + return "Your parcel has been disposed" + }*/ + + @Cmd("clear") + @RequireParcelPrivilege(Privilege.OWNER) + fun ParcelScope.cmdClear(context: ExecutionContext, @Flag sure: Boolean): Any? { + Validate.isTrue(!parcel.hasBlockVisitors, "A process is already running in this parcel") + if (!sure) return areYouSureMessage(context) + world.blockManager.clearParcel(parcel.id)?.reportProgressUpdates(context, "Clear") + return null + } + + @Cmd("setbiome") + @RequireParcelPrivilege(Privilege.OWNER) + fun ParcelScope.cmdSetbiome(context: ExecutionContext, biome: Biome): Any? { + Validate.isTrue(!parcel.hasBlockVisitors, "A process is already running in this parcel") + world.blockManager.setBiome(parcel.id, biome)?.reportProgressUpdates(context, "Biome change") + return null + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt b/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt index 934a993..9ba3c25 100644 --- a/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt +++ b/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt @@ -106,7 +106,8 @@ sealed class ParcelTarget(val world: ParcelWorld, val parsedKind: Int, val isDef parcelProvider.getWorld(player.world) ?: invalidInput(parameter, "You cannot omit the world if you're not in a parcel world") } else { - parcelProvider.getWorld(worldString) ?: invalidInput(parameter, "$worldString is not a parcel world") + parcelProvider.getWorld(worldString) + ?: invalidInput(parameter, "$worldString is not a parcel world") } val kind = parameter.paramInfo ?: DEFAULT_KIND diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelContainer.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelContainer.kt index b49cad4..f3cd8d7 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelContainer.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelContainer.kt @@ -1,73 +1,73 @@ -package io.dico.parcels2.defaultimpl - -import io.dico.parcels2.Parcel -import io.dico.parcels2.ParcelContainer -import io.dico.parcels2.ParcelId -import io.dico.parcels2.ParcelWorld - -class DefaultParcelContainer(val world: ParcelWorld) : ParcelContainer { - private var parcels: Array> - - init { - parcels = initArray(world.options.axisLimit, world) - } - - fun resizeIfSizeChanged() { - if (parcels.size != world.options.axisLimit * 2 + 1) { - resize(world.options.axisLimit) - } - } - - fun resize(axisLimit: Int) { - parcels = initArray(axisLimit, world, this) - } - - fun initArray(axisLimit: Int, world: ParcelWorld, cur: DefaultParcelContainer? = null): Array> { - val arraySize = 2 * axisLimit + 1 - return Array(arraySize) { - val x = it - axisLimit - Array(arraySize) { - val z = it - axisLimit - cur?.getParcelById(x, z) ?: ParcelImpl(world, x, z) - } - } - } - - override fun getParcelById(x: Int, z: Int): Parcel? { - return parcels.getOrNull(x + world.options.axisLimit)?.getOrNull(z + world.options.axisLimit) - } - - override fun getParcelById(id: ParcelId): Parcel? { - if (!world.id.equals(id.worldId)) throw IllegalArgumentException() - return when (id) { - is Parcel -> id - else -> getParcelById(id.x, id.z) - } - } - - override fun nextEmptyParcel(): Parcel? { - return walkInCircle().find { it.owner == null } - } - - private fun walkInCircle(): Iterable = Iterable { - iterator { - val center = world.options.axisLimit - yield(parcels[center][center]) - for (radius in 0..center) { - var x = center - radius; - var z = center - radius - repeat(radius * 2) { yield(parcels[x++][z]) } - repeat(radius * 2) { yield(parcels[x][z++]) } - repeat(radius * 2) { yield(parcels[x--][z]) } - repeat(radius * 2) { yield(parcels[x][z--]) } - } - } - } - - fun getAllParcels(): Iterator = iterator { - for (array in parcels) { - yieldAll(array.iterator()) - } - } - +package io.dico.parcels2.defaultimpl + +import io.dico.parcels2.Parcel +import io.dico.parcels2.ParcelContainer +import io.dico.parcels2.ParcelId +import io.dico.parcels2.ParcelWorld + +class DefaultParcelContainer(val world: ParcelWorld) : ParcelContainer { + private var parcels: Array> + + init { + parcels = initArray(world.options.axisLimit, world) + } + + fun resizeIfSizeChanged() { + if (parcels.size != world.options.axisLimit * 2 + 1) { + resize(world.options.axisLimit) + } + } + + fun resize(axisLimit: Int) { + parcels = initArray(axisLimit, world, this) + } + + fun initArray(axisLimit: Int, world: ParcelWorld, cur: DefaultParcelContainer? = null): Array> { + val arraySize = 2 * axisLimit + 1 + return Array(arraySize) { + val x = it - axisLimit + Array(arraySize) { + val z = it - axisLimit + cur?.getParcelById(x, z) ?: ParcelImpl(world, x, z) + } + } + } + + override fun getParcelById(x: Int, z: Int): Parcel? { + return parcels.getOrNull(x + world.options.axisLimit)?.getOrNull(z + world.options.axisLimit) + } + + override fun getParcelById(id: ParcelId): Parcel? { + if (!world.id.equals(id.worldId)) throw IllegalArgumentException() + return when (id) { + is Parcel -> id + else -> getParcelById(id.x, id.z) + } + } + + override suspend fun nextEmptyParcel(): Parcel? { + return walkInCircle().find { it.owner == null } + } + + private fun walkInCircle(): Iterable = Iterable { + iterator { + val center = world.options.axisLimit + yield(parcels[center][center]) + for (radius in 0..center) { + var x = center - radius; + var z = center - radius + repeat(radius * 2) { yield(parcels[x++][z]) } + repeat(radius * 2) { yield(parcels[x][z++]) } + repeat(radius * 2) { yield(parcels[x--][z]) } + repeat(radius * 2) { yield(parcels[x][z--]) } + } + } + } + + fun getAllParcels(): Iterator = iterator { + for (array in parcels) { + yieldAll(array.iterator()) + } + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelWorldImpl.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelWorldImpl.kt index 6ce4f26..cb322d4 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelWorldImpl.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelWorldImpl.kt @@ -1,75 +1,75 @@ -@file:Suppress("CanBePrimaryConstructorProperty", "UsePropertyAccessSyntax") - -package io.dico.parcels2.defaultimpl - -import io.dico.parcels2.* -import io.dico.parcels2.options.RuntimeWorldOptions -import io.dico.parcels2.storage.Storage -import kotlinx.coroutines.CoroutineScope -import org.bukkit.GameRule -import org.bukkit.World -import org.joda.time.DateTime -import java.util.UUID - -class ParcelWorldImpl( - val plugin: ParcelsPlugin, - override val world: World, - override val generator: ParcelGenerator, - override var options: RuntimeWorldOptions, - containerFactory: ParcelContainerFactory -) : ParcelWorld, ParcelWorldId, ParcelContainer, ParcelLocator { - override val id: ParcelWorldId get() = this - override val uid: UUID? get() = world.uid - - override val storage get() = plugin.storage - override val globalPrivileges get() = plugin.globalPrivileges - - init { - if (generator.world != world) { - throw IllegalArgumentException() - } - } - - override val name: String = world.name!! - override val container: ParcelContainer = containerFactory(this) - override val locator: ParcelLocator - override val blockManager: ParcelBlockManager - - init { - val (locator, blockManager) = generator.makeParcelLocatorAndBlockManager(plugin.parcelProvider, container, plugin, plugin.jobDispatcher) - this.locator = locator - this.blockManager = blockManager - enforceOptions() - } - - fun enforceOptions() { - if (options.dayTime) { - world.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false) - world.setTime(6000) - } - - if (options.noWeather) { - world.setStorm(false) - world.setThundering(false) - world.weatherDuration = Int.MAX_VALUE - } - - world.setGameRule(GameRule.DO_TILE_DROPS, options.doTileDrops) - } - - // Accessed by ParcelProviderImpl - override var creationTime: DateTime? = null - - - override fun getParcelAt(x: Int, z: Int): Parcel? = locator.getParcelAt(x, z) - - override fun getParcelIdAt(x: Int, z: Int): ParcelId? = locator.getParcelIdAt(x, z) - - override fun getParcelById(x: Int, z: Int): Parcel? = container.getParcelById(x, z) - - override fun getParcelById(id: ParcelId): Parcel? = container.getParcelById(id) - - override fun nextEmptyParcel(): Parcel? = container.nextEmptyParcel() - - override fun toString() = parcelWorldIdToString() -} +@file:Suppress("CanBePrimaryConstructorProperty", "UsePropertyAccessSyntax") + +package io.dico.parcels2.defaultimpl + +import io.dico.parcels2.* +import io.dico.parcels2.options.RuntimeWorldOptions +import io.dico.parcels2.storage.Storage +import kotlinx.coroutines.CoroutineScope +import org.bukkit.GameRule +import org.bukkit.World +import org.joda.time.DateTime +import java.util.UUID + +class ParcelWorldImpl( + val plugin: ParcelsPlugin, + override val world: World, + override val generator: ParcelGenerator, + override var options: RuntimeWorldOptions, + containerFactory: ParcelContainerFactory +) : ParcelWorld, ParcelWorldId, ParcelContainer, ParcelLocator { + override val id: ParcelWorldId get() = this + override val uid: UUID? get() = world.uid + + override val storage get() = plugin.storage + override val globalPrivileges get() = plugin.globalPrivileges + + init { + if (generator.world != world) { + throw IllegalArgumentException() + } + } + + override val name: String = world.name!! + override val container: ParcelContainer = containerFactory(this) + override val locator: ParcelLocator + override val blockManager: ParcelBlockManager + + init { + val (locator, blockManager) = generator.makeParcelLocatorAndBlockManager(plugin.parcelProvider, container, plugin, plugin.jobDispatcher) + this.locator = locator + this.blockManager = blockManager + enforceOptions() + } + + fun enforceOptions() { + if (options.dayTime) { + world.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false) + world.setTime(6000) + } + + if (options.noWeather) { + world.setStorm(false) + world.setThundering(false) + world.weatherDuration = Int.MAX_VALUE + } + + world.setGameRule(GameRule.DO_TILE_DROPS, options.doTileDrops) + } + + // Accessed by ParcelProviderImpl + override var creationTime: DateTime? = null + + + override fun getParcelAt(x: Int, z: Int): Parcel? = locator.getParcelAt(x, z) + + override fun getParcelIdAt(x: Int, z: Int): ParcelId? = locator.getParcelIdAt(x, z) + + override fun getParcelById(x: Int, z: Int): Parcel? = container.getParcelById(x, z) + + override fun getParcelById(id: ParcelId): Parcel? = container.getParcelById(id) + + override suspend fun nextEmptyParcel(): Parcel? = container.nextEmptyParcel() + + override fun toString() = parcelWorldIdToString() +} diff --git a/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt b/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt index 8bef7d1..e87dd68 100644 --- a/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt +++ b/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt @@ -1,661 +1,679 @@ -package io.dico.parcels2.listener - -import gnu.trove.TLongCollection -import gnu.trove.set.hash.TLongHashSet -import io.dico.dicore.Formatting -import io.dico.dicore.ListenerMarker -import io.dico.dicore.RegistratorListener -import io.dico.parcels2.* -import io.dico.parcels2.storage.Storage -import io.dico.parcels2.util.ext.* -import io.dico.parcels2.util.math.* -import org.bukkit.Location -import org.bukkit.Material.* -import org.bukkit.World -import org.bukkit.block.Biome -import org.bukkit.block.Block -import org.bukkit.block.data.Directional -import org.bukkit.block.data.type.Bed -import org.bukkit.entity.* -import org.bukkit.entity.minecart.ExplosiveMinecart -import org.bukkit.event.EventPriority -import org.bukkit.event.EventPriority.NORMAL -import org.bukkit.event.block.* -import org.bukkit.event.entity.* -import org.bukkit.event.hanging.HangingBreakByEntityEvent -import org.bukkit.event.hanging.HangingBreakEvent -import org.bukkit.event.hanging.HangingPlaceEvent -import org.bukkit.event.inventory.InventoryInteractEvent -import org.bukkit.event.player.* -import org.bukkit.event.vehicle.VehicleMoveEvent -import org.bukkit.event.weather.WeatherChangeEvent -import org.bukkit.event.world.ChunkLoadEvent -import org.bukkit.event.world.StructureGrowEvent -import org.bukkit.inventory.InventoryHolder -import java.util.EnumSet - -class ParcelListeners( - val parcelProvider: ParcelProvider, - val entityTracker: ParcelEntityTracker, - val storage: Storage -) { - private fun canBuildOnArea(user: Player, area: Parcel?) = - if (area == null) user.hasPermBuildAnywhere else area.canBuild(user) - - private fun canInteract(user: Player, area: Parcel?, interactClass: String) = - canBuildOnArea(user, area) || (area != null && area.interactableConfig(interactClass)) - - /** - * Get the world and parcel that the block resides in - * the parcel is nullable, and often named area because that means path. - * returns null if not in a registered parcel world - should always return in that case to not affect other worlds. - */ - private fun getWorldAndArea(block: Block): Pair? { - val world = parcelProvider.getWorld(block.world) ?: return null - return world to world.getParcelAt(block) - } - - - /* - * Prevents players from entering plots they are banned from - */ - @field:ListenerMarker(priority = NORMAL) - val onPlayerMoveEvent = RegistratorListener l@{ event -> - val user = event.player - if (user.hasPermBanBypass) return@l - val toLoc = event.to - val parcel = parcelProvider.getParcelAt(toLoc) ?: return@l - - if (!parcel.canEnterFast(user)) { - val region = parcel.world.blockManager.getRegion(parcel.id) - val dimension = region.getFirstUncontainedDimensionOf(Vec3i(event.from)) - - if (dimension == null) { - user.teleport(parcel.homeLocation) - user.sendParcelMessage(nopermit = true, message = "You are banned from this parcel") - - } else { - val speed = getPlayerSpeed(user) - val from = Vec3d(event.from) - val to = Vec3d(toLoc).with(dimension, from[dimension]) - - var newTo = to - dimension.otherDimensions.forEach { - val delta = to[it] - from[it] - newTo = newTo.add(it, delta * 100 * if (it == Dimension.Y) 0.5 else speed) - } - - event.to = Location( - toLoc.world, - newTo.x, newTo.y.clampMin(0.0).clampMax(255.0), newTo.z, - toLoc.yaw, toLoc.pitch - ) - } - } - } - - /* - * Prevents players from breaking blocks outside of their parcels - * Prevents containers from dropping their contents when broken, if configured - */ - @field:ListenerMarker(priority = NORMAL) - val onBlockBreakEvent = RegistratorListener l@{ event -> - val (world, area) = getWorldAndArea(event.block) ?: return@l - if (!canBuildOnArea(event.player, area)) { - event.isCancelled = true; return@l - } - - if (!world.options.dropEntityItems) { - val state = event.block.state - if (state is InventoryHolder) { - state.inventory.clear() - state.update() - } - } - } - - /* - * Prevents players from placing blocks outside of their parcels - */ - @field:ListenerMarker(priority = NORMAL) - val onBlockPlaceEvent = RegistratorListener l@{ event -> - val (_, area) = getWorldAndArea(event.block) ?: return@l - if (!canBuildOnArea(event.player, area)) { - event.isCancelled = true - } - - area?.updateOwnerSign() - } - - /* - * Control pistons - */ - @field:ListenerMarker(priority = NORMAL) - val onBlockPistonExtendEvent = RegistratorListener l@{ event -> - checkPistonMovement(event, event.blocks) - } - - @field:ListenerMarker(priority = NORMAL) - val onBlockPistonRetractEvent = RegistratorListener l@{ event -> - checkPistonMovement(event, event.blocks) - } - - // Doing some unnecessary optimizations here.. - //@formatter:off - private inline fun Column(x: Int, z: Int): Long = x.toLong() or (z.toLong().shl(32)) - - private inline val Long.columnX get() = and(0xFFFF_FFFFL).toInt() - private inline val Long.columnZ get() = ushr(32).and(0xFFFF_FFFFL).toInt() - private inline fun TLongCollection.troveForEach(block: (Long) -> Unit) = iterator().let { while (it.hasNext()) block(it.next()) } - //@formatter:on - private fun checkPistonMovement(event: BlockPistonEvent, blocks: List) { - val world = parcelProvider.getWorld(event.block.world) ?: return - val direction = event.direction - val columns = TLongHashSet(blocks.size * 2) - - blocks.forEach { - columns.add(Column(it.x, it.z)) - it.getRelative(direction).let { columns.add(Column(it.x, it.z)) } - } - - columns.troveForEach { - val area = world.getParcelAt(it.columnX, it.columnZ) - if (area == null || area.hasBlockVisitors) { - event.isCancelled = true - return - } - } - } - - /* - * Prevents explosions if enabled by the configs for that world - */ - @field:ListenerMarker(priority = NORMAL) - val onExplosionPrimeEvent = RegistratorListener l@{ event -> - val (world, area) = getWorldAndArea(event.entity.location.block) ?: return@l - if (area != null && area.hasBlockVisitors) { - event.radius = 0F; event.isCancelled = true - } else if (world.options.disableExplosions) { - event.radius = 0F - } - } - - /* - * Prevents creepers and tnt minecarts from exploding if explosions are disabled - */ - @field:ListenerMarker(priority = NORMAL) - val onEntityExplodeEvent = RegistratorListener l@{ event -> - entityTracker.untrack(event.entity) - val world = parcelProvider.getWorld(event.entity.world) ?: return@l - if (world.options.disableExplosions || world.getParcelAt(event.entity).let { it != null && it.hasBlockVisitors }) { - event.isCancelled = true - } - } - - /* - * Prevents liquids from flowing out of plots - */ - @field:ListenerMarker(priority = NORMAL) - val onBlockFromToEvent = RegistratorListener l@{ event -> - val (_, area) = getWorldAndArea(event.toBlock) ?: return@l - if (area == null || area.hasBlockVisitors) event.isCancelled = true - } - - private val bedTypes = EnumSet.copyOf(getMaterialsWithWoolColorPrefix("BED").toList()) - /* - * Prevents players from placing liquids, using flint and steel, changing redstone components, - * using inputs (unless allowed by the plot), - * and using items disabled in the configuration for that world. - * Prevents player from using beds in HELL or SKY biomes if explosions are disabled. - */ - @Suppress("NON_EXHAUSTIVE_WHEN") - @field:ListenerMarker(priority = NORMAL) - val onPlayerInteractEvent = RegistratorListener l@{ event -> - val user = event.player - val world = parcelProvider.getWorld(user.world) ?: return@l - val clickedBlock = event.clickedBlock - val parcel = clickedBlock?.let { world.getParcelAt(it) } - - if (!user.hasPermBuildAnywhere && parcel != null && !parcel.canEnter(user)) { - user.sendParcelMessage(nopermit = true, message = "You cannot interact with parcels you're banned from") - event.isCancelled = true; return@l - } - - when (event.action) { - Action.RIGHT_CLICK_BLOCK -> run { - val type = clickedBlock.type - - val interactableClass = Interactables[type] - if (interactableClass != null && !parcel.effectiveInteractableConfig.isInteractable(type) && (parcel == null || !parcel.canBuild(user))) { - user.sendParcelMessage(nopermit = true, message = "You cannot interact with ${interactableClass.name} here") - event.isCancelled = true - return@l - } - - if (bedTypes.contains(type)) { - val bed = clickedBlock.blockData as Bed - val head = if (bed.part == Bed.Part.FOOT) clickedBlock.getRelative(bed.facing) else clickedBlock - when (head.biome) { - Biome.NETHER, Biome.THE_END -> { - if (world.options.disableExplosions) { - user.sendParcelMessage(nopermit = true, message = "You cannot use this bed because it would explode") - event.isCancelled = true; return@l - } - } - } - - if (!canBuildOnArea(user, parcel)) { - user.sendParcelMessage(nopermit = true, message = "You may not sleep here") - event.isCancelled = true; return@l - } - } - - onPlayerRightClick(event, world, parcel) - - if (!event.isCancelled && parcel == null) { - world.blockManager.getParcelForInfoBlockInteraction(Vec3i(clickedBlock), type, event.blockFace) - ?.apply { user.sendMessage(Formatting.GREEN + infoString) } - } - } - - Action.RIGHT_CLICK_AIR -> onPlayerRightClick(event, world, parcel) - Action.PHYSICAL -> if (!canBuildOnArea(user, parcel) && !(parcel != null && parcel.interactableConfig("pressure_plates"))) { - user.sendParcelMessage(nopermit = true, message = "You cannot use inputs in this parcel") - event.isCancelled = true; return@l - } - } - } - - // private val blockPlaceInteractItems = EnumSet.of(LAVA_BUCKET, WATER_BUCKET, BUCKET, FLINT_AND_STEEL) - - @Suppress("NON_EXHAUSTIVE_WHEN") - private fun onPlayerRightClick(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 - } - - when (item) { - LAVA_BUCKET, WATER_BUCKET, BUCKET, FLINT_AND_STEEL -> { - val block = event.clickedBlock.getRelative(event.blockFace) - val otherParcel = world.getParcelAt(block) - if (!canBuildOnArea(event.player, otherParcel)) { - event.isCancelled = true - } - } - } - } - } - - /* - * Prevents players from breeding mobs, entering or opening boats/minecarts, - * rotating item frames, doing stuff with leashes, and putting stuff on armor stands. - */ - @Suppress("NON_EXHAUSTIVE_WHEN") - @field:ListenerMarker(priority = NORMAL) - val onPlayerInteractEntityEvent = RegistratorListener l@{ event -> - val (_, area) = getWorldAndArea(event.rightClicked.location.block) ?: return@l - if (canBuildOnArea(event.player, area)) return@l - when (event.rightClicked.type) { - EntityType.BOAT, - EntityType.MINECART, - EntityType.MINECART_CHEST, - EntityType.MINECART_COMMAND, - EntityType.MINECART_FURNACE, - EntityType.MINECART_HOPPER, - EntityType.MINECART_MOB_SPAWNER, - EntityType.MINECART_TNT, - - EntityType.ARMOR_STAND, - EntityType.PAINTING, - EntityType.ITEM_FRAME, - EntityType.LEASH_HITCH, - - EntityType.CHICKEN, - EntityType.COW, - EntityType.HORSE, - EntityType.SHEEP, - EntityType.VILLAGER, - EntityType.WOLF -> event.isCancelled = true - } - } - - /* - * Prevents endermen from griefing. - * Prevents sand blocks from exiting the parcel in which they became an entity. - */ - @field:ListenerMarker(priority = NORMAL) - val onEntityChangeBlockEvent = RegistratorListener l@{ event -> - val (_, area) = getWorldAndArea(event.block) ?: return@l - if (event.entity.type == EntityType.ENDERMAN || area == null || area.hasBlockVisitors) { - event.isCancelled = true; return@l - } - - if (event.entity.type == EntityType.FALLING_BLOCK) { - // a sand block started falling. Track it and delete it if it gets out of this parcel. - entityTracker.track(event.entity, area) - } - } - - /* - * Prevents portals from being created if set so in the configs for that world - */ - @field:ListenerMarker(priority = NORMAL) - val onEntityCreatePortalEvent = RegistratorListener l@{ event -> - val world = parcelProvider.getWorld(event.entity.world) ?: return@l - if (world.options.blockPortalCreation) event.isCancelled = true - } - - /* - * Prevents players from dropping items - */ - @field:ListenerMarker(priority = NORMAL) - val onPlayerDropItemEvent = RegistratorListener l@{ event -> - val (_, area) = getWorldAndArea(event.itemDrop.location.block) ?: return@l - if (!canInteract(event.player, area, "containers")) event.isCancelled = true - } - - /* - * Prevents players from picking up items - */ - @field:ListenerMarker(priority = NORMAL) - val onEntityPickupItemEvent = RegistratorListener l@{ event -> - val user = event.entity as? Player ?: return@l - val (_, area) = getWorldAndArea(event.item.location.block) ?: return@l - if (!canInteract(user, area, "containers")) event.isCancelled = true - } - - /* - * Prevents players from editing inventories - */ - @field:ListenerMarker(priority = NORMAL, events = ["inventory.InventoryClickEvent", "inventory.InventoryDragEvent"]) - val onInventoryClickEvent = RegistratorListener l@{ event -> - val user = event.whoClicked as? Player ?: return@l - if ((event.inventory ?: return@l).holder === user) return@l // inventory null: hotbar - val (_, area) = getWorldAndArea(event.inventory.location.block) ?: return@l - if (!canInteract(user, area, "containers")) { - event.isCancelled = true - } - } - - /* - * Cancels weather changes and sets the weather to sunny if requested by the config for that world. - */ - @field:ListenerMarker(priority = NORMAL) - val onWeatherChangeEvent = RegistratorListener l@{ event -> - val world = parcelProvider.getWorld(event.world) ?: return@l - if (world.options.noWeather && event.toWeatherState()) { - event.isCancelled = true - } - } - - private fun resetWeather(world: World) { - world.setStorm(false) - world.isThundering = false - world.weatherDuration = Int.MAX_VALUE - } - -// TODO: BlockFormEvent, BlockSpreadEvent, BlockFadeEvent, Fireworks - - /* - * Prevents natural blocks forming - */ - @ListenerMarker(priority = NORMAL) - val onBlockFormEvent = RegistratorListener l@{ event -> - val block = event.block - val (world, area) = getWorldAndArea(block) ?: return@l - - // prevent any generation whatsoever on paths - if (area == null) { - event.isCancelled = true; return@l - } - - val hasEntity = event is EntityBlockFormEvent - val player = (event as? EntityBlockFormEvent)?.entity as? Player - - val cancel: Boolean = when (event.newState.type) { - - // prevent ice generation from Frost Walkers enchantment - FROSTED_ICE -> player != null && !area.canBuild(player) - - // prevent snow generation from weather - SNOW -> !hasEntity && world.options.preventWeatherBlockChanges - - else -> false - } - - if (cancel) { - event.isCancelled = true - } - } - - /* - * Prevents mobs (living entities) from spawning if that is disabled for that world in the config. - */ - @field:ListenerMarker(priority = NORMAL) - val onEntitySpawnEvent = RegistratorListener l@{ event -> - val world = parcelProvider.getWorld(event.entity.world) ?: return@l - if (event.entity is Creature && world.options.blockMobSpawning) { - event.isCancelled = true - } else if (world.getParcelAt(event.entity).let { it != null && it.hasBlockVisitors }) { - event.isCancelled = true - } - } - - /* - * Prevents minecarts/boats from moving outside a plot - */ - @field:ListenerMarker(priority = NORMAL) - val onVehicleMoveEvent = RegistratorListener l@{ event -> - val (_, area) = getWorldAndArea(event.to.block) ?: return@l - if (area == null) { - 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 (area.hasBlockVisitors) { - event.to.subtract(event.to).add(event.from) - } - } - - /* - * Prevents players from removing items from item frames - * Prevents TNT Minecarts and creepers from destroying entities (This event is called BEFORE EntityExplodeEvent GG) - * Actually doesn't prevent this because the entities are destroyed anyway, even though the code works? - */ - @field:ListenerMarker(priority = NORMAL) - val onEntityDamageByEntityEvent = RegistratorListener l@{ event -> - val world = parcelProvider.getWorld(event.entity.world) ?: return@l - if (world.options.disableExplosions && event.damager is ExplosiveMinecart || event.damager is Creeper) { - event.isCancelled = true; return@l - } - - val user = event.damager as? Player - ?: (event.damager as? Projectile)?.let { it.shooter as? Player } - ?: return@l - - if (!canBuildOnArea(user, world.getParcelAt(event.entity))) { - event.isCancelled = true - } - } - - @field:ListenerMarker(priority = NORMAL) - val onHangingBreakEvent = RegistratorListener l@{ event -> - val world = parcelProvider.getWorld(event.entity.world) ?: return@l - if (event.cause == HangingBreakEvent.RemoveCause.EXPLOSION && world.options.disableExplosions) { - event.isCancelled = true; return@l - } - - if (world.getParcelAt(event.entity).let { it != null && it.hasBlockVisitors }) { - event.isCancelled = true - } - } - - /* - * Prevents players from deleting paintings and item frames - * This appears to take care of shooting with a bow, throwing snowballs or throwing ender pearls. - */ - @field:ListenerMarker(priority = NORMAL) - val onHangingBreakByEntityEvent = RegistratorListener l@{ event -> - val world = parcelProvider.getWorld(event.entity.world) ?: return@l - val user = event.remover as? Player ?: return@l - if (!canBuildOnArea(user, world.getParcelAt(event.entity))) { - event.isCancelled = true - } - } - - /* - * Prevents players from placing paintings and item frames - */ - @field:ListenerMarker(priority = NORMAL) - val onHangingPlaceEvent = RegistratorListener l@{ event -> - val world = parcelProvider.getWorld(event.entity.world) ?: return@l - val block = event.block.getRelative(event.blockFace) - if (!canBuildOnArea(event.player, world.getParcelAt(block))) { - event.isCancelled = true - } - } - - /* - * Prevents stuff from growing outside of plots - */ - @field:ListenerMarker(priority = NORMAL) - val onStructureGrowEvent = RegistratorListener l@{ event -> - val (world, area) = getWorldAndArea(event.location.block) ?: return@l - if (area == null) { - event.isCancelled = true; return@l - } - - if (!event.player.hasPermBuildAnywhere && !area.canBuild(event.player)) { - event.isCancelled = true; return@l - } - - event.blocks.removeIf { world.getParcelAt(it.block) !== area } - } - - /* - * Prevents dispensers/droppers from dispensing out of parcels - */ - @field:ListenerMarker(priority = NORMAL) - val onBlockDispenseEvent = RegistratorListener l@{ event -> - val block = event.block - if (!block.type.let { it == DISPENSER || it == DROPPER }) return@l - val world = parcelProvider.getWorld(block.world) ?: return@l - val data = block.blockData as Directional - val targetBlock = block.getRelative(data.facing) - if (world.getParcelAt(targetBlock) == null) { - event.isCancelled = true - } - } - - /* - * Track spawned items, making sure they don't leave the parcel. - */ - @field:ListenerMarker(priority = NORMAL) - val onItemSpawnEvent = RegistratorListener l@{ event -> - val (_, area) = getWorldAndArea(event.location.block) ?: return@l - if (area == null) event.isCancelled = true - else entityTracker.track(event.entity, area) - } - - /* - * Prevents endermen and endermite from teleporting outside their parcel - */ - @field:ListenerMarker(priority = NORMAL) - val onEntityTeleportEvent = RegistratorListener l@{ event -> - val (world, area) = getWorldAndArea(event.from.block) ?: return@l - if (area !== world.getParcelAt(event.to)) { - event.isCancelled = true - } - } - - /* - * Prevents projectiles from flying out of parcels - * Prevents players from firing projectiles if they cannot build - */ - @field:ListenerMarker(priority = NORMAL) - val onProjectileLaunchEvent = RegistratorListener l@{ event -> - val (_, area) = getWorldAndArea(event.entity.location.block) ?: return@l - if (area == null || (event.entity.shooter as? Player)?.let { !canBuildOnArea(it, area) } == true) { - event.isCancelled = true - } else { - entityTracker.track(event.entity, area) - } - } - - /* - * Prevents entities from dropping items upon death, if configured that way - */ - @field:ListenerMarker(priority = NORMAL) - val onEntityDeathEvent = RegistratorListener l@{ event -> - entityTracker.untrack(event.entity) - val world = parcelProvider.getWorld(event.entity.world) ?: return@l - if (!world.options.dropEntityItems) { - event.drops.clear() - event.droppedExp = 0 - } - } - - /* - * Assigns players their default game mode upon entering the world - */ - @field:ListenerMarker(priority = NORMAL) - val onPlayerChangedWorldEvent = RegistratorListener l@{ event -> - val world = parcelProvider.getWorld(event.player.world) ?: return@l - if (world.options.gameMode != null && !event.player.hasPermGamemodeBypass) { - event.player.gameMode = world.options.gameMode - } - } - - /** - * Updates owner signs of parcels that get loaded if it is marked outdated - */ - @ListenerMarker(priority = EventPriority.NORMAL) - val onChunkLoadEvent = RegistratorListener l@{ event -> - val world = parcelProvider.getWorld(event.chunk.world) ?: return@l - val parcels = world.blockManager.getParcelsWithOwnerBlockIn(event.chunk) - if (parcels.isEmpty()) return@l - - parcels.forEach { id -> - val parcel = world.getParcelById(id)?.takeIf { it.isOwnerSignOutdated } ?: return@forEach - world.blockManager.updateParcelInfo(parcel.id, parcel.owner) - parcel.isOwnerSignOutdated = false - } - - } - - @ListenerMarker - val onPlayerJoinEvent = RegistratorListener l@{ event -> - storage.updatePlayerName(event.player.uuid, event.player.name) - } - - /** - * Attempts to prevent redstone contraptions from breaking while they are being swapped - * Might remove if it causes lag - */ - @ListenerMarker - val onBlockRedstoneEvent = RegistratorListener l@{ event -> - val (_, area) = getWorldAndArea(event.block) ?: return@l - if (area == null || area.hasBlockVisitors) { - event.newCurrent = event.oldCurrent - } - } - - - private fun getPlayerSpeed(player: Player): Double = - if (player.isFlying) { - player.flySpeed * if (player.isSprinting) 21.6 else 10.92 - } else { - player.walkSpeed * when { - player.isSprinting -> 5.612 - player.isSneaking -> 1.31 - else -> 4.317 - } / 1.5 //? - } / 20.0 - +package io.dico.parcels2.listener + +import gnu.trove.TLongCollection +import gnu.trove.set.hash.TLongHashSet +import io.dico.dicore.Formatting +import io.dico.dicore.ListenerMarker +import io.dico.dicore.RegistratorListener +import io.dico.parcels2.* +import io.dico.parcels2.storage.Storage +import io.dico.parcels2.util.ext.* +import io.dico.parcels2.util.math.* +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.Material.* +import org.bukkit.World +import org.bukkit.block.Biome +import org.bukkit.block.Block +import org.bukkit.block.data.Directional +import org.bukkit.block.data.type.Bed +import org.bukkit.entity.* +import org.bukkit.entity.minecart.ExplosiveMinecart +import org.bukkit.event.EventPriority +import org.bukkit.event.EventPriority.NORMAL +import org.bukkit.event.block.* +import org.bukkit.event.entity.* +import org.bukkit.event.hanging.HangingBreakByEntityEvent +import org.bukkit.event.hanging.HangingBreakEvent +import org.bukkit.event.hanging.HangingPlaceEvent +import org.bukkit.event.inventory.InventoryInteractEvent +import org.bukkit.event.player.* +import org.bukkit.event.vehicle.VehicleMoveEvent +import org.bukkit.event.weather.WeatherChangeEvent +import org.bukkit.event.world.ChunkLoadEvent +import org.bukkit.event.world.StructureGrowEvent +import org.bukkit.inventory.InventoryHolder +import java.util.EnumSet + +class ParcelListeners( + val parcelProvider: ParcelProvider, + val entityTracker: ParcelEntityTracker, + val storage: Storage +) { + private fun canBuildOnArea(user: Player, area: Parcel?) = + if (area == null) user.hasPermBuildAnywhere else area.canBuild(user) + + private fun canInteract(user: Player, area: Parcel?, interactClass: String) = + canBuildOnArea(user, area) || (area != null && area.interactableConfig(interactClass)) + + /** + * Get the world and parcel that the block resides in + * the parcel is nullable, and often named area because that means path. + * returns null if not in a registered parcel world - should always return in that case to not affect other worlds. + */ + private fun getWorldAndArea(block: Block): Pair? { + val world = parcelProvider.getWorld(block.world) ?: return null + return world to world.getParcelAt(block) + } + + + /* + * Prevents players from entering plots they are banned from + */ + @field:ListenerMarker(priority = NORMAL) + val onPlayerMoveEvent = RegistratorListener l@{ event -> + val user = event.player + if (user.hasPermBanBypass) return@l + val toLoc = event.to + val parcel = parcelProvider.getParcelAt(toLoc) ?: return@l + + if (!parcel.canEnterFast(user)) { + val region = parcel.world.blockManager.getRegion(parcel.id) + val dimension = region.getFirstUncontainedDimensionOf(Vec3i(event.from)) + + if (dimension == null) { + user.teleport(parcel.homeLocation) + user.sendParcelMessage(nopermit = true, message = "You are banned from this parcel") + + } else { + val speed = getPlayerSpeed(user) + val from = Vec3d(event.from) + val to = Vec3d(toLoc).with(dimension, from[dimension]) + + var newTo = to + dimension.otherDimensions.forEach { + val delta = to[it] - from[it] + newTo = newTo.add(it, delta * 100 * if (it == Dimension.Y) 0.5 else speed) + } + + event.to = Location( + toLoc.world, + newTo.x, newTo.y.clampMin(0.0).clampMax(255.0), newTo.z, + toLoc.yaw, toLoc.pitch + ) + } + } + } + + /* + * Prevents players from breaking blocks outside of their parcels + * Prevents containers from dropping their contents when broken, if configured + */ + @field:ListenerMarker(priority = NORMAL) + val onBlockBreakEvent = RegistratorListener l@{ event -> + val (world, area) = getWorldAndArea(event.block) ?: return@l + if (!canBuildOnArea(event.player, area)) { + event.isCancelled = true; return@l + } + + if (!world.options.dropEntityItems) { + val state = event.block.state + if (state is InventoryHolder) { + state.inventory.clear() + state.update() + } + } + } + + /* + * Prevents players from placing blocks outside of their parcels + */ + @field:ListenerMarker(priority = NORMAL) + val onBlockPlaceEvent = RegistratorListener l@{ event -> + val (_, area) = getWorldAndArea(event.block) ?: return@l + if (!canBuildOnArea(event.player, area)) { + event.isCancelled = true + } + + area?.updateOwnerSign() + } + + /* + * Control pistons + */ + @field:ListenerMarker(priority = NORMAL) + val onBlockPistonExtendEvent = RegistratorListener l@{ event -> + checkPistonMovement(event, event.blocks) + } + + @field:ListenerMarker(priority = NORMAL) + val onBlockPistonRetractEvent = RegistratorListener l@{ event -> + checkPistonMovement(event, event.blocks) + } + + // Doing some unnecessary optimizations here.. + //@formatter:off + private inline fun Column(x: Int, z: Int): Long = x.toLong() or (z.toLong().shl(32)) + + private inline val Long.columnX get() = and(0xFFFF_FFFFL).toInt() + private inline val Long.columnZ get() = ushr(32).and(0xFFFF_FFFFL).toInt() + private inline fun TLongCollection.troveForEach(block: (Long) -> Unit) = iterator().let { while (it.hasNext()) block(it.next()) } + //@formatter:on + private fun checkPistonMovement(event: BlockPistonEvent, blocks: List) { + val world = parcelProvider.getWorld(event.block.world) ?: return + val direction = event.direction + val columns = TLongHashSet(blocks.size * 2) + + blocks.forEach { + columns.add(Column(it.x, it.z)) + it.getRelative(direction).let { columns.add(Column(it.x, it.z)) } + } + + columns.troveForEach { + val area = world.getParcelAt(it.columnX, it.columnZ) + if (area == null || area.hasBlockVisitors) { + event.isCancelled = true + return + } + } + } + + /* + * Prevents explosions if enabled by the configs for that world + */ + @field:ListenerMarker(priority = NORMAL) + val onExplosionPrimeEvent = RegistratorListener l@{ event -> + val (world, area) = getWorldAndArea(event.entity.location.block) ?: return@l + if (area != null && area.hasBlockVisitors) { + event.radius = 0F; event.isCancelled = true + } else if (world.options.disableExplosions) { + event.radius = 0F + } + } + + /* + * Prevents creepers and tnt minecarts from exploding if explosions are disabled + */ + @field:ListenerMarker(priority = NORMAL) + val onEntityExplodeEvent = RegistratorListener l@{ event -> + entityTracker.untrack(event.entity) + val world = parcelProvider.getWorld(event.entity.world) ?: return@l + if (world.options.disableExplosions || world.getParcelAt(event.entity).let { it != null && it.hasBlockVisitors }) { + event.isCancelled = true + } + } + + /* + * Prevents liquids from flowing out of plots + */ + @field:ListenerMarker(priority = NORMAL) + val onBlockFromToEvent = RegistratorListener l@{ event -> + val (_, area) = getWorldAndArea(event.toBlock) ?: return@l + if (area == null || area.hasBlockVisitors) event.isCancelled = true + } + + private val bedTypes = EnumSet.copyOf(getMaterialsWithWoolColorPrefix("BED").toList()) + /* + * Prevents players from placing liquids, using flint and steel, changing redstone components, + * using inputs (unless allowed by the plot), + * and using items disabled in the configuration for that world. + * Prevents player from using beds in HELL or SKY biomes if explosions are disabled. + */ + @Suppress("NON_EXHAUSTIVE_WHEN") + @field:ListenerMarker(priority = NORMAL, ignoreCancelled = false) + val onPlayerInteractEvent = RegistratorListener l@{ event -> + val user = event.player + val world = parcelProvider.getWorld(user.world) ?: return@l + val clickedBlock = event.clickedBlock + val parcel = clickedBlock?.let { world.getParcelAt(it) } + + if (!user.hasPermBuildAnywhere && parcel != null && !parcel.canEnter(user)) { + user.sendParcelMessage(nopermit = true, message = "You cannot interact with parcels you're banned from") + event.isCancelled = true; return@l + } + + when (event.action) { + Action.RIGHT_CLICK_BLOCK -> run { + if (event.isCancelled) return@l + val type = clickedBlock.type + + val interactableClass = Interactables[type] + if (interactableClass != null && !parcel.effectiveInteractableConfig.isInteractable(type) && (parcel == null || !parcel.canBuild(user))) { + user.sendParcelMessage(nopermit = true, message = "You cannot interact with ${interactableClass.name} here") + event.isCancelled = true + return@l + } + + if (bedTypes.contains(type)) { + val bed = clickedBlock.blockData as Bed + val head = if (bed.part == Bed.Part.FOOT) clickedBlock.getRelative(bed.facing) else clickedBlock + when (head.biome) { + Biome.NETHER, Biome.THE_END -> { + if (world.options.disableExplosions) { + user.sendParcelMessage(nopermit = true, message = "You cannot use this bed because it would explode") + event.isCancelled = true; return@l + } + } + } + + if (!canBuildOnArea(user, parcel)) { + user.sendParcelMessage(nopermit = true, message = "You may not sleep here") + event.isCancelled = true; return@l + } + } + + onPlayerRightClick(event, world, parcel) + + if (!event.isCancelled && parcel == null) { + world.blockManager.getParcelForInfoBlockInteraction(Vec3i(clickedBlock), type, event.blockFace) + ?.apply { user.sendMessage(Formatting.GREEN + infoString) } + } + } + + Action.RIGHT_CLICK_AIR -> onPlayerRightClick(event, world, parcel) + Action.PHYSICAL -> if (!event.isCancelled && !canBuildOnArea(user, parcel)) { + if (clickedBlock.type == Material.TURTLE_EGG) { + event.isCancelled = true; return@l + } + + if (!(parcel != null && parcel.interactableConfig("pressure_plates"))) { + user.sendParcelMessage(nopermit = true, message = "You cannot use inputs in this parcel") + event.isCancelled = true; return@l + } + } + } + } + + // private val blockPlaceInteractItems = EnumSet.of(LAVA_BUCKET, WATER_BUCKET, BUCKET, FLINT_AND_STEEL) + + @Suppress("NON_EXHAUSTIVE_WHEN") + private fun onPlayerRightClick(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 + } + + when (item) { + LAVA_BUCKET, WATER_BUCKET, BUCKET, FLINT_AND_STEEL -> { + val block = event.clickedBlock.getRelative(event.blockFace) + val otherParcel = world.getParcelAt(block) + if (!canBuildOnArea(event.player, otherParcel)) { + event.isCancelled = true + } + } + } + } + } + + /* + * Prevents players from breeding mobs, entering or opening boats/minecarts, + * rotating item frames, doing stuff with leashes, and putting stuff on armor stands. + */ + @Suppress("NON_EXHAUSTIVE_WHEN") + @field:ListenerMarker(priority = NORMAL) + val onPlayerInteractEntityEvent = RegistratorListener l@{ event -> + val (_, area) = getWorldAndArea(event.rightClicked.location.block) ?: return@l + if (canBuildOnArea(event.player, area)) return@l + when (event.rightClicked.type) { + EntityType.BOAT, + EntityType.MINECART, + EntityType.MINECART_CHEST, + EntityType.MINECART_COMMAND, + EntityType.MINECART_FURNACE, + EntityType.MINECART_HOPPER, + EntityType.MINECART_MOB_SPAWNER, + EntityType.MINECART_TNT, + + EntityType.ARMOR_STAND, + EntityType.PAINTING, + EntityType.ITEM_FRAME, + EntityType.LEASH_HITCH, + + EntityType.CHICKEN, + EntityType.COW, + EntityType.HORSE, + EntityType.SHEEP, + EntityType.VILLAGER, + EntityType.WOLF -> event.isCancelled = true + } + } + + /* + * Prevents endermen from griefing. + * Prevents sand blocks from exiting the parcel in which they became an entity. + */ + @field:ListenerMarker(priority = NORMAL) + val onEntityChangeBlockEvent = RegistratorListener l@{ event -> + val (_, area) = getWorldAndArea(event.block) ?: return@l + if (event.entity.type == EntityType.ENDERMAN || area == null || area.hasBlockVisitors) { + event.isCancelled = true; return@l + } + + if (event.entity.type == EntityType.FALLING_BLOCK) { + // a sand block started falling. Track it and delete it if it gets out of this parcel. + entityTracker.track(event.entity, area) + } + } + + /* + * Prevents portals from being created if set so in the configs for that world + */ + @field:ListenerMarker(priority = NORMAL) + val onEntityCreatePortalEvent = RegistratorListener l@{ event -> + val world = parcelProvider.getWorld(event.entity.world) ?: return@l + if (world.options.blockPortalCreation) event.isCancelled = true + } + + /* + * Prevents players from dropping items + */ + @field:ListenerMarker(priority = NORMAL) + val onPlayerDropItemEvent = RegistratorListener l@{ event -> + val (_, area) = getWorldAndArea(event.itemDrop.location.block) ?: return@l + if (!canInteract(event.player, area, "containers")) event.isCancelled = true + } + + /* + * Prevents players from picking up items + */ + @field:ListenerMarker(priority = NORMAL) + val onEntityPickupItemEvent = RegistratorListener l@{ event -> + val user = event.entity as? Player ?: return@l + val (_, area) = getWorldAndArea(event.item.location.block) ?: return@l + if (!canInteract(user, area, "containers")) event.isCancelled = true + } + + /* + * Prevents players from editing inventories + */ + @field:ListenerMarker(priority = NORMAL, events = ["inventory.InventoryClickEvent", "inventory.InventoryDragEvent"]) + val onInventoryClickEvent = RegistratorListener l@{ event -> + val user = event.whoClicked as? Player ?: return@l + if ((event.inventory ?: return@l).holder === user) return@l // inventory null: hotbar + val (_, area) = getWorldAndArea(event.inventory.location.block) ?: return@l + if (!canInteract(user, area, "containers")) { + event.isCancelled = true + } + } + + /* + * Cancels weather changes and sets the weather to sunny if requested by the config for that world. + */ + @field:ListenerMarker(priority = NORMAL) + val onWeatherChangeEvent = RegistratorListener l@{ event -> + val world = parcelProvider.getWorld(event.world) ?: return@l + if (world.options.noWeather && event.toWeatherState()) { + event.isCancelled = true + } + } + + private fun resetWeather(world: World) { + world.setStorm(false) + world.isThundering = false + world.weatherDuration = Int.MAX_VALUE + } + +// TODO: BlockFormEvent, BlockSpreadEvent, BlockFadeEvent, Fireworks + + /* + * Prevents natural blocks forming + */ + @ListenerMarker(priority = NORMAL) + val onBlockFormEvent = RegistratorListener l@{ event -> + val block = event.block + val (world, area) = getWorldAndArea(block) ?: return@l + + // prevent any generation whatsoever on paths + if (area == null) { + event.isCancelled = true; return@l + } + + val hasEntity = event is EntityBlockFormEvent + val player = (event as? EntityBlockFormEvent)?.entity as? Player + + val cancel: Boolean = when (event.newState.type) { + + // prevent ice generation from Frost Walkers enchantment + FROSTED_ICE -> player != null && !area.canBuild(player) + + // prevent snow generation from weather + SNOW -> !hasEntity && world.options.preventWeatherBlockChanges + + else -> false + } + + if (cancel) { + event.isCancelled = true + } + } + + /* + * Prevents mobs (living entities) from spawning if that is disabled for that world in the config. + */ + @field:ListenerMarker(priority = NORMAL) + val onEntitySpawnEvent = RegistratorListener l@{ event -> + val world = parcelProvider.getWorld(event.entity.world) ?: return@l + if (event.entity is Mob && world.options.blockMobSpawning) { + event.isCancelled = true + } else if (world.getParcelAt(event.entity).let { it != null && it.hasBlockVisitors }) { + event.isCancelled = true + } + } + + + + /* + * Prevents minecarts/boats from moving outside a plot + */ + @field:ListenerMarker(priority = NORMAL) + val onVehicleMoveEvent = RegistratorListener l@{ event -> + val (_, area) = getWorldAndArea(event.to.block) ?: return@l + if (area == null) { + 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 (area.hasBlockVisitors) { + event.to.subtract(event.to).add(event.from) + } + } + + /* + * Prevents players from removing items from item frames + * Prevents TNT Minecarts and creepers from destroying entities (This event is called BEFORE EntityExplodeEvent GG) + * Actually doesn't prevent this because the entities are destroyed anyway, even though the code works? + */ + @field:ListenerMarker(priority = NORMAL) + val onEntityDamageByEntityEvent = RegistratorListener l@{ event -> + val world = parcelProvider.getWorld(event.entity.world) ?: return@l + if (world.options.disableExplosions && (event.damager is ExplosiveMinecart || event.damager is Creeper)) { + event.isCancelled = true; return@l + } + + val user = event.damager as? Player + ?: (event.damager as? Projectile)?.let { it.shooter as? Player } + ?: return@l + + if (!canBuildOnArea(user, world.getParcelAt(event.entity))) { + event.isCancelled = true + } + } + + @field:ListenerMarker(priority = NORMAL) + val onHangingBreakEvent = RegistratorListener l@{ event -> + val world = parcelProvider.getWorld(event.entity.world) ?: return@l + if (event.cause == HangingBreakEvent.RemoveCause.EXPLOSION && world.options.disableExplosions) { + event.isCancelled = true; return@l + } + + if (world.getParcelAt(event.entity).let { it != null && it.hasBlockVisitors }) { + event.isCancelled = true + } + } + + /* + * Prevents players from deleting paintings and item frames + * This appears to take care of shooting with a bow, throwing snowballs or throwing ender pearls. + */ + @field:ListenerMarker(priority = NORMAL) + val onHangingBreakByEntityEvent = RegistratorListener l@{ event -> + val world = parcelProvider.getWorld(event.entity.world) ?: return@l + val user = event.remover as? Player ?: return@l + if (!canBuildOnArea(user, world.getParcelAt(event.entity))) { + event.isCancelled = true + } + } + + /* + * Prevents players from placing paintings and item frames + */ + @field:ListenerMarker(priority = NORMAL) + val onHangingPlaceEvent = RegistratorListener l@{ event -> + val world = parcelProvider.getWorld(event.entity.world) ?: return@l + val block = event.block.getRelative(event.blockFace) + if (!canBuildOnArea(event.player, world.getParcelAt(block))) { + event.isCancelled = true + } + } + + /* + * Prevents stuff from growing outside of plots + */ + @field:ListenerMarker(priority = NORMAL) + val onStructureGrowEvent = RegistratorListener l@{ event -> + val (world, area) = getWorldAndArea(event.location.block) ?: return@l + if (area == null) { + event.isCancelled = true; return@l + } + + if (!event.player.hasPermBuildAnywhere && !area.canBuild(event.player)) { + event.isCancelled = true; return@l + } + + event.blocks.removeIf { world.getParcelAt(it.block) !== area } + } + + @field:ListenerMarker(priority = NORMAL) + val onBlockGrowEvent = RegistratorListener l@{ event -> + val (world, area) = getWorldAndArea(event.block) ?: return@l + if (area == null) { + event.isCancelled = true + } + } + + /* + * Prevents dispensers/droppers from dispensing out of parcels + */ + @field:ListenerMarker(priority = NORMAL) + val onBlockDispenseEvent = RegistratorListener l@{ event -> + val block = event.block + if (!block.type.let { it == DISPENSER || it == DROPPER }) return@l + val world = parcelProvider.getWorld(block.world) ?: return@l + val data = block.blockData as Directional + val targetBlock = block.getRelative(data.facing) + if (world.getParcelAt(targetBlock) == null) { + event.isCancelled = true + } + } + + /* + * Track spawned items, making sure they don't leave the parcel. + */ + @field:ListenerMarker(priority = NORMAL) + val onItemSpawnEvent = RegistratorListener l@{ event -> + val (_, area) = getWorldAndArea(event.location.block) ?: return@l + if (area == null) event.isCancelled = true + else entityTracker.track(event.entity, area) + } + + /* + * Prevents endermen and endermite from teleporting outside their parcel + */ + @field:ListenerMarker(priority = NORMAL) + val onEntityTeleportEvent = RegistratorListener l@{ event -> + val (world, area) = getWorldAndArea(event.from.block) ?: return@l + if (area !== world.getParcelAt(event.to)) { + event.isCancelled = true + } + } + + /* + * Prevents projectiles from flying out of parcels + * Prevents players from firing projectiles if they cannot build + */ + @field:ListenerMarker(priority = NORMAL) + val onProjectileLaunchEvent = RegistratorListener l@{ event -> + val (_, area) = getWorldAndArea(event.entity.location.block) ?: return@l + if (area == null || (event.entity.shooter as? Player)?.let { !canBuildOnArea(it, area) } == true) { + event.isCancelled = true + } else { + entityTracker.track(event.entity, area) + } + } + + /* + * Prevents entities from dropping items upon death, if configured that way + */ + @field:ListenerMarker(priority = NORMAL) + val onEntityDeathEvent = RegistratorListener l@{ event -> + entityTracker.untrack(event.entity) + val world = parcelProvider.getWorld(event.entity.world) ?: return@l + if (!world.options.dropEntityItems) { + event.drops.clear() + event.droppedExp = 0 + } + } + + /* + * Assigns players their default game mode upon entering the world + */ + @field:ListenerMarker(priority = NORMAL) + val onPlayerChangedWorldEvent = RegistratorListener l@{ event -> + val world = parcelProvider.getWorld(event.player.world) ?: return@l + if (world.options.gameMode != null && !event.player.hasPermGamemodeBypass) { + event.player.gameMode = world.options.gameMode + } + } + + /** + * Updates owner signs of parcels that get loaded if it is marked outdated + */ + @ListenerMarker(priority = EventPriority.NORMAL) + val onChunkLoadEvent = RegistratorListener l@{ event -> + val world = parcelProvider.getWorld(event.chunk.world) ?: return@l + val parcels = world.blockManager.getParcelsWithOwnerBlockIn(event.chunk) + if (parcels.isEmpty()) return@l + + parcels.forEach { id -> + val parcel = world.getParcelById(id)?.takeIf { it.isOwnerSignOutdated } ?: return@forEach + world.blockManager.updateParcelInfo(parcel.id, parcel.owner) + parcel.isOwnerSignOutdated = false + } + + } + + @ListenerMarker + val onPlayerJoinEvent = RegistratorListener l@{ event -> + storage.updatePlayerName(event.player.uuid, event.player.name) + } + + /** + * Attempts to prevent redstone contraptions from breaking while they are being swapped + * Might remove if it causes lag + */ + @ListenerMarker + val onBlockRedstoneEvent = RegistratorListener l@{ event -> + val (_, area) = getWorldAndArea(event.block) ?: return@l + if (area == null || area.hasBlockVisitors) { + event.newCurrent = event.oldCurrent + } + } + + + private fun getPlayerSpeed(player: Player): Double = + if (player.isFlying) { + player.flySpeed * if (player.isSprinting) 21.6 else 10.92 + } else { + player.walkSpeed * when { + player.isSprinting -> 5.612 + player.isSneaking -> 1.31 + else -> 4.317 + } / 1.5 //? + } / 20.0 + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/listener/WorldEditListener.kt b/src/main/kotlin/io/dico/parcels2/listener/WorldEditListener.kt index fc31305..5bab29a 100644 --- a/src/main/kotlin/io/dico/parcels2/listener/WorldEditListener.kt +++ b/src/main/kotlin/io/dico/parcels2/listener/WorldEditListener.kt @@ -1,79 +1,78 @@ -package io.dico.parcels2.listener - -import com.sk89q.worldedit.EditSession.Stage.BEFORE_REORDER -import com.sk89q.worldedit.Vector -import com.sk89q.worldedit.Vector2D -import com.sk89q.worldedit.WorldEdit -import com.sk89q.worldedit.bukkit.WorldEditPlugin -import com.sk89q.worldedit.event.extent.EditSessionEvent -import com.sk89q.worldedit.extent.AbstractDelegateExtent -import com.sk89q.worldedit.extent.Extent -import com.sk89q.worldedit.util.eventbus.EventHandler.Priority.VERY_EARLY -import com.sk89q.worldedit.util.eventbus.Subscribe -import com.sk89q.worldedit.world.biome.BaseBiome -import com.sk89q.worldedit.world.block.BlockStateHolder -import io.dico.parcels2.ParcelWorld -import io.dico.parcels2.ParcelsPlugin -import io.dico.parcels2.canBuildFast -import io.dico.parcels2.util.ext.hasPermBuildAnywhere -import io.dico.parcels2.util.ext.sendParcelMessage -import org.bukkit.entity.Player -import org.bukkit.plugin.Plugin - -class WorldEditListener(val parcels: ParcelsPlugin, val worldEdit: WorldEdit) { - - @Subscribe(priority = VERY_EARLY) - fun onEditSession(event: EditSessionEvent) { - val worldName = event.world?.name ?: return - val world = parcels.parcelProvider.getWorld(worldName) ?: return - if (event.stage == BEFORE_REORDER) return - - val actor = event.actor - if (actor == null || !actor.isPlayer) return - - val player = parcels.server.getPlayer(actor.uniqueId) - if (player.hasPermBuildAnywhere) return - - event.extent = ParcelsExtent(event.extent, world, player) - } - - private class ParcelsExtent(extent: Extent, - val world: ParcelWorld, - val player: Player) : AbstractDelegateExtent(extent) { - private var messageSent = false - - private fun canBuild(x: Int, z: Int): Boolean { - world.getParcelAt(x, z)?.let { parcel -> - if (parcel.canBuildFast(player)) { - return true - } - } - - if (!messageSent) { - messageSent = true - player.sendParcelMessage(except = true, message = "You can't use WorldEdit there") - } - - return false - } - - override fun setBlock(location: Vector, block: BlockStateHolder<*>): Boolean { - return canBuild(location.blockX, location.blockZ) && super.setBlock(location, block) - } - - override fun setBiome(coord: Vector2D, biome: BaseBiome): Boolean { - return canBuild(coord.blockX, coord.blockZ) && super.setBiome(coord, biome) - } - - } - - companion object { - fun register(parcels: ParcelsPlugin, worldEditPlugin: Plugin) { - if (worldEditPlugin !is WorldEditPlugin) return - val worldEdit = worldEditPlugin.worldEdit - val listener = WorldEditListener(parcels, worldEdit) - worldEdit.eventBus.register(listener) - } - } - +package io.dico.parcels2.listener + +import com.sk89q.worldedit.EditSession.Stage.BEFORE_REORDER +import com.sk89q.worldedit.WorldEdit +import com.sk89q.worldedit.bukkit.WorldEditPlugin +import com.sk89q.worldedit.event.extent.EditSessionEvent +import com.sk89q.worldedit.extent.AbstractDelegateExtent +import com.sk89q.worldedit.extent.Extent +import com.sk89q.worldedit.math.BlockVector2 +import com.sk89q.worldedit.math.BlockVector3 +import com.sk89q.worldedit.util.eventbus.EventHandler.Priority.VERY_EARLY +import com.sk89q.worldedit.util.eventbus.Subscribe +import com.sk89q.worldedit.world.biome.BaseBiome +import com.sk89q.worldedit.world.block.BlockStateHolder +import io.dico.parcels2.ParcelWorld +import io.dico.parcels2.ParcelsPlugin +import io.dico.parcels2.canBuildFast +import io.dico.parcels2.util.ext.hasPermBuildAnywhere +import io.dico.parcels2.util.ext.sendParcelMessage +import org.bukkit.entity.Player +import org.bukkit.plugin.Plugin + +class WorldEditListener(val parcels: ParcelsPlugin, val worldEdit: WorldEdit) { + + @Subscribe(priority = VERY_EARLY) + fun onEditSession(event: EditSessionEvent) { + val worldName = event.world?.name ?: return + val world = parcels.parcelProvider.getWorld(worldName) ?: return + if (event.stage == BEFORE_REORDER) return + + val actor = event.actor + if (actor == null || !actor.isPlayer) return + + val player = parcels.server.getPlayer(actor.uniqueId) + if (player.hasPermBuildAnywhere) return + + event.extent = ParcelsExtent(event.extent, world, player) + } + + private class ParcelsExtent(extent: Extent, + val world: ParcelWorld, + val player: Player) : AbstractDelegateExtent(extent) { + private var messageSent = false + + private fun canBuild(x: Int, z: Int): Boolean { + world.getParcelAt(x, z)?.let { parcel -> + if (parcel.canBuildFast(player)) { + return true + } + } + + if (!messageSent) { + messageSent = true + player.sendParcelMessage(except = true, message = "You can't use WorldEdit there") + } + + return false + } + + override fun setBiome(coord: BlockVector2, biome: BaseBiome): Boolean { + return canBuild(coord.blockX, coord.blockZ) && super.setBiome(coord, biome) + } + + override fun > setBlock(location: BlockVector3, block: T): Boolean { + return canBuild(location.blockX, location.blockZ) && super.setBlock(location, block) + } + } + + companion object { + fun register(parcels: ParcelsPlugin, worldEditPlugin: Plugin) { + if (worldEditPlugin !is WorldEditPlugin) return + val worldEdit = worldEditPlugin.worldEdit + val listener = WorldEditListener(parcels, worldEdit) + worldEdit.eventBus.register(listener) + } + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/util/PluginAware.kt b/src/main/kotlin/io/dico/parcels2/util/PluginAware.kt index de75519..b55f991 100644 --- a/src/main/kotlin/io/dico/parcels2/util/PluginAware.kt +++ b/src/main/kotlin/io/dico/parcels2/util/PluginAware.kt @@ -1,3 +1,5 @@ +@file:Suppress("RedundantLambdaArrow") + package io.dico.parcels2.util import org.bukkit.plugin.Plugin @@ -8,11 +10,10 @@ interface PluginAware { } inline fun PluginAware.schedule(delay: Int = 0, crossinline task: () -> Unit): BukkitTask { - return plugin.server.scheduler.runTaskLater(plugin, { task() }, delay.toLong()) + return plugin.server.scheduler.runTaskLater(plugin, { -> task() }, delay.toLong()) } inline fun PluginAware.scheduleRepeating(interval: Int, delay: Int = 0, crossinline task: () -> Unit): BukkitTask { - return plugin.server.scheduler.runTaskTimer(plugin, { task() }, delay.toLong(), interval.toLong()) + return plugin.server.scheduler.runTaskTimer(plugin, { -> task() }, delay.toLong(), interval.toLong()) } - diff --git a/src/main/kotlin/io/dico/parcels2/util/parallel.kt b/src/main/kotlin/io/dico/parcels2/util/parallel.kt deleted file mode 100644 index a4edc3c..0000000 --- a/src/main/kotlin/io/dico/parcels2/util/parallel.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.dico.parcels2.util - -fun doParallel() { - - val array = IntArray(1000) - IntRange(0, 1000).chunked() - - -} \ No newline at end of file -- cgit v1.2.3 From 78e8496d5a20a8e62ea7f46fa85608aec2ac5a55 Mon Sep 17 00:00:00 2001 From: Dico Karssiens Date: Sun, 6 Jan 2019 12:55:21 +0000 Subject: Add permission info --- .../dico/parcels2/command/ParcelCommandBuilder.kt | 276 +++++++++++---------- 1 file changed, 139 insertions(+), 137 deletions(-) (limited to 'src') diff --git a/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt b/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt index 155f4f1..b4eae30 100644 --- a/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt +++ b/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt @@ -1,137 +1,139 @@ -package io.dico.parcels2.command - -import io.dico.dicore.command.* -import io.dico.dicore.command.parameter.ArgumentBuffer -import io.dico.dicore.command.predef.DefaultGroupCommand -import io.dico.dicore.command.registration.reflect.ReflectiveRegistration -import io.dico.parcels2.Interactables -import io.dico.parcels2.ParcelsPlugin -import io.dico.parcels2.logger -import io.dico.parcels2.util.ext.hasPermAdminManage -import org.bukkit.command.CommandSender -import java.util.LinkedList -import java.util.Queue - -@Suppress("UsePropertyAccessSyntax") -fun getParcelCommands(plugin: ParcelsPlugin): ICommandDispatcher = CommandBuilder().apply { - val parcelsAddress = SpecialCommandAddress() - - setChatHandler(ParcelsChatHandler()) - addParameterType(false, ParcelParameterType(plugin.parcelProvider)) - addParameterType(false, ProfileParameterType()) - addParameterType(true, ParcelTarget.PType(plugin.parcelProvider, parcelsAddress)) - - group(parcelsAddress, "parcel", "plot", "plots", "p") { - addContextFilter(IContextFilter.inheritablePermission("parcels.command")) - registerCommands(CommandsGeneral(plugin, parcelsAddress)) - registerCommands(CommandsPrivilegesLocal(plugin)) - - group("option", "opt", "o") { - setGroupDescription( - "changes interaction options for this parcel", - "Sets whether players who are not allowed to", - "build here can interact with certain things." - ) - - group("interact", "i") { - val command = ParcelOptionsInteractCommand(plugin) - Interactables.classesById.forEach { - addSubCommand(it.name, command) - } - } - } - - val adminPrivilegesGlobal = CommandsAdminPrivilegesGlobal(plugin) - - group("global", "g") { - registerCommands(CommandsPrivilegesGlobal(plugin, adminVersion = adminPrivilegesGlobal)) - } - - group("admin", "a") { - setCommand(AdminGroupCommand()) - registerCommands(CommandsAdmin(plugin)) - - group("global", "g") { - registerCommands(adminPrivilegesGlobal) - } - } - - if (!logger.isDebugEnabled) return@group - - group("debug", "d") { - registerCommands(CommandsDebug(plugin)) - } - } - - generateHelpAndSyntaxCommands(parcelsAddress) -}.getDispatcher() - -private inline fun CommandBuilder.group(name: String, vararg aliases: String, config: CommandBuilder.() -> Unit) { - group(name, *aliases) - config() - parent() -} - -private inline fun CommandBuilder.group(address: ICommandAddress, name: String, vararg aliases: String, config: CommandBuilder.() -> Unit) { - group(address, name, *aliases) - config() - parent() -} - -private fun CommandBuilder.generateHelpAndSyntaxCommands(root: ICommandAddress): CommandBuilder { - generateCommands(root, "help", "syntax") - return this -} - -private fun generateCommands(address: ICommandAddress, vararg names: String) { - val addresses: Queue = LinkedList() - addresses.offer(address) - - while (addresses.isNotEmpty()) { - val cur = addresses.poll() - cur.childrenMainKeys.mapTo(addresses) { cur.getChild(it) } - if (cur.hasCommand()) { - ReflectiveRegistration.generateCommands(cur, names) - } - } -} - -class SpecialCommandAddress : ChildCommandAddress() { - private val speciallyTreatedKeys = mutableListOf() - - // Used to allow /p h:1 syntax, which is the same as what PlotMe uses. - var speciallyParsedIndex: Int? = null; private set - - fun addSpeciallyTreatedKeys(vararg keys: String) { - for (key in keys) { - speciallyTreatedKeys.add(key + ":") - } - } - - // h:1 - @Throws(CommandException::class) - override fun getChild(context: ExecutionContext, buffer: ArgumentBuffer): ChildCommandAddress? { - speciallyParsedIndex = null - - val key = buffer.next() ?: return null - for (specialKey in speciallyTreatedKeys) { - if (key.startsWith(specialKey)) { - val result = getChild(specialKey.substring(0, specialKey.length - 1)) - ?: return null - - val text = key.substring(specialKey.length) - val num = text.toIntOrNull() ?: throw CommandException("$text is not a number") - speciallyParsedIndex = num - - return result - } - } - - return super.getChild(key) - } - -} - -private class AdminGroupCommand : DefaultGroupCommand() { - override fun isVisibleTo(sender: CommandSender) = sender.hasPermAdminManage -} +package io.dico.parcels2.command + +import io.dico.dicore.command.* +import io.dico.dicore.command.parameter.ArgumentBuffer +import io.dico.dicore.command.predef.DefaultGroupCommand +import io.dico.dicore.command.registration.reflect.ReflectiveRegistration +import io.dico.parcels2.Interactables +import io.dico.parcels2.ParcelsPlugin +import io.dico.parcels2.logger +import io.dico.parcels2.util.ext.hasPermAdminManage +import org.bukkit.command.CommandSender +import java.util.LinkedList +import java.util.Queue + +@Suppress("UsePropertyAccessSyntax") +fun getParcelCommands(plugin: ParcelsPlugin): ICommandDispatcher = CommandBuilder().apply { + val parcelsAddress = SpecialCommandAddress() + + setChatHandler(ParcelsChatHandler()) + addParameterType(false, ParcelParameterType(plugin.parcelProvider)) + addParameterType(false, ProfileParameterType()) + addParameterType(true, ParcelTarget.PType(plugin.parcelProvider, parcelsAddress)) + + group(parcelsAddress, "parcel", "plot", "plots", "p") { + addContextFilter(IContextFilter.inheritablePermission("parcels.command")) + registerCommands(CommandsGeneral(plugin, parcelsAddress)) + registerCommands(CommandsPrivilegesLocal(plugin)) + + group("option", "opt", "o") { + setGroupDescription( + "changes interaction options for this parcel", + "Sets whether players who are not allowed to", + "build here can interact with certain things." + ) + + group("interact", "i") { + val command = ParcelOptionsInteractCommand(plugin) + Interactables.classesById.forEach { + addSubCommand(it.name, command) + } + } + } + + val adminPrivilegesGlobal = CommandsAdminPrivilegesGlobal(plugin) + + group("global", "g") { + registerCommands(CommandsPrivilegesGlobal(plugin, adminVersion = adminPrivilegesGlobal)) + } + + group("admin", "a") { + setCommand(AdminGroupCommand()) + registerCommands(CommandsAdmin(plugin)) + + group("global", "g") { + registerCommands(adminPrivilegesGlobal) + } + } + + if (!logger.isDebugEnabled) return@group + + group("debug", "d") { + registerCommands(CommandsDebug(plugin)) + } + } + + generateHelpAndSyntaxCommands(parcelsAddress) +}.getDispatcher().also { + RootCommandAddress.debugChildren(it as ModifiableCommandAddress) +} + +private inline fun CommandBuilder.group(name: String, vararg aliases: String, config: CommandBuilder.() -> Unit) { + group(name, *aliases) + config() + parent() +} + +private inline fun CommandBuilder.group(address: ICommandAddress, name: String, vararg aliases: String, config: CommandBuilder.() -> Unit) { + group(address, name, *aliases) + config() + parent() +} + +private fun CommandBuilder.generateHelpAndSyntaxCommands(root: ICommandAddress): CommandBuilder { + generateCommands(root, "help", "syntax") + return this +} + +private fun generateCommands(address: ICommandAddress, vararg names: String) { + val addresses: Queue = LinkedList() + addresses.offer(address) + + while (addresses.isNotEmpty()) { + val cur = addresses.poll() + cur.childrenMainKeys.mapTo(addresses) { cur.getChild(it) } + if (cur.hasCommand()) { + ReflectiveRegistration.generateCommands(cur, names) + } + } +} + +class SpecialCommandAddress : ChildCommandAddress() { + private val speciallyTreatedKeys = mutableListOf() + + // Used to allow /p h:1 syntax, which is the same as what PlotMe uses. + var speciallyParsedIndex: Int? = null; private set + + fun addSpeciallyTreatedKeys(vararg keys: String) { + for (key in keys) { + speciallyTreatedKeys.add(key + ":") + } + } + + // h:1 + @Throws(CommandException::class) + override fun getChild(context: ExecutionContext, buffer: ArgumentBuffer): ChildCommandAddress? { + speciallyParsedIndex = null + + val key = buffer.next() ?: return null + for (specialKey in speciallyTreatedKeys) { + if (key.startsWith(specialKey)) { + val result = getChild(specialKey.substring(0, specialKey.length - 1)) + ?: return null + + val text = key.substring(specialKey.length) + val num = text.toIntOrNull() ?: throw CommandException("$text is not a number") + speciallyParsedIndex = num + + return result + } + } + + return super.getChild(key) + } + +} + +private class AdminGroupCommand : DefaultGroupCommand() { + override fun isVisibleTo(sender: CommandSender) = sender.hasPermAdminManage +} -- cgit v1.2.3 From 778e301efc09eb8326146a084bb6788be748e6f1 Mon Sep 17 00:00:00 2001 From: Dico Karssiens Date: Sun, 6 Jan 2019 12:55:46 +0000 Subject: Remove debug --- src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'src') diff --git a/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt b/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt index b4eae30..721ce2d 100644 --- a/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt +++ b/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt @@ -64,9 +64,7 @@ fun getParcelCommands(plugin: ParcelsPlugin): ICommandDispatcher = CommandBuilde } generateHelpAndSyntaxCommands(parcelsAddress) -}.getDispatcher().also { - RootCommandAddress.debugChildren(it as ModifiableCommandAddress) -} +}.getDispatcher() private inline fun CommandBuilder.group(name: String, vararg aliases: String, config: CommandBuilder.() -> Unit) { group(name, *aliases) -- cgit v1.2.3