Archived
0

Make progress

This commit is contained in:
Dico
2018-09-26 07:08:42 +01:00
parent 2225bdae95
commit 520ae530d2
25 changed files with 434 additions and 281 deletions

View File

@@ -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;

View File

@@ -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();
for (n = contextFilters.size(); i < n; i++) {
contextFilters.get(i).filterContext(executionContext);
}
//System.out.println("Post-contextfilters");
String message = execute(sender, executionContext);
caller.getChatController().sendMessage(sender, EMessageType.RESULT, message);
executeWithContext(executionContext);
} catch (Throwable t) {
caller.getChatController().handleException(sender, executionContext, t);
}
}
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);
}
context.parseParameters();
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<String> 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();
}
}
executionContext.parseParametersQuietly();
return tabComplete(sender, executionContext, location);
public List<String> tabCompleteWithContext(ExecutionContext context, Location location) throws CommandException {
int i, n;
for (i = 0, n = contextFilterPostParameterIndex; i < n; i++) {
contextFilters.get(i).filterContext(context);
}
context.parseParametersQuietly();
return tabComplete(context.getSender(), context, location);
}
public List<String> tabComplete(CommandSender sender, ExecutionContext context, Location location) {

View File

@@ -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}.
*
* <p>The address must not have a parent or any keys</p>
*
* @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...)}

View File

@@ -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();
}

View File

@@ -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
*

View File

@@ -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)}
* <p>
* 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
*

View File

@@ -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");

View File

@@ -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();
ExecutionContext context = new ExecutionContext(sender, false);
if (target == null || target instanceof DefaultGroupCommand) {
if (targetAddress.hasHelpCommand()) {
target = targetAddress.getHelpCommand().getCommand();
} else if (target == null){
return 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<String> getTabCompletions(CommandSender sender, Location location, ArgumentBuffer buffer) {
ICommandAddress target = getCommandTarget(sender, buffer);
List<String> out = target.hasCommand() ? target.getCommand().tabComplete(sender, target, location, buffer.getUnaffectingCopy()) : Collections.emptyList();
ExecutionContext context = new ExecutionContext(sender, true);
int cursor = buffer.getCursor();
String input;
if (cursor >= buffer.size()) {
input = "";
} else {
input = buffer.get(cursor).toLowerCase();
}
try {
ICommandAddress target = getCommandTarget(context, buffer);
List<String> out = target.hasCommand()
? target.getCommand().tabComplete(sender, target, location, buffer.getUnaffectingCopy())
: Collections.emptyList();
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);
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;
}
out.add(child);
}
}
return out;
} catch (CommandException ex) {
return Collections.emptyList();
}
return out;
}
}

View File

@@ -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];
}

View File

@@ -37,12 +37,12 @@ abstract class ParcelGenerator : ChunkGenerator() {
abstract fun makeParcelLocatorAndBlockManager(worldId: ParcelWorldId,
container: ParcelContainer,
coroutineScope: CoroutineScope,
worktimeLimiter: WorktimeLimiter): Pair<ParcelLocator, ParcelBlockManager>
workDispatcher: WorkDispatcher): Pair<ParcelLocator, ParcelBlockManager>
}
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

View File

@@ -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.")
}

View File

@@ -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<T : BlockState> : ExtraBlockChange {
abstract val stateClass: KClass<T>
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<Sign>() {
val lines = state.lines
override val stateClass: KClass<Sign>
get() = Sign::class
override fun update(state: Sign) {
for (i in lines.indices) {
val line = lines[i]
state.setLine(i, line)
}
}
}

View File

@@ -235,87 +235,3 @@ inline class TraverseDirection(val bits: Int) {
}
}
/*
private typealias Scope = SequenceScope<Vec3i>
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))
}
}
}
}
}
}
}
*/

View File

@@ -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<BlockData?>? = null
private val extra = mutableMapOf<Vec3i, (Block) -> Unit>()
private val extra = mutableListOf<Pair<Vec3i, ExtraBlockChange>>()
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<Vec3i, BlockData>()
// 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)
val total = region.blockCount.toDouble()
var processed = 0
if (!postponed.containsKey(supportingBlock) && traverser.comesFirst(vec, supportingBlock)) {
block.blockData = type
} else {
postponed[vec] = type
}
for ((index, vec) in blocks.withIndex()) {
markSuspensionPoint()
setProgress(index / total)
} else {
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<Vec3i, BlockData>()
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<Vec3i, BlockData>()
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)
}

View File

