diff options
62 files changed, 1544 insertions, 554 deletions
diff --git a/build.gradle.kts b/build.gradle.kts index fa4eb09..34a9737 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,12 +14,10 @@ version = "0.2" plugins { java - kotlin("jvm") version "1.2.51" + kotlin("jvm") version "1.3.0-rc-57" id("com.github.johnrengelman.plugin-shadow") version "2.0.3" } -kotlin.experimental.coroutines = ENABLE - allprojects { apply<JavaPlugin>() @@ -28,6 +26,8 @@ allprojects { maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots") maven("https://hub.spigotmc.org/nexus/content/repositories/sonatype-nexus-snapshots") maven("https://dl.bintray.com/kotlin/exposed") + maven("https://dl.bintray.com/kotlin/kotlin-eap") + maven("https://dl.bintray.com/kotlin/kotlinx/") } dependencies { @@ -51,7 +51,7 @@ project(":dicore3:dicore3-command") { dependencies { c.kotlinStd(kotlin("stdlib-jdk8")) c.kotlinStd(kotlin("reflect")) - c.kotlinStd(kotlinx("coroutines-core:0.24.0")) + c.kotlinStd(kotlinx("coroutines-core:0.26.1-eap13")) compile(project(":dicore3:dicore3-core")) compile("com.thoughtworks.paranamer:paranamer:2.8") @@ -59,17 +59,19 @@ project(":dicore3:dicore3-command") { } } - dependencies { compile(project(":dicore3:dicore3-core")) compile(project(":dicore3:dicore3-command")) c.kotlinStd(kotlin("stdlib-jdk8")) c.kotlinStd(kotlin("reflect")) - c.kotlinStd(kotlinx("coroutines-core:0.23.4")) - c.kotlinStd("org.jetbrains.kotlinx:atomicfu-common:0.11.0") + c.kotlinStd(kotlinx("coroutines-core:0.26.1-eap13")) + c.kotlinStd("org.jetbrains.kotlinx:atomicfu-common:0.11.7-rc-conf") + + // not on sk89q maven repo yet + compileClasspath(files("$rootDir/debug/plugins/worldedit-bukkit-7.0.0-beta-01.jar")) - compile("org.jetbrains.exposed:exposed:0.10.3") { isTransitive = false } + compile("org.jetbrains.exposed:exposed:0.10.5") { isTransitive = false } compile("joda-time:joda-time:2.10") compile("com.zaxxer:HikariCP:3.2.0") compile("ch.qos.logback:logback-classic:1.2.3") { isTransitive = false } @@ -131,6 +133,7 @@ tasks { } val createDebugServer by creating { + // todo val jarUrl = URL("https://yivesmirror.com/files/spigot/spigot-latest.jar") val serverJarFile = file("$serverDir/lib/spigot.jar") diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/ChildCommandAddress.java b/dicore3/command/src/main/java/io/dico/dicore/command/ChildCommandAddress.java index 7593492..73d82ca 100644 --- a/dicore3/command/src/main/java/io/dico/dicore/command/ChildCommandAddress.java +++ b/dicore3/command/src/main/java/io/dico/dicore/command/ChildCommandAddress.java @@ -1,5 +1,6 @@ package io.dico.dicore.command; +import io.dico.dicore.command.predef.DefaultGroupCommand; import io.dico.dicore.command.predef.HelpCommand; import java.util.*; @@ -23,7 +24,7 @@ public class ChildCommandAddress extends ModifiableCommandAddress { } public static ChildCommandAddress newPlaceHolderCommand(String name, String... aliases) { - ChildCommandAddress rv = new ChildCommandAddress(null, name, aliases); + ChildCommandAddress rv = new ChildCommandAddress(DefaultGroupCommand.getInstance(), name, aliases); HelpCommand.registerAsChild(rv); return rv; } diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/CommandBuilder.java b/dicore3/command/src/main/java/io/dico/dicore/command/CommandBuilder.java index 0ffc960..e72d478 100644 --- a/dicore3/command/src/main/java/io/dico/dicore/command/CommandBuilder.java +++ b/dicore3/command/src/main/java/io/dico/dicore/command/CommandBuilder.java @@ -210,9 +210,11 @@ public final class CommandBuilder { * @param shortDescription a short description * @param description the lines of a full description. * @return this + * @throws IllegalStateException if the current group has no command */ public CommandBuilder setGroupDescription(String shortDescription, String... description) { Command command = cur.getCommand(); + if (command == null) throw new IllegalStateException(); cur.setCommand(command .setShortDescription(shortDescription) .setDescription(description)); @@ -220,6 +222,28 @@ public final class CommandBuilder { } /** + * Add a context filter to the command of the current group + * @return this + * @throws IllegalStateException if the current group has no command + */ + public CommandBuilder addContextFilter(IContextFilter contextFilter) { + Command command = cur.getCommand(); + if (command == null) throw new IllegalStateException(); + cur.setCommand(command + .addContextFilter(contextFilter)); + return this; + } + + /** + * Add a required permission to the command of the current group + * @return this + * @throws IllegalStateException if the current group has no command + */ + public CommandBuilder addRequiredPermission(String permission) { + return addContextFilter(IContextFilter.permission(permission)); + } + + /** * Jump up a level in the address * * @return this diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/ExecutionContext.java b/dicore3/command/src/main/java/io/dico/dicore/command/ExecutionContext.java index 4c014fb..4450a92 100644 --- a/dicore3/command/src/main/java/io/dico/dicore/command/ExecutionContext.java +++ b/dicore3/command/src/main/java/io/dico/dicore/command/ExecutionContext.java @@ -200,6 +200,15 @@ public class ExecutionContext { return originalBuffer.getArrayFromIndex(cursorStart); } + /** + * The path used to access this address. + * + * @return the path used to access this address. + */ + public String[] getRoute() { + return Arrays.copyOf(originalBuffer.toArray(), address.getDepth()); + } + public Formatting getFormat(EMessageType type) { return address.getChatController().getChatFormatForType(type); } diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/ICommandReceiver.java b/dicore3/command/src/main/java/io/dico/dicore/command/ICommandReceiver.java index 88b0d50..6660bf8 100644 --- a/dicore3/command/src/main/java/io/dico/dicore/command/ICommandReceiver.java +++ b/dicore3/command/src/main/java/io/dico/dicore/command/ICommandReceiver.java @@ -12,6 +12,11 @@ public interface ICommandReceiver { Plugin getPlugin(); + // type is CoroutineContext, but we avoid referring to Kotlin runtime here + default Object getCoroutineContext() { + return null; + } + } } diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/ModifiableCommandAddress.java b/dicore3/command/src/main/java/io/dico/dicore/command/ModifiableCommandAddress.java index 698eee8..8c2ab67 100644 --- a/dicore3/command/src/main/java/io/dico/dicore/command/ModifiableCommandAddress.java +++ b/dicore3/command/src/main/java/io/dico/dicore/command/ModifiableCommandAddress.java @@ -2,6 +2,7 @@ package io.dico.dicore.command; import io.dico.dicore.command.chat.ChatControllers; import io.dico.dicore.command.chat.IChatController; +import io.dico.dicore.command.predef.DefaultGroupCommand; import io.dico.dicore.command.predef.HelpCommand; import io.dico.dicore.command.predef.PredefinedCommand; @@ -32,7 +33,7 @@ public abstract class ModifiableCommandAddress implements ICommandAddress { @Override public boolean hasUserDeclaredCommand() { Command command = getCommand(); - return command != null && !(command instanceof PredefinedCommand); + return command != null && !(command instanceof PredefinedCommand) && !(command instanceof DefaultGroupCommand); } @Override diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/RootCommandAddress.java b/dicore3/command/src/main/java/io/dico/dicore/command/RootCommandAddress.java index 91dcc5b..6d38174 100644 --- a/dicore3/command/src/main/java/io/dico/dicore/command/RootCommandAddress.java +++ b/dicore3/command/src/main/java/io/dico/dicore/command/RootCommandAddress.java @@ -1,6 +1,7 @@ package io.dico.dicore.command; import io.dico.dicore.command.parameter.ArgumentBuffer; +import io.dico.dicore.command.predef.DefaultGroupCommand; import io.dico.dicore.command.registration.BukkitCommand; import org.bukkit.Location; import org.bukkit.command.CommandSender; @@ -167,10 +168,10 @@ public class RootCommandAddress extends ModifiableCommandAddress implements ICom ModifiableCommandAddress targetAddress = getCommandTarget(sender, buffer); Command target = targetAddress.getCommand(); - if (target == null) { + if (target == null || target instanceof DefaultGroupCommand) { if (targetAddress.hasHelpCommand()) { target = targetAddress.getHelpCommand().getCommand(); - } else { + } else if (target == null){ return false; } } diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/annotation/RequirePermissions.java b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/RequirePermissions.java index 0fbe9a4..d2ba782 100644 --- a/dicore3/command/src/main/java/io/dico/dicore/command/annotation/RequirePermissions.java +++ b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/RequirePermissions.java @@ -7,6 +7,9 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * If this annotation is not present, inheriting permissions is default. + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RequirePermissions { diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/predef/DefaultGroupCommand.java b/dicore3/command/src/main/java/io/dico/dicore/command/predef/DefaultGroupCommand.java new file mode 100644 index 0000000..3d96a7d --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/predef/DefaultGroupCommand.java @@ -0,0 +1,24 @@ +package io.dico.dicore.command.predef; + +import io.dico.dicore.command.Command; +import io.dico.dicore.command.CommandException; +import io.dico.dicore.command.ExecutionContext; +import org.bukkit.command.CommandSender; + +public class DefaultGroupCommand extends Command { + private static final DefaultGroupCommand instance = new DefaultGroupCommand(); + + public static DefaultGroupCommand getInstance() { + return instance; + } + + private DefaultGroupCommand() { + + } + + @Override public String execute(CommandSender sender, ExecutionContext context) throws CommandException { + context.getAddress().getChatController().sendHelpMessage(sender, context, context.getAddress(), 1); + return null; + } + +} diff --git a/dicore3/command/src/main/kotlin/io/dico/dicore/command/registration/reflect/KotlinReflectiveRegistration.kt b/dicore3/command/src/main/kotlin/io/dico/dicore/command/registration/reflect/KotlinReflectiveRegistration.kt index 9d23ee6..c09088e 100644 --- a/dicore3/command/src/main/kotlin/io/dico/dicore/command/registration/reflect/KotlinReflectiveRegistration.kt +++ b/dicore3/command/src/main/kotlin/io/dico/dicore/command/registration/reflect/KotlinReflectiveRegistration.kt @@ -4,15 +4,15 @@ import io.dico.dicore.command.CommandException import io.dico.dicore.command.EMessageType import io.dico.dicore.command.ExecutionContext import io.dico.dicore.command.ICommandReceiver -import kotlinx.coroutines.experimental.CoroutineStart.UNDISPATCHED -import kotlinx.coroutines.experimental.Deferred -import kotlinx.coroutines.experimental.asCoroutineDispatcher -import kotlinx.coroutines.experimental.async +import kotlinx.coroutines.CoroutineStart.UNDISPATCHED +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async import java.lang.reflect.Method -import java.util.* import java.util.concurrent.CancellationException -import java.util.concurrent.Executor -import kotlin.coroutines.experimental.intrinsics.suspendCoroutineOrReturn +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.intrinsics.intercepted +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn import kotlin.reflect.jvm.kotlinFunction fun isSuspendFunction(method: Method): Boolean { @@ -20,16 +20,21 @@ fun isSuspendFunction(method: Method): Boolean { return func.isSuspend } -fun callAsCoroutine(command: ReflectiveCommand, - factory: ICommandReceiver.Factory, - context: ExecutionContext, - args: Array<Any?>): String? { - val dispatcher = Executor { task -> factory.plugin.server.scheduler.runTask(factory.plugin, task) }.asCoroutineDispatcher() +fun callAsCoroutine( + command: ReflectiveCommand, + factory: ICommandReceiver.Factory, + context: ExecutionContext, + args: Array<Any?> +): String? { // UNDISPATCHED causes the handler to run until the first suspension point on the current thread, // meaning command handlers that don't have suspension points will run completely synchronously. // Tasks that take time to compute should suspend the coroutine and resume on another thread. - val job = async(context = dispatcher, start = UNDISPATCHED) { command.method.invokeSuspend(command.instance, args) } + val job = GlobalScope.async(context = factory.coroutineContext as CoroutineContext, start = UNDISPATCHED) { + suspendCoroutineUninterceptedOrReturn<Any?> { cont -> + command.method.invoke(command.instance, *args, cont.intercepted()) + } + } if (job.isCompleted) { return job.getResult() @@ -48,12 +53,6 @@ fun callAsCoroutine(command: ReflectiveCommand, return null } -private suspend fun Method.invokeSuspend(instance: Any?, args: Array<Any?>): Any? { - return suspendCoroutineOrReturn { cont -> - invoke(instance, *args, cont) - } -} - @Throws(CommandException::class) private fun Deferred<Any?>.getResult(): String? { getCompletionExceptionOrNull()?.let { ex -> diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..29e08e8 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official
\ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 020ed4e..2977cab 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,9 @@ +pluginManagement.repositories { + maven("http://dl.bintray.com/kotlin/kotlin-eap") + mavenCentral() + maven("https://plugins.gradle.org/m2/") +} + rootProject.name = "parcels2" include("dicore3:core") diff --git a/src/main/kotlin/io/dico/parcels2/AddedData.kt b/src/main/kotlin/io/dico/parcels2/AddedData.kt index 9249c7f..a23b36e 100644 --- a/src/main/kotlin/io/dico/parcels2/AddedData.kt +++ b/src/main/kotlin/io/dico/parcels2/AddedData.kt @@ -1,7 +1,12 @@ package io.dico.parcels2 +import io.dico.parcels2.AddedStatus.* import org.bukkit.OfflinePlayer +enum class AddedStatus { + DEFAULT, ALLOWED, BANNED; +} + typealias StatusKey = PlayerProfile.Real typealias MutableAddedDataMap = MutableMap<StatusKey, AddedStatus> typealias AddedDataMap = Map<StatusKey, AddedStatus> @@ -11,20 +16,20 @@ fun MutableAddedDataMap(): MutableAddedDataMap = hashMapOf() interface AddedData { val addedMap: AddedDataMap - var addedStatusOfStar: AddedStatus + var statusOfStar: AddedStatus - fun getAddedStatus(key: StatusKey): AddedStatus - fun setAddedStatus(key: StatusKey, status: AddedStatus): Boolean + fun getStatus(key: StatusKey): AddedStatus + fun setStatus(key: StatusKey, status: AddedStatus): Boolean - fun compareAndSetAddedStatus(key: StatusKey, expect: AddedStatus, status: AddedStatus): Boolean = - (getAddedStatus(key) == expect).also { if (it) setAddedStatus(key, status) } + fun casStatus(key: StatusKey, expect: AddedStatus, status: AddedStatus): Boolean = + getStatus(key) == expect && setStatus(key, status) - fun isAllowed(key: StatusKey) = getAddedStatus(key) == AddedStatus.ALLOWED - fun allow(key: StatusKey) = setAddedStatus(key, AddedStatus.ALLOWED) - fun disallow(key: StatusKey) = compareAndSetAddedStatus(key, AddedStatus.ALLOWED, AddedStatus.DEFAULT) - fun isBanned(key: StatusKey) = getAddedStatus(key) == AddedStatus.BANNED - fun ban(key: StatusKey) = setAddedStatus(key, AddedStatus.BANNED) - fun unban(key: StatusKey) = compareAndSetAddedStatus(key, AddedStatus.BANNED, AddedStatus.DEFAULT) + fun isAllowed(key: StatusKey) = getStatus(key) == ALLOWED + fun allow(key: StatusKey) = setStatus(key, ALLOWED) + fun disallow(key: StatusKey) = casStatus(key, ALLOWED, DEFAULT) + fun isBanned(key: StatusKey) = getStatus(key) == BANNED + fun ban(key: StatusKey) = setStatus(key, BANNED) + fun unban(key: StatusKey) = casStatus(key, BANNED, DEFAULT) fun isAllowed(player: OfflinePlayer) = isAllowed(player.statusKey) fun allow(player: OfflinePlayer) = allow(player.statusKey) @@ -34,29 +39,20 @@ interface AddedData { fun unban(player: OfflinePlayer) = unban(player.statusKey) } -inline val OfflinePlayer.statusKey get() = PlayerProfile.nameless(this) +inline val OfflinePlayer.statusKey: StatusKey + get() = PlayerProfile.nameless(this) -open class AddedDataHolder(override var addedMap: MutableAddedDataMap = mutableMapOf()) : AddedData { - override var addedStatusOfStar: AddedStatus = AddedStatus.DEFAULT +open class AddedDataHolder(override var addedMap: MutableAddedDataMap = MutableAddedDataMap()) : AddedData { + override var statusOfStar: AddedStatus = DEFAULT - override fun getAddedStatus(key: StatusKey): AddedStatus = addedMap.getOrDefault(key, addedStatusOfStar) + override fun getStatus(key: StatusKey): AddedStatus = addedMap.getOrDefault(key, statusOfStar) - override fun setAddedStatus(key: StatusKey, status: AddedStatus): Boolean { - return if (status.isDefault) addedMap.remove(key) != null + override fun setStatus(key: StatusKey, status: AddedStatus): Boolean { + return if (status == DEFAULT) addedMap.remove(key) != null else addedMap.put(key, status) != status } } -enum class AddedStatus { - DEFAULT, - ALLOWED, - BANNED; - - inline val isDefault get() = this == DEFAULT - inline val isAllowed get() = this == ALLOWED - inline val isBanned get() = this == BANNED -} - interface GlobalAddedData : AddedData { val owner: PlayerProfile } diff --git a/src/main/kotlin/io/dico/parcels2/Interactable.kt b/src/main/kotlin/io/dico/parcels2/Interactable.kt new file mode 100644 index 0000000..60539aa --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/Interactable.kt @@ -0,0 +1,165 @@ +package io.dico.parcels2 + +import io.dico.parcels2.util.ext.getMaterialsWithWoodTypePrefix +import org.bukkit.Material +import java.lang.IllegalArgumentException +import java.util.EnumMap + +class Interactables +private constructor( + val id: Int, + val name: String, + val interactableByDefault: Boolean, + vararg val materials: Material +) { + + companion object { + val classesById: List<Interactables> + val classesByName: Map<String, Interactables> + val listedMaterials: Map<Material, Int> + + init { + val array = getClassesArray() + classesById = array.asList() + classesByName = mapOf(*array.map { it.name to it }.toTypedArray()) + listedMaterials = EnumMap(mapOf(*array.flatMap { clazz -> clazz.materials.map { it to clazz.id } }.toTypedArray())) + } + + operator fun get(material: Material): Interactables? { + val id = listedMaterials[material] ?: return null + return classesById[id] + } + + operator fun get(name: String): Interactables { + return classesByName[name] ?: throw IllegalArgumentException("Interactables class does not exist: $name") + } + + operator fun get(id: Int): Interactables { + return classesById[id] + } + + private fun getClassesArray() = run { + var id = 0 + @Suppress("UNUSED_CHANGED_VALUE") + arrayOf( + Interactables( + id++, "buttons", true, + Material.STONE_BUTTON, + *getMaterialsWithWoodTypePrefix("BUTTON") + ), + + Interactables( + id++, "levers", true, + Material.LEVER + ), + + Interactables( + id++, "pressure_plates", true, + Material.STONE_PRESSURE_PLATE, + *getMaterialsWithWoodTypePrefix("PRESSURE_PLATE"), + Material.HEAVY_WEIGHTED_PRESSURE_PLATE, + Material.LIGHT_WEIGHTED_PRESSURE_PLATE + ), + + Interactables( + id++, "redstone", false, + Material.COMPARATOR, + Material.REPEATER + ), + + Interactables( + id++, "containers", false, + Material.CHEST, + Material.TRAPPED_CHEST, + Material.DISPENSER, + Material.DROPPER, + Material.HOPPER, + Material.FURNACE + ), + + Interactables( + id++, "gates", true, + *getMaterialsWithWoodTypePrefix("DOOR"), + *getMaterialsWithWoodTypePrefix("TRAPDOOR"), + *getMaterialsWithWoodTypePrefix("FENCE_GATE") + ) + ) + } + + } + +} + +val Parcel?.effectiveInteractableConfig: InteractableConfiguration + get() = this?.interactableConfig ?: pathInteractableConfig + +val pathInteractableConfig: InteractableConfiguration = run { + val data = BitmaskInteractableConfiguration().apply { + Interactables.classesById.forEach { + setInteractable(it, false) + } + } + object : InteractableConfiguration by data { + override fun setInteractable(clazz: Interactables, interactable: Boolean) = + throw IllegalStateException("pathInteractableConfig is immutable") + + override fun clear() = + throw IllegalStateException("pathInteractableConfig is immutable") + + override fun copyFrom(other: InteractableConfiguration) = + throw IllegalStateException("pathInteractableConfig is immutable") + } +} + +interface InteractableConfiguration { + val interactableClasses: List<Interactables> get() = Interactables.classesById.filter { isInteractable(it) } + fun isInteractable(material: Material): Boolean + fun isInteractable(clazz: Interactables): Boolean + fun setInteractable(clazz: Interactables, interactable: Boolean): Boolean + fun clear(): Boolean + fun copyFrom(other: InteractableConfiguration) { + Interactables.classesById.forEach { setInteractable(it, other.isInteractable(it)) } + } + + operator fun invoke(material: Material) = isInteractable(material) + operator fun invoke(className: String) = isInteractable(Interactables[className]) +} + +fun InteractableConfiguration.isInteractable(clazz: Interactables?) = clazz != null && isInteractable(clazz) + +class BitmaskInteractableConfiguration : InteractableConfiguration { + val bitmaskArray = IntArray((Interactables.classesById.size + 31) / 32) + + private fun isBitSet(classId: Int): Boolean { + val idx = classId.ushr(5) + return idx < bitmaskArray.size && bitmaskArray[idx].and(0x1.shl(classId.and(0x1F))) != 0 + } + + override fun isInteractable(material: Material): Boolean { + val classId = Interactables.listedMaterials[material] ?: return false + return isBitSet(classId) != Interactables.classesById[classId].interactableByDefault + } + + override fun isInteractable(clazz: Interactables): Boolean { + return isBitSet(clazz.id) != clazz.interactableByDefault + } + + override fun setInteractable(clazz: Interactables, interactable: Boolean): Boolean { + val idx = clazz.id.ushr(5) + if (idx >= bitmaskArray.size) return false + val bit = 0x1.shl(clazz.id.and(0x1F)) + val oldBitmask = bitmaskArray[idx] + bitmaskArray[idx] = if (interactable != clazz.interactableByDefault) oldBitmask.or(bit) else oldBitmask.and(bit.inv()) + return bitmaskArray[idx] != oldBitmask + } + + override fun clear(): Boolean { + var change = false + for (i in bitmaskArray.indices) { + if (!change && bitmaskArray[i] != 0) change = true + bitmaskArray[i] = 0 + } + return change + } + +}
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/Parcel.kt b/src/main/kotlin/io/dico/parcels2/Parcel.kt index e7c5209..2c56627 100644 --- a/src/main/kotlin/io/dico/parcels2/Parcel.kt +++ b/src/main/kotlin/io/dico/parcels2/Parcel.kt @@ -1,7 +1,8 @@ package io.dico.parcels2 import io.dico.parcels2.util.Vec2i -import io.dico.parcels2.util.hasBuildAnywhere +import io.dico.parcels2.util.ext.hasBuildAnywhere +import org.bukkit.Location import org.bukkit.OfflinePlayer import org.bukkit.entity.Player import org.joda.time.DateTime @@ -30,31 +31,38 @@ interface Parcel : ParcelData { fun copyData(data: ParcelData) fun dispose() + + suspend fun withBlockVisitorPermit(block: suspend () -> Unit) + + val homeLocation: Location get() = world.blockManager.getHomeLocation(id) } interface ParcelData : AddedData { var owner: PlayerProfile? - val since: DateTime? + val lastClaimTime: DateTime? + var ownerSignOutdated: Boolean + var interactableConfig: InteractableConfiguration fun canBuild(player: OfflinePlayer, checkAdmin: Boolean = true, checkGlobal: Boolean = true): Boolean - var allowInteractInputs: Boolean - var allowInteractInventory: Boolean - fun isOwner(uuid: UUID): Boolean { return owner?.uuid == uuid } -} -class ParcelDataHolder(addedMap: MutableAddedDataMap = mutableMapOf()) : AddedDataHolder(addedMap), ParcelData { + fun isOwner(profile: PlayerProfile?): Boolean { + return owner == profile + } +} +class ParcelDataHolder(addedMap: MutableAddedDataMap = mutableMapOf()) + : ParcelData, AddedDataHolder(addedMap) { override var owner: PlayerProfile? = null - override var since: DateTime? = null + override var lastClaimTime: DateTime? = null + override var ownerSignOutdated = false override fun canBuild(player: OfflinePlayer, checkAdmin: Boolean, checkGlobal: Boolean) = isAllowed(player.statusKey) || owner.let { it != null && it.matches(player, allowNameMatch = false) } || (checkAdmin && player is Player && player.hasBuildAnywhere) - override var allowInteractInputs = true - override var allowInteractInventory = true + override var interactableConfig: InteractableConfiguration = BitmaskInteractableConfiguration() } diff --git a/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt b/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt index b9d2e3d..d69984b 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt @@ -1,9 +1,10 @@ package io.dico.parcels2 -import io.dico.parcels2.blockvisitor.RegionTraversal -import io.dico.parcels2.blockvisitor.Worker -import io.dico.parcels2.blockvisitor.WorktimeLimiter +import io.dico.parcels2.blockvisitor.* +import io.dico.parcels2.util.Region import io.dico.parcels2.util.Vec2i +import io.dico.parcels2.util.get +import kotlinx.coroutines.CoroutineScope import org.bukkit.Chunk import org.bukkit.Location import org.bukkit.World @@ -15,6 +16,8 @@ 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 @@ -31,31 +34,60 @@ abstract class ParcelGenerator : ChunkGenerator() { }) } - abstract fun makeParcelBlockManager(worktimeLimiter: WorktimeLimiter): ParcelBlockManager - - abstract fun makeParcelLocator(container: ParcelContainer): ParcelLocator + abstract fun makeParcelLocatorAndBlockManager(worldId: ParcelWorldId, + container: ParcelContainer, + coroutineScope: CoroutineScope, + worktimeLimiter: WorktimeLimiter): Pair<ParcelLocator, ParcelBlockManager> } -@Suppress("DeprecatedCallableAddReplaceWith") interface ParcelBlockManager { val world: World val worktimeLimiter: WorktimeLimiter + val parcelTraverser: RegionTraverser - fun getBottomBlock(parcel: ParcelId): Vec2i + // fun getBottomBlock(parcel: ParcelId): Vec2i fun getHomeLocation(parcel: ParcelId): Location - fun setOwnerBlock(parcel: ParcelId, owner: PlayerProfile?) + fun getRegion(parcel: ParcelId): Region - @Deprecated("") - fun getEntities(parcel: ParcelId): Collection<Entity> = TODO() + fun getEntities(parcel: ParcelId): Collection<Entity> - @Deprecated("") - fun getBlocks(parcel: ParcelId, yRange: IntRange = 0..255): Iterator<Block> = TODO() + fun setOwnerBlock(parcel: ParcelId, owner: PlayerProfile?) fun setBiome(parcel: ParcelId, biome: Biome): Worker fun clearParcel(parcel: ParcelId): Worker - fun doBlockOperation(parcel: ParcelId, direction: RegionTraversal = RegionTraversal.DOWNWARD, operation: (Block) -> Unit): Worker + fun submitBlockVisitor(parcelId: ParcelId, task: TimeLimitedTask): Worker + + /** + * Used to update owner blocks in the corner of the parcel + */ + fun getParcelsWithOwnerBlockIn(chunk: Chunk): Collection<Vec2i> +} + +inline fun ParcelBlockManager.doBlockOperation(parcel: ParcelId, + traverser: RegionTraverser, + crossinline operation: suspend WorkerScope.(Block) -> Unit) = submitBlockVisitor(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) + } + } diff --git a/src/main/kotlin/io/dico/parcels2/ParcelId.kt b/src/main/kotlin/io/dico/parcels2/ParcelId.kt index 951a172..72eceb9 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelId.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelId.kt @@ -12,7 +12,7 @@ import java.util.UUID interface ParcelWorldId { val name: String val uid: UUID? - fun equals(id: ParcelWorldId): Boolean = name == name || (uid != null && uid == id.uid) + fun equals(id: ParcelWorldId): Boolean = name == id.name || (uid != null && uid == id.uid) val bukkitWorld: World? get() = Bukkit.getWorld(name) ?: uid?.let { Bukkit.getWorld(it) } @@ -22,6 +22,8 @@ interface ParcelWorldId { } } +fun ParcelWorldId.toStringExt() = "ParcelWorld($name)" + /** * Used by storage backing options to encompass the location of a parcel * Does NOT support equality operator. @@ -31,6 +33,7 @@ interface ParcelId { val x: Int val z: Int val pos: Vec2i get() = Vec2i(x, z) + val idString get() = "$x,$z" fun equals(id: ParcelId): Boolean = x == id.x && z == id.z && worldId.equals(id.worldId) companion object { @@ -41,9 +44,15 @@ interface ParcelId { } } +fun ParcelId.toStringExt() = "Parcel(${worldId.name},$idString)" + private class ParcelWorldIdImpl(override val name: String, - override val uid: UUID?) : ParcelWorldId + override val uid: UUID?) : ParcelWorldId { + override fun toString() = toStringExt() +} private class ParcelIdImpl(override val worldId: ParcelWorldId, override val x: Int, - override val z: Int) : ParcelId + override val z: Int) : ParcelId { + override fun toString() = toStringExt() +} diff --git a/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt b/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt index 0dcdfbd..d70368f 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt @@ -3,11 +3,12 @@ package io.dico.parcels2 import io.dico.parcels2.options.RuntimeWorldOptions import io.dico.parcels2.storage.Storage import io.dico.parcels2.util.Vec2i -import io.dico.parcels2.util.floor +import io.dico.parcels2.util.ext.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.util.UUID interface ParcelProvider { @@ -58,7 +59,6 @@ interface ParcelLocator { 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 @@ -69,11 +69,13 @@ interface ParcelContainer { fun getParcelById(id: Vec2i): Parcel? = getParcelById(id.x, id.z) + fun getParcelById(id: ParcelId): Parcel? = getParcelById(id.x, id.z) + fun nextEmptyParcel(): Parcel? } -interface ParcelWorld : ParcelLocator, ParcelContainer, ParcelBlockManager { +interface ParcelWorld : ParcelLocator, ParcelContainer { val id: ParcelWorldId val name: String val uid: UUID? @@ -84,4 +86,7 @@ interface ParcelWorld : ParcelLocator, ParcelContainer, ParcelBlockManager { val locator: ParcelLocator val blockManager: ParcelBlockManager val globalAddedData: GlobalAddedDataManager + + val creationTime: DateTime? + } diff --git a/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt b/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt index e9c70f6..c863716 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt @@ -10,22 +10,27 @@ import io.dico.parcels2.defaultimpl.GlobalAddedDataManagerImpl import io.dico.parcels2.defaultimpl.ParcelProviderImpl import io.dico.parcels2.listener.ParcelEntityTracker import io.dico.parcels2.listener.ParcelListeners +import io.dico.parcels2.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.FunctionHelper -import io.dico.parcels2.util.tryCreate +import io.dico.parcels2.util.MainThreadDispatcher +import io.dico.parcels2.util.PluginScheduler +import io.dico.parcels2.util.ext.tryCreate +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() { +class ParcelsPlugin : JavaPlugin(), CoroutineScope, PluginScheduler { lateinit var optionsFile: File; private set lateinit var options: Options; private set lateinit var parcelProvider: ParcelProvider; private set @@ -37,7 +42,8 @@ class ParcelsPlugin : JavaPlugin() { private var listeners: ParcelListeners? = null private var cmdDispatcher: ICommandDispatcher? = null - val functionHelper: FunctionHelper = FunctionHelper(this) + override val coroutineContext: CoroutineContext = MainThreadDispatcher(this) + override val plugin: Plugin get() = this val worktimeLimiter: WorktimeLimiter by lazy { TickWorktimeLimiter(this, options.tickWorktime) } override fun onEnable() { @@ -49,7 +55,15 @@ class ParcelsPlugin : JavaPlugin() { } override fun onDisable() { + val hasWorkers = worktimeLimiter.workers.isNotEmpty() + if (hasWorkers) { + plogger.warn("Parcels is attempting to complete all ${worktimeLimiter.workers.size} remaining jobs before shutdown...") + } worktimeLimiter.completeAllTasks() + if (hasWorkers) { + plogger.info("Parcels has completed the remaining jobs.") + } + cmdDispatcher?.unregisterFromCommandMap() } @@ -124,11 +138,16 @@ class ParcelsPlugin : JavaPlugin() { private fun registerListeners() { if (listeners == null) { - listeners = ParcelListeners(parcelProvider, entityTracker) + listeners = ParcelListeners(parcelProvider, entityTracker, storage) registrator.registerListeners(listeners!!) + + val worldEditPlugin = server.pluginManager.getPlugin("WorldEdit") + if (worldEditPlugin != null) { + WorldEditListener.register(this, worldEditPlugin) + } } - functionHelper.scheduleRepeating(100, 5, entityTracker::tick) + scheduleRepeating(100, 5, entityTracker::tick) } }
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/PlayerProfile.kt b/src/main/kotlin/io/dico/parcels2/PlayerProfile.kt index c5403cd..0ef9f9d 100644 --- a/src/main/kotlin/io/dico/parcels2/PlayerProfile.kt +++ b/src/main/kotlin/io/dico/parcels2/PlayerProfile.kt @@ -3,12 +3,13 @@ package io.dico.parcels2 import io.dico.parcels2.storage.Storage -import io.dico.parcels2.util.getPlayerNameOrDefault -import io.dico.parcels2.util.isValid -import io.dico.parcels2.util.uuid -import kotlinx.coroutines.experimental.Deferred -import kotlinx.coroutines.experimental.Unconfined -import kotlinx.coroutines.experimental.async +import io.dico.parcels2.util.PLAYER_NAME_PLACEHOLDER +import io.dico.parcels2.util.getPlayerName +import io.dico.parcels2.util.ext.isValid +import io.dico.parcels2.util.ext.uuid +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Unconfined +import kotlinx.coroutines.async import org.bukkit.Bukkit import org.bukkit.OfflinePlayer import java.util.UUID @@ -16,8 +17,10 @@ import java.util.UUID interface PlayerProfile { val uuid: UUID? get() = null val name: String? + val nameOrBukkitName: String? val notNullName: String - val isStar: Boolean get() = false + val isStar: Boolean get() = this is Star + val exists: Boolean get() = this is RealImpl fun matches(player: OfflinePlayer, allowNameMatch: Boolean = false): Boolean @@ -74,8 +77,11 @@ interface PlayerProfile { 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() = name ?: getPlayerNameOrDefault(uuid) + get() = nameOrBukkitName ?: PLAYER_NAME_PLACEHOLDER val player: OfflinePlayer? get() = Bukkit.getOfflinePlayer(uuid).takeIf { it.isValid } val playerUnchecked: OfflinePlayer get() = Bukkit.getOfflinePlayer(uuid) @@ -110,8 +116,8 @@ interface PlayerProfile { object Star : BaseImpl(), Real { override val name: String = "*" + // hopefully nobody will have this random UUID :) override val uuid: UUID = UUID.fromString("7d09c4c6-117d-4f36-9778-c4d24618cee1") - override val isStar: Boolean get() = true override fun matches(player: OfflinePlayer, allowNameMatch: Boolean): Boolean { return true @@ -120,6 +126,7 @@ interface PlayerProfile { 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 @@ -142,7 +149,7 @@ interface PlayerProfile { } suspend fun tryResolveSuspendedly(storage: Storage): Real? { - return storage.getPlayerUuidForName(name).await()?.let { RealImpl(it, name) } + return storage.getPlayerUuidForName(name).await()?.let { resolve(it) } } fun resolve(uuid: UUID): Real { diff --git a/src/main/kotlin/io/dico/parcels2/blockvisitor/RegionTraversal.kt b/src/main/kotlin/io/dico/parcels2/blockvisitor/RegionTraversal.kt deleted file mode 100644 index 85fe946..0000000 --- a/src/main/kotlin/io/dico/parcels2/blockvisitor/RegionTraversal.kt +++ /dev/null @@ -1,43 +0,0 @@ -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) { - DOWNWARD({ 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, size.y - y - 1, z)) - } - } - } - - }), - - UPWARD({ 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) } } - -} - - - diff --git a/src/main/kotlin/io/dico/parcels2/blockvisitor/RegionTraverser.kt b/src/main/kotlin/io/dico/parcels2/blockvisitor/RegionTraverser.kt new file mode 100644 index 0000000..3899db9 --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/blockvisitor/RegionTraverser.kt @@ -0,0 +1,65 @@ +package io.dico.parcels2.blockvisitor + +import io.dico.parcels2.util.Region +import io.dico.parcels2.util.Vec3i + +abstract class RegionTraverser { + fun traverseRegion(region: Region): Iterable<Vec3i> = Iterable { iterator<Vec3i> { build(region) } } + + protected abstract suspend fun SequenceScope<Vec3i>.build(region: Region) + + companion object { + val upward = create { traverseUpward(it) } + val downward = create { traverseDownward(it) } + val forClearing get() = downward + val forFilling get() = upward + + inline fun create(crossinline builder: suspend SequenceScope<Vec3i>.(Region) -> Unit) = object : RegionTraverser() { + override suspend fun SequenceScope<Vec3i>.build(region: Region) { + builder(region) + } + } + + private suspend fun SequenceScope<Vec3i>.traverseDownward(region: 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, size.y - y - 1, z)) + } + } + } + } + + private suspend fun SequenceScope<Vec3i>.traverseUpward(region: 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, size.y - y - 1, z)) + } + } + } + } + + private fun slice(region: Region, atY: Int): Pair<Region, Region?> { + if (atY < region.size.y + 1) { + val first = Region(region.origin, region.size.withY(atY + 1)) + val second = Region(region.origin.withY(atY), region.size.addY(-atY-1)) + return first to second + } + return region to null + } + + fun upToAndDownUntil(y: Int) = create { region -> + val (bottom, top) = slice(region, y) + traverseUpward(bottom) + top?.let { traverseDownward(it) } + } + + } + + +} diff --git a/src/main/kotlin/io/dico/parcels2/blockvisitor/Schematic.kt b/src/main/kotlin/io/dico/parcels2/blockvisitor/Schematic.kt index c375e5a..7e109c8 100644 --- a/src/main/kotlin/io/dico/parcels2/blockvisitor/Schematic.kt +++ b/src/main/kotlin/io/dico/parcels2/blockvisitor/Schematic.kt @@ -23,7 +23,7 @@ class Schematic { 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.DOWNWARD.regionTraverser(region) + val blocks = RegionTraverser.downward.traverseRegion(region) for ((index, vec) in blocks.withIndex()) { markSuspensionPoint() @@ -39,7 +39,7 @@ class Schematic { fun getPasteTask(world: World, position: Vec3i): TimeLimitedTask = { if (!isLoaded) throw IllegalStateException() val region = Region(position, _size!!) - val blocks = RegionTraversal.DOWNWARD.regionTraverser(region) + val blocks = RegionTraverser.downward.traverseRegion(region) val data = _data!! for ((index, vec) in blocks.withIndex()) { diff --git a/src/main/kotlin/io/dico/parcels2/blockvisitor/WorktimeLimiter.kt b/src/main/kotlin/io/dico/parcels2/blockvisitor/WorktimeLimiter.kt index 33ac351..4cfd2a3 100644 --- a/src/main/kotlin/io/dico/parcels2/blockvisitor/WorktimeLimiter.kt +++ b/src/main/kotlin/io/dico/parcels2/blockvisitor/WorktimeLimiter.kt @@ -1,16 +1,20 @@ package io.dico.parcels2.blockvisitor import io.dico.parcels2.ParcelsPlugin -import io.dico.parcels2.util.FunctionHelper -import kotlinx.coroutines.experimental.CancellationException -import kotlinx.coroutines.experimental.Job +import io.dico.parcels2.logger +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart.LAZY +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import org.bukkit.scheduler.BukkitTask import java.lang.System.currentTimeMillis import java.util.LinkedList -import java.util.logging.Level -import kotlin.coroutines.experimental.Continuation -import kotlin.coroutines.experimental.intrinsics.COROUTINE_SUSPENDED -import kotlin.coroutines.experimental.intrinsics.suspendCoroutineUninterceptedOrReturn +import kotlin.coroutines.Continuation +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine typealias TimeLimitedTask = suspend WorkerScope.() -> Unit typealias WorkerUpdateLister = Worker.(Double, Long) -> Unit @@ -77,9 +81,14 @@ interface Worker : Timed { /** * Calls the given [block] when this worker completes, with the progress value 1.0. - * Repeated invocations of this method result in an [IllegalStateException] + * Multiple listeners may be registered to this function. */ fun onCompleted(block: WorkerUpdateLister): Worker + + /** + * Await completion of this worker + */ + suspend fun awaitCompletion() } interface WorkerScope : Timed { @@ -121,9 +130,14 @@ class TickWorktimeLimiter(private val plugin: ParcelsPlugin, var options: TickWo override val workers: List<Worker> = _workers override fun submit(task: TimeLimitedTask): Worker { - val worker: WorkerInternal = WorkerImpl(plugin.functionHelper, task) + val worker: WorkerInternal = WorkerImpl(plugin, task) + + if (bukkitTask == null) { + val completed = worker.resume(options.workTime.toLong()) + if (completed) return worker + bukkitTask = plugin.scheduleRepeating(0, options.tickInterval) { tickJobs() } + } _workers.addFirst(worker) - if (bukkitTask == null) bukkitTask = plugin.functionHelper.scheduleRepeating(0, options.tickInterval) { tickJobs() } return worker } @@ -165,8 +179,10 @@ class TickWorktimeLimiter(private val plugin: ParcelsPlugin, var options: TickWo } -private class WorkerImpl(val functionHelper: FunctionHelper, - val task: TimeLimitedTask) : WorkerInternal { +private class WorkerImpl( + val scope: CoroutineScope, + val task: TimeLimitedTask +) : WorkerInternal, CoroutineScope by scope { override var job: Job? = null; private set override val elapsedTime @@ -198,27 +214,44 @@ private class WorkerImpl(val functionHelper: FunctionHelper, // report any error that occurred completionException = exception?.also { if (it !is CancellationException) - functionHelper.plugin.logger.log(Level.SEVERE, "TimeLimitedTask for plugin ${functionHelper.plugin.name} generated an exception", it) + logger.error("TimeLimitedTask 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: WorkerUpdateLister): Worker { onProgressUpdate?.let { throw IllegalStateException() } + if (asCompletionListener) onCompleted(block) + if (isComplete) return this onProgressUpdate = block progressUpdateInterval = minInterval lastUpdateTime = System.currentTimeMillis() + minDelay - minInterval - if (asCompletionListener) onCompleted(block) + return this } override fun onCompleted(block: WorkerUpdateLister): Worker { - onCompleted?.let { throw IllegalStateException() } - onCompleted = block + if (isComplete) { + block(1.0, startTimeOrElapsedTime) + return this + } + + val cur = onCompleted + onCompleted = if (cur == null) { + block + } else { + fun Worker.(prog: Double, el: Long) { + cur(prog, el) + block(prog, el) + } + } return this } @@ -259,7 +292,7 @@ private class WorkerImpl(val functionHelper: FunctionHelper, } try { - val job = functionHelper.launchLazilyOnMainThread { task() } + val job = launch(start = LAZY) { task() } initJob(job = job) job.start() } catch (t: Throwable) { @@ -269,6 +302,18 @@ private class WorkerImpl(val functionHelper: FunctionHelper, return continuation == null } + override suspend fun awaitCompletion() { + if (isComplete) return + + // easy path - if the job was initialized already + job?.apply { join(); return } + + // other way. + return suspendCoroutine { cont -> + onCompleted { prog, el -> cont.resume(Unit) } + } + } + } /* diff --git a/src/main/kotlin/io/dico/parcels2/command/AbstractParcelCommands.kt b/src/main/kotlin/io/dico/parcels2/command/AbstractParcelCommands.kt index 31a551a..2339309 100644 --- a/src/main/kotlin/io/dico/parcels2/command/AbstractParcelCommands.kt +++ b/src/main/kotlin/io/dico/parcels2/command/AbstractParcelCommands.kt @@ -1,26 +1,22 @@ package io.dico.parcels2.command -import io.dico.dicore.command.CommandException -import io.dico.dicore.command.ExecutionContext -import io.dico.dicore.command.ICommandReceiver -import io.dico.parcels2.PlayerProfile +import io.dico.dicore.command.* import io.dico.parcels2.ParcelWorld import io.dico.parcels2.ParcelsPlugin -import io.dico.parcels2.util.hasAdminManage -import io.dico.parcels2.util.parcelLimit +import io.dico.parcels2.PlayerProfile +import io.dico.parcels2.util.ext.hasAdminManage +import io.dico.parcels2.util.ext.parcelLimit import org.bukkit.entity.Player import org.bukkit.plugin.Plugin import java.lang.reflect.Method abstract class AbstractParcelCommands(val plugin: ParcelsPlugin) : ICommandReceiver.Factory { - override fun getPlugin(): Plugin = plugin + override fun getReceiver(context: ExecutionContext, target: Method, cmdName: String): ICommandReceiver { return getParcelCommandReceiver(plugin.parcelProvider, context, target, cmdName) } - protected inline val worlds get() = plugin.parcelProvider - protected fun error(message: String): Nothing { throw CommandException(message) } @@ -40,5 +36,22 @@ abstract class AbstractParcelCommands(val plugin: ParcelsPlugin) : ICommandRecei } } + protected fun areYouSureMessage(context: ExecutionContext) = "Are you sure? You cannot undo this action!\n" + + "Run \"/${context.route.joinToString(" ")} -sure\" if you want to go through with this." + + protected fun ParcelScope.clearWithProgressUpdates(context: ExecutionContext, action: String) { + Validate.isTrue(!parcel.hasBlockVisitors, "A process is already running in this parcel") + world.blockManager.clearParcel(parcel.id) + .onProgressUpdate(1000, 1000) { progress, elapsedTime -> + val alt = context.getFormat(EMessageType.NUMBER) + val main = context.getFormat(EMessageType.INFORMATIVE) + context.sendMessage( + EMessageType.INFORMATIVE, false, "$action progress: $alt%.02f$main%%, $alt%.2f${main}s elapsed" + .format(progress * 100, elapsedTime / 1000.0) + ) + } + } + + override fun getCoroutineContext() = plugin.coroutineContext } diff --git a/src/main/kotlin/io/dico/parcels2/command/CommandsAddedStatusLocal.kt b/src/main/kotlin/io/dico/parcels2/command/CommandsAddedStatusLocal.kt index 69da341..223e504 100644 --- a/src/main/kotlin/io/dico/parcels2/command/CommandsAddedStatusLocal.kt +++ b/src/main/kotlin/io/dico/parcels2/command/CommandsAddedStatusLocal.kt @@ -4,7 +4,7 @@ import io.dico.dicore.command.Validate import io.dico.dicore.command.annotation.Cmd import io.dico.dicore.command.annotation.Desc import io.dico.parcels2.ParcelsPlugin -import io.dico.parcels2.util.hasAdminManage +import io.dico.parcels2.util.ext.hasAdminManage import org.bukkit.OfflinePlayer import org.bukkit.entity.Player diff --git a/src/main/kotlin/io/dico/parcels2/command/CommandsAdmin.kt b/src/main/kotlin/io/dico/parcels2/command/CommandsAdmin.kt index 2fe18ed..35ede71 100644 --- a/src/main/kotlin/io/dico/parcels2/command/CommandsAdmin.kt +++ b/src/main/kotlin/io/dico/parcels2/command/CommandsAdmin.kt @@ -1,7 +1,43 @@ package io.dico.parcels2.command +import io.dico.dicore.command.ExecutionContext +import io.dico.dicore.command.annotation.Cmd +import io.dico.dicore.command.annotation.Flag import io.dico.parcels2.ParcelsPlugin +import io.dico.parcels2.PlayerProfile class CommandsAdmin(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) { + @Cmd("setowner") + @ParcelRequire(admin = true) + fun ParcelScope.cmdSetowner(target: PlayerProfile): Any? { + parcel.owner = target + + val fakeString = if (target.isFake) " (fake)" else "" + return "${target.notNullName}$fakeString is the new owner of (${parcel.id.idString})" + } + + @Cmd("dispose") + @ParcelRequire(admin = true) + fun ParcelScope.cmdDispose(): Any? { + parcel.dispose() + return "Data of (${parcel.id.idString}) has been disposed" + } + + @Cmd("reset") + @ParcelRequire(admin = true) + fun ParcelScope.cmdReset(context: ExecutionContext, @Flag sure: Boolean): Any? { + if (!sure) return areYouSureMessage(context) + parcel.dispose() + clearWithProgressUpdates(context, "Reset") + return null + } + + @Cmd("swap") + fun ParcelScope.cmdSwap(context: ExecutionContext, @Flag sure: Boolean): Any? { + if (!sure) return areYouSureMessage(context) + TODO() + } + + }
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/command/CommandsDebug.kt b/src/main/kotlin/io/dico/parcels2/command/CommandsDebug.kt index 3c5ba41..3ae17f2 100644 --- a/src/main/kotlin/io/dico/parcels2/command/CommandsDebug.kt +++ b/src/main/kotlin/io/dico/parcels2/command/CommandsDebug.kt @@ -5,7 +5,8 @@ import io.dico.dicore.command.EMessageType import io.dico.dicore.command.ExecutionContext import io.dico.dicore.command.annotation.Cmd import io.dico.parcels2.ParcelsPlugin -import io.dico.parcels2.blockvisitor.RegionTraversal +import io.dico.parcels2.blockvisitor.RegionTraverser +import io.dico.parcels2.doBlockOperation import org.bukkit.Bukkit import org.bukkit.Material import org.bukkit.entity.Player @@ -43,7 +44,7 @@ class CommandsDebug(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) { ) val random = Random() - world.doBlockOperation(parcel.id, direction = RegionTraversal.UPWARD) { block -> + world.blockManager.doBlockOperation(parcel.id, traverser = RegionTraverser.upward) { block -> block.blockData = blockDatas[random.nextInt(7)] }.onProgressUpdate(1000, 1000) { progress, elapsedTime -> context.sendMessage(EMessageType.INFORMATIVE, "Mess progress: %.02f%%, %.2fs elapsed" diff --git a/src/main/kotlin/io/dico/parcels2/command/CommandsGeneral.kt b/src/main/kotlin/io/dico/parcels2/command/CommandsGeneral.kt index 7442019..eef07fd 100644 --- a/src/main/kotlin/io/dico/parcels2/command/CommandsGeneral.kt +++ b/src/main/kotlin/io/dico/parcels2/command/CommandsGeneral.kt @@ -1,24 +1,28 @@ package io.dico.parcels2.command -import io.dico.dicore.command.EMessageType import io.dico.dicore.command.ExecutionContext +import io.dico.dicore.command.Validate import io.dico.dicore.command.annotation.Cmd import io.dico.dicore.command.annotation.Desc import io.dico.dicore.command.annotation.Flag import io.dico.dicore.command.annotation.RequireParameters -import io.dico.parcels2.PlayerProfile import io.dico.parcels2.ParcelsPlugin -import io.dico.parcels2.util.hasAdminManage -import io.dico.parcels2.util.hasParcelHomeOthers -import io.dico.parcels2.util.uuid +import io.dico.parcels2.PlayerProfile +import io.dico.parcels2.command.ParcelTarget.Kind +import io.dico.parcels2.util.ext.hasAdminManage +import io.dico.parcels2.util.ext.hasParcelHomeOthers +import io.dico.parcels2.util.ext.uuid +import org.bukkit.block.Biome import org.bukkit.entity.Player class CommandsGeneral(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) { @Cmd("auto") - @Desc("Finds the unclaimed parcel nearest to origin,", + @Desc( + "Finds the unclaimed parcel nearest to origin,", "and gives it to you", - shortVersion = "sets you up with a fresh, unclaimed parcel") + shortVersion = "sets you up with a fresh, unclaimed parcel" + ) suspend fun WorldScope.cmdAuto(player: Player): Any? { checkConnected("be claimed") checkParcelLimit(player, world) @@ -26,45 +30,73 @@ class CommandsGeneral(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) { val parcel = world.nextEmptyParcel() ?: error("This world is full, please ask an admin to upsize it") parcel.owner = PlayerProfile(uuid = player.uuid) - player.teleport(parcel.world.getHomeLocation(parcel.id)) + player.teleport(parcel.homeLocation) return "Enjoy your new parcel!" } @Cmd("info", aliases = ["i"]) - @Desc("Displays general information", + @Desc( + "Displays general information", "about the parcel you're on", - shortVersion = "displays information about this parcel") + shortVersion = "displays information about this parcel" + ) fun ParcelScope.cmdInfo(player: Player) = parcel.infoString @Cmd("home", aliases = ["h"]) - @Desc("Teleports you to your parcels,", + @Desc( + "Teleports you to your parcels,", "unless another player was specified.", "You can specify an index number if you have", "more than one parcel", - shortVersion = "teleports you to parcels") + shortVersion = "teleports you to parcels" + ) @RequireParameters(0) - suspend fun cmdHome(player: Player, @ParcelTarget.Kind(ParcelTarget.OWNER_REAL) target: ParcelTarget): Any? { - val ownerTarget = target as ParcelTarget.ByOwner - if (!ownerTarget.owner.matches(player) && !player.hasParcelHomeOthers) { - error("You do not have permission to teleport to other people's parcels") - } + suspend fun cmdHome( + player: Player, + @Kind(ParcelTarget.OWNER_REAL) target: ParcelTarget + ): Any? { + return cmdGoto(player, target) + } - val ownedParcelsResult = plugin.storage.getOwnedParcels(ownerTarget.owner).await() + @Cmd("tp", aliases = ["teleport"]) + suspend fun cmdTp( + player: Player, + @Kind(ParcelTarget.ID) target: ParcelTarget + ): Any? { + return cmdGoto(player, target) + } - val ownedParcels = ownedParcelsResult - .map { worlds.getParcelById(it) } - .filter { it != null && ownerTarget.world == it.world } + @Cmd("goto") + suspend fun cmdGoto( + player: Player, + @Kind(ParcelTarget.ANY) target: ParcelTarget + ): Any? { + if (target is ParcelTarget.ByOwner) { + target.resolveOwner(plugin.storage) + if (!target.owner.matches(player) && !player.hasParcelHomeOthers) { + error("You do not have permission to teleport to other people's parcels") + } + } - val targetMatch = ownedParcels.getOrNull(target.index) + val match = target.getParcelSuspend(plugin.storage) ?: error("The specified parcel could not be matched") + player.teleport(match.homeLocation) + return null + } - player.teleport(targetMatch.world.getHomeLocation(targetMatch.id)) - return "" + @Cmd("goto_fake") + suspend fun cmdGotoFake( + player: Player, + @Kind(ParcelTarget.OWNER_FAKE) target: ParcelTarget + ): Any? { + return cmdGoto(player, target) } @Cmd("claim") - @Desc("If this parcel is unowned, makes you the owner", - shortVersion = "claims this parcel") + @Desc( + "If this parcel is unowned, makes you the owner", + shortVersion = "claims this parcel" + ) suspend fun ParcelScope.cmdClaim(player: Player): Any? { checkConnected("be claimed") parcel.owner.takeIf { !player.hasAdminManage }?.let { @@ -87,23 +119,18 @@ class CommandsGeneral(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) { @Cmd("clear") @ParcelRequire(owner = true) fun ParcelScope.cmdClear(context: ExecutionContext, @Flag sure: Boolean): Any? { - if (!sure) return "Are you sure? You cannot undo this action!\n" + - "Run \"/${context.rawInput} -sure\" if you want to go through with this." - - world.clearParcel(parcel.id) - .onProgressUpdate(1000, 1000) { progress, elapsedTime -> - val alt = context.getFormat(EMessageType.NUMBER) - val main = context.getFormat(EMessageType.INFORMATIVE) - context.sendMessage(EMessageType.INFORMATIVE, false, "Clear progress: $alt%.02f$main%%, $alt%.2f${main}s elapsed" - .format(progress * 100, elapsedTime / 1000.0)) - } + if (!sure) return areYouSureMessage(context) + clearWithProgressUpdates(context, "Clear") return null } - @Cmd("swap") - fun ParcelScope.cmdSwap(context: ExecutionContext, @Flag sure: Boolean): Any? { - TODO() + @Cmd("setbiome") + @ParcelRequire(owner = true) + fun ParcelScope.cmdSetbiome(context: ExecutionContext, biome: Biome): Any? { + Validate.isTrue(!parcel.hasBlockVisitors, "A process is already running in this parcel") + world.blockManager.setBiome(parcel.id, biome) + return "Biome has been set to $biome" } }
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/command/CommandsParcelOptions.kt b/src/main/kotlin/io/dico/parcels2/command/CommandsParcelOptions.kt index c77686e..b46e972 100644 --- a/src/main/kotlin/io/dico/parcels2/command/CommandsParcelOptions.kt +++ b/src/main/kotlin/io/dico/parcels2/command/CommandsParcelOptions.kt @@ -11,6 +11,7 @@ import org.bukkit.entity.Player import kotlin.reflect.KMutableProperty class CommandsParcelOptions(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) { + /* TODO options @Cmd("inputs") @Desc("Sets whether players who are not allowed to", "build here can use levers, buttons,", @@ -28,7 +29,7 @@ class CommandsParcelOptions(plugin: ParcelsPlugin) : AbstractParcelCommands(plug @RequireParameters(0) fun ParcelScope.cmdInventory(player: Player, enabled: Boolean?): Any? { return runOptionCommand(player, Parcel::allowInteractInventory, enabled, "interaction with inventories") - } + }*/ private inline val Boolean.enabledWord get() = if (this) "enabled" else "disabled" private fun ParcelScope.runOptionCommand(player: Player, diff --git a/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt b/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt index b633c3e..e2c7a1d 100644 --- a/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt +++ b/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt @@ -17,11 +17,12 @@ fun getParcelCommands(plugin: ParcelsPlugin): ICommandDispatcher { .addParameterType(true, ParcelTarget.PType(plugin.parcelProvider)) .group("parcel", "plot", "plots", "p") + .addRequiredPermission("parcels.command") .registerCommands(CommandsGeneral(plugin)) .registerCommands(CommandsAddedStatusLocal(plugin)) .group("option", "opt", "o") - //.apply { CommandsParcelOptions.setGroupDescription(this) } + .apply { CommandsParcelOptions.setGroupDescription(this) } .registerCommands(CommandsParcelOptions(plugin)) .parent() diff --git a/src/main/kotlin/io/dico/parcels2/command/ParcelCommandReceivers.kt b/src/main/kotlin/io/dico/parcels2/command/ParcelCommandReceivers.kt index eab02c4..488148d 100644 --- a/src/main/kotlin/io/dico/parcels2/command/ParcelCommandReceivers.kt +++ b/src/main/kotlin/io/dico/parcels2/command/ParcelCommandReceivers.kt @@ -7,8 +7,8 @@ import io.dico.dicore.command.Validate import io.dico.parcels2.Parcel import io.dico.parcels2.ParcelProvider import io.dico.parcels2.ParcelWorld -import io.dico.parcels2.util.hasAdminManage -import io.dico.parcels2.util.uuid +import io.dico.parcels2.util.ext.hasAdminManage +import io.dico.parcels2.util.ext.uuid import org.bukkit.entity.Player import java.lang.reflect.Method import kotlin.reflect.full.extensionReceiverParameter diff --git a/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt b/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt index cbab80a..abf7d40 100644 --- a/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt +++ b/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt @@ -5,20 +5,23 @@ 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.storage.Storage import io.dico.parcels2.util.Vec2i -import io.dico.parcels2.util.floor -import kotlinx.coroutines.experimental.Deferred +import io.dico.parcels2.util.ext.floor +import kotlinx.coroutines.CoroutineStart.* +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async import org.bukkit.command.CommandSender import org.bukkit.entity.Player -sealed class ParcelTarget(val world: ParcelWorld, val isDefault: Boolean) { +sealed class ParcelTarget(val world: ParcelWorld, val parsedKind: Int, val isDefault: Boolean) { - abstract suspend fun ParcelsPlugin.getParcelSuspend(): Parcel? + abstract suspend fun getParcelSuspend(storage: Storage): Parcel? - fun ParcelsPlugin.getParcelDeferred(): Deferred<Parcel?> = functionHelper.deferUndispatchedOnMainThread { getParcelSuspend() } + fun ParcelsPlugin.getParcelDeferred(): Deferred<Parcel?> = async(start = UNDISPATCHED) { getParcelSuspend(storage) } - class ByID(world: ParcelWorld, val id: Vec2i?, isDefault: Boolean) : ParcelTarget(world, isDefault) { - override suspend fun ParcelsPlugin.getParcelSuspend(): Parcel? = getParcel() + 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 } @@ -26,46 +29,40 @@ sealed class ParcelTarget(val world: ParcelWorld, val isDefault: Boolean) { class ByOwner(world: ParcelWorld, owner: PlayerProfile, val index: Int, + parsedKind: Int, isDefault: Boolean, - val onResolveFailure: (() -> Unit)? = null) : ParcelTarget(world, isDefault) { + val onResolveFailure: (() -> Unit)? = null) : ParcelTarget(world, parsedKind, isDefault) { init { if (index < 0) throw IllegalArgumentException("Invalid parcel home index: $index") } var owner = owner; private set - override suspend fun ParcelsPlugin.getParcelSuspend(): Parcel? { - onResolveFailure?.let { onFail -> - val owner = owner - if (owner is PlayerProfile.Unresolved) { - val new = owner.tryResolveSuspendedly(storage) - if (new == null) { - onFail() - return@let - } - this@ByOwner.owner = new - } + 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 - .map { parcelProvider.getParcelById(it) } - .filter { it != null && world == it.world && owner == it.owner } + .filter { it.worldId.equals(world.id) } + .map { world.getParcelById(it.x, it.z) } return ownedParcels.getOrNull(index) } } - annotation class Kind(val kind: Int) - - companion object Config : ParameterConfig<Kind, Int>(Kind::class.java) { - override fun toParameterInfo(annotation: Kind): Int { - return annotation.kind - } - + companion object { const val ID = 1 // ID const val OWNER_REAL = 2 // an owner backed by a UUID - const val OWNER_FAKE = 3 // an owner not 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 @@ -73,11 +70,18 @@ sealed class ParcelTarget(val world: ParcelWorld, val isDefault: Boolean) { const val DEFAULT_KIND = REAL - const val PREFER_OWNED_FOR_DEFAULT = 4 // if the kind can be ID and OWNER_REAL, prefer OWNER_REAL for default + 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 } - class PType(val parcelProvider: ParcelProvider) : ParameterType<ParcelTarget, Int>(ParcelTarget::class.java, ParcelTarget.Config) { + annotation class Kind(val kind: Int) + private object Config : ParameterConfig<Kind, Int>(Kind::class.java) { + override fun toParameterInfo(annotation: Kind): Int { + return annotation.kind + } + } + + class PType(val parcelProvider: ParcelProvider) : ParameterType<ParcelTarget, Int>(ParcelTarget::class.java, Config) { override fun parse(parameter: Parameter<ParcelTarget, Int>, sender: CommandSender, buffer: ArgumentBuffer): ParcelTarget { var input = buffer.next() @@ -95,12 +99,12 @@ sealed class ParcelTarget(val world: ParcelWorld, val isDefault: Boolean) { 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), false) + 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, false, onResolveFailure = { invalidInput(parameter, "The player $input does not exist") }) + return ByOwner(world, owner, index, kind, false, onResolveFailure = { invalidInput(parameter, "The player $input does not exist") }) } private fun getId(parameter: Parameter<*, *>, input: String): Vec2i { @@ -116,15 +120,18 @@ sealed class ParcelTarget(val world: ParcelWorld, val isDefault: Boolean) { private fun getHomeIndex(parameter: Parameter<*, *>, kind: Int, sender: CommandSender, input: String): Pair<PlayerProfile, Int> { val splitIdx = input.indexOf(':') val ownerString: String - val indexString: String + val index: Int? if (splitIdx == -1) { // just the index. - ownerString = "" - indexString = input + index = input.toIntOrNull() + ownerString = if (index == null) input else "" } else { ownerString = input.substring(0, splitIdx) - indexString = input.substring(splitIdx + 1) + + 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()) @@ -132,10 +139,7 @@ sealed class ParcelTarget(val world: ParcelWorld, val isDefault: Boolean) { else PlayerProfile.byName(ownerString, allowReal = kind and OWNER_REAL != 0, allowFake = kind and OWNER_FAKE != 0) - val index = if (indexString.isEmpty()) 0 else indexString.toIntOrNull() - ?: invalidInput(parameter, "The home index must be an integer, $indexString is not an integer") - - return owner to index + return owner to (index ?: 0) } private fun requirePlayer(sender: CommandSender, parameter: Parameter<*, *>, objName: String): Player { @@ -156,10 +160,10 @@ sealed class ParcelTarget(val world: ParcelWorld, val isDefault: Boolean) { 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, true) + return ByID(world, id, kind, true) } - return ByOwner(world, PlayerProfile(player), 0, true) + return ByOwner(world, PlayerProfile(player), 0, kind, true) } } diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelContainer.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelContainer.kt index 0597a9f..e6d29f4 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelContainer.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelContainer.kt @@ -2,9 +2,8 @@ package io.dico.parcels2.defaultimpl import io.dico.parcels2.Parcel import io.dico.parcels2.ParcelContainer +import io.dico.parcels2.ParcelId import io.dico.parcels2.ParcelWorld -import kotlin.coroutines.experimental.buildIterator -import kotlin.coroutines.experimental.buildSequence class DefaultParcelContainer(val world: ParcelWorld) : ParcelContainer { private var parcels: Array<Array<Parcel>> @@ -38,12 +37,20 @@ class DefaultParcelContainer(val world: ParcelWorld) : ParcelContainer { return parcels.getOrNull(x + world.options.axisLimit)?.getOrNull(z + world.options.axisLimit) } + override fun getParcelById(id: ParcelId): Parcel? { + if (!world.id.equals(id.worldId)) throw IllegalArgumentException() + return when (id) { + is Parcel -> id + else -> getParcelById(id.x, id.z) + } + } + override fun nextEmptyParcel(): Parcel? { return walkInCircle().find { it.owner == null } } private fun walkInCircle(): Iterable<Parcel> = Iterable { - buildIterator { + iterator { val center = world.options.axisLimit for (radius in 0..center) { var x = center - radius; @@ -56,7 +63,7 @@ class DefaultParcelContainer(val world: ParcelWorld) : ParcelContainer { } } - fun allParcels(): Sequence<Parcel> = buildSequence { + fun allParcels(): Sequence<Parcel> = sequence { for (array in parcels) { yieldAll(array.iterator()) } diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt index 7e75f52..08d5f2f 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt @@ -1,28 +1,42 @@ package io.dico.parcels2.defaultimpl import io.dico.parcels2.* -import io.dico.parcels2.blockvisitor.RegionTraversal +import io.dico.parcels2.blockvisitor.RegionTraverser +import io.dico.parcels2.blockvisitor.TimeLimitedTask import io.dico.parcels2.blockvisitor.Worker import io.dico.parcels2.blockvisitor.WorktimeLimiter import io.dico.parcels2.options.DefaultGeneratorOptions -import io.dico.parcels2.util.* +import io.dico.parcels2.util.Region +import io.dico.parcels2.util.Vec2i +import io.dico.parcels2.util.Vec3i +import io.dico.parcels2.util.ext.even +import io.dico.parcels2.util.ext.umod +import io.dico.parcels2.util.get +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart.UNDISPATCHED +import kotlinx.coroutines.launch import org.bukkit.* import org.bukkit.block.Biome -import org.bukkit.block.Block import org.bukkit.block.BlockFace import org.bukkit.block.Skull import org.bukkit.block.data.BlockData import org.bukkit.block.data.type.Sign import org.bukkit.block.data.type.Slab +import java.lang.IllegalArgumentException import java.util.Random private val airType = Bukkit.createBlockData(Material.AIR) -class DefaultParcelGenerator(val name: String, private val o: DefaultGeneratorOptions) : ParcelGenerator() { +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) _world = Bukkit.getWorld(name)!!.also { + if (_world == null) _world = Bukkit.getWorld(worldName)!!.also { maxHeight = it.maxHeight return it } @@ -35,13 +49,15 @@ class DefaultParcelGenerator(val name: String, private val o: DefaultGeneratorOp 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) { + 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 @@ -103,12 +119,13 @@ class DefaultParcelGenerator(val name: String, private val o: DefaultGeneratorOp return Location(world, o.offsetX + fix, o.floorHeight + 1.0, o.offsetZ + fix) } - override fun makeParcelBlockManager(worktimeLimiter: WorktimeLimiter): ParcelBlockManager { - return ParcelBlockManagerImpl(worktimeLimiter) - } - - override fun makeParcelLocator(container: ParcelContainer): ParcelLocator { - return ParcelLocatorImpl(container) + override fun makeParcelLocatorAndBlockManager( + worldId: ParcelWorldId, + container: ParcelContainer, + coroutineScope: CoroutineScope, + worktimeLimiter: WorktimeLimiter + ): Pair<ParcelLocator, ParcelBlockManager> { + return ParcelLocatorImpl(worldId, container) to ParcelBlockManagerImpl(worldId, coroutineScope, worktimeLimiter) } private inline fun <T> convertBlockLocationToId(x: Int, z: Int, mapper: (Int, Int) -> T): T? { @@ -124,22 +141,31 @@ class DefaultParcelGenerator(val name: String, private val o: DefaultGeneratorOp return null } - private inner class ParcelLocatorImpl(val container: ParcelContainer) : ParcelLocator { + private inner class ParcelLocatorImpl( + val worldId: ParcelWorldId, + val container: ParcelContainer + ) : ParcelLocator { override val world: World = this@DefaultParcelGenerator.world + 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(world.name, world.uid, idx, idz) } + return convertBlockLocationToId(x, z) { idx, idz -> ParcelId(worldId, idx, idz) } } } @Suppress("DEPRECATION") - private inner class ParcelBlockManagerImpl(override val worktimeLimiter: WorktimeLimiter) : ParcelBlockManager { + private inner class ParcelBlockManagerImpl( + val worldId: ParcelWorldId, + coroutineScope: CoroutineScope, + override val worktimeLimiter: WorktimeLimiter + ) : ParcelBlockManagerBase(), CoroutineScope by coroutineScope { override val world: World = this@DefaultParcelGenerator.world + override val parcelTraverser: RegionTraverser = RegionTraverser.upToAndDownUntil(o.floorHeight) - override fun getBottomBlock(parcel: ParcelId): Vec2i = Vec2i( + /*override*/ fun getBottomBlock(parcel: ParcelId): Vec2i = Vec2i( sectionSize * (parcel.x - 1) + pathOffset + o.offsetX, sectionSize * (parcel.z - 1) + pathOffset + o.offsetZ ) @@ -151,6 +177,11 @@ class DefaultParcelGenerator(val name: String, private val o: DefaultGeneratorOp return Location(world, x + 0.5, o.floorHeight + 1.0, z + 0.5, 0F, 0F) } + override fun getRegion(parcel: ParcelId): Region { + val bottom = getBottomBlock(parcel) + return Region(Vec3i(bottom.x, 0, bottom.z), Vec3i(o.parcelSize, maxHeight + 1, o.parcelSize)) + } + override fun setOwnerBlock(parcel: ParcelId, owner: PlayerProfile?) { val b = getBottomBlock(parcel) @@ -190,7 +221,28 @@ class DefaultParcelGenerator(val name: String, private val o: DefaultGeneratorOp } } - override fun setBiome(parcel: ParcelId, biome: Biome): Worker = worktimeLimiter.submit { + private fun getParcel(parcelId: ParcelId): Parcel? { + // todo dont rely on this cast + val world = worldId as? ParcelWorld ?: return null + return world.getParcelById(parcelId) + } + + override fun submitBlockVisitor(parcelId: ParcelId, task: TimeLimitedTask): Worker { + val parcel = getParcel(parcelId) ?: return worktimeLimiter.submit(task) + if (parcel.hasBlockVisitors) throw IllegalArgumentException("This parcel already has a block visitor") + + val worker = worktimeLimiter.submit(task) + + launch(start = UNDISPATCHED) { + parcel.withBlockVisitorPermit { + worker.awaitCompletion() + } + } + + return worker + } + + override fun setBiome(parcel: ParcelId, biome: Biome): Worker = submitBlockVisitor(parcel) { val world = world val b = getBottomBlock(parcel) val parcelSize = o.parcelSize @@ -202,10 +254,9 @@ class DefaultParcelGenerator(val name: String, private val o: DefaultGeneratorOp } } - override fun clearParcel(parcel: ParcelId): Worker = worktimeLimiter.submit { - val bottom = getBottomBlock(parcel) - val region = Region(Vec3i(bottom.x, 0, bottom.z), Vec3i(o.parcelSize, maxHeight + 1, o.parcelSize)) - val blocks = RegionTraversal.DOWNWARD.regionTraverser(region) + override fun clearParcel(parcel: ParcelId): Worker = submitBlockVisitor(parcel) { + val region = getRegion(parcel) + val blocks = parcelTraverser.traverseRegion(region) val blockCount = region.blockCount.toDouble() val world = world @@ -227,17 +278,78 @@ class DefaultParcelGenerator(val name: String, private val o: DefaultGeneratorOp } } - override fun doBlockOperation(parcel: ParcelId, direction: RegionTraversal, operation: (Block) -> Unit): Worker = worktimeLimiter.submit { - val bottom = getBottomBlock(parcel) - val region = Region(Vec3i(bottom.x, 0, bottom.z), Vec3i(o.parcelSize, maxHeight + 1, o.parcelSize)) - val blocks = direction.regionTraverser(region) - val blockCount = region.blockCount.toDouble() - val world = world + 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)) + } - for ((index, vec) in blocks.withIndex()) { - markSuspensionPoint() - operation(world[vec]) - setProgress((index + 1) / blockCount) + return (0 until matchesOnDimensionX).flatMap { idOffsetX -> + (0 until matchesOnDimensionZ).map { idOffsetZ -> Vec2i(firstIdX + idOffsetX, firstIdZ + idOffsetZ) } } } diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/GlobalAddedDataManagerImpl.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/GlobalAddedDataManagerImpl.kt index ba54375..ad7048c 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/GlobalAddedDataManagerImpl.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/GlobalAddedDataManagerImpl.kt @@ -3,7 +3,7 @@ package io.dico.parcels2.defaultimpl import io.dico.parcels2.* -import io.dico.parcels2.util.alsoIfTrue +import io.dico.parcels2.util.ext.alsoIfTrue import java.util.Collections class GlobalAddedDataManagerImpl(val plugin: ParcelsPlugin) : GlobalAddedDataManager { @@ -20,12 +20,12 @@ class GlobalAddedDataManagerImpl(val plugin: ParcelsPlugin) : GlobalAddedDataMan private inline var data get() = addedMap; set(value) = run { addedMap = value } private inline val isEmpty get() = data === emptyData - override fun setAddedStatus(key: StatusKey, status: AddedStatus): Boolean { + override fun setStatus(key: StatusKey, status: AddedStatus): Boolean { if (isEmpty) { if (status == AddedStatus.DEFAULT) return false data = mutableMapOf() } - return super.setAddedStatus(key, status).alsoIfTrue { + return super.setStatus(key, status).alsoIfTrue { plugin.storage.setGlobalAddedStatus(owner, key, status) } } diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelImpl.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelImpl.kt index 128705f..d392a8f 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelImpl.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelImpl.kt @@ -3,21 +3,22 @@ package io.dico.parcels2.defaultimpl import io.dico.dicore.Formatting import io.dico.parcels2.* import io.dico.parcels2.util.Vec2i -import io.dico.parcels2.util.alsoIfTrue -import io.dico.parcels2.util.getPlayerName +import io.dico.parcels2.util.ext.alsoIfTrue +import org.bukkit.Material import org.bukkit.OfflinePlayer import org.joda.time.DateTime -import java.util.UUID -import kotlin.reflect.KProperty +import java.util.concurrent.atomic.AtomicInteger -class ParcelImpl(override val world: ParcelWorld, - override val x: Int, - override val z: Int) : Parcel, ParcelId { +class ParcelImpl( + override val world: ParcelWorld, + override val x: Int, + override val z: Int +) : Parcel, ParcelId { override val id: ParcelId = this override val pos get() = Vec2i(x, z) override var data: ParcelDataHolder = ParcelDataHolder(); private set - override val infoString by ParcelInfoStringComputer - override var hasBlockVisitors: Boolean = false; private set + override val infoString get() = ParcelInfoStringComputer.getInfoString(this) + override val hasBlockVisitors get() = blockVisitors.get() > 0 override val worldId: ParcelWorldId get() = world.id override fun copyDataIgnoringDatabase(data: ParcelData) { @@ -35,7 +36,7 @@ class ParcelImpl(override val world: ParcelWorld, } override val addedMap: AddedDataMap get() = data.addedMap - override fun getAddedStatus(key: StatusKey) = data.getAddedStatus(key) + override fun getStatus(key: StatusKey) = data.getStatus(key) override fun isBanned(key: StatusKey) = data.isBanned(key) override fun isAllowed(key: StatusKey) = data.isAllowed(key) override fun canBuild(player: OfflinePlayer, checkAdmin: Boolean, checkGlobal: Boolean): Boolean { @@ -43,46 +44,77 @@ class ParcelImpl(override val world: ParcelWorld, || checkGlobal && world.globalAddedData[owner ?: return false].isAllowed(player) } - override var addedStatusOfStar: AddedStatus - get() = data.addedStatusOfStar - set(value) = run { setAddedStatus(PlayerProfile.Star, value) } + override var statusOfStar: AddedStatus + get() = data.statusOfStar + set(value) = run { setStatus(PlayerProfile.Star, value) } val globalAddedMap: AddedDataMap? get() = owner?.let { world.globalAddedData[it].addedMap } - override val since: DateTime? get() = data.since + override val lastClaimTime: DateTime? get() = data.lastClaimTime + + override var ownerSignOutdated: Boolean + get() = data.ownerSignOutdated + set(value) { + if (data.ownerSignOutdated != value) { + world.storage.setParcelOwnerSignOutdated(this, value) + data.ownerSignOutdated = value + } + } override var owner: PlayerProfile? get() = data.owner set(value) { if (data.owner != value) { world.storage.setParcelOwner(this, value) + world.blockManager.setOwnerBlock(this, value) data.owner = value } } - override fun setAddedStatus(key: StatusKey, status: AddedStatus): Boolean { - return data.setAddedStatus(key, status).alsoIfTrue { + override fun setStatus(key: StatusKey, status: AddedStatus): Boolean { + return data.setStatus(key, status).alsoIfTrue { world.storage.setParcelPlayerStatus(this, key, status) } } - override var allowInteractInputs: Boolean - get() = data.allowInteractInputs - set(value) { - if (data.allowInteractInputs == value) return - world.storage.setParcelAllowsInteractInputs(this, value) - data.allowInteractInputs = value + private var _interactableConfig: InteractableConfiguration? = null + override var interactableConfig: InteractableConfiguration + get() { + if (_interactableConfig == null) { + _interactableConfig = object : InteractableConfiguration { + override fun isInteractable(material: Material): Boolean = data.interactableConfig.isInteractable(material) + override fun isInteractable(clazz: Interactables): Boolean = data.interactableConfig.isInteractable(clazz) + + override fun setInteractable(clazz: Interactables, interactable: Boolean): Boolean = + data.interactableConfig.setInteractable(clazz, interactable).alsoIfTrue { + // TODO update storage + } + + override fun clear(): Boolean = + data.interactableConfig.clear().alsoIfTrue { + // TODO update storage + } + } + } + return _interactableConfig!! } - - override var allowInteractInventory: Boolean - get() = data.allowInteractInventory set(value) { - if (data.allowInteractInventory == value) return - world.storage.setParcelAllowsInteractInventory(this, value) - data.allowInteractInventory = value + data.interactableConfig.copyFrom(value) + // TODO update storage } + private var blockVisitors = AtomicInteger(0) + override suspend fun withBlockVisitorPermit(block: suspend () -> Unit) { + try { + blockVisitors.getAndIncrement() + block() + } finally { + blockVisitors.getAndDecrement() + } + } + + override fun toString() = toStringExt() } private object ParcelInfoStringComputer { @@ -121,13 +153,15 @@ private object ParcelInfoStringComputer { append(infoStringColor1) append(')') }) { - stringList.joinTo(this, + stringList.joinTo( + this, separator = infoStringColor1.toString() + ", " + infoStringColor2, - limit = 150) + limit = 150 + ) } } - operator fun getValue(parcel: Parcel, property: KProperty<*>): String = buildString { + fun getInfoString(parcel: Parcel): String = buildString { appendField("ID") { append(parcel.x) append(',') @@ -154,6 +188,7 @@ private object ParcelInfoStringComputer { append('\n') appendAddedList(local, global, AddedStatus.BANNED, "Banned") + /* TODO options if (!parcel.allowInteractInputs || !parcel.allowInteractInventory) { appendField("Options") { append("(") @@ -162,7 +197,7 @@ private object ParcelInfoStringComputer { appendField("inventory") { append(parcel.allowInteractInventory) } append(")") } - } + }*/ } }
\ 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 70d428d..8920e2e 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt @@ -1,8 +1,14 @@ package io.dico.parcels2.defaultimpl import io.dico.parcels2.* +import io.dico.parcels2.util.schedule +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.CoroutineStart.* +import kotlinx.coroutines.Unconfined +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 @@ -39,7 +45,7 @@ class ParcelProviderImpl(val plugin: ParcelsPlugin) : ParcelProvider { private fun loadWorlds0() { if (Bukkit.getWorlds().isEmpty()) { - plugin.functionHelper.schedule(::loadWorlds0) + plugin.schedule(::loadWorlds0) plugin.logger.warning("Scheduling to load worlds in the next tick, \nbecause no bukkit worlds are loaded yet") return } @@ -49,9 +55,24 @@ class ParcelProviderImpl(val plugin: ParcelsPlugin) : ParcelProvider { if (parcelWorld != null) continue val generator: ParcelGenerator = getWorldGenerator(worldName)!! - val bukkitWorld = Bukkit.getWorld(worldName) ?: WorldCreator(worldName).generator(generator).createWorld() + val worldExists = Bukkit.getWorld(worldName) != null + val bukkitWorld = + if (worldExists) Bukkit.getWorld(worldName)!! + else WorldCreator(worldName).generator(generator).createWorld().also { logger.info("Creating world $worldName") } + parcelWorld = ParcelWorldImpl(bukkitWorld, generator, worldOptions.runtime, plugin.storage, - plugin.globalAddedData, ::DefaultParcelContainer, plugin.worktimeLimiter) + plugin.globalAddedData, ::DefaultParcelContainer, plugin, plugin.worktimeLimiter) + + if (!worldExists) { + val time = DateTime.now() + plugin.storage.setWorldCreationTime(parcelWorld.id, time) + parcelWorld.creationTime = time + } else { + launch(context = Unconfined) { + parcelWorld.creationTime = plugin.storage.getWorldCreationTime(parcelWorld.id).await() ?: DateTime.now() + } + } + _worlds[worldName] = parcelWorld } @@ -59,7 +80,7 @@ class ParcelProviderImpl(val plugin: ParcelsPlugin) : ParcelProvider { } private fun loadStoredData() { - plugin.functionHelper.launchLazilyOnMainThread { + plugin.launch { val migration = plugin.options.migration if (migration.enabled) { migration.instance?.newInstance()?.apply { @@ -84,7 +105,7 @@ class ParcelProviderImpl(val plugin: ParcelsPlugin) : ParcelProvider { logger.info("Loading data completed") _dataIsLoaded = true - }.start() + } } /* diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelWorldImpl.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelWorldImpl.kt index 4a168f5..9f96a8c 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelWorldImpl.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelWorldImpl.kt @@ -6,22 +6,24 @@ import io.dico.parcels2.* import io.dico.parcels2.blockvisitor.WorktimeLimiter import io.dico.parcels2.options.RuntimeWorldOptions import io.dico.parcels2.storage.Storage +import kotlinx.coroutines.CoroutineScope import org.bukkit.World +import org.joda.time.DateTime import java.util.UUID -class ParcelWorldImpl private -constructor(override val world: World, - override val generator: ParcelGenerator, - override var options: RuntimeWorldOptions, - override val storage: Storage, - override val globalAddedData: GlobalAddedDataManager, - containerFactory: ParcelContainerFactory, - blockManager: ParcelBlockManager) +class ParcelWorldImpl(override val world: World, + override val generator: ParcelGenerator, + override var options: RuntimeWorldOptions, + override val storage: Storage, + override val globalAddedData: GlobalAddedDataManager, + containerFactory: ParcelContainerFactory, + coroutineScope: CoroutineScope, + worktimeLimiter: WorktimeLimiter) : ParcelWorld, ParcelWorldId, - ParcelContainer, // missing delegation - ParcelLocator, // missing delegation - ParcelBlockManager by blockManager { + ParcelContainer, /* missing delegation */ + ParcelLocator /* missing delegation */ { + override val id: ParcelWorldId get() = this override val uid: UUID? get() = world.uid @@ -33,10 +35,14 @@ constructor(override val world: World, override val name: String = world.name!! override val container: ParcelContainer = containerFactory(this) - override val locator: ParcelLocator = generator.makeParcelLocator(container) - override val blockManager: ParcelBlockManager = blockManager + override val locator: ParcelLocator + override val blockManager: ParcelBlockManager init { + val pair = generator.makeParcelLocatorAndBlockManager(id, container, coroutineScope, worktimeLimiter) + locator = pair.first + blockManager = pair.second + enforceOptions() } @@ -55,24 +61,13 @@ constructor(override val world: World, world.setGameRuleValue("doTileDrops", "${options.doTileDrops}") } + // Updated by ParcelProviderImpl + override var creationTime: DateTime? = null + /* Interface delegation needs to be implemented manually because JetBrains has yet to fix it. */ - companion object { - // Use this to be able to delegate blockManager and assign it to a property too, at least. - operator fun invoke(world: World, - generator: ParcelGenerator, - options: RuntimeWorldOptions, - storage: Storage, - globalAddedData: GlobalAddedDataManager, - containerFactory: ParcelContainerFactory, - worktimeLimiter: WorktimeLimiter): ParcelWorldImpl { - val blockManager = generator.makeParcelBlockManager(worktimeLimiter) - return ParcelWorldImpl(world, generator, options, storage, globalAddedData, containerFactory, blockManager) - } - } - // ParcelLocator interface override fun getParcelAt(x: Int, z: Int): Parcel? { return locator.getParcelAt(x, z) @@ -91,5 +86,5 @@ constructor(override val world: World, return container.nextEmptyParcel() } - + override fun toString() = toStringExt() } diff --git a/src/main/kotlin/io/dico/parcels2/listener/ParcelEntityTracker.kt b/src/main/kotlin/io/dico/parcels2/listener/ParcelEntityTracker.kt index edb48b5..eaacf93 100644 --- a/src/main/kotlin/io/dico/parcels2/listener/ParcelEntityTracker.kt +++ b/src/main/kotlin/io/dico/parcels2/listener/ParcelEntityTracker.kt @@ -2,8 +2,8 @@ package io.dico.parcels2.listener import io.dico.parcels2.Parcel import io.dico.parcels2.ParcelProvider -import io.dico.parcels2.util.editLoop -import io.dico.parcels2.util.isPresentAnd +import io.dico.parcels2.util.ext.editLoop +import io.dico.parcels2.util.ext.isPresentAnd import org.bukkit.entity.Entity class ParcelEntityTracker(val parcelProvider: ParcelProvider) { diff --git a/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt b/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt index fa5ccb4..e39583c 100644 --- a/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt +++ b/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt @@ -3,11 +3,9 @@ package io.dico.parcels2.listener import gnu.trove.TLongCollection import io.dico.dicore.ListenerMarker import io.dico.dicore.RegistratorListener -import io.dico.parcels2.Parcel -import io.dico.parcels2.ParcelProvider -import io.dico.parcels2.ParcelWorld -import io.dico.parcels2.statusKey -import io.dico.parcels2.util.* +import io.dico.parcels2.* +import io.dico.parcels2.storage.Storage +import io.dico.parcels2.util.ext.* import org.bukkit.Material.* import org.bukkit.World import org.bukkit.block.Biome @@ -16,6 +14,7 @@ import org.bukkit.block.data.Directional import org.bukkit.block.data.type.Bed import org.bukkit.entity.* import org.bukkit.entity.minecart.ExplosiveMinecart +import org.bukkit.event.EventPriority import org.bukkit.event.EventPriority.NORMAL import org.bukkit.event.block.* import org.bukkit.event.entity.* @@ -26,11 +25,17 @@ import org.bukkit.event.inventory.InventoryInteractEvent import org.bukkit.event.player.* import org.bukkit.event.vehicle.VehicleMoveEvent import org.bukkit.event.weather.WeatherChangeEvent +import org.bukkit.event.world.ChunkLoadEvent import org.bukkit.event.world.StructureGrowEvent import org.bukkit.inventory.InventoryHolder +import java.util.EnumSet @Suppress("NOTHING_TO_INLINE") -class ParcelListeners(val parcelProvider: ParcelProvider, val entityTracker: ParcelEntityTracker) { +class ParcelListeners( + val parcelProvider: ParcelProvider, + val entityTracker: ParcelEntityTracker, + val storage: Storage +) { private inline fun Parcel?.canBuildN(user: Player) = isPresentAnd { canBuild(user) } || user.hasBuildAnywhere /** @@ -54,7 +59,7 @@ class ParcelListeners(val parcelProvider: ParcelProvider, val entityTracker: Par val parcel = parcelProvider.getParcelAt(event.to) ?: return@l if (parcel.isBanned(user.statusKey)) { parcelProvider.getParcelAt(event.from)?.also { - user.teleport(it.world.getHomeLocation(it.id)) + user.teleport(it.homeLocation) user.sendParcelMessage(nopermit = true, message = "You are banned from this parcel") } ?: run { event.to = event.from } } @@ -165,6 +170,7 @@ class ParcelListeners(val parcelProvider: ParcelProvider, val entityTracker: Par if (ppa.isNullOr { hasBlockVisitors }) event.isCancelled = true } + private val bedTypes = EnumSet.copyOf(getMaterialsWithWoodTypePrefix("BED").toList()) /* * Prevents players from placing liquids, using flint and steel, changing redstone components, * using inputs (unless allowed by the plot), @@ -186,49 +192,33 @@ class ParcelListeners(val parcelProvider: ParcelProvider, val entityTracker: Par when (event.action) { Action.RIGHT_CLICK_BLOCK -> run { - when (clickedBlock.type) { - REPEATER, - COMPARATOR -> run { - if (!parcel.canBuildN(user)) { - event.isCancelled = true; return@l - } - } - LEVER, - STONE_BUTTON, - ANVIL, - TRAPPED_CHEST, - OAK_BUTTON, BIRCH_BUTTON, SPRUCE_BUTTON, JUNGLE_BUTTON, ACACIA_BUTTON, DARK_OAK_BUTTON, - OAK_FENCE_GATE, BIRCH_FENCE_GATE, SPRUCE_FENCE_GATE, JUNGLE_FENCE_GATE, ACACIA_FENCE_GATE, DARK_OAK_FENCE_GATE, - OAK_DOOR, BIRCH_DOOR, SPRUCE_DOOR, JUNGLE_DOOR, ACACIA_DOOR, DARK_OAK_DOOR, - OAK_TRAPDOOR, BIRCH_TRAPDOOR, SPRUCE_TRAPDOOR, JUNGLE_TRAPDOOR, ACACIA_TRAPDOOR, DARK_OAK_TRAPDOOR - -> run { - if (!user.hasBuildAnywhere && !parcel.isNullOr { canBuild(user) || allowInteractInputs }) { - user.sendParcelMessage(nopermit = true, message = "You cannot use inputs in this parcel") - event.isCancelled = true; return@l - } - } + val type = clickedBlock.type + val interactable = parcel.effectiveInteractableConfig.isInteractable(type) || parcel.isPresentAnd { canBuild(user) } + if (!interactable) { + val interactableClassName = Interactables[type]!!.name + user.sendParcelMessage(nopermit = true, message = "You cannot interact with $interactableClassName in this parcel") + event.isCancelled = true + return@l + } - WHITE_BED, ORANGE_BED, MAGENTA_BED, LIGHT_BLUE_BED, YELLOW_BED, LIME_BED, PINK_BED, GRAY_BED, LIGHT_GRAY_BED, CYAN_BED, PURPLE_BED, BLUE_BED, BROWN_BED, GREEN_BED, RED_BED, BLACK_BED - -> run { - if (world.options.disableExplosions) { - val bed = clickedBlock.blockData as Bed - val head = if (bed == Bed.Part.FOOT) clickedBlock.getRelative(bed.facing) else clickedBlock - when (head.biome) { - Biome.NETHER, Biome.THE_END -> run { - user.sendParcelMessage(nopermit = true, message = "You cannot use this bed because it would explode") - event.isCancelled = true; return@l - } + if (bedTypes.contains(type)) { + val bed = clickedBlock.blockData as Bed + val head = if (bed == Bed.Part.FOOT) clickedBlock.getRelative(bed.facing) else clickedBlock + when (head.biome) { + Biome.NETHER, Biome.THE_END -> { + if (world.options.disableExplosions || parcel.isNullOr { !canBuild(user) }) { + user.sendParcelMessage(nopermit = true, message = "You cannot use this bed because it would explode") + event.isCancelled = true; return@l } - } - } } + onPlayerInteractEvent_RightClick(event, world, parcel) } Action.RIGHT_CLICK_AIR -> onPlayerInteractEvent_RightClick(event, world, parcel) - Action.PHYSICAL -> if (!user.hasBuildAnywhere && !parcel.isPresentAnd { canBuild(user) || allowInteractInputs }) { + Action.PHYSICAL -> if (!user.hasBuildAnywhere && !parcel.isPresentAnd { canBuild(user) || interactableConfig("pressure_plates") }) { user.sendParcelMessage(nopermit = true, message = "You cannot use inputs in this parcel") event.isCancelled = true; return@l } @@ -317,7 +307,7 @@ class ParcelListeners(val parcelProvider: ParcelProvider, val entityTracker: Par @field:ListenerMarker(priority = NORMAL) val onPlayerDropItemEvent = RegistratorListener<PlayerDropItemEvent> l@{ event -> val (wo, ppa) = getWoAndPPa(event.itemDrop.location.block) ?: return@l - if (!ppa.canBuildN(event.player) && !ppa.isPresentAnd { allowInteractInventory }) event.isCancelled = true + if (!ppa.canBuildN(event.player) && !ppa.isPresentAnd { interactableConfig("containers") }) event.isCancelled = true } /* @@ -338,7 +328,7 @@ class ParcelListeners(val parcelProvider: ParcelProvider, val entityTracker: Par val user = event.whoClicked as? Player ?: return@l if ((event.inventory ?: return@l).holder === user) return@l // inventory null: hotbar val (wo, ppa) = getWoAndPPa(event.inventory.location.block) ?: return@l - if (ppa.isNullOr { !canBuild(user) && !allowInteractInventory }) { + if (ppa.isNullOr { !canBuild(user) && !interactableConfig("containers") }) { event.isCancelled = true } } @@ -380,10 +370,10 @@ class ParcelListeners(val parcelProvider: ParcelProvider, val entityTracker: Par val cancel: Boolean = when (event.newState.type) { - // prevent ice generation from Frost Walkers enchantment + // prevent ice generation from Frost Walkers enchantment FROSTED_ICE -> player != null && !ppa.canBuild(player) - // prevent snow generation from weather + // prevent snow generation from weather SNOW -> !hasEntity && wo.options.preventWeatherBlockChanges else -> false @@ -575,4 +565,26 @@ class ParcelListeners(val parcelProvider: ParcelProvider, val entityTracker: Par } } + /** + * Updates owner signs of parcels that get loaded if it is marked outdated + */ + @ListenerMarker(priority = EventPriority.NORMAL) + val onChunkLoadEvent = RegistratorListener<ChunkLoadEvent> l@{ event -> + val world = parcelProvider.getWorld(event.chunk.world) ?: return@l + val parcels = world.blockManager.getParcelsWithOwnerBlockIn(event.chunk) + if (parcels.isEmpty()) return@l + + parcels.forEach { id -> + val parcel = world.getParcelById(id)?.takeIf { it.ownerSignOutdated } ?: return@forEach + world.blockManager.setOwnerBlock(parcel.id, parcel.owner) + parcel.ownerSignOutdated = false + } + + } + + @ListenerMarker + val onPlayerJoinEvent = RegistratorListener<PlayerJoinEvent> l@{ event -> + storage.updatePlayerName(event.player.uuid, event.player.name) + } + }
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/listener/WorldEditListener.kt b/src/main/kotlin/io/dico/parcels2/listener/WorldEditListener.kt new file mode 100644 index 0000000..b68f7c2 --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/listener/WorldEditListener.kt @@ -0,0 +1,80 @@ +package io.dico.parcels2.listener + +import com.sk89q.worldedit.EditSession.Stage.BEFORE_REORDER +import com.sk89q.worldedit.Vector +import com.sk89q.worldedit.Vector2D +import com.sk89q.worldedit.WorldEdit +import com.sk89q.worldedit.WorldEditException +import com.sk89q.worldedit.bukkit.WorldEditPlugin +import com.sk89q.worldedit.event.extent.EditSessionEvent +import com.sk89q.worldedit.extent.AbstractDelegateExtent +import com.sk89q.worldedit.extent.Extent +import com.sk89q.worldedit.util.eventbus.EventHandler.Priority.VERY_EARLY +import com.sk89q.worldedit.util.eventbus.Subscribe +import com.sk89q.worldedit.world.biome.BaseBiome +import com.sk89q.worldedit.world.block.BaseBlock +import com.sk89q.worldedit.world.block.BlockStateHolder +import io.dico.parcels2.ParcelWorld +import io.dico.parcels2.ParcelsPlugin +import io.dico.parcels2.util.ext.hasBuildAnywhere +import io.dico.parcels2.util.ext.sendParcelMessage +import org.bukkit.entity.Player +import org.bukkit.plugin.Plugin + +class WorldEditListener(val parcels: ParcelsPlugin, val worldEdit: WorldEdit) { + + @Subscribe(priority = VERY_EARLY) + fun onEditSession(event: EditSessionEvent) { + val worldName = event.world?.name ?: return + val world = parcels.parcelProvider.getWorld(worldName) ?: return + if (event.stage == BEFORE_REORDER) return + + val actor = event.actor + if (actor == null || !actor.isPlayer) return + + val player = parcels.server.getPlayer(actor.uniqueId) + if (player.hasBuildAnywhere) return + + event.extent = ParcelsExtent(event.extent, world, player) + } + + private class ParcelsExtent(extent: Extent, + val world: ParcelWorld, + val player: Player) : AbstractDelegateExtent(extent) { + private var messageSent = false + + private fun canBuild(x: Int, z: Int): Boolean { + world.getParcelAt(x, z)?.let { parcel -> + if (parcel.canBuild(player, checkAdmin = false)) { + return true + } + } + + if (!messageSent) { + messageSent = true + player.sendParcelMessage(except = true, message = "You can't use WorldEdit there") + } + + return false + } + + override fun setBlock(location: Vector, block: BlockStateHolder<*>): Boolean { + return canBuild(location.blockX, location.blockZ) && super.setBlock(location, block) + } + + override fun setBiome(coord: Vector2D, biome: BaseBiome): Boolean { + return canBuild(coord.blockX, coord.blockZ) && super.setBiome(coord, biome) + } + + } + + companion object { + fun register(parcels: ParcelsPlugin, worldEditPlugin: Plugin) { + if (worldEditPlugin !is WorldEditPlugin) return + val worldEdit = worldEditPlugin.worldEdit + val listener = WorldEditListener(parcels, worldEdit) + worldEdit.eventBus.register(listener) + } + } + +}
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/storage/Backing.kt b/src/main/kotlin/io/dico/parcels2/storage/Backing.kt index b658d10..6c91714 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/Backing.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/Backing.kt @@ -1,11 +1,12 @@ package io.dico.parcels2.storage import io.dico.parcels2.* -import kotlinx.coroutines.experimental.CoroutineDispatcher -import kotlinx.coroutines.experimental.Deferred -import kotlinx.coroutines.experimental.Job -import kotlinx.coroutines.experimental.channels.ReceiveChannel -import kotlinx.coroutines.experimental.channels.SendChannel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel +import org.joda.time.DateTime import java.util.UUID interface Backing { @@ -30,8 +31,14 @@ interface Backing { fun shutdown() + fun getWorldCreationTime(worldId: ParcelWorldId): DateTime? + + fun setWorldCreationTime(worldId: ParcelWorldId, time: DateTime) + fun getPlayerUuidForName(name: String): UUID? + fun updatePlayerName(uuid: UUID, name: String) + fun transmitParcelData(channel: SendChannel<DataPair>, parcels: Sequence<ParcelId>) fun transmitAllParcelData(channel: SendChannel<DataPair>) @@ -47,12 +54,16 @@ interface Backing { fun setParcelOwner(parcel: ParcelId, owner: PlayerProfile?) + fun setParcelOwnerSignOutdated(parcel: ParcelId, outdated: Boolean) + fun setLocalPlayerStatus(parcel: ParcelId, player: PlayerProfile, status: AddedStatus) + fun setParcelOptionsInteractBitmask(parcel: ParcelId, bitmask: IntArray?) +/* fun setParcelAllowsInteractInventory(parcel: ParcelId, value: Boolean) fun setParcelAllowsInteractInputs(parcel: ParcelId, value: Boolean) - +*/ fun transmitAllGlobalAddedData(channel: SendChannel<AddedDataPair<PlayerProfile>>) diff --git a/src/main/kotlin/io/dico/parcels2/storage/DataConverters.kt b/src/main/kotlin/io/dico/parcels2/storage/DataConverters.kt new file mode 100644 index 0000000..80f41b2 --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/storage/DataConverters.kt @@ -0,0 +1,38 @@ +package io.dico.parcels2.storage + +import java.lang.IllegalArgumentException +import java.nio.ByteBuffer +import java.util.UUID + +/* For putting it into the database */ +fun UUID.toByteArray(): ByteArray = + ByteBuffer.allocate(16).apply { + putLong(mostSignificantBits) + putLong(leastSignificantBits) + }.array() + +/* For getting it out of the database */ +fun ByteArray.toUUID(): UUID = + ByteBuffer.wrap(this).run { + val mostSignificantBits = getLong() + val leastSignificantBits = getLong() + UUID(mostSignificantBits, leastSignificantBits) + } + +/* For putting it into the database */ +fun IntArray.toByteArray(): ByteArray = + ByteBuffer.allocate(size * Int.SIZE_BYTES).also { buf -> + buf.asIntBuffer().put(this) + }.array() + +/* For getting it out of the database */ +fun ByteArray.toIntArray(): IntArray { + if (this.size % Int.SIZE_BYTES != 0) + throw IllegalArgumentException("Size must be divisible by ${Int.SIZE_BYTES}") + + return ByteBuffer.wrap(this).run { + IntArray(remaining() / 4).also { array -> + asIntBuffer().get(array) + } + } +}
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/storage/Storage.kt b/src/main/kotlin/io/dico/parcels2/storage/Storage.kt index 2116b46..c9c0d4a 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/Storage.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/Storage.kt @@ -3,11 +3,12 @@ package io.dico.parcels2.storage import io.dico.parcels2.* -import kotlinx.coroutines.experimental.Deferred -import kotlinx.coroutines.experimental.Job -import kotlinx.coroutines.experimental.channels.ReceiveChannel -import kotlinx.coroutines.experimental.channels.SendChannel -import kotlinx.coroutines.experimental.launch +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.launch +import org.joda.time.DateTime import java.util.UUID typealias DataPair = Pair<ParcelId, ParcelData?> @@ -22,8 +23,14 @@ interface Storage { fun shutdown(): Job + fun getWorldCreationTime(worldId: ParcelWorldId): Deferred<DateTime?> + + fun setWorldCreationTime(worldId: ParcelWorldId, time: DateTime): Job + fun getPlayerUuidForName(name: String): Deferred<UUID?> + fun updatePlayerName(uuid: UUID, name: String): Job + fun readParcelData(parcel: ParcelId): Deferred<ParcelData?> fun transmitParcelData(parcels: Sequence<ParcelId>): ReceiveChannel<DataPair> @@ -39,11 +46,11 @@ interface Storage { fun setParcelOwner(parcel: ParcelId, owner: PlayerProfile?): Job - fun setParcelPlayerStatus(parcel: ParcelId, player: PlayerProfile, status: AddedStatus): Job + fun setParcelOwnerSignOutdated(parcel: ParcelId, outdated: Boolean): Job - fun setParcelAllowsInteractInventory(parcel: ParcelId, value: Boolean): Job + fun setParcelPlayerStatus(parcel: ParcelId, player: PlayerProfile, status: AddedStatus): Job - fun setParcelAllowsInteractInputs(parcel: ParcelId, value: Boolean): Job + fun setParcelOptionsInteractBitmask(parcel: ParcelId, bitmask: IntArray): Job fun transmitAllGlobalAddedData(): ReceiveChannel<AddedDataPair<PlayerProfile>> @@ -65,8 +72,14 @@ class BackedStorage internal constructor(val b: Backing) : Storage { override fun shutdown() = launch(b.dispatcher) { b.shutdown() } + override fun getWorldCreationTime(worldId: ParcelWorldId): Deferred<DateTime?> = b.launchFuture { b.getWorldCreationTime(worldId) } + + override fun setWorldCreationTime(worldId: ParcelWorldId, time: DateTime): Job = b.launchJob { b.setWorldCreationTime(worldId, time) } + override fun getPlayerUuidForName(name: String): Deferred<UUID?> = b.launchFuture { b.getPlayerUuidForName(name) } + override fun updatePlayerName(uuid: UUID, name: String): Job = b.launchJob { b.updatePlayerName(uuid, name) } + override fun readParcelData(parcel: ParcelId) = b.launchFuture { b.readParcelData(parcel) } override fun transmitParcelData(parcels: Sequence<ParcelId>) = b.openChannel<DataPair> { b.transmitParcelData(it, parcels) } @@ -81,11 +94,11 @@ class BackedStorage internal constructor(val b: Backing) : Storage { override fun setParcelOwner(parcel: ParcelId, owner: PlayerProfile?) = b.launchJob { b.setParcelOwner(parcel, owner) } - override fun setParcelPlayerStatus(parcel: ParcelId, player: PlayerProfile, status: AddedStatus) = b.launchJob { b.setLocalPlayerStatus(parcel, player, status) } + override fun setParcelOwnerSignOutdated(parcel: ParcelId, outdated: Boolean): Job = b.launchJob { b.setParcelOwnerSignOutdated(parcel, outdated) } - override fun setParcelAllowsInteractInventory(parcel: ParcelId, value: Boolean) = b.launchJob { b.setParcelAllowsInteractInventory(parcel, value) } + override fun setParcelPlayerStatus(parcel: ParcelId, player: PlayerProfile, status: AddedStatus) = b.launchJob { b.setLocalPlayerStatus(parcel, player, status) } - override fun setParcelAllowsInteractInputs(parcel: ParcelId, value: Boolean) = b.launchJob { b.setParcelAllowsInteractInputs(parcel, value) } + override fun setParcelOptionsInteractBitmask(parcel: ParcelId, bitmask: IntArray) = b.launchJob { b.setParcelOptionsInteractBitmask(parcel, bitmask) } override fun transmitAllGlobalAddedData(): ReceiveChannel<AddedDataPair<PlayerProfile>> = b.openChannel { b.transmitAllGlobalAddedData(it) } 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 8ea6653..b7f9f82 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedBacking.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedBacking.kt @@ -4,16 +4,15 @@ package io.dico.parcels2.storage.exposed import com.zaxxer.hikari.HikariDataSource import io.dico.parcels2.* -import io.dico.parcels2.storage.AddedDataPair -import io.dico.parcels2.storage.Backing -import io.dico.parcels2.storage.DataPair -import io.dico.parcels2.util.synchronized -import io.dico.parcels2.util.toUUID -import kotlinx.coroutines.experimental.* -import kotlinx.coroutines.experimental.channels.ArrayChannel -import kotlinx.coroutines.experimental.channels.LinkedListChannel -import kotlinx.coroutines.experimental.channels.ReceiveChannel -import kotlinx.coroutines.experimental.channels.SendChannel +import io.dico.parcels2.PlayerProfile.Star.name +import io.dico.parcels2.storage.* +import io.dico.parcels2.util.ext.clampMax +import io.dico.parcels2.util.ext.synchronized +import kotlinx.coroutines.* +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 @@ -92,8 +91,9 @@ class ExposedBacking(private val dataSourceFactory: () -> DataSource, val poolSi } } + @Suppress("RedundantObjectTypeCheck") private fun PlayerProfile.toOwnerProfile(): PlayerProfile { - if (this is PlayerProfile.Star) return PlayerProfile.Fake(PlayerProfile.Star.name) + if (this is PlayerProfile.Star) return PlayerProfile.Fake(name) return this } @@ -113,11 +113,28 @@ class ExposedBacking(private val dataSourceFactory: () -> DataSource, val poolSi 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) @@ -176,8 +193,9 @@ class ExposedBacking(private val dataSourceFactory: () -> DataSource, val poolSi AddedLocalT.setPlayerStatus(parcel, profile, status) } - setParcelAllowsInteractInputs(parcel, data.allowInteractInputs) - setParcelAllowsInteractInventory(parcel, data.allowInteractInventory) + val bitmaskArray = (data.interactableConfig as? BitmaskInteractableConfiguration ?: return).bitmaskArray + val isAllZero = bitmaskArray.fold(false) { cur, elem -> cur || elem != 0 } + setParcelOptionsInteractBitmask(parcel, if (isAllZero) null else bitmaskArray) } override fun setParcelOwner(parcel: ParcelId, owner: PlayerProfile?) { @@ -192,6 +210,14 @@ class ExposedBacking(private val dataSourceFactory: () -> DataSource, val poolSi 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 } } @@ -199,19 +225,19 @@ class ExposedBacking(private val dataSourceFactory: () -> DataSource, val poolSi AddedLocalT.setPlayerStatus(parcel, player.toRealProfile(), status) } - override fun setParcelAllowsInteractInventory(parcel: ParcelId, value: Boolean) { - val id = ParcelsT.getOrInitId(parcel) - ParcelOptionsT.upsert(ParcelOptionsT.parcel_id) { - it[ParcelOptionsT.parcel_id] = id - it[ParcelOptionsT.interact_inventory] = value + override fun setParcelOptionsInteractBitmask(parcel: ParcelId, bitmask: IntArray?) { + if (bitmask == null) { + val id = ParcelsT.getId(parcel) ?: return + ParcelOptionsT.deleteWhere { ParcelOptionsT.parcel_id eq id } + return } - } - override fun setParcelAllowsInteractInputs(parcel: ParcelId, value: Boolean) { + if (bitmask.size != 1) throw IllegalArgumentException() + val array = bitmask.toByteArray() val id = ParcelsT.getOrInitId(parcel) ParcelOptionsT.upsert(ParcelOptionsT.parcel_id) { - it[ParcelOptionsT.parcel_id] = id - it[ParcelOptionsT.interact_inputs] = value + it[parcel_id] = id + it[interact_bitmask] = array } } @@ -230,12 +256,14 @@ class ExposedBacking(private val dataSourceFactory: () -> DataSource, val poolSi private fun rowToParcelData(row: ResultRow) = ParcelDataHolder().apply { owner = row[ParcelsT.owner_id]?.let { ProfilesT.getItem(it) } - since = row[ParcelsT.claim_time] + lastClaimTime = row[ParcelsT.claim_time] + ownerSignOutdated = row[ParcelsT.sign_oudated] val id = row[ParcelsT.id] ParcelOptionsT.select { ParcelOptionsT.parcel_id eq id }.firstOrNull()?.let { optrow -> - allowInteractInputs = optrow[ParcelOptionsT.interact_inputs] - allowInteractInventory = optrow[ParcelOptionsT.interact_inventory] + 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)) } addedMap = AddedLocalT.readAddedData(id) diff --git a/src/main/kotlin/io/dico/parcels2/storage/exposed/IdTables.kt b/src/main/kotlin/io/dico/parcels2/storage/exposed/IdTables.kt index 0aa4ba0..696b84c 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/exposed/IdTables.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/exposed/IdTables.kt @@ -5,10 +5,11 @@ package io.dico.parcels2.storage.exposed import io.dico.parcels2.ParcelId import io.dico.parcels2.ParcelWorldId import io.dico.parcels2.PlayerProfile -import io.dico.parcels2.util.toByteArray -import io.dico.parcels2.util.toUUID +import io.dico.parcels2.storage.toByteArray +import io.dico.parcels2.storage.toUUID import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.statements.UpdateBuilder +import org.joda.time.DateTime import java.util.UUID abstract class IdTransactionsTable<TableT : IdTransactionsTable<TableT, QueryObj>, QueryObj>(tableName: String, columnName: String) @@ -24,7 +25,8 @@ abstract class IdTransactionsTable<TableT : IdTransactionsTable<TableT, QueryObj } internal inline fun getOrInitId(getId: () -> Int?, noinline body: TableT.(UpdateBuilder<*>) -> Unit, objName: () -> String): Int { - return getId() ?: table.insertIgnore(body)[id] ?: getId() ?: throw ExposedDatabaseException("This should not happen - failed to insert ${objName()} and get its id") + return getId() ?: table.insertIgnore(body)[id] ?: getId() + ?: throw ExposedDatabaseException("This should not happen - failed to insert ${objName()} and get its id") } abstract fun getId(obj: QueryObj): Int? @@ -35,9 +37,10 @@ abstract class IdTransactionsTable<TableT : IdTransactionsTable<TableT, QueryObj fun getId(obj: QueryObj, init: Boolean): Int? = if (init) getOrInitId(obj) else getId(obj) } -object WorldsT : IdTransactionsTable<WorldsT, ParcelWorldId>("parcel_worlds", "world_id") { +object WorldsT : IdTransactionsTable<WorldsT, ParcelWorldId>("parcels_worlds", "world_id") { val name = varchar("name", 50) val uid = binary("uid", 16).nullable() + val creation_time = datetime("creation_time").nullable() val index_name = uniqueIndexR("index_name", name) val index_uid = uniqueIndexR("index_uid", uid) @@ -56,6 +59,18 @@ object WorldsT : IdTransactionsTable<WorldsT, ParcelWorldId>("parcel_worlds", "w override fun getItem(row: ResultRow): ParcelWorldId { return ParcelWorldId(row[name], row[uid]?.toUUID()) } + + fun getWorldCreationTime(worldId: ParcelWorldId): DateTime? { + val id = getId(worldId) ?: return null + return select { WorldsT.id eq id }.firstOrNull()?.let { it[WorldsT.creation_time] } + } + + fun setWorldCreationTime(worldId: ParcelWorldId, time: DateTime) { + val id = getOrInitId(worldId) + update({ WorldsT.id eq id }) { + it[WorldsT.creation_time] = time + } + } } object ParcelsT : IdTransactionsTable<ParcelsT, ParcelId>("parcels", "parcel_id") { @@ -63,6 +78,7 @@ object ParcelsT : IdTransactionsTable<ParcelsT, ParcelId>("parcels", "parcel_id" val px = integer("px") val pz = integer("pz") val owner_id = integer("owner_id").references(ProfilesT.id).nullable() + val sign_oudated = bool("sign_outdated").default(false) val claim_time = datetime("claim_time").nullable() val index_location = uniqueIndexR("index_location", world_id, px, pz) @@ -89,17 +105,22 @@ object ParcelsT : IdTransactionsTable<ParcelsT, ParcelId>("parcels", "parcel_id" } } -object ProfilesT : IdTransactionsTable<ProfilesT, PlayerProfile>("parcel_profiles", "owner_id") { +object ProfilesT : IdTransactionsTable<ProfilesT, PlayerProfile>("parcels_profiles", "owner_id") { val uuid = binary("uuid", 16).nullable() - val name = varchar("name", 32) + val name = varchar("name", 32).nullable() + + // MySQL dialect MUST permit multiple null values for this to work. Server SQL does not allow this. That dialect is shit anyway. + val uuid_constraint = uniqueIndexR("uuid_constraint", uuid) val index_pair = uniqueIndexR("index_pair", uuid, name) + private inline fun getId(binaryUuid: ByteArray) = getId { uuid eq binaryUuid } private inline fun getId(uuid: UUID) = getId(uuid.toByteArray()) private inline fun getId(nameIn: String) = getId { uuid.isNull() and (name.lowerCase() eq nameIn.toLowerCase()) } private inline fun getRealId(nameIn: String) = getId { uuid.isNotNull() and (name.lowerCase() eq nameIn.toLowerCase()) } - private inline fun getOrInitId(uuid: UUID, name: String) = uuid.toByteArray().let { binaryUuid -> getOrInitId( + private inline fun getOrInitId(uuid: UUID, name: String?) = uuid.toByteArray().let { binaryUuid -> + getOrInitId( { getId(binaryUuid) }, { it[this@ProfilesT.uuid] = binaryUuid; it[this@ProfilesT.name] = name }, { "profile(uuid = $uuid, name = $name)" }) @@ -119,9 +140,9 @@ object ProfilesT : IdTransactionsTable<ProfilesT, PlayerProfile>("parcel_profile } override fun getOrInitId(profile: PlayerProfile): Int = when (profile) { - is PlayerProfile.Real -> getOrInitId(profile.uuid, profile.notNullName) + is PlayerProfile.Real -> getOrInitId(profile.uuid, profile.nameOrBukkitName) is PlayerProfile.Fake -> getOrInitId(profile.name) - else -> throw IllegalArgumentException() + else -> throw IllegalArgumentException() // Unresolved profiles cannot be added to the database } override fun getItem(row: ResultRow): PlayerProfile { @@ -132,6 +153,13 @@ object ProfilesT : IdTransactionsTable<ProfilesT, PlayerProfile>("parcel_profile return getItem(id) as? PlayerProfile.Real } + /* + fun updatePlayerProfile(profile: PlayerProfile.Real) { + update({ uuid eq profile.uuid.toByteArray() }) { + it[name] = profile.nameOrBukkitName + } + }*/ + } // val ParcelsWithOptionsT = ParcelsT.join(ParcelOptionsT, JoinType.INNER, onColumn = ParcelsT.id, otherColumn = ParcelOptionsT.parcel_id)
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/storage/exposed/ListTables.kt b/src/main/kotlin/io/dico/parcels2/storage/exposed/ListTables.kt index bbf6872..f41d545 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/exposed/ListTables.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/exposed/ListTables.kt @@ -3,7 +3,9 @@ package io.dico.parcels2.storage.exposed import io.dico.parcels2.* -import kotlinx.coroutines.experimental.channels.SendChannel +import io.dico.parcels2.AddedStatus.ALLOWED +import io.dico.parcels2.AddedStatus.DEFAULT +import kotlinx.coroutines.channels.SendChannel import org.jetbrains.exposed.sql.* import java.util.UUID @@ -12,8 +14,7 @@ object AddedGlobalT : AddedTable<PlayerProfile>("parcels_added_global", Profiles object ParcelOptionsT : Table("parcel_options") { val parcel_id = integer("parcel_id").primaryKey().references(ParcelsT.id, ReferenceOption.CASCADE) - val interact_inventory = bool("interact_inventory").default(true) - val interact_inputs = bool("interact_inputs").default(true) + val interact_bitmask = binary("interact_bitmask", 4).default(ByteArray(4) { 0 }) // all zero by default } typealias AddedStatusSendChannel<AttachT> = SendChannel<Pair<AttachT, MutableAddedDataMap>> @@ -25,7 +26,7 @@ sealed class AddedTable<AttachT>(name: String, val idTable: IdTransactionsTable< val index_pair = uniqueIndexR("index_pair", attach_id, profile_id) fun setPlayerStatus(attachedOn: AttachT, player: PlayerProfile.Real, status: AddedStatus) { - if (status.isDefault) { + if (status == DEFAULT) { val player_id = ProfilesT.getId(player) ?: return idTable.getId(attachedOn)?.let { holder -> deleteWhere { (attach_id eq holder) and (profile_id eq player_id) } @@ -38,7 +39,7 @@ sealed class AddedTable<AttachT>(name: String, val idTable: IdTransactionsTable< upsert(conflictIndex = index_pair) { it[attach_id] = holder it[profile_id] = player_id - it[allowed_flag] = status.isAllowed + it[allowed_flag] = status == ALLOWED } } @@ -97,6 +98,6 @@ sealed class AddedTable<AttachT>(name: String, val idTable: IdTransactionsTable< } } - private inline fun Boolean?.asAddedStatus() = if (this == null) AddedStatus.DEFAULT else if (this) AddedStatus.ALLOWED else AddedStatus.BANNED + private inline fun Boolean?.asAddedStatus() = if (this == null) AddedStatus.DEFAULT else if (this) ALLOWED else AddedStatus.BANNED } diff --git a/src/main/kotlin/io/dico/parcels2/storage/migration/Migration.kt b/src/main/kotlin/io/dico/parcels2/storage/migration/Migration.kt index 0db669a..acc7c5e 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/migration/Migration.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/migration/Migration.kt @@ -1,7 +1,7 @@ package io.dico.parcels2.storage.migration import io.dico.parcels2.storage.Storage -import kotlinx.coroutines.experimental.Job +import kotlinx.coroutines.Job interface Migration { fun migrateTo(storage: Storage): Job diff --git a/src/main/kotlin/io/dico/parcels2/storage/migration/plotme/PlotmeMigration.kt b/src/main/kotlin/io/dico/parcels2/storage/migration/plotme/PlotmeMigration.kt index f0c0cd8..0dcf36d 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/migration/plotme/PlotmeMigration.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/migration/plotme/PlotmeMigration.kt @@ -9,10 +9,10 @@ import io.dico.parcels2.storage.Storage import io.dico.parcels2.storage.exposed.abs import io.dico.parcels2.storage.exposed.greater import io.dico.parcels2.storage.migration.Migration -import io.dico.parcels2.util.toUUID -import kotlinx.coroutines.experimental.Job -import kotlinx.coroutines.experimental.launch -import kotlinx.coroutines.experimental.newFixedThreadPoolContext +import io.dico.parcels2.storage.toUUID +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.newFixedThreadPoolContext import org.jetbrains.exposed.sql.* import org.slf4j.LoggerFactory import java.sql.Blob @@ -83,6 +83,7 @@ class PlotmeMigration(val options: PlotmeMigrationOptions) : Migration { val parcel = getParcelId(PlotmePlotsT, row) ?: return@forEach val owner = PlayerProfile.safe(row[PlotmePlotsT.owner_uuid]?.toUUID(), row[PlotmePlotsT.owner_name]) target.setParcelOwner(parcel, owner) + target.setParcelOwnerSignOutdated(parcel, true) } } diff --git a/src/main/kotlin/io/dico/parcels2/util/FunctionHelper.kt b/src/main/kotlin/io/dico/parcels2/util/FunctionHelper.kt deleted file mode 100644 index ea16652..0000000 --- a/src/main/kotlin/io/dico/parcels2/util/FunctionHelper.kt +++ /dev/null @@ -1,53 +0,0 @@ -package io.dico.parcels2.util - -import io.dico.parcels2.ParcelsPlugin -import kotlinx.coroutines.experimental.* -import org.bukkit.scheduler.BukkitTask -import kotlin.coroutines.experimental.CoroutineContext - -@Suppress("NOTHING_TO_INLINE") -class FunctionHelper(val plugin: ParcelsPlugin) { - val mainThreadDispatcher: MainThreadDispatcher = MainThreadDispatcherImpl() - - fun <T> deferLazilyOnMainThread(block: suspend CoroutineScope.() -> T): Deferred<T> { - return async(context = mainThreadDispatcher, start = CoroutineStart.LAZY, block = block) - } - - fun <T> deferUndispatchedOnMainThread(block: suspend CoroutineScope.() -> T): Deferred<T> { - return async(context = mainThreadDispatcher, start = CoroutineStart.UNDISPATCHED, block = block) - } - - fun launchLazilyOnMainThread(block: suspend CoroutineScope.() -> Unit): Job { - return launch(context = mainThreadDispatcher, start = CoroutineStart.LAZY, block = block) - } - - inline fun schedule(noinline task: () -> Unit) = schedule(0, task) - - 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()) - } - - abstract class MainThreadDispatcher : CoroutineDispatcher() { - abstract val mainThread: Thread - abstract fun runOnMainThread(task: Runnable) - } - - private inner class MainThreadDispatcherImpl : MainThreadDispatcher() { - override val mainThread: Thread = Thread.currentThread() - - override fun dispatch(context: CoroutineContext, block: Runnable) { - runOnMainThread(block) - } - - @Suppress("OVERRIDE_BY_INLINE") - override inline fun runOnMainThread(task: Runnable) { - if (Thread.currentThread() === mainThread) task.run() - else plugin.server.scheduler.runTaskLater(plugin, task, 0) - } - } - -} diff --git a/src/main/kotlin/io/dico/parcels2/util/MainThreadDispatcher.kt b/src/main/kotlin/io/dico/parcels2/util/MainThreadDispatcher.kt new file mode 100644 index 0000000..b1d18ab --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/util/MainThreadDispatcher.kt @@ -0,0 +1,31 @@ +package io.dico.parcels2.util + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Runnable +import org.bukkit.plugin.Plugin +import kotlin.coroutines.CoroutineContext + +abstract class MainThreadDispatcher : CoroutineDispatcher() { + abstract val mainThread: Thread + abstract fun runOnMainThread(task: Runnable) +} + +@Suppress("FunctionName") +fun MainThreadDispatcher(plugin: Plugin): MainThreadDispatcher { + return object : MainThreadDispatcher() { + override val mainThread: Thread = Thread.currentThread() + + override fun dispatch(context: CoroutineContext, block: Runnable) { + doDispatch(block) + } + + override fun runOnMainThread(task: Runnable) { + doDispatch(task) + } + + private fun doDispatch(task: Runnable) { + if (Thread.currentThread() === mainThread) task.run() + else plugin.server.scheduler.runTaskLater(plugin, task, 0) + } + } +}
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/util/PluginScheduler.kt b/src/main/kotlin/io/dico/parcels2/util/PluginScheduler.kt new file mode 100644 index 0000000..f29ba2b --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/util/PluginScheduler.kt @@ -0,0 +1,20 @@ +package io.dico.parcels2.util + +import org.bukkit.plugin.Plugin +import org.bukkit.scheduler.BukkitTask + +interface PluginScheduler { + val plugin: Plugin + + fun schedule(delay: Int, task: () -> Unit): BukkitTask { + return plugin.server.scheduler.runTaskLater(plugin, task, delay.toLong()) + } + + fun scheduleRepeating(delay: Int, interval: Int, task: () -> Unit): BukkitTask { + return plugin.server.scheduler.runTaskTimer(plugin, task, delay.toLong(), interval.toLong()) + } +} + +@Suppress("NOTHING_TO_INLINE") +inline fun PluginScheduler.schedule(noinline task: () -> Unit) = schedule(0, task) + diff --git a/src/main/kotlin/io/dico/parcels2/util/Region.kt b/src/main/kotlin/io/dico/parcels2/util/Region.kt index 5717906..f786693 100644 --- a/src/main/kotlin/io/dico/parcels2/util/Region.kt +++ b/src/main/kotlin/io/dico/parcels2/util/Region.kt @@ -2,4 +2,12 @@ package io.dico.parcels2.util data class Region(val origin: Vec3i, val size: Vec3i) { val blockCount: Int get() = size.x * size.y * size.z + + val center: Vec3d + get() { + val x = (origin.x + size.x) / 2.0 + val y = (origin.y + size.y) / 2.0 + val z = (origin.z + size.z) / 2.0 + return Vec3d(x, y, z) + } }
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/util/UUIDUtil.kt b/src/main/kotlin/io/dico/parcels2/util/UUIDUtil.kt index bca2428..1398037 100644 --- a/src/main/kotlin/io/dico/parcels2/util/UUIDUtil.kt +++ b/src/main/kotlin/io/dico/parcels2/util/UUIDUtil.kt @@ -1,25 +1,14 @@ package io.dico.parcels2.util +import io.dico.parcels2.util.ext.isValid import org.bukkit.Bukkit import java.nio.ByteBuffer import java.util.UUID -@Suppress("UsePropertyAccessSyntax") -fun getPlayerNameOrDefault(uuid: UUID?, ifUnknown: String? = null): String { - return uuid - ?.let { getPlayerName(it) } - ?: ifUnknown - ?: ":unknown_name:" -} +const val PLAYER_NAME_PLACEHOLDER = ":unknown_name:" fun getPlayerName(uuid: UUID): String? { return Bukkit.getOfflinePlayer(uuid)?.takeIf { it.isValid }?.name } -fun UUID.toByteArray(): ByteArray = - ByteBuffer.allocate(16).apply { - putLong(mostSignificantBits) - putLong(leastSignificantBits) - }.array() -fun ByteArray.toUUID(): UUID = ByteBuffer.wrap(this).run { UUID(long, long) } diff --git a/src/main/kotlin/io/dico/parcels2/util/Vec2i.kt b/src/main/kotlin/io/dico/parcels2/util/Vec2i.kt index 933c18b..62ac97f 100644 --- a/src/main/kotlin/io/dico/parcels2/util/Vec2i.kt +++ b/src/main/kotlin/io/dico/parcels2/util/Vec2i.kt @@ -5,3 +5,7 @@ data class Vec2i( val z: Int ) +data class Region2i( + val bottom: Vec2i, + val top: Vec2i +)
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/util/Vec3i.kt b/src/main/kotlin/io/dico/parcels2/util/Vec3i.kt index 6db98af..ded1e0c 100644 --- a/src/main/kotlin/io/dico/parcels2/util/Vec3i.kt +++ b/src/main/kotlin/io/dico/parcels2/util/Vec3i.kt @@ -3,6 +3,22 @@ package io.dico.parcels2.util import org.bukkit.World import org.bukkit.block.Block +data class Vec3d( + val x: Double, + val y: Double, + val z: Double +) { + 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) +} + data class Vec3i( val x: Int, val y: Int, @@ -12,6 +28,9 @@ data class Vec3i( 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) } diff --git a/src/main/kotlin/io/dico/parcels2/util/MaterialExtensions.kt b/src/main/kotlin/io/dico/parcels2/util/ext/Material.kt index a2aefc8..e160e55 100644 --- a/src/main/kotlin/io/dico/parcels2/util/MaterialExtensions.kt +++ b/src/main/kotlin/io/dico/parcels2/util/ext/Material.kt @@ -1,4 +1,4 @@ -package io.dico.parcels2.util +package io.dico.parcels2.util.ext import org.bukkit.Material import org.bukkit.Material.* @@ -74,3 +74,35 @@ val Material.isWoodButton DARK_OAK_BUTTON -> true else -> false } + +private fun getMaterialPrefixed(prefix: String, name: String): Material { + return Material.getMaterial("${prefix}_$name") ?: throw IllegalArgumentException("Material ${prefix}_$name doesn't exist") +} + +fun getMaterialsWithWoodTypePrefix(name: String) = arrayOf( + getMaterialPrefixed("OAK", name), + getMaterialPrefixed("BIRCH", name), + getMaterialPrefixed("SPRUCE", name), + getMaterialPrefixed("JUNGLE", name), + getMaterialPrefixed("ACACIA", name), + getMaterialPrefixed("DARK_OAK", name) +) + +fun getMaterialsWithWoolColorPrefix(name: String) = arrayOf( + getMaterialPrefixed("WHITE", name), + getMaterialPrefixed("ORANGE", name), + getMaterialPrefixed("MAGENTA", name), + getMaterialPrefixed("LIGHT_BLUE", name), + getMaterialPrefixed("YELLOW", name), + getMaterialPrefixed("LIME", name), + getMaterialPrefixed("PINK", name), + getMaterialPrefixed("GRAY", name), + getMaterialPrefixed("LIGHT_GRAY", name), + getMaterialPrefixed("CYAN", name), + getMaterialPrefixed("PURPLE", name), + getMaterialPrefixed("BLUE", name), + getMaterialPrefixed("BROWN", name), + getMaterialPrefixed("GREEN", name), + getMaterialPrefixed("RED", name), + getMaterialPrefixed("BLACK", name) +)
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/util/NumberExtensions.kt b/src/main/kotlin/io/dico/parcels2/util/ext/Math.kt index 214a797..62ee220 100644 --- a/src/main/kotlin/io/dico/parcels2/util/NumberExtensions.kt +++ b/src/main/kotlin/io/dico/parcels2/util/ext/Math.kt @@ -1,4 +1,4 @@ -package io.dico.parcels2.util +package io.dico.parcels2.util.ext fun Double.floor(): Int { val down = toInt() @@ -29,4 +29,7 @@ fun IntRange.clamp(min: Int, max: Int): IntRange { return IntRange(first, max) } return this -}
\ No newline at end of file +} + +// the name coerceAtMost is bad +fun Int.clampMax(max: Int) = coerceAtMost(max)
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/util/MiscExtensions.kt b/src/main/kotlin/io/dico/parcels2/util/ext/Misc.kt index 877d1cc..d7feabf 100644 --- a/src/main/kotlin/io/dico/parcels2/util/MiscExtensions.kt +++ b/src/main/kotlin/io/dico/parcels2/util/ext/Misc.kt @@ -1,4 +1,4 @@ -package io.dico.parcels2.util +package io.dico.parcels2.util.ext import io.dico.parcels2.logger import java.io.File @@ -22,6 +22,10 @@ inline fun <R> Any.synchronized(block: () -> R): R = synchronized(this, block) inline fun <T> T?.isNullOr(condition: T.() -> Boolean): Boolean = this == null || condition() inline fun <T> T?.isPresentAnd(condition: T.() -> Boolean): Boolean = this != null && condition() +inline fun <T> T?.ifNullRun(block: () -> Unit): T? { + if (this == null) block() + return this +} inline fun <T, U> MutableMap<T, U>.editLoop(block: EditLoopScope<T, U>.(T, U) -> Unit) { return EditLoopScope(this).doEditLoop(block) diff --git a/src/main/kotlin/io/dico/parcels2/util/PlayerExtensions.kt b/src/main/kotlin/io/dico/parcels2/util/ext/Player.kt index 9604365..38402f0 100644 --- a/src/main/kotlin/io/dico/parcels2/util/PlayerExtensions.kt +++ b/src/main/kotlin/io/dico/parcels2/util/ext/Player.kt @@ -1,8 +1,7 @@ -package io.dico.parcels2.util +package io.dico.parcels2.util.ext import io.dico.dicore.Formatting import io.dico.parcels2.ParcelsPlugin -import io.dico.parcels2.PlayerProfile import io.dico.parcels2.logger import org.bukkit.OfflinePlayer import org.bukkit.entity.Player @@ -0,0 +1,84 @@ +# Parcels Todo list + +Commands +- +Basically all admin commands. +* ~~setowner~~ +* ~~dispose~~ +* ~~reset~~ +* swap +* New admin commands that I can't think of right now. + +Also +* ~~setbiome~~ +* random + +Modify home command: +* ~~Make `:` not be required if prior component cannot be parsed to an int~~ +* Listen for command events that use plotme-style argument, and transform the command + +~~Add permissions to commands (replace or fix `IContextFilter` from command lib +to allow inheriting permissions properly).~~ + +Parcel Options +- + +Parcel options apply to any player with `DEFAULT` added status. +They affect what their permissions might be within the parcel. + +Apart from `/p option inputs`, `/p option inventory`, the following might be considered. + +Move existing options to "interact" namespace (`/p o interact`) + +Then, +* Split `/p option interact inputs` into a list of interactible block types. +The list could include container blocks, merging the existing inventory option. +* Players cannot launch projectiles in locations where they can't build. +This could become optional. +* Option to control spreading and/or forming of blocks such as grass and ice within the parcel. + +Block Management +- +~~Update the parcel corner with owner info when a player flies into the parcel (after migrations). +Parcels has a player head in that corner in addition to the sign that PlotMe uses.~~ + +Commands that modify parcel blocks must be kept track of to prevent multiple +from running simultaneously in the same parcel. `hasBlockVisitors` field must be updated. +In general, spamming the commands must be caught at all cost to avoid lots of lag. + +Swap - schematic is in place, but proper placement order must be enforced to make sure that attachable +blocks are placed properly. Alternatively, if a block change method can be found that doesn't +cause block updates, that would be preferred subject to having good performance. + +~~Change `RegionTraversal` to allow traversing different parts of a region in a different order. +This could apply to clearing of plots, for example. It would be better if the bottom 64 (floor height) +layers are done upwards, and the rest downwards.~~ + +Events +- +Prevent block spreading subject to conditions. + +Scan through blocks that were added since original Parcels implementation, +that might introduce things that need to be checked or listened for. + +~~WorldEdit Listener.~~ + +Limit number of beacons in a parcel and/or avoid potion effects being applied outside the parcel. + +Database +- +Find and patch ways to add new useless entries (for regular players at least) + +Prevent invalid player names from being saved to the database. +Here, invalid player names mean names that contain invalid characters. + +Use an atomic GET OR INSERT query so that parallel execution doesn't cause problems +(as is currently the case when migrating). + +Implement a container that doesn't require loading all parcel data on startup (Complex). + +~~Update player profiles in the database on join to account for name changes.~~ + +Store player status on parcel (allowed, default banned) as a number to allow for future additions to this set of possibilities + + |