From 520ae530d2de076fa9e87da7f04fcf78e080f4de Mon Sep 17 00:00:00 2001 From: Dico Date: Wed, 26 Sep 2018 07:08:42 +0100 Subject: Make progress --- .../dico/dicore/command/ChildCommandAddress.java | 13 +- .../main/java/io/dico/dicore/command/Command.java | 51 +-- .../io/dico/dicore/command/CommandBuilder.java | 28 +- .../io/dico/dicore/command/ExecutionContext.java | 43 ++- .../io/dico/dicore/command/ICommandAddress.java | 10 + .../io/dico/dicore/command/ICommandDispatcher.java | 15 + .../dicore/command/ModifiableCommandAddress.java | 5 + .../io/dico/dicore/command/RootCommandAddress.java | 103 ++++-- .../reflect/ReflectiveRegistration.java | 10 +- .../kotlin/io/dico/parcels2/ParcelGenerator.kt | 6 +- src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt | 12 +- .../dico/parcels2/blockvisitor/ExtraBlockChange.kt | 38 +++ .../dico/parcels2/blockvisitor/RegionTraverser.kt | 84 ----- .../io/dico/parcels2/blockvisitor/Schematic.kt | 83 +++-- .../dico/parcels2/blockvisitor/WorkDispatcher.kt | 345 ++++++++++++++++++++ .../dico/parcels2/blockvisitor/WorktimeLimiter.kt | 361 --------------------- .../io/dico/parcels2/command/CommandsDebug.kt | 7 +- .../io/dico/parcels2/command/CommandsGeneral.kt | 6 +- .../dico/parcels2/command/ParcelCommandBuilder.kt | 52 ++- .../io/dico/parcels2/command/ParcelTarget.kt | 24 +- .../parcels2/defaultimpl/DefaultParcelGenerator.kt | 20 +- .../parcels2/defaultimpl/ParcelProviderImpl.kt | 2 +- .../dico/parcels2/defaultimpl/ParcelWorldImpl.kt | 6 +- .../io/dico/parcels2/listener/ParcelListeners.kt | 16 +- src/main/kotlin/io/dico/parcels2/util/Vec3i.kt | 1 + todo.md | 32 +- 26 files changed, 763 insertions(+), 610 deletions(-) create mode 100644 src/main/kotlin/io/dico/parcels2/blockvisitor/ExtraBlockChange.kt create mode 100644 src/main/kotlin/io/dico/parcels2/blockvisitor/WorkDispatcher.kt delete mode 100644 src/main/kotlin/io/dico/parcels2/blockvisitor/WorktimeLimiter.kt 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 73d82ca..022904e 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 @@ -24,11 +24,20 @@ public class ChildCommandAddress extends ModifiableCommandAddress { } public static ChildCommandAddress newPlaceHolderCommand(String name, String... aliases) { - ChildCommandAddress rv = new ChildCommandAddress(DefaultGroupCommand.getInstance(), name, aliases); - HelpCommand.registerAsChild(rv); + ChildCommandAddress rv = new ChildCommandAddress(); + rv.setupAsPlaceholder(name, aliases); return rv; } + public void setupAsPlaceholder(String name, String... aliases) { + if (!hasCommand()) { + setCommand(DefaultGroupCommand.getInstance()); + } + + addNameAndAliases(name, aliases); + HelpCommand.registerAsChild(this); + } + @Override public boolean isRoot() { return false; diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/Command.java b/dicore3/command/src/main/java/io/dico/dicore/command/Command.java index b66b5f5..868aa95 100644 --- a/dicore3/command/src/main/java/io/dico/dicore/command/Command.java +++ b/dicore3/command/src/main/java/io/dico/dicore/command/Command.java @@ -145,43 +145,50 @@ public abstract class Command { ExecutionContext executionContext = new ExecutionContext(sender, caller, this, buffer, false); try { - //System.out.println("In Command.execute(sender, caller, buffer)#try{"); - int i, n; - for (i = 0, n = contextFilterPostParameterIndex; i < n; i++) { - contextFilters.get(i).filterContext(executionContext); - } - - executionContext.parseParameters(); + executeWithContext(executionContext); + } catch (Throwable t) { + caller.getChatController().handleException(sender, executionContext, t); + } + } - for (n = contextFilters.size(); i < n; i++) { - contextFilters.get(i).filterContext(executionContext); - } + public void executeWithContext(ExecutionContext context) throws CommandException { + //System.out.println("In Command.execute(sender, caller, buffer)#try{"); + int i, n; + for (i = 0, n = contextFilterPostParameterIndex; i < n; i++) { + contextFilters.get(i).filterContext(context); + } - //System.out.println("Post-contextfilters"); + context.parseParameters(); - String message = execute(sender, executionContext); - caller.getChatController().sendMessage(sender, EMessageType.RESULT, message); - } catch (Throwable t) { - caller.getChatController().handleException(sender, executionContext, t); + for (n = contextFilters.size(); i < n; i++) { + contextFilters.get(i).filterContext(context); } + + //System.out.println("Post-contextfilters"); + + String message = execute(context.getSender(), context); + context.getAddress().getChatController().sendMessage(context.getSender(), EMessageType.RESULT, message); } public abstract String execute(CommandSender sender, ExecutionContext context) throws CommandException; public List tabComplete(CommandSender sender, ICommandAddress caller, Location location, ArgumentBuffer buffer) { ExecutionContext executionContext = new ExecutionContext(sender, caller, this, buffer, true); - try { - int i, n; - for (i = 0, n = contextFilterPostParameterIndex; i < n; i++) { - contextFilters.get(i).filterContext(executionContext); - } + return tabCompleteWithContext(executionContext, location); } catch (CommandException ex) { return Collections.emptyList(); } + } + + public List tabCompleteWithContext(ExecutionContext context, Location location) throws CommandException { + int i, n; + for (i = 0, n = contextFilterPostParameterIndex; i < n; i++) { + contextFilters.get(i).filterContext(context); + } - executionContext.parseParametersQuietly(); - return tabComplete(sender, executionContext, location); + context.parseParametersQuietly(); + return tabComplete(context.getSender(), context, location); } public List tabComplete(CommandSender sender, ExecutionContext context, Location location) { 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 e72d478..76211c2 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 @@ -197,12 +197,38 @@ public final class CommandBuilder { public CommandBuilder group(String name, String... aliases) { ChildCommandAddress address = cur.getChild(name); if (address == null || !name.equals(address.getMainKey())) { - cur.addChild(address = ChildCommandAddress.newPlaceHolderCommand(name, aliases)); + address = new ChildCommandAddress(); + address.setupAsPlaceholder(name, aliases); + cur.addChild(address); } cur = address; return this; } + /** + * Similar to {@link #group(String, String[])} but this will force overwrite any present group, + * using the address passed. The address MUST be an instance of {@link ChildCommandAddress}. + * + *

The address must not have a parent or any keys

+ * + * @param address the address object to use + * @param name the main key + * @param aliases any aliases + * @return this + * @throws IllegalArgumentException if any of the requirements set out above aren't met + */ + public CommandBuilder group(ICommandAddress address, String name, String... aliases) { + if (address.hasParent() || address.getMainKey() != null || !(address instanceof ChildCommandAddress)) { + throw new IllegalArgumentException(); + } + + ChildCommandAddress asChild = (ChildCommandAddress) address; + asChild.setupAsPlaceholder(name, aliases); + cur.addChild(address); + cur = asChild; + return this; + } + /** * Sets the description of a group created by {@link #group(String, String...)} * Can be called subsequently to making a call to {@link #group(String, String...)} 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 4450a92..0608b80 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 @@ -17,14 +17,14 @@ import java.util.*; * It is also responsible for keeping track of the parameter to complete in the case of a tab completion. */ public class ExecutionContext { - private final CommandSender sender; - private final ICommandAddress address; - private final Command command; - private final ArgumentBuffer originalBuffer; - private final ArgumentBuffer processedBuffer; + private CommandSender sender; + private ICommandAddress address; + private Command command; + private ArgumentBuffer originalBuffer; + private ArgumentBuffer processedBuffer; // caches the buffer's cursor before parsing. This is needed to provide the original input of the player. - private final int cursorStart; + private int cursorStart; // when the context starts parsing parameters, this flag is set, and any subsequent calls to #parseParameters() throw an IllegalStateException. private boolean attemptedToParse; @@ -48,8 +48,14 @@ public class ExecutionContext { // if this flag is set, any messages sent through the sendMessage methods are discarded. private boolean muted; + public ExecutionContext(CommandSender sender, boolean tabComplete) { + this.sender = Objects.requireNonNull(sender); + this.muted = tabComplete; + this.tabComplete = tabComplete; + } + /** - * Construct an execution context, making it ready to parse the parameter values. + * Construct an execution context that is ready to parse the parameter values. * * @param sender the sender * @param address the address @@ -57,11 +63,22 @@ public class ExecutionContext { * @param tabComplete true if this execution is a tab-completion */ public ExecutionContext(CommandSender sender, ICommandAddress address, Command command, ArgumentBuffer buffer, boolean tabComplete) { - this.sender = Objects.requireNonNull(sender); + this(sender, tabComplete); + targetAcquired(address, command, buffer); + } + + void requireAddressPresent(boolean present) { + //noinspection DoubleNegation + if ((address != null) != present) { + throw new IllegalStateException(); + } + } + + void targetAcquired(ICommandAddress address, Command command, ArgumentBuffer buffer) { + requireAddressPresent(false); + this.address = Objects.requireNonNull(address); this.command = Objects.requireNonNull(command); - this.muted = tabComplete; - this.tabComplete = tabComplete; // If its tab completing, keep the empty element that might be at the end of the buffer // due to a space at the end of the command. @@ -80,7 +97,8 @@ public class ExecutionContext { * * @throws CommandException if an error occurs while parsing the parameters. */ - public synchronized void parseParameters() throws CommandException { + synchronized void parseParameters() throws CommandException { + requireAddressPresent(true); if (attemptedToParse) { throw new IllegalStateException(); } @@ -101,7 +119,8 @@ public class ExecutionContext { * This method is typically used by tab completions. * After calling this method, the context is ready to provide completions. */ - public synchronized void parseParametersQuietly() { + synchronized void parseParametersQuietly() { + requireAddressPresent(true); if (attemptedToParse) { throw new IllegalStateException(); } diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/ICommandAddress.java b/dicore3/command/src/main/java/io/dico/dicore/command/ICommandAddress.java index d6cd350..befdefa 100644 --- a/dicore3/command/src/main/java/io/dico/dicore/command/ICommandAddress.java +++ b/dicore3/command/src/main/java/io/dico/dicore/command/ICommandAddress.java @@ -125,6 +125,16 @@ public interface ICommandAddress { */ ICommandAddress getChild(String key); + /** + * Query for a child at the given key, with the given context for reference. + * Can be used to override behaviour of the tree. + * + * @param key the key. The name or alias of a command. + * @param context context of a command being executed + * @return the child, or null if it's not found, altered freely by the implementation + */ + ICommandAddress getChild(String key, ExecutionContext context) throws CommandException; + /** * Get the command dispatcher for this tree * diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/ICommandDispatcher.java b/dicore3/command/src/main/java/io/dico/dicore/command/ICommandDispatcher.java index b18694e..055171d 100644 --- a/dicore3/command/src/main/java/io/dico/dicore/command/ICommandDispatcher.java +++ b/dicore3/command/src/main/java/io/dico/dicore/command/ICommandDispatcher.java @@ -30,8 +30,23 @@ public interface ICommandDispatcher { * @param buffer the command itself as a buffer. * @return the address that is the target of the command. */ + @Deprecated ICommandAddress getCommandTarget(CommandSender sender, ArgumentBuffer buffer); + /** + * Similar to {@link #getDeepChild(ArgumentBuffer)}, + * but this method incorporates checks on the command of traversed children: + * {@link Command#isVisibleTo(CommandSender)} + * and {@link Command#takePrecedenceOverSubcommand(String, ArgumentBuffer)} + *

+ * The target of a command is never null, however, the same instance might be returned, and the returned address might not hold a command. + * + * @param context the context of the command. The context must not have its address set. + * @param buffer the command itself as a buffer. + * @return the address that is the target of the command. + */ + ICommandAddress getCommandTarget(ExecutionContext context, ArgumentBuffer buffer) throws CommandException; + /** * dispatch the command * 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 8c2ab67..484eb5e 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 @@ -120,6 +120,11 @@ public abstract class ModifiableCommandAddress implements ICommandAddress { return children.get(key); } + @Override + public ChildCommandAddress getChild(String key, ExecutionContext context) throws CommandException { + return getChild(key); + } + public void addChild(ICommandAddress child) { if (!(child instanceof ChildCommandAddress)) { throw new IllegalArgumentException("Argument must be a ChildCommandAddress"); 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 6d38174..7577808 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 @@ -123,8 +123,6 @@ public class RootCommandAddress extends ModifiableCommandAddress implements ICom @Override public ModifiableCommandAddress getCommandTarget(CommandSender sender, ArgumentBuffer buffer) { - //System.out.println("Buffer cursor upon getCommandTarget: " + buffer.getCursor()); - ModifiableCommandAddress cur = this; ChildCommandAddress child; while (buffer.hasNext()) { @@ -139,16 +137,25 @@ public class RootCommandAddress extends ModifiableCommandAddress implements ICom cur = child; } - /* - if (!cur.hasCommand() && cur.hasHelpCommand()) { - cur = cur.getHelpCommand(); - } else { - while (!cur.hasCommand() && cur.hasParent()) { - cur = cur.getParent(); + return cur; + } + + @Override + public ModifiableCommandAddress getCommandTarget(ExecutionContext context, ArgumentBuffer buffer) throws CommandException { + CommandSender sender = context.getSender(); + ModifiableCommandAddress cur = this; + ChildCommandAddress child; + while (buffer.hasNext()) { + child = cur.getChild(buffer.next(), context); + if (child == null + || (child.hasCommand() && !child.getCommand().isVisibleTo(sender)) + || (cur.hasCommand() && cur.getCommand().takePrecedenceOverSubcommand(buffer.peekPrevious(), buffer.getUnaffectingCopy()))) { buffer.rewind(); + break; } + + cur = child; } - */ return cur; } @@ -165,18 +172,32 @@ public class RootCommandAddress extends ModifiableCommandAddress implements ICom @Override public boolean dispatchCommand(CommandSender sender, ArgumentBuffer buffer) { - ModifiableCommandAddress targetAddress = getCommandTarget(sender, buffer); - Command target = targetAddress.getCommand(); - - if (target == null || target instanceof DefaultGroupCommand) { - if (targetAddress.hasHelpCommand()) { - target = targetAddress.getHelpCommand().getCommand(); - } else if (target == null){ - return false; + ExecutionContext context = new ExecutionContext(sender, false); + + ModifiableCommandAddress targetAddress = null; + + try { + targetAddress = getCommandTarget(context, buffer); + Command target = targetAddress.getCommand(); + + if (target == null || target instanceof DefaultGroupCommand) { + if (targetAddress.hasHelpCommand()) { + target = targetAddress.getHelpCommand().getCommand(); + } else if (target == null){ + return false; + } + } + + context.targetAcquired(targetAddress, target, buffer); + target.executeWithContext(context); + + } catch (Throwable t) { + if (targetAddress == null) { + targetAddress = this; } + targetAddress.getChatController().handleException(sender, context, t); } - target.execute(sender, targetAddress, buffer); return true; } @@ -192,28 +213,38 @@ public class RootCommandAddress extends ModifiableCommandAddress implements ICom @Override public List getTabCompletions(CommandSender sender, Location location, ArgumentBuffer buffer) { - ICommandAddress target = getCommandTarget(sender, buffer); - List out = target.hasCommand() ? target.getCommand().tabComplete(sender, target, location, buffer.getUnaffectingCopy()) : Collections.emptyList(); - - int cursor = buffer.getCursor(); - String input; - if (cursor >= buffer.size()) { - input = ""; - } else { - input = buffer.get(cursor).toLowerCase(); - } + ExecutionContext context = new ExecutionContext(sender, true); + + try { + ICommandAddress target = getCommandTarget(context, buffer); + List out = target.hasCommand() + ? target.getCommand().tabComplete(sender, target, location, buffer.getUnaffectingCopy()) + : Collections.emptyList(); + + int cursor = buffer.getCursor(); + String input; + if (cursor >= buffer.size()) { + input = ""; + } else { + input = buffer.get(cursor).toLowerCase(); + } - boolean wrapped = false; - for (String child : target.getChildren().keySet()) { - if (child.toLowerCase().startsWith(input)) { - if (!wrapped) { - out = new ArrayList<>(out); - wrapped = true; + boolean wrapped = false; + for (String child : target.getChildren().keySet()) { + if (child.toLowerCase().startsWith(input)) { + if (!wrapped) { + out = new ArrayList<>(out); + wrapped = true; + } + out.add(child); } - out.add(child); } + + return out; + + } catch (CommandException ex) { + return Collections.emptyList(); } - return out; } } diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/ReflectiveRegistration.java b/dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/ReflectiveRegistration.java index 0495ba9..0c64533 100644 --- a/dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/ReflectiveRegistration.java +++ b/dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/ReflectiveRegistration.java @@ -177,10 +177,12 @@ public class ReflectiveRegistration { GroupEntry matchEntry = matchEntries[i]; if (patterns[i].matcher(name).matches()) { if (addresses[i] == null) { - addresses[i] = ChildCommandAddress.newPlaceHolderCommand(matchEntry.group(), matchEntry.groupAliases()); - groupRootAddress.addChild(addresses[i]); - generateCommands(addresses[i], matchEntry.generatedCommands()); - setDescription(addresses[i], matchEntry.description(), matchEntry.shortDescription()); + ChildCommandAddress placeholder = new ChildCommandAddress(); + placeholder.setupAsPlaceholder(matchEntry.group(), matchEntry.groupAliases()); + addresses[i] = placeholder; + groupRootAddress.addChild(placeholder); + generateCommands(placeholder, matchEntry.generatedCommands()); + setDescription(placeholder, matchEntry.description(), matchEntry.shortDescription()); } return addresses[i]; } diff --git a/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt b/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt index c11d557..4e0aeb4 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt @@ -37,12 +37,12 @@ abstract class ParcelGenerator : ChunkGenerator() { abstract fun makeParcelLocatorAndBlockManager(worldId: ParcelWorldId, container: ParcelContainer, coroutineScope: CoroutineScope, - worktimeLimiter: WorktimeLimiter): Pair + workDispatcher: WorkDispatcher): Pair } interface ParcelBlockManager { val world: World - val worktimeLimiter: WorktimeLimiter + val workDispatcher: WorkDispatcher val parcelTraverser: RegionTraverser // fun getBottomBlock(parcel: ParcelId): Vec2i @@ -61,7 +61,7 @@ interface ParcelBlockManager { fun swapParcels(parcel1: ParcelId, parcel2: ParcelId): Worker - fun submitBlockVisitor(vararg parcelIds: ParcelId, task: TimeLimitedTask): Worker + fun submitBlockVisitor(vararg parcelIds: ParcelId, task: WorkerTask): Worker /** * Used to update owner blocks in the corner of the parcel diff --git a/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt b/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt index 3e12ba5..7af7468 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt @@ -3,8 +3,8 @@ package io.dico.parcels2 import io.dico.dicore.Registrator import io.dico.dicore.command.EOverridePolicy import io.dico.dicore.command.ICommandDispatcher -import io.dico.parcels2.blockvisitor.TickWorktimeLimiter -import io.dico.parcels2.blockvisitor.WorktimeLimiter +import io.dico.parcels2.blockvisitor.BukkitWorkDispatcher +import io.dico.parcels2.blockvisitor.WorkDispatcher import io.dico.parcels2.command.getParcelCommands import io.dico.parcels2.defaultimpl.GlobalPrivilegesManagerImpl import io.dico.parcels2.defaultimpl.ParcelProviderImpl @@ -44,7 +44,7 @@ class ParcelsPlugin : JavaPlugin(), CoroutineScope, PluginScheduler { override val coroutineContext: CoroutineContext = MainThreadDispatcher(this) override val plugin: Plugin get() = this - val worktimeLimiter: WorktimeLimiter by lazy { TickWorktimeLimiter(this, options.tickWorktime) } + val workDispatcher: WorkDispatcher by lazy { BukkitWorkDispatcher(this, options.tickWorktime) } override fun onEnable() { plogger.info("Debug enabled: ${plogger.isDebugEnabled}") @@ -55,11 +55,11 @@ class ParcelsPlugin : JavaPlugin(), CoroutineScope, PluginScheduler { } override fun onDisable() { - val hasWorkers = worktimeLimiter.workers.isNotEmpty() + val hasWorkers = workDispatcher.workers.isNotEmpty() if (hasWorkers) { - plogger.warn("Parcels is attempting to complete all ${worktimeLimiter.workers.size} remaining jobs before shutdown...") + plogger.warn("Parcels is attempting to complete all ${workDispatcher.workers.size} remaining jobs before shutdown...") } - worktimeLimiter.completeAllTasks() + workDispatcher.completeAllTasks() if (hasWorkers) { plogger.info("Parcels has completed the remaining jobs.") } diff --git a/src/main/kotlin/io/dico/parcels2/blockvisitor/ExtraBlockChange.kt b/src/main/kotlin/io/dico/parcels2/blockvisitor/ExtraBlockChange.kt new file mode 100644 index 0000000..3f7e070 --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/blockvisitor/ExtraBlockChange.kt @@ -0,0 +1,38 @@ +package io.dico.parcels2.blockvisitor + +import org.bukkit.block.Block +import org.bukkit.block.BlockState +import org.bukkit.block.Sign +import kotlin.reflect.KClass + +interface ExtraBlockChange { + fun update(block: Block) +} + +abstract class BlockStateChange : ExtraBlockChange { + abstract val stateClass: KClass + + abstract fun update(state: T) + + override fun update(block: Block) { + val state = block.state + if (stateClass.isInstance(state)) { + @Suppress("UNCHECKED_CAST") + update(state as T) + } + } +} + +class SignStateChange(state: Sign) : BlockStateChange() { + val lines = state.lines + + override val stateClass: KClass + get() = Sign::class + + override fun update(state: Sign) { + for (i in lines.indices) { + val line = lines[i] + state.setLine(i, line) + } + } +} diff --git a/src/main/kotlin/io/dico/parcels2/blockvisitor/RegionTraverser.kt b/src/main/kotlin/io/dico/parcels2/blockvisitor/RegionTraverser.kt index 8f7f8f8..5326baa 100644 --- a/src/main/kotlin/io/dico/parcels2/blockvisitor/RegionTraverser.kt +++ b/src/main/kotlin/io/dico/parcels2/blockvisitor/RegionTraverser.kt @@ -235,87 +235,3 @@ inline class TraverseDirection(val bits: Int) { } } - -/* -private typealias Scope = SequenceScope -private typealias ScopeAction = suspend Scope.(Int, Int, Int) -> Unit - -@Suppress("NON_EXHAUSTIVE_WHEN") -suspend fun Scope.traverserLogic( - region: Region, - order: TraverseOrder, - direction: TraverseDirection -) = with(direction) { - val (primary, secondary, tertiary) = order.toArray() - val (origin, size) = region - - when (order.primary) { - Dimension.X -> - when (order.secondary) { - Dimension.Y -> { - directionOf(primary).traverse(primary.extract(size)) { p -> - directionOf(secondary).traverse(secondary.extract(size)) { s -> - directionOf(tertiary).traverse(tertiary.extract(size)) { t -> - yield(origin.add(p, s, t)) - } - } - } - } - Dimension.Z -> { - directionOf(primary).traverse(primary.extract(size)) { p -> - directionOf(secondary).traverse(secondary.extract(size)) { s -> - directionOf(tertiary).traverse(tertiary.extract(size)) { t -> - yield(origin.add(p, t, s)) - } - } - } - } - } - - Dimension.Y -> - when (order.secondary) { - Dimension.X -> { - directionOf(primary).traverse(primary.extract(size)) { p -> - directionOf(secondary).traverse(secondary.extract(size)) { s -> - directionOf(tertiary).traverse(tertiary.extract(size)) { t -> - yield(origin.add(s, p, t)) - } - } - } - } - Dimension.Z -> { - directionOf(primary).traverse(primary.extract(size)) { p -> - directionOf(secondary).traverse(secondary.extract(size)) { s -> - directionOf(tertiary).traverse(tertiary.extract(size)) { t -> - yield(origin.add(t, p, s)) - } - } - } - } - } - - Dimension.Z -> - when (order.secondary) { - Dimension.X -> { - directionOf(primary).traverse(primary.extract(size)) { p -> - directionOf(secondary).traverse(secondary.extract(size)) { s -> - directionOf(tertiary).traverse(tertiary.extract(size)) { t -> - yield(origin.add(s, t, p)) - } - } - } - } - Dimension.Y -> { - directionOf(primary).traverse(primary.extract(size)) { p -> - directionOf(secondary).traverse(secondary.extract(size)) { s -> - directionOf(tertiary).traverse(tertiary.extract(size)) { t -> - yield(origin.add(t, s, p)) - } - } - } - } - } - } -} - -*/ \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/blockvisitor/Schematic.kt b/src/main/kotlin/io/dico/parcels2/blockvisitor/Schematic.kt index 9d9a2aa..9f88fd9 100644 --- a/src/main/kotlin/io/dico/parcels2/blockvisitor/Schematic.kt +++ b/src/main/kotlin/io/dico/parcels2/blockvisitor/Schematic.kt @@ -6,12 +6,11 @@ import io.dico.parcels2.util.get import org.bukkit.Bukkit import org.bukkit.Material import org.bukkit.World -import org.bukkit.block.Block +import org.bukkit.block.Sign import org.bukkit.block.data.BlockData private val air = Bukkit.createBlockData(Material.AIR) -// TODO order paste such that attachables are placed after the block they depend on class Schematic { val size: Vec3i get() = _size!! private var _size: Vec3i? = null @@ -21,7 +20,7 @@ class Schematic { } private var blockDatas: Array? = null - private val extra = mutableMapOf Unit>() + private val extra = mutableListOf>() private var isLoaded = false; private set private val traverser: RegionTraverser = RegionTraverser.upward @@ -32,7 +31,7 @@ class Schematic { val blocks = traverser.traverseRegion(region) val total = region.blockCount.toDouble() - for ((index, vec) in blocks.withIndex()) { + loop@ for ((index, vec) in blocks.withIndex()) { markSuspensionPoint() setProgress(index / total) @@ -40,6 +39,14 @@ class Schematic { if (block.y > 255) continue val blockData = block.blockData data[index] = blockData + + val extraChange = when (blockData.material) { + Material.SIGN, + Material.WALL_SIGN -> SignStateChange(block.state as Sign) + else -> continue@loop + } + + extra += (vec - region.origin) to extraChange } isLoaded = true @@ -47,53 +54,65 @@ class Schematic { suspend fun WorkerScope.paste(world: World, position: Vec3i) { if (!isLoaded) throw IllegalStateException() + val region = Region(position, _size!!) val blocks = traverser.traverseRegion(region, worldHeight = world.maxHeight) val blockDatas = blockDatas!! var postponed = hashMapOf() - // 90% of the progress of this job is allocated to this code block - delegateWork(0.9) { - for ((index, vec) in blocks.withIndex()) { - markSuspensionPoint() - val block = world[vec] - val type = blockDatas[index] ?: air - if (type !== air && isAttachable(type.material)) { - val supportingBlock = vec + getSupportingBlock(type) - - if (!postponed.containsKey(supportingBlock) && traverser.comesFirst(vec, supportingBlock)) { - block.blockData = type - } else { - postponed[vec] = type - } + val total = region.blockCount.toDouble() + var processed = 0 - } else { + for ((index, vec) in blocks.withIndex()) { + markSuspensionPoint() + setProgress(index / total) + + val block = world[vec] + val type = blockDatas[index] ?: air + if (type !== air && isAttachable(type.material)) { + val supportingBlock = vec + getSupportingBlock(type) + + if (!postponed.containsKey(supportingBlock) && traverser.comesFirst(vec, supportingBlock)) { block.blockData = type + setProgress(++processed / total) + } else { + postponed[vec] = type } + + } else { + block.blockData = type + setProgress(++processed / total) } } - delegateWork { - while (!postponed.isEmpty()) { - val newMap = hashMapOf() - for ((vec, type) in postponed) { - val supportingBlock = vec + getSupportingBlock(type) - if (supportingBlock in postponed && supportingBlock != vec) { - newMap[vec] = type - } else { - world[vec].blockData = type - } + while (!postponed.isEmpty()) { + markSuspensionPoint() + val newMap = hashMapOf() + for ((vec, type) in postponed) { + val supportingBlock = vec + getSupportingBlock(type) + if (supportingBlock in postponed && supportingBlock != vec) { + newMap[vec] = type + } else { + world[vec].blockData = type + setProgress(++processed / total) } - postponed = newMap } + postponed = newMap + } + + // Should be negligible so we don't track progress + for ((vec, extraChange) in extra) { + markSuspensionPoint() + val block = world[position + vec] + extraChange.update(block) } } - fun getLoadTask(world: World, region: Region): TimeLimitedTask = { + fun getLoadTask(world: World, region: Region): WorkerTask = { load(world, region) } - fun getPasteTask(world: World, position: Vec3i): TimeLimitedTask = { + fun getPasteTask(world: World, position: Vec3i): WorkerTask = { paste(world, position) } diff --git a/src/main/kotlin/io/dico/parcels2/blockvisitor/WorkDispatcher.kt b/src/main/kotlin/io/dico/parcels2/blockvisitor/WorkDispatcher.kt new file mode 100644 index 0000000..7201a06 --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/blockvisitor/WorkDispatcher.kt @@ -0,0 +1,345 @@ +package io.dico.parcels2.blockvisitor + +import io.dico.parcels2.ParcelsPlugin +import io.dico.parcels2.logger +import io.dico.parcels2.util.ext.clampMin +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 kotlin.coroutines.Continuation +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn +import kotlin.coroutines.resume + +typealias WorkerTask = suspend WorkerScope.() -> Unit +typealias WorkerUpdateLister = Worker.(Double, Long) -> Unit + +data class TickWorktimeOptions(var workTime: Int, var tickInterval: Int) + +interface WorkDispatcher { + /** + * Submit a [task] that should be run synchronously, but limited such that it does not stall the server + * a bunch + */ + fun dispatch(task: WorkerTask): Worker + + /** + * Get a list of all workers + */ + val workers: List + + /** + * Attempts to complete any remaining tasks immediately, without suspension. + */ + fun completeAllTasks() +} + +interface WorkerAndScopeMembersUnion { + /** + * The time that elapsed since this worker was dispatched, in milliseconds + */ + val elapsedTime: Long + + /** + * A value indicating the progress of this worker, in the range 0.0 <= progress <= 1.0 + * with no guarantees to its accuracy. + */ + val progress: Double +} + +interface Worker : WorkerAndScopeMembersUnion { + /** + * The coroutine associated with this worker + */ + val job: Job + + /** + * true if this worker has completed + */ + val isComplete: Boolean + + /** + * If an exception was thrown during the execution of this task, + * returns that exception. Returns null otherwise. + */ + val completionException: Throwable? + + /** + * Calls the given [block] whenever the progress of this worker is updated, + * if [minInterval] milliseconds expired since the last call. + * The first call occurs after at least [minDelay] milliseconds in a likewise manner. + * Repeated invocations of this method result in an [IllegalStateException] + * + * if [asCompletionListener] is true, [onCompleted] is called with the same [block] + */ + fun onProgressUpdate(minDelay: Int, minInterval: Int, asCompletionListener: Boolean = true, block: WorkerUpdateLister): Worker + + /** + * Calls the given [block] when this worker completes, with the progress value 1.0. + * Multiple listeners may be registered to this function. + */ + fun onCompleted(block: WorkerUpdateLister): Worker + + /** + * Await completion of this worker + */ + suspend fun awaitCompletion() + + /** + * An object attached to this worker + */ + //val attachment: Any? +} + +interface WorkerScope : WorkerAndScopeMembersUnion { + /** + * A task should call this frequently during its execution, such that the timer can suspend it when necessary. + */ + suspend fun markSuspensionPoint() + + /** + * A task should call this method to indicate its progress + */ + fun setProgress(progress: Double) + + /** + * Indicate that this job is complete + */ + fun markComplete() = setProgress(1.0) + + /** + * Get a [WorkerScope] that is responsible for [portion] part of the progress + * If [portion] is negative, the remainder of the progress is used + */ + fun delegateWork(portion: Double = -1.0): WorkerScope +} + +inline fun WorkerScope.delegateWork(portion: Double = -1.0, block: WorkerScope.() -> T): T { + delegateWork(portion).apply { + val result = block() + markComplete() + return result + } +} + +interface WorkerInternal : Worker, WorkerScope { + /** + * Start or resumes the execution of this worker + * and returns true if the worker completed + * + * [worktime] is the maximum amount of time, in milliseconds, + * that this job may run for until suspension. + * + * If [worktime] is not positive, the worker will complete + * without suspension and this method will always return true. + */ + fun resume(worktime: Long): Boolean +} + +/** + * An object that controls one or more jobs, ensuring that they don't stall the server too much. + * There is a configurable maxiumum amount of milliseconds that can be allocated to all workers together in each server tick + * This object attempts to split that maximum amount of milliseconds equally between all jobs + */ +class BukkitWorkDispatcher(private val plugin: ParcelsPlugin, var options: TickWorktimeOptions) : WorkDispatcher { + // The currently registered bukkit scheduler task + private var bukkitTask: BukkitTask? = null + // The workers. + private val _workers = LinkedList() + override val workers: List = _workers + + override fun dispatch(task: WorkerTask): Worker { + 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) + return worker + } + + private fun tickJobs() { + val workers = _workers + if (workers.isEmpty()) return + val tickStartTime = System.currentTimeMillis() + + val iterator = workers.listIterator(index = 0) + while (iterator.hasNext()) { + val time = System.currentTimeMillis() + val timeElapsed = time - tickStartTime + val timeLeft = options.workTime - timeElapsed + if (timeLeft <= 0) return + + val count = workers.size - iterator.nextIndex() + val timePerJob = (timeLeft + count - 1) / count + val worker = iterator.next() + val completed = worker.resume(timePerJob) + if (completed) { + iterator.remove() + } + } + + if (workers.isEmpty()) { + bukkitTask?.cancel() + bukkitTask = null + } + } + + override fun completeAllTasks() { + _workers.forEach { + it.resume(-1) + } + _workers.clear() + bukkitTask?.cancel() + bukkitTask = null + } + +} + +private class WorkerImpl(scope: CoroutineScope, task: WorkerTask) : WorkerInternal { + override val job: Job = scope.launch(start = LAZY) { task() } + + private var continuation: Continuation? = null + private var nextSuspensionTime: Long = 0L + private var completeForcefully = false + private var isStarted = false + + override val elapsedTime + get() = + if (job.isCompleted) startTimeOrElapsedTime + else currentTimeMillis() - startTimeOrElapsedTime + + override val isComplete get() = job.isCompleted + + private var _progress = 0.0 + override val progress get() = _progress + override var completionException: Throwable? = null; private set + + private var startTimeOrElapsedTime: Long = 0L // startTime before completed, elapsed time otherwise + private var onProgressUpdate: WorkerUpdateLister? = null + private var progressUpdateInterval: Int = 0 + private var lastUpdateTime: Long = 0L + private var onCompleted: WorkerUpdateLister? = null + + init { + job.invokeOnCompletion { exception -> + // report any error that occurred + completionException = exception?.also { + if (it !is CancellationException) + logger.error("WorkerTask 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 + + return this + } + + override fun onCompleted(block: WorkerUpdateLister): Worker { + 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 + } + + override suspend fun markSuspensionPoint() { + if (System.currentTimeMillis() >= nextSuspensionTime && !completeForcefully) + suspendCoroutineUninterceptedOrReturn { cont: Continuation -> + continuation = cont + COROUTINE_SUSPENDED + } + } + + override fun setProgress(progress: Double) { + this._progress = progress + val onProgressUpdate = onProgressUpdate ?: return + val time = System.currentTimeMillis() + if (time > lastUpdateTime + progressUpdateInterval) { + onProgressUpdate(progress, elapsedTime) + lastUpdateTime = time + } + } + + override fun resume(worktime: Long): Boolean { + if (isComplete) return true + + if (worktime > 0) { + nextSuspensionTime = currentTimeMillis() + worktime + } else { + completeForcefully = true + } + + if (isStarted) { + continuation?.let { + continuation = null + it.resume(Unit) + return continuation == null + } + return true + } + + isStarted = true + startTimeOrElapsedTime = System.currentTimeMillis() + job.start() + + return continuation == null + } + + override suspend fun awaitCompletion() { + job.join() + } + + private fun delegateWork(curPortion: Double, portion: Double): WorkerScope = + DelegateScope(progress, curPortion * (if (portion < 0) 1.0 - progress else portion).clampMin(0.0)) + + override fun delegateWork(portion: Double): WorkerScope = delegateWork(1.0, portion) + + private inner class DelegateScope(val progressStart: Double, val portion: Double) : WorkerScope { + override val elapsedTime: Long + get() = this@WorkerImpl.elapsedTime + + override suspend fun markSuspensionPoint() = + this@WorkerImpl.markSuspensionPoint() + + override val progress: Double + get() = (this@WorkerImpl.progress - progressStart) / portion + + override fun setProgress(progress: Double) = + this@WorkerImpl.setProgress(progressStart + progress * portion) + + override fun delegateWork(portion: Double): WorkerScope = + this@WorkerImpl.delegateWork(this.portion, portion) + } +} diff --git a/src/main/kotlin/io/dico/parcels2/blockvisitor/WorktimeLimiter.kt b/src/main/kotlin/io/dico/parcels2/blockvisitor/WorktimeLimiter.kt deleted file mode 100644 index 553362e..0000000 --- a/src/main/kotlin/io/dico/parcels2/blockvisitor/WorktimeLimiter.kt +++ /dev/null @@ -1,361 +0,0 @@ -package io.dico.parcels2.blockvisitor - -import io.dico.parcels2.ParcelsPlugin -import io.dico.parcels2.logger -import io.dico.parcels2.util.ext.clampMin -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 kotlin.coroutines.Continuation -import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED -import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn -import kotlin.coroutines.resume - -typealias TimeLimitedTask = suspend WorkerScope.() -> Unit -typealias WorkerUpdateLister = Worker.(Double, Long) -> Unit - -data class TickWorktimeOptions(var workTime: Int, var tickInterval: Int) - -interface WorktimeLimiter { - /** - * Submit a [task] that should be run synchronously, but limited such that it does not stall the server - * a bunch - */ - fun submit(task: TimeLimitedTask): Worker - - /** - * Get a list of all workers - */ - val workers: List - - /** - * Attempts to complete any remaining tasks immediately, without suspension. - */ - fun completeAllTasks() -} - -interface Timed { - /** - * The time that elapsed since this worker was dispatched, in milliseconds - */ - val elapsedTime: Long -} - -interface Worker : Timed { - /** - * The coroutine associated with this worker - */ - val job: Job - - /** - * true if this worker has completed - */ - val isComplete: Boolean - - /** - * If an exception was thrown during the execution of this task, - * returns that exception. Returns null otherwise. - */ - val completionException: Throwable? - - /** - * A value indicating the progress of this worker, in the range 0.0 <= progress <= 1.0 - * with no guarantees to its accuracy. - */ - val progress: Double - - /** - * Calls the given [block] whenever the progress of this worker is updated, - * if [minInterval] milliseconds expired since the last call. - * The first call occurs after at least [minDelay] milliseconds in a likewise manner. - * Repeated invocations of this method result in an [IllegalStateException] - * - * if [asCompletionListener] is true, [onCompleted] is called with the same [block] - */ - fun onProgressUpdate(minDelay: Int, minInterval: Int, asCompletionListener: Boolean = true, block: WorkerUpdateLister): Worker - - /** - * Calls the given [block] when this worker completes, with the progress value 1.0. - * Multiple listeners may be registered to this function. - */ - fun onCompleted(block: WorkerUpdateLister): Worker - - /** - * Await completion of this worker - */ - suspend fun awaitCompletion() - - /** - * An object attached to this worker - */ - //val attachment: Any? -} - -interface WorkerScope : Timed { - /** - * A task should call this frequently during its execution, such that the timer can suspend it when necessary. - */ - suspend fun markSuspensionPoint() - - /** - * A value indicating the progress of this worker, in the range 0.0 <= progress <= 1.0 - * with no guarantees to its accuracy. - */ - val progress: Double - - /** - * A task should call this method to indicate its progress - */ - fun setProgress(progress: Double) - - /** - * Indicate that this job is complete - */ - fun markComplete() = setProgress(1.0) - - /** - * Get a [WorkerScope] that is responsible for [portion] part of the progress - * If [portion] is negative, the remainder of the progress is used - */ - fun delegateWork(portion: Double = -1.0): WorkerScope -} - -inline fun WorkerScope.delegateWork(portion: Double = -1.0, block: WorkerScope.() -> T): T { - delegateWork(portion).apply { - val result = block() - markComplete() - return result - } -} - -interface WorkerInternal : Worker, WorkerScope { - /** - * Start or resumes the execution of this worker - * and returns true if the worker completed - * - * [worktime] is the maximum amount of time, in milliseconds, - * that this job may run for until suspension. - * - * If [worktime] is not positive, the worker will complete - * without suspension and this method will always return true. - */ - fun resume(worktime: Long): Boolean -} - -/** - * An object that controls one or more jobs, ensuring that they don't stall the server too much. - * There is a configurable maxiumum amount of milliseconds that can be allocated to all workers together in each server tick - * This object attempts to split that maximum amount of milliseconds equally between all jobs - */ -class TickWorktimeLimiter(private val plugin: ParcelsPlugin, var options: TickWorktimeOptions) : WorktimeLimiter { - // The currently registered bukkit scheduler task - private var bukkitTask: BukkitTask? = null - // The workers. - private val _workers = LinkedList() - override val workers: List = _workers - - override fun submit(task: TimeLimitedTask): Worker { - 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) - return worker - } - - private fun tickJobs() { - val workers = _workers - if (workers.isEmpty()) return - val tickStartTime = System.currentTimeMillis() - - val iterator = workers.listIterator(index = 0) - while (iterator.hasNext()) { - val time = System.currentTimeMillis() - val timeElapsed = time - tickStartTime - val timeLeft = options.workTime - timeElapsed - if (timeLeft <= 0) return - - val count = workers.size - iterator.nextIndex() - val timePerJob = (timeLeft + count - 1) / count - val worker = iterator.next() - val completed = worker.resume(timePerJob) - if (completed) { - iterator.remove() - } - } - - if (workers.isEmpty()) { - bukkitTask?.cancel() - bukkitTask = null - } - } - - override fun completeAllTasks() { - _workers.forEach { - it.resume(-1) - } - _workers.clear() - bukkitTask?.cancel() - bukkitTask = null - } - -} - -private class WorkerImpl(scope: CoroutineScope, task: TimeLimitedTask) : WorkerInternal { - override val job: Job = scope.launch(start = LAZY) { task() } - - private var continuation: Continuation? = null - private var nextSuspensionTime: Long = 0L - private var completeForcefully = false - private var isStarted = false - - override val elapsedTime - get() = - if (job.isCompleted) startTimeOrElapsedTime - else currentTimeMillis() - startTimeOrElapsedTime - - override val isComplete get() = job.isCompleted - - private var _progress = 0.0 - override val progress get() = _progress - override var completionException: Throwable? = null; private set - - private var startTimeOrElapsedTime: Long = 0L // startTime before completed, elapsed time otherwise - private var onProgressUpdate: WorkerUpdateLister? = null - private var progressUpdateInterval: Int = 0 - private var lastUpdateTime: Long = 0L - private var onCompleted: WorkerUpdateLister? = null - - init { - job.invokeOnCompletion { exception -> - // report any error that occurred - completionException = exception?.also { - if (it !is CancellationException) - 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 - - return this - } - - override fun onCompleted(block: WorkerUpdateLister): Worker { - 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 - } - - override suspend fun markSuspensionPoint() { - if (System.currentTimeMillis() >= nextSuspensionTime && !completeForcefully) - suspendCoroutineUninterceptedOrReturn { cont: Continuation -> - continuation = cont - COROUTINE_SUSPENDED - } - } - - override fun setProgress(progress: Double) { - this._progress = progress - val onProgressUpdate = onProgressUpdate ?: return - val time = System.currentTimeMillis() - if (time > lastUpdateTime + progressUpdateInterval) { - onProgressUpdate(progress, elapsedTime) - lastUpdateTime = time - } - } - - override fun resume(worktime: Long): Boolean { - if (isComplete) return true - - if (worktime > 0) { - nextSuspensionTime = currentTimeMillis() + worktime - } else { - completeForcefully = true - } - - if (isStarted) { - continuation?.let { - continuation = null - it.resume(Unit) - return continuation == null - } - return true - } - - startTimeOrElapsedTime = System.currentTimeMillis() - job.start() - - return continuation == null - } - - override suspend fun awaitCompletion() { - job.join() - } - - private fun delegateWork(curPortion: Double, portion: Double): WorkerScope = - DelegateScope(progress, curPortion * (if (portion < 0) 1.0 - progress else portion).clampMin(0.0)) - - override fun delegateWork(portion: Double): WorkerScope = delegateWork(1.0, portion) - - private inner class DelegateScope(val progressStart: Double, val portion: Double) : WorkerScope { - override val elapsedTime: Long - get() = this@WorkerImpl.elapsedTime - - override suspend fun markSuspensionPoint() = - this@WorkerImpl.markSuspensionPoint() - - override val progress: Double - get() = (this@WorkerImpl.progress - progressStart) / portion - - override fun setProgress(progress: Double) = - this@WorkerImpl.setProgress(progressStart + progress * portion) - - override fun delegateWork(portion: Double): WorkerScope = - this@WorkerImpl.delegateWork(this.portion, portion) - } -} - -/* -/** - * While the implementation of [kotlin.coroutines.experimental.intrinsics.intercepted] is intrinsic, it should look something like this - * We don't care for intercepting the coroutine as we want it to resume immediately when we call resume(). - * Thus, above, we use an unintercepted suspension. It's not necessary as the dispatcher (or interceptor) also calls it synchronously, but whatever. - */ -private fun Continuation.interceptedImpl(): Continuation { - return context[ContinuationInterceptor]?.interceptContinuation(this) ?: this -} - */ diff --git a/src/main/kotlin/io/dico/parcels2/command/CommandsDebug.kt b/src/main/kotlin/io/dico/parcels2/command/CommandsDebug.kt index 969d964..b6c7acd 100644 --- a/src/main/kotlin/io/dico/parcels2/command/CommandsDebug.kt +++ b/src/main/kotlin/io/dico/parcels2/command/CommandsDebug.kt @@ -8,7 +8,6 @@ import io.dico.dicore.command.annotation.Cmd import io.dico.parcels2.ParcelsPlugin import io.dico.parcels2.Privilege import io.dico.parcels2.blockvisitor.RegionTraverser -import io.dico.parcels2.blockvisitor.TickWorktimeLimiter import io.dico.parcels2.doBlockOperation import org.bukkit.Bukkit import org.bukkit.Material @@ -78,15 +77,15 @@ class CommandsDebug(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) { @Cmd("visitors") fun cmdVisitors(): Any? { - val workers = plugin.worktimeLimiter.workers + val workers = plugin.workDispatcher.workers println(workers.map { it.job }.joinToString(separator = "\n")) return "Task count: ${workers.size}" } @Cmd("force_visitors") fun cmdForceVisitors(): Any? { - val workers = plugin.worktimeLimiter.workers - plugin.worktimeLimiter.completeAllTasks() + val workers = plugin.workDispatcher.workers + plugin.workDispatcher.completeAllTasks() return "Task count: ${workers.size}" } diff --git a/src/main/kotlin/io/dico/parcels2/command/CommandsGeneral.kt b/src/main/kotlin/io/dico/parcels2/command/CommandsGeneral.kt index 750fd96..67b873b 100644 --- a/src/main/kotlin/io/dico/parcels2/command/CommandsGeneral.kt +++ b/src/main/kotlin/io/dico/parcels2/command/CommandsGeneral.kt @@ -16,7 +16,7 @@ import io.dico.parcels2.util.ext.uuid import org.bukkit.block.Biome import org.bukkit.entity.Player -class CommandsGeneral(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) { +class CommandsGeneral(plugin: ParcelsPlugin, parent: SpecialCommandAddress) : AbstractParcelCommands(plugin) { @Cmd("auto") @Desc( @@ -43,6 +43,10 @@ class CommandsGeneral(plugin: ParcelsPlugin) : AbstractParcelCommands(plugin) { ) fun ParcelScope.cmdInfo(player: Player) = parcel.infoString + init { + parent.addSpeciallyTreatedKeys("home", "h") + } + @Cmd("home", aliases = ["h"]) @Desc( "Teleports you to your parcels,", diff --git a/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt b/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt index 1eddf97..1f1e4a7 100644 --- a/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt +++ b/src/main/kotlin/io/dico/parcels2/command/ParcelCommandBuilder.kt @@ -1,8 +1,6 @@ package io.dico.parcels2.command -import io.dico.dicore.command.CommandBuilder -import io.dico.dicore.command.ICommandAddress -import io.dico.dicore.command.ICommandDispatcher +import io.dico.dicore.command.* import io.dico.dicore.command.registration.reflect.ReflectiveRegistration import io.dico.parcels2.Interactables import io.dico.parcels2.ParcelsPlugin @@ -13,14 +11,16 @@ import java.util.Queue @Suppress("UsePropertyAccessSyntax") fun getParcelCommands(plugin: ParcelsPlugin): ICommandDispatcher = with(CommandBuilder()) { + val parcelsAddress = SpecialCommandAddress() + setChatController(ParcelsChatController()) addParameterType(false, ParcelParameterType(plugin.parcelProvider)) addParameterType(false, ProfileParameterType()) - addParameterType(true, ParcelTarget.PType(plugin.parcelProvider)) + addParameterType(true, ParcelTarget.PType(plugin.parcelProvider, parcelsAddress)) - group("parcel", "plot", "plots", "p") { + group(parcelsAddress, "parcel", "plot", "plots", "p") { addRequiredPermission("parcels.command") - registerCommands(CommandsGeneral(plugin)) + registerCommands(CommandsGeneral(plugin, parcelsAddress)) registerCommands(CommandsPrivilegesLocal(plugin)) group("option", "opt", "o") { @@ -63,6 +63,12 @@ inline fun CommandBuilder.group(name: String, vararg aliases: String, config: Co parent() } +inline fun CommandBuilder.group(address: ICommandAddress, name: String, vararg aliases: String, config: CommandBuilder.() -> Unit) { + group(address, name, *aliases) + config() + parent() +} + private fun CommandBuilder.generateHelpAndSyntaxCommands(): CommandBuilder { generateCommands(dispatcher as ICommandAddress, "help", "syntax") return this @@ -80,3 +86,37 @@ private fun generateCommands(address: ICommandAddress, vararg names: String) { } } } + +class SpecialCommandAddress : ChildCommandAddress() { + private val speciallyTreatedKeys = mutableListOf() + + // Used to allow /p h:1 syntax, which is the same as what PlotMe uses. + var speciallyParsedIndex: Int? = null; private set + + fun addSpeciallyTreatedKeys(vararg keys: String) { + for (key in keys) { + speciallyTreatedKeys.add(key + ":") + } + } + + @Throws(CommandException::class) + override fun getChild(key: String, context: ExecutionContext): ChildCommandAddress? { + speciallyParsedIndex = null + + for (specialKey in speciallyTreatedKeys) { + if (key.startsWith(specialKey)) { + val result = getChild(specialKey.substring(0, specialKey.length - 1)) + ?: return null + + val text = key.substring(specialKey.length) + val num = text.toIntOrNull() ?: throw CommandException("$text is not a number") + speciallyParsedIndex = num + + return result + } + } + + return super.getChild(key) + } + +} diff --git a/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt b/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt index 956da94..8c0d718 100644 --- a/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt +++ b/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt @@ -83,7 +83,7 @@ sealed class ParcelTarget(val world: ParcelWorld, val parsedKind: Int, val isDef } } - class PType(val parcelProvider: ParcelProvider) : ParameterType(ParcelTarget::class.java, Config) { + class PType(val parcelProvider: ParcelProvider, val parcelAddress: SpecialCommandAddress? = null) : ParameterType(ParcelTarget::class.java, Config) { override fun parse(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): ParcelTarget { var input = buffer.next() @@ -124,11 +124,25 @@ sealed class ParcelTarget(val world: ParcelWorld, val parsedKind: Int, val isDef val ownerString: String val index: Int? + val speciallyParsedIndex = parcelAddress?.speciallyParsedIndex + if (splitIdx == -1) { - // just the index. - index = input.toIntOrNull() - ownerString = if (index == null) input else "" + + if (speciallyParsedIndex == null) { + // just the index. + index = input.toIntOrNull() + ownerString = if (index == null) input else "" + } else { + // just the owner. + index = speciallyParsedIndex + ownerString = input + } + } else { + if (speciallyParsedIndex != null) { + invalidInput(parameter, "Duplicate home index") + } + ownerString = input.substring(0, splitIdx) val indexString = input.substring(splitIdx + 1) @@ -165,7 +179,7 @@ sealed class ParcelTarget(val world: ParcelWorld, val parsedKind: Int, val isDef return ByID(world, id, kind, true) } - return ByOwner(world, PlayerProfile(player), 0, kind, true) + return ByOwner(world, PlayerProfile(player), parcelAddress?.speciallyParsedIndex ?: 0, kind, true) } } diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt index dddb221..90cadc6 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt @@ -119,9 +119,9 @@ class DefaultParcelGenerator( worldId: ParcelWorldId, container: ParcelContainer, coroutineScope: CoroutineScope, - worktimeLimiter: WorktimeLimiter + workDispatcher: WorkDispatcher ): Pair { - return ParcelLocatorImpl(worldId, container) to ParcelBlockManagerImpl(worldId, coroutineScope, worktimeLimiter) + return ParcelLocatorImpl(worldId, container) to ParcelBlockManagerImpl(worldId, coroutineScope, workDispatcher) } private inline fun convertBlockLocationToId(x: Int, z: Int, mapper: (Int, Int) -> T): T? { @@ -156,7 +156,7 @@ class DefaultParcelGenerator( private inner class ParcelBlockManagerImpl( val worldId: ParcelWorldId, coroutineScope: CoroutineScope, - override val worktimeLimiter: WorktimeLimiter + override val workDispatcher: WorkDispatcher ) : ParcelBlockManagerBase(), CoroutineScope by coroutineScope { override val world: World = this@DefaultParcelGenerator.world override val parcelTraverser: RegionTraverser = RegionTraverser.convergingTo(o.floorHeight) @@ -222,12 +222,12 @@ class DefaultParcelGenerator( return world.getParcelById(parcelId) } - override fun submitBlockVisitor(vararg parcelIds: ParcelId, task: TimeLimitedTask): Worker { + override fun submitBlockVisitor(vararg parcelIds: ParcelId, task: WorkerTask): Worker { val parcels = parcelIds.mapNotNull { getParcel(it) } - if (parcels.isEmpty()) return worktimeLimiter.submit(task) + if (parcels.isEmpty()) return workDispatcher.dispatch(task) if (parcels.any { it.hasBlockVisitors }) throw IllegalArgumentException("This parcel already has a block visitor") - val worker = worktimeLimiter.submit(task) + val worker = workDispatcher.dispatch(task) for (parcel in parcels) { launch(start = UNDISPATCHED) { @@ -277,10 +277,10 @@ class DefaultParcelGenerator( } override fun swapParcels(parcel1: ParcelId, parcel2: ParcelId): Worker = submitBlockVisitor(parcel1, parcel2) { - val schematicOf1 = delegateWork(0.15) { Schematic().apply { load(world, getRegion(parcel1)) } } - val schematicOf2 = delegateWork(0.15) { Schematic().apply { load(world, getRegion(parcel2)) } } - delegateWork(0.35) { with(schematicOf1) { paste(world, getRegion(parcel2).origin) } } - delegateWork(0.35) { with(schematicOf2) { paste(world, getRegion(parcel1).origin) } } + val schematicOf1 = delegateWork(0.25) { Schematic().apply { load(world, getRegion(parcel1)) } } + val schematicOf2 = delegateWork(0.25) { Schematic().apply { load(world, getRegion(parcel2)) } } + delegateWork(0.25) { with(schematicOf1) { paste(world, getRegion(parcel2).origin) } } + delegateWork(0.25) { with(schematicOf2) { paste(world, getRegion(parcel1).origin) } } } override fun getParcelsWithOwnerBlockIn(chunk: Chunk): Collection { diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt index 1112047..ff2fcd4 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt @@ -59,7 +59,7 @@ class ParcelProviderImpl(val plugin: ParcelsPlugin) : ParcelProvider { else WorldCreator(worldName).generator(generator).createWorld().also { logger.info("Creating world $worldName") } parcelWorld = ParcelWorldImpl(bukkitWorld, generator, worldOptions.runtime, plugin.storage, - plugin.globalPrivileges, ::DefaultParcelContainer, plugin, plugin.worktimeLimiter) + plugin.globalPrivileges, ::DefaultParcelContainer, plugin, plugin.workDispatcher) if (!worldExists) { val time = DateTime.now() diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelWorldImpl.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelWorldImpl.kt index c143ff6..24bad79 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelWorldImpl.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelWorldImpl.kt @@ -3,7 +3,7 @@ package io.dico.parcels2.defaultimpl import io.dico.parcels2.* -import io.dico.parcels2.blockvisitor.WorktimeLimiter +import io.dico.parcels2.blockvisitor.WorkDispatcher import io.dico.parcels2.options.RuntimeWorldOptions import io.dico.parcels2.storage.Storage import kotlinx.coroutines.CoroutineScope @@ -18,7 +18,7 @@ class ParcelWorldImpl(override val world: World, override val globalPrivileges: GlobalPrivilegesManager, containerFactory: ParcelContainerFactory, coroutineScope: CoroutineScope, - worktimeLimiter: WorktimeLimiter) + workDispatcher: WorkDispatcher) : ParcelWorld, ParcelWorldId, ParcelContainer, /* missing delegation */ @@ -39,7 +39,7 @@ class ParcelWorldImpl(override val world: World, override val blockManager: ParcelBlockManager init { - val pair = generator.makeParcelLocatorAndBlockManager(id, container, coroutineScope, worktimeLimiter) + val pair = generator.makeParcelLocatorAndBlockManager(id, container, coroutineScope, workDispatcher) locator = pair.first blockManager = pair.second diff --git a/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt b/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt index 9d91cda..f838950 100644 --- a/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt +++ b/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt @@ -198,8 +198,8 @@ class ParcelListeners( val type = clickedBlock.type val interactableClass = Interactables[type] - if (interactableClass != null && (parcel.effectiveInteractableConfig.isInteractable(type) || (parcel != null && parcel.canBuild(user)))) { - user.sendParcelMessage(nopermit = true, message = "You cannot interact with ${interactableClass.name} in this parcel") + if (interactableClass != null && !parcel.effectiveInteractableConfig.isInteractable(type) && (parcel == null || !parcel.canBuild(user))) { + user.sendParcelMessage(nopermit = true, message = "You cannot interact with ${interactableClass.name} here") event.isCancelled = true return@l } @@ -595,4 +595,16 @@ class ParcelListeners( storage.updatePlayerName(event.player.uuid, event.player.name) } + /** + * Attempts to prevent redstone contraptions from breaking while they are being swapped + * Might remove if it causes lag + */ + @ListenerMarker + val onBlockRedstoneEvent = RegistratorListener l@{ event -> + val (_, area) = getWorldAndArea(event.block) ?: return@l + if (area == null || area.hasBlockVisitors) { + event.newCurrent = event.oldCurrent + } + } + } \ 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 36a51c1..af71dc4 100644 --- a/src/main/kotlin/io/dico/parcels2/util/Vec3i.kt +++ b/src/main/kotlin/io/dico/parcels2/util/Vec3i.kt @@ -26,6 +26,7 @@ data class Vec3i( val z: Int ) { operator fun plus(o: Vec3i) = Vec3i(x + o.x, y + o.y, z + o.z) + operator fun minus(o: Vec3i) = Vec3i(x - o.x, y - o.y, z - o.z) infix fun addX(o: Int) = Vec3i(x + o, y, z) infix fun addY(o: Int) = Vec3i(x, y + o, z) infix fun addZ(o: Int) = Vec3i(x, y, z + o) diff --git a/todo.md b/todo.md index 93e9c02..03d491b 100644 --- a/todo.md +++ b/todo.md @@ -6,16 +6,16 @@ Basically all admin commands. * ~~setowner~~ * ~~dispose~~ * ~~reset~~ -* swap +* ~~swap~~ * New admin commands that I can't think of right now. Also * ~~setbiome~~ * random -Modify home command: +~~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 +* ~~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).~~ @@ -23,32 +23,34 @@ to allow inheriting permissions properly).~~ Parcel Options - -Parcel options apply to any player with `DEFAULT` added status. +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`) +~~Move existing options to "interact" namespace (`/p o interact`) +Add classes for different things you can interact with~~ -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. +~~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. + +* 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 +~~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. +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 +~~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. +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) @@ -79,6 +81,6 @@ Implement a container that doesn't require loading all parcel data on startup (C ~~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 +~~Store player status on parcel (allowed, default banned) as a number to allow for future additions to this set of possibilities~~ -- cgit v1.2.3