diff options
author | Dico Karssiens <dico.karssiens@gmail.com> | 2018-11-17 21:32:43 +0000 |
---|---|---|
committer | Dico Karssiens <dico.karssiens@gmail.com> | 2018-11-17 21:32:43 +0000 |
commit | 5ef2584fdb6e4db482aa4c57e6ecf0202c67a48d (patch) | |
tree | 7954c8fb9048c81246d5c53a4cba5b7646d4b6e1 /src | |
parent | 0f196f59c6a4cb76ab8409da62ff1f35505f94a8 (diff) |
Tweak some command stuff, clear/swap entities
Diffstat (limited to 'src')
17 files changed, 2450 insertions, 2224 deletions
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<Job>
-
- /**
- * 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 <T> 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<JobInternal>()
- override val jobs: List<Job> = _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<Unit>? = 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<Unit> ->
- 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<Job> + + /** + * 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 <T> 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<JobInternal>() + override val jobs: List<Job> = _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<Unit>? = 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<Unit> -> + 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<BlockPopulator> {
- 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<ParcelLocator, ParcelBlockManager>
-}
-
-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<Entity>
-
- 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<Vec2i>
-}
-
-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<Entity> {
- 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<BlockPopulator> { + 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<ParcelLocator, ParcelBlockManager> +} + +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<Entity> + + 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<Vec2i> +} + +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<Entity> { + 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<String, ParcelWorld>
-
- 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<out ParcelId>, 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<String, ParcelWorld> + + 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<out ParcelId>, 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<Options>(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<Options>(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<T : Entity>(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<T : Minecart>(entity: T) : EntityCopy<T>(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, Void>(Parcel::class.java) {
- val regex = Regex.fromLiteral("((.+)->)?([0-9]+):([0-9]+)")
-
- override fun parse(parameter: Parameter<Parcel, Void>, 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, Int>(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, Int>(PlayerProfile::class.java, ProfileKind) {
-
- override fun parse(parameter: Parameter<PlayerProfile, Int>, 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<PlayerProfile, Int>,
- sender: CommandSender,
- location: Location?,
- buffer: ArgumentBuffer
- ): MutableList<String> {
- 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, Void>(Parcel::class.java) { + val regex = Regex.fromLiteral("((.+)->)?([0-9]+):([0-9]+)") + + override fun parse(parameter: Parameter<Parcel, Void>, 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, Int>(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, Int>(PlayerProfile::class.java, ProfileKind) { + + override fun parse(parameter: Parameter<PlayerProfile, Int>, 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<PlayerProfile, Int>, + sender: CommandSender, + location: Location?, + buffer: ArgumentBuffer + ): MutableList<String> { + 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, Int>(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, Int>(ParcelTarget::class.java, TargetKind) {
-
- override fun parse(parameter: Parameter<ParcelTarget, Int>, 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<PlayerProfile, Int> {
- 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<ParcelTarget, Int>, 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, Int>(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, Int>(ParcelTarget::class.java, TargetKind) { + + override fun parse( + parameter: Parameter<ParcelTarget, Int>, + 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<PlayerProfile, Int> { + 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<ParcelTarget, Int>, + 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 <T> generate(
- chunkX: Int,
- chunkZ: Int,
- floor: T, wall:
- T, pathMain: T,
- pathAlt: T,
- fill: T,
- setter: (Int, Int, Int, T) -> Unit
- ) {
-
- val floorHeight = o.floorHeight
- val parcelSize = o.parcelSize
- val sectionSize = sectionSize
- val pathOffset = pathOffset
- val makePathMain = makePathMain
- val makePathAlt = makePathAlt
-
- // parcel bottom x and z
- // umod is unsigned %: the result is always >= 0
- val pbx = ((chunkX shl 4) - o.offsetX) umod sectionSize
- val pbz = ((chunkZ shl 4) - o.offsetZ) umod sectionSize
-
- var curHeight: Int
- var x: Int
- var z: Int
- for (cx in 0..15) {
- for (cz in 0..15) {
- x = (pbx + cx) % sectionSize - pathOffset
- z = (pbz + cz) % sectionSize - pathOffset
- curHeight = floorHeight
-
- val type = when {
- (x in 0 until parcelSize && z in 0 until parcelSize) -> floor
- (x in -1..parcelSize && z in -1..parcelSize) -> {
- curHeight++
- wall
- }
- (makePathAlt && x in -2 until parcelSize + 2 && z in -2 until parcelSize + 2) -> pathAlt
- (makePathMain) -> pathMain
- else -> {
- curHeight++
- wall
- }
- }
-
- for (y in 0 until curHeight) {
- setter(cx, y, cz, fill)
- }
- setter(cx, curHeight, cz, type)
- }
- }
- }
-
- override fun generateChunkData(world: World?, random: Random?, chunkX: Int, chunkZ: Int, biome: BiomeGrid?): ChunkData {
- val out = Bukkit.createChunkData(world)
- generate(chunkX, chunkZ, o.floorType, o.wallType, o.pathMainType, o.pathAltType, o.fillType) { x, y, z, type ->
- out.setBlock(x, y, z, type)
- }
- return out
- }
-
- override fun populate(world: World?, random: Random?, chunk: Chunk?) {
- // 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<ParcelLocator, ParcelBlockManager> {
- val impl = ParcelLocatorAndBlockManagerImpl(parcelProvider, container, coroutineScope, jobDispatcher)
- return impl to impl
- }
-
- private inline fun <T> 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<Vec2i> {
- /*
- * 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 <T> generate( + chunkX: Int, + chunkZ: Int, + floor: T, wall: + T, pathMain: T, + pathAlt: T, + fill: T, + setter: (Int, Int, Int, T) -> Unit + ) { + + val floorHeight = o.floorHeight + val parcelSize = o.parcelSize + val sectionSize = sectionSize + val pathOffset = pathOffset + val makePathMain = makePathMain + val makePathAlt = makePathAlt + + // parcel bottom x and z + // umod is unsigned %: the result is always >= 0 + val pbx = ((chunkX shl 4) - o.offsetX) umod sectionSize + val pbz = ((chunkZ shl 4) - o.offsetZ) umod sectionSize + + var curHeight: Int + var x: Int + var z: Int + for (cx in 0..15) { + for (cz in 0..15) { + x = (pbx + cx) % sectionSize - pathOffset + z = (pbz + cz) % sectionSize - pathOffset + curHeight = floorHeight + + val type = when { + (x in 0 until parcelSize && z in 0 until parcelSize) -> floor + (x in -1..parcelSize && z in -1..parcelSize) -> { + curHeight++ + wall + } + (makePathAlt && x in -2 until parcelSize + 2 && z in -2 until parcelSize + 2) -> pathAlt + (makePathMain) -> pathMain + else -> { + curHeight++ + wall + } + } + + for (y in 0 until curHeight) { + setter(cx, y, cz, fill) + } + setter(cx, curHeight, cz, type) + } + } + } + + override fun generateChunkData(world: World?, random: Random?, chunkX: Int, chunkZ: Int, biome: BiomeGrid?): ChunkData { + val out = Bukkit.createChunkData(world) + generate(chunkX, chunkZ, o.floorType, o.wallType, o.pathMainType, o.pathAltType, o.fillType) { x, y, z, type -> + out.setBlock(x, y, z, type) + } + return out + } + + override fun populate(world: World?, random: Random?, chunk: Chunk?) { + // 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<ParcelLocator, ParcelBlockManager> { + val impl = ParcelLocatorAndBlockManagerImpl(parcelProvider, container, coroutineScope, jobDispatcher) + return impl to impl + } + + private inline fun <T> 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<Vec2i> { + /* + * 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<String, ParcelWorld> get() = _worlds
- private val _worlds: MutableMap<String, ParcelWorld> = hashMapOf()
- private val _generators: MutableMap<String, ParcelGenerator> = 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<ParcelWorld>()
- 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<ParcelWorld> = 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<String, ParcelWorld> get() = _worlds + private val _worlds: MutableMap<String, ParcelWorld> = hashMapOf() + private val _generators: MutableMap<String, ParcelGenerator> = 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<ParcelWorld>() + 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<ParcelWorld> = 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<Entity>) + + 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 <T> launchFuture(future: Backing.() -> T): Deferred<T> = async { transaction { future() } }
-
- override fun <T> openChannel(future: Backing.(SendChannel<T>) -> Unit): ReceiveChannel<T> {
- val channel = LinkedListChannel<T>()
- launchJob { future(channel) }
- return channel
- }
-
- override fun <T> openChannelForWriting(action: Backing.(T) -> Unit): SendChannel<T> {
- val channel = ArrayChannel<T>(poolSize * 2)
-
- repeat(poolSize.clampMax(3)) {
- launch {
- try {
- while (true) {
- action(channel.receive())
- }
- } catch (ex: Exception) {
- // channel closed
- }
- }
- }
-
- return channel
- }
-
- private fun <T> 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<DataPair>, parcels: Sequence<ParcelId>) {
- for (parcel in parcels) {
- val data = readParcelData(parcel)
- channel.offer(parcel to data)
- }
- channel.close()
- }
-
- override fun transmitAllParcelData(channel: SendChannel<DataPair>) {
- 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<ParcelId> {
- 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<PrivilegePair<PlayerProfile>>) {
- 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 <T> launchFuture(future: Backing.() -> T): Deferred<T> = async { transaction { future() } } + + override fun <T> openChannel(future: Backing.(SendChannel<T>) -> Unit): ReceiveChannel<T> { + val channel = LinkedListChannel<T>() + launchJob { future(channel) } + return channel + } + + override fun <T> openChannelForWriting(action: Backing.(T) -> Unit): SendChannel<T> { + val channel = ArrayChannel<T>(poolSize * 2) + + repeat(poolSize.clampMax(3)) { + launch { + try { + while (true) { + action(channel.receive()) + } + } catch (ex: Exception) { + // channel closed + } + } + } + + return channel + } + + private fun <T> 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<DataPair>, parcels: Sequence<ParcelId>) { + for (parcel in parcels) { + val data = readParcelData(parcel) + channel.offer(parcel to data) + } + channel.close() + } + + override fun transmitAllParcelData(channel: SendChannel<DataPair>) { + 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<ParcelId> { + 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<PrivilegePair<PlayerProfile>>) { + 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 |