@@ -16,17 +16,17 @@ import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
import kotlin.coroutines.resume
typealias TimeLimitedTask = suspend WorkerScope.() -> Unit
typealias WorkerTask = suspend WorkerScope.() -> Unit
typealias WorkerUpdateLister = Worker.(Double, Long) -> Unit
data class TickWorktimeOptions(var workTime: Int, var tickInterval: Int)
interface WorktimeLimiter {
interface WorkDispatcher {
/**
* 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
fun dispatch(task: WorkerTask): Worker
/**
* Get a list of all workers
@@ -39,14 +39,20 @@ interface WorktimeLimiter {
fun completeAllTasks()
}
interface Timed {
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 : Timed {
interface Worker : WorkerAndScopeMembersUnion {
/**
* The coroutine associated with this worker
*/
@@ -63,12 +69,6 @@ interface Worker : Timed {
*/
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.
@@ -96,18 +96,12 @@ interface Worker : Timed {
//val attachment: Any?
}
interface WorkerScope : Timed {
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 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
*/
@@ -152,14 +146,14 @@ interface WorkerInternal : Worker, WorkerScope {
* 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 {
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<WorkerInternal>()
override val workers: List<Worker> = _workers
override fun submit(task: TimeLimitedTask): Worker {
override fun dispatch(task: WorkerTask): Worker {
val worker: WorkerInternal = WorkerImpl(plugin, task)
if (bukkitTask == null) {
@@ -209,7 +203,7 @@ class TickWorktimeLimiter(private val plugin: ParcelsPlugin, var options: TickWo
}
private class WorkerImpl(scope: CoroutineScope, task: TimeLimitedTask) : WorkerInternal {
private class WorkerImpl(scope: CoroutineScope, task: WorkerTask) : WorkerInternal {
override val job: Job = scope.launch(start = LAZY) { task() }
private var continuation: Continuation<Unit>? = null
@@ -239,7 +233,7 @@ private class WorkerImpl(scope: CoroutineScope, task: TimeLimitedTask) : WorkerI
// report any error that occurred
completionException = exception?.also {
if (it !is CancellationException)
logger.error("TimeLimitedTask generated an exception", it)
logger.error("WorkerTask generated an exception", it)
}
// convert to elapsed time here
@@ -316,6 +310,7 @@ private class WorkerImpl(scope: CoroutineScope, task: TimeLimitedTask) : WorkerI
return true
}
isStarted = true
startTimeOrElapsedTime = System.currentTimeMillis()
job.start()
@@ -348,14 +343,3 @@ private class WorkerImpl(scope: CoroutineScope, task: TimeLimitedTask) : WorkerI
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 <T> Continuation<T>.interceptedImpl(): Continuation<T> {
return context[ContinuationInterceptor]?.interceptContinuation(this) ?: this
}
*/

View File

@@ -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}"
}

View File

@@ -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,",

View File

@@ -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<String>()
// 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)
}
}

View File

@@ -83,7 +83,7 @@ sealed class ParcelTarget(val world: ParcelWorld, val parsedKind: Int, val isDef
}
}
class PType(val parcelProvider: ParcelProvider) : ParameterType<ParcelTarget, Int>(ParcelTarget::class.java, Config) {
class PType(val parcelProvider: ParcelProvider, val parcelAddress: SpecialCommandAddress? = null) : ParameterType<ParcelTarget, Int>(ParcelTarget::class.java, Config) {
override fun parse(parameter: Parameter<ParcelTarget, Int>, 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)
}
}

View File

@@ -119,9 +119,9 @@ class DefaultParcelGenerator(
worldId: ParcelWorldId,
container: ParcelContainer,
coroutineScope: CoroutineScope,
worktimeLimiter: WorktimeLimiter
workDispatcher: WorkDispatcher
): Pair<ParcelLocator, ParcelBlockManager> {
return ParcelLocatorImpl(worldId, container) to ParcelBlockManagerImpl(worldId, coroutineScope, worktimeLimiter)
return ParcelLocatorImpl(worldId, container) to ParcelBlockManagerImpl(worldId, coroutineScope, workDispatcher)
}
private inline fun <T> 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<Vec2i> {

View File

@@ -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()

View File

@@ -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

View File

@@ -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<BlockRedstoneEvent> l@{ event ->
val (_, area) = getWorldAndArea(event.block) ?: return@l
if (area == null || area.hasBlockVisitors) {
event.newCurrent = event.oldCurrent
}
}
}

View File

@@ -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)

32
todo.md
View File

@@ -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~~