Archived
0

Add WorktimeLimiter API, basic /parcel clear functionality

This commit is contained in:
Dico Karssiens
2018-07-30 04:49:43 +01:00
parent 33bb19a547
commit dee994b992
15 changed files with 415 additions and 104 deletions

View File

@@ -2,7 +2,7 @@ package io.dico.parcels2
import com.fasterxml.jackson.annotation.JsonIgnore
import io.dico.parcels2.blockvisitor.BlockVisitorOptions
import io.dico.parcels2.blockvisitor.TickWorktimeOptions
import io.dico.parcels2.storage.Storage
import io.dico.parcels2.storage.StorageFactory
import io.dico.parcels2.storage.yamlObjectMapper
@@ -19,6 +19,7 @@ class Options {
var worlds: Map<String, WorldOptions> = HashMap()
private set
var storage: StorageOptions = StorageOptions("postgresql", DataConnectionOptions())
var tickWorktime: TickWorktimeOptions = TickWorktimeOptions(30, 1)
fun addWorld(name: String, options: WorldOptions) = (worlds as MutableMap).put(name, options)
@@ -40,8 +41,7 @@ data class WorldOptions(var gameMode: GameMode? = GameMode.CREATIVE,
var blockMobSpawning: Boolean = true,
var blockedItems: Set<Material> = EnumSet.of(Material.FLINT_AND_STEEL, Material.SNOWBALL),
var axisLimit: Int = 10,
var generator: GeneratorOptions = DefaultGeneratorOptions(),
var blockVisitor: BlockVisitorOptions = BlockVisitorOptions()) {
var generator: GeneratorOptions = DefaultGeneratorOptions()) {
}
@@ -73,7 +73,8 @@ class StorageOptions(val dialect: String,
val options: Any) {
@get:JsonIgnore
val factory = StorageFactory.getFactory(dialect) ?: throw IllegalArgumentException("Invalid storage dialect: $dialect")
val factory = StorageFactory.getFactory(dialect)
?: throw IllegalArgumentException("Invalid storage dialect: $dialect")
fun newStorageInstance(): Storage = factory.newStorageInstance(dialect, options)

View File

@@ -20,7 +20,7 @@ import kotlin.coroutines.experimental.buildSequence
import kotlin.reflect.jvm.javaMethod
import kotlin.reflect.jvm.kotlinFunction
class Worlds(private val plugin: ParcelsPlugin) {
class Worlds(val plugin: ParcelsPlugin) {
val worlds: Map<String, ParcelWorld> get() = _worlds
private val _worlds: MutableMap<String, ParcelWorld> = HashMap()

View File

@@ -3,6 +3,8 @@ 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.blockvisitor.TickWorktimeLimiter
import io.dico.parcels2.blockvisitor.WorktimeLimiter
import io.dico.parcels2.command.getParcelCommands
import io.dico.parcels2.listener.ParcelEntityTracker
import io.dico.parcels2.listener.ParcelListeners
@@ -29,8 +31,7 @@ class ParcelsPlugin : JavaPlugin() {
lateinit var entityTracker: ParcelEntityTracker; private set
private var listeners: ParcelListeners? = null
private var cmdDispatcher: ICommandDispatcher? = null
val mainThreadDispatcher = Executor { server.scheduler.runTask(this, it) }.asCoroutineDispatcher()
val worktimeLimiter: WorktimeLimiter by lazy { TickWorktimeLimiter(this, options) }
override fun onEnable() {
plogger.info("Debug enabled: ${plogger.isDebugEnabled}")

View File

@@ -1,9 +1,8 @@
package io.dico.parcels2
import io.dico.parcels2.util.Vec2i
import io.dico.parcels2.util.clamp
import io.dico.parcels2.util.even
import io.dico.parcels2.util.umod
import io.dico.parcels2.blockvisitor.JobData
import io.dico.parcels2.blockvisitor.RegionTraversal
import io.dico.parcels2.util.*
import org.bukkit.*
import org.bukkit.Bukkit.createBlockData
import org.bukkit.block.Biome
@@ -51,6 +50,8 @@ abstract class ParcelGenerator : ChunkGenerator(), ParcelProvider {
abstract fun getBlocks(parcel: Parcel, yRange: IntRange = 0..255): Iterator<Block>
abstract fun clearParcel(parcel: Parcel): JobData
}
interface GeneratorFactory {
@@ -78,6 +79,9 @@ interface GeneratorFactory {
class DefaultParcelGenerator(val worlds: Worlds, val name: String, private val o: DefaultGeneratorOptions) : ParcelGenerator() {
override val world: ParcelWorld by lazy { worlds.getWorld(name)!! }
override val factory = Factory
val worktimeLimiter = worlds.plugin.worktimeLimiter
val maxHeight by lazy { world.world.maxHeight }
val airType = worlds.plugin.server.createBlockData(Material.AIR)
companion object Factory : GeneratorFactory {
override val name get() = "default"
@@ -260,4 +264,28 @@ class DefaultParcelGenerator(val worlds: Worlds, val name: String, private val o
}
}
override fun clearParcel(parcel: Parcel) = worktimeLimiter.submit {
val bottom = getBottomCoord(parcel)
val region = Region(Vec3i(bottom.x, 0, bottom.z), Vec3i(o.parcelSize, maxHeight + 1, o.parcelSize))
val blocks = RegionTraversal.XZY.regionTraverser(region)
val blockCount = region.blockCount.toDouble()
val world = 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)
}
}
}

View File

@@ -0,0 +1,67 @@
package io.dico.parcels2.blockvisitor
import org.bukkit.Material
import org.bukkit.Material.*
import java.util.*
val attachables: Set<Material> = EnumSet.of(
ACACIA_DOOR,
ACTIVATOR_RAIL,
BIRCH_DOOR,
BROWN_MUSHROOM,
CACTUS,
CAKE,
WHITE_CARPET, ORANGE_CARPET, MAGENTA_CARPET, LIGHT_BLUE_CARPET, YELLOW_CARPET, LIME_CARPET, PINK_CARPET, GRAY_CARPET, LIGHT_GRAY_CARPET, CYAN_CARPET, PURPLE_CARPET, BLUE_CARPET, BROWN_CARPET, GREEN_CARPET, RED_CARPET, BLACK_CARPET,
CARROT,
COCOA,
WHEAT,
DARK_OAK_DOOR,
DEAD_BUSH,
DETECTOR_RAIL,
REPEATER,
TALL_GRASS, TALL_SEAGRASS,
DRAGON_EGG,
FIRE,
FLOWER_POT,
OAK_PRESSURE_PLATE, BIRCH_PRESSURE_PLATE, SPRUCE_PRESSURE_PLATE, JUNGLE_PRESSURE_PLATE, ACACIA_PRESSURE_PLATE, DARK_OAK_PRESSURE_PLATE,
STONE_PRESSURE_PLATE, LIGHT_WEIGHTED_PRESSURE_PLATE, HEAVY_WEIGHTED_PRESSURE_PLATE,
IRON_DOOR,
OAK_DOOR, BIRCH_DOOR, SPRUCE_DOOR, JUNGLE_DOOR, ACACIA_DOOR, DARK_OAK_DOOR,
OAK_BUTTON, BIRCH_BUTTON, SPRUCE_BUTTON, JUNGLE_BUTTON, ACACIA_BUTTON, DARK_OAK_BUTTON,
STONE_BUTTON,
OAK_TRAPDOOR, BIRCH_TRAPDOOR, SPRUCE_TRAPDOOR, JUNGLE_TRAPDOOR, ACACIA_TRAPDOOR, DARK_OAK_TRAPDOOR,
IRON_TRAPDOOR,
LADDER,
LEVER,
MELON_STEM,
NETHER_WART,
PISTON,
STICKY_PISTON,
NETHER_PORTAL,
POTATO,
POWERED_RAIL,
PUMPKIN_STEM,
RAIL,
COMPARATOR,
REDSTONE_TORCH,
REDSTONE_WIRE,
RED_MUSHROOM,
SUNFLOWER,
FLOWER_POT,
CHORUS_FLOWER,
OAK_SAPLING, BIRCH_SAPLING, SPRUCE_SAPLING, JUNGLE_SAPLING, ACACIA_SAPLING, DARK_OAK_SAPLING,
SIGN,
SNOW,
SPRUCE_DOOR,
STONE_BUTTON,
SUGAR_CANE,
TORCH,
TRIPWIRE,
TRIPWIRE_HOOK,
VINE,
WHITE_BANNER, ORANGE_BANNER, MAGENTA_BANNER, LIGHT_BLUE_BANNER, YELLOW_BANNER, LIME_BANNER, PINK_BANNER, GRAY_BANNER, LIGHT_GRAY_BANNER, CYAN_BANNER, PURPLE_BANNER, BLUE_BANNER, BROWN_BANNER, GREEN_BANNER, RED_BANNER, BLACK_BANNER,
WHITE_WALL_BANNER, ORANGE_WALL_BANNER, MAGENTA_WALL_BANNER, LIGHT_BLUE_WALL_BANNER, YELLOW_WALL_BANNER, LIME_WALL_BANNER, PINK_WALL_BANNER, GRAY_WALL_BANNER, LIGHT_GRAY_WALL_BANNER, CYAN_WALL_BANNER, PURPLE_WALL_BANNER, BLUE_WALL_BANNER, BROWN_WALL_BANNER, GREEN_WALL_BANNER, RED_WALL_BANNER, BLACK_WALL_BANNER,
WALL_SIGN,
LILY_PAD,
DANDELION
);

View File

@@ -1,5 +0,0 @@
package io.dico.parcels2.blockvisitor
import io.dico.dicore.task.IteratorTask
abstract class BlockVisitor<T>(iterator: Iterator<T>?) : IteratorTask<T>(iterator)

View File

@@ -1,52 +0,0 @@
package io.dico.parcels2.blockvisitor
import io.dico.parcels2.util.MutableVec3i
import io.dico.parcels2.util.Region
import kotlinx.coroutines.experimental.Deferred
import org.bukkit.block.Block
import org.bukkit.plugin.Plugin
import kotlin.coroutines.experimental.SequenceBuilder
import kotlin.coroutines.experimental.buildIterator
typealias BlockProcessor = (Block) -> Boolean
class BlockVisitorManager(val plugin: Plugin, var options: BlockVisitorOptions) {
fun doOperationSynchronously(region: Region, processor: BlockProcessor): Deferred<Unit> {
}
}
class RegionOperation(val region: Region, val processor: BlockProcessor) {
fun process(maxMillis: Int) {
}
}
enum class RegionTraversal(private val builder: suspend SequenceBuilder<MutableVec3i>.(Region) -> Unit) {
XZY({ region ->
val origin = region.origin
val result = MutableVec3i(origin.x, origin.y, origin.z)
val size = region.size
repeat(size.y) { y ->
repeat()
result.y++
}
})
;
fun regionTraverser(region: Region) = Iterable { buildIterator { builder(region) } }
}

View File

@@ -1,3 +0,0 @@
package io.dico.parcels2.blockvisitor
data class BlockVisitorOptions(var pauseTicks: Int = 1, var workMillis: Int = 30)

View File

@@ -0,0 +1,30 @@
package io.dico.parcels2.blockvisitor
import io.dico.parcels2.util.Region
import io.dico.parcels2.util.Vec3i
import kotlin.coroutines.experimental.SequenceBuilder
import kotlin.coroutines.experimental.buildIterator
enum class RegionTraversal(private val builder: suspend SequenceBuilder<Vec3i>.(Region) -> Unit) {
XZY({ region ->
val origin = region.origin
val size = region.size
repeat(size.y) { y ->
repeat(size.z) { z ->
repeat(size.x) { x ->
yield(origin.add(x, y, z))
}
}
}
}),
;
fun regionTraverser(region: Region) = Iterable { buildIterator { builder(region) } }
}

View File

@@ -1,13 +1,52 @@
package io.dico.parcels2.blockvisitor
import io.dico.parcels2.util.Region
import io.dico.parcels2.util.Vec3i
import io.dico.parcels2.util.get
import org.bukkit.World
import org.bukkit.block.data.BlockData
class Schematic(val origin: Vec3i, val size: Vec3i) {
private var data: Array<BlockData>? = null
class Schematic {
val size: Vec3i get() = _size!!
private var _size: Vec3i? = null
set(value) {
field?.let { throw IllegalStateException() }
field = value
}
private var _data: Array<BlockData?>? = null
//private var extra: Map<Vec3i, (Block) -> Unit>? = null
private var isLoaded = false; private set
fun getLoadTask(world: World, region: Region): TimeLimitedTask = {
val size = region.size.also { _size = it }
val data = arrayOfNulls<BlockData>(region.blockCount).also { _data = it }
//val extra = mutableMapOf<Vec3i, (Block) -> Unit>().also { extra = it }
val blocks = RegionTraversal.XZY.regionTraverser(region)
for ((index, vec) in blocks.withIndex()) {
markSuspensionPoint()
val block = world[vec]
if (block.y > 255) continue
val blockData = block.blockData
data[index] = blockData
}
isLoaded = true
}
}
fun getPasteTask(world: World, position: Vec3i): TimeLimitedTask = {
if (!isLoaded) throw IllegalStateException()
val region = Region(position, _size!!)
val blocks = RegionTraversal.XZY.regionTraverser(region)
val data = _data!!
for ((index, vec) in blocks.withIndex()) {
markSuspensionPoint()
val block = world[vec]
if (block.y > 255) continue
data[index]?.let { block.blockData = it }
}
}
}

View File

@@ -0,0 +1,207 @@
package io.dico.parcels2.blockvisitor
import io.dico.parcels2.Options
import kotlinx.coroutines.experimental.CoroutineStart
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.asCoroutineDispatcher
import kotlinx.coroutines.experimental.launch
import org.bukkit.plugin.Plugin
import org.bukkit.scheduler.BukkitTask
import java.util.*
import java.util.concurrent.Executor
import kotlin.coroutines.experimental.Continuation
import kotlin.coroutines.experimental.ContinuationInterceptor
import kotlin.coroutines.experimental.intrinsics.COROUTINE_SUSPENDED
import kotlin.coroutines.experimental.intrinsics.suspendCoroutineUninterceptedOrReturn
interface WorktimeLimiter {
/**
* Submit a task that should be run synchronously, but limited such that it does not stall the server
* a bunch
*/
fun submit(job: TimeLimitedTask): JobData
/**
* 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)
}
typealias TimeLimitedTask = suspend WorktimeLimiter.() -> Unit
interface JobData {
val job: Job?
val isComplete: Boolean
val progress: Double?
/**
* Calls the given [block] whenever the progress 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]
*/
fun onProgressUpdate(minDelay: Int, minInterval: Int, block: JobUpdateListener): JobData
val isUpdateBlockPresent: Boolean
/**
* Calls the given [block] when this job completes.
*/
fun onCompleted(block: JobUpdateListener): JobData
}
typealias JobUpdateListener = JobData.(Double) -> Unit
class JobDataImpl(val task: TimeLimitedTask) : JobData {
override var job: Job? = null
set(value) {
field?.let { throw IllegalStateException() }
field = value!!
value.invokeOnCompletion { onCompletedBlock?.invoke(this, 1.0) }
}
var next: Continuation<Unit>? = null
override var progress: Double? = null
set(value) {
field = value
doProgressUpdate()
}
private fun doProgressUpdate() {
val progressUpdate = progressUpdateBlock ?: return
val time = System.currentTimeMillis()
if (time > lastUpdateTime + progressUpdateInterval) {
progressUpdate(progress!!)
lastUpdateTime = time
}
}
override val isUpdateBlockPresent get() = progressUpdateBlock != null
private var progressUpdateBlock: JobUpdateListener? = null
private var progressUpdateInterval: Int = 0
private var lastUpdateTime: Long = 0L
override fun onProgressUpdate(minDelay: Int, minInterval: Int, block: JobUpdateListener): JobDataImpl {
progressUpdateBlock?.let { throw IllegalStateException() }
progressUpdateBlock = block
progressUpdateInterval = minInterval
lastUpdateTime = System.currentTimeMillis() + minDelay - minInterval
return this
}
override val isComplete get() = job?.isCompleted == true
private var onCompletedBlock: JobUpdateListener? = null
override fun onCompleted(block: JobUpdateListener): JobDataImpl {
onCompletedBlock?.let { throw IllegalStateException() }
onCompletedBlock = block
return this
}
}
/**
* An object that controls one or more jobs, ensuring that they don't stall the server too much.
* The amount of milliseconds that can accumulate each server tick is configurable
*/
class TickWorktimeLimiter(private val plugin: Plugin, private val optionsRoot: Options) : WorktimeLimiter {
// Coroutine dispatcher for jobs
private val dispatcher = Executor(Runnable::run).asCoroutineDispatcher()
// union of Continuation<Unit> and suspend WorktimeLimited.() -> Unit
private var jobs = LinkedList<JobDataImpl>()
// The currently registered bukkit scheduler task
private var task: BukkitTask? = null
// The data associated with the task that is currently being executed
private var curJobData: JobDataImpl? = null
// Used to keep track of when the current task should end
private var curJobEndTime = 0L
// Tick work time options
private inline val options get() = optionsRoot.tickWorktime
override fun submit(job: TimeLimitedTask): JobData {
val jobData = JobDataImpl(job)
jobs.addFirst(jobData)
if (task == null) task = plugin.server.scheduler.runTaskTimer(plugin, ::tickJobs, 0, options.tickInterval.toLong())
return jobData
}
override suspend fun markSuspensionPoint() {
if (System.currentTimeMillis() >= curJobEndTime)
suspendCoroutineUninterceptedOrReturn(::scheduleContinuation)
}
override fun setProgress(progress: Double) {
curJobData!!.progress = progress
}
private fun tickJobs() {
if (jobs.isEmpty()) return
val tickStartTime = System.currentTimeMillis()
val jobs = this.jobs; this.jobs = LinkedList()
var count = jobs.size
while (!jobs.isEmpty()) {
val job = jobs.poll()
val time = System.currentTimeMillis()
val timeElapsed = time - tickStartTime
val timeLeft = options.workTime - timeElapsed
if (timeLeft <= 0) {
this.jobs.addAll(0, jobs)
return
}
val timePerJob = (timeLeft + count - 1) / count
tickJob(job, time + timePerJob)
count--
}
if (jobs.isEmpty() && this.jobs.isEmpty()) {
task?.cancel()
task = null
}
}
@Suppress("UNCHECKED_CAST")
private fun tickJob(job: JobDataImpl, endTime: Long) {
curJobData = job
curJobEndTime = endTime
try {
val next = job.next
if (next == null) startJob(job)
else next.resume(Unit)
}
finally {
curJobData = null
curJobEndTime = 0L
}
}
private fun startJob(job: JobDataImpl) {
job.job = launch(context = dispatcher, start = CoroutineStart.UNDISPATCHED) { job.task(this@TickWorktimeLimiter) }
}
private fun scheduleContinuation(continuation: Continuation<Unit>): Any? {
curJobData!!.next = continuation
jobs.addLast(curJobData)
return COROUTINE_SUSPENDED
}
}
data class TickWorktimeOptions(var workTime: Int, var tickInterval: Int)
/**
* While the implementation of [kotlin.coroutines.experimental.intrinsics.intercepted] is intrinsic, it should look something like this
* We don't care for intercepting the coroutine as we want it to resume immediately when we call resume().
* Thus, above, we use an unintercepted suspension. It's not necessary as the dispatcher (or interceptor) also calls it synchronously, but whatever.
*/
private fun <T> Continuation<T>.interceptedImpl(): Continuation<T> {
return context[ContinuationInterceptor]?.interceptContinuation(this) ?: this
}

View File

@@ -1,15 +1,18 @@
package io.dico.parcels2.command
import io.dico.dicore.command.ExecutionContext
import io.dico.dicore.command.annotation.Cmd
import io.dico.dicore.command.annotation.Desc
import io.dico.dicore.command.annotation.RequireParameters
import io.dico.parcels2.ParcelOwner
import io.dico.parcels2.ParcelsPlugin
import io.dico.parcels2.blockvisitor.JobUpdateListener
import io.dico.parcels2.command.NamedParcelDefaultValue.FIRST_OWNED
import io.dico.parcels2.storage.getParcelBySerializedValue
import io.dico.parcels2.util.hasAdminManage
import io.dico.parcels2.util.hasParcelHomeOthers
import io.dico.parcels2.util.uuid
import kotlinx.coroutines.experimental.Job
import org.bukkit.entity.Player
//@Suppress("unused")
@@ -77,5 +80,13 @@ class CommandsGeneral(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) {
return "Enjoy your new parcel!"
}
@Cmd("clear")
@ParcelRequire(owner = true)
fun ParcelScope.cmdClear(player: Player, context: ExecutionContext) {
val onProgressUpdate: JobUpdateListener = { progress -> context.sendMessage("[Clearing] Progress: %.06f%%".format(progress * 100)) }
world.generator.clearParcel(parcel)
.onProgressUpdate(1000, 1500, onProgressUpdate)
.onCompleted(onProgressUpdate)
}
}

View File

@@ -5,30 +5,9 @@ import org.bukkit.Material.*
/*
colors:
WHITE_$,
ORANGE_$,
MAGENTA_$,
LIGHT_BLUE_$,
YELLOW_$,
LIME_$,
PINK_$,
GRAY_$,
LIGHT_GRAY_$,
CYAN_$,
PURPLE_$,
BLUE_$,
BROWN_$,
GREEN_$,
RED_$,
BLACK_$,
WHITE_$, ORANGE_$, MAGENTA_$, LIGHT_BLUE_$, YELLOW_$, LIME_$, PINK_$, GRAY_$, LIGHT_GRAY_$, CYAN_$, PURPLE_$, BLUE_$, BROWN_$, GREEN_$, RED_$, BLACK_$,
wood:
OAK_$,
BIRCH_$,
SPRUCE_$,
JUNGLE_$,
ACACIA_$,
DARK_OAK_$,
OAK_$, BIRCH_$, SPRUCE_$, JUNGLE_$, ACACIA_$, DARK_OAK_$,
*/
val Material.isBed get() = when(this) {

View File

@@ -1,3 +1,5 @@
package io.dico.parcels2.util
data class Region(val origin: Vec3i, val size: Vec3i)
data class Region(val origin: Vec3i, val size: Vec3i) {
val blockCount: Int get() = size.x * size.y * size.z
}

View File

@@ -1,13 +1,19 @@
package io.dico.parcels2.util
import org.bukkit.World
import org.bukkit.block.Block
data class Vec3i(
val x: Int,
val y: Int,
val z: Int
)
) {
operator fun plus(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)
fun add(ox: Int, oy: Int, oz: Int) = Vec3i(x + ox, y + oy, z + oz)
}
data class MutableVec3i(
var x: Int,
var y: Int,
var z: Int
)
@Suppress("NOTHING_TO_INLINE")
inline operator fun World.get(vec: Vec3i): Block = getBlockAt(vec.x, vec.y, vec.z)