From 5ef2584fdb6e4db482aa4c57e6ecf0202c67a48d Mon Sep 17 00:00:00 2001 From: Dico Karssiens Date: Sat, 17 Nov 2018 21:32:43 +0000 Subject: Tweak some command stuff, clear/swap entities --- build.gradle.kts | 19 +- .../io/dico/dicore/command/RootCommandAddress.java | 556 +++++++------- .../dicore/command/parameter/ContextParser.java | 554 +++++++------- .../registration/reflect/ReflectiveCallFlags.java | 186 +++++ .../registration/reflect/ReflectiveCommand.java | 356 +++++---- .../reflect/ReflectiveRegistration.java | 821 ++++++++++----------- .../reflect/KotlinReflectiveRegistration.kt | 135 ++-- src/main/kotlin/io/dico/parcels2/JobDispatcher.kt | 705 +++++++++--------- .../kotlin/io/dico/parcels2/ParcelGenerator.kt | 205 +++-- src/main/kotlin/io/dico/parcels2/ParcelWorld.kt | 206 +++--- src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt | 305 ++++---- src/main/kotlin/io/dico/parcels2/PlayerProfile.kt | 390 +++++----- .../io/dico/parcels2/blockvisitor/Entities.kt | 38 + .../dico/parcels2/command/ParcelParameterTypes.kt | 173 ++--- .../io/dico/parcels2/command/ParcelTarget.kt | 406 +++++----- .../parcels2/defaultimpl/DefaultParcelGenerator.kt | 767 +++++++++---------- .../parcels2/defaultimpl/ParcelProviderImpl.kt | 505 +++++++------ .../parcels2/storage/exposed/ExposedBacking.kt | 566 +++++++------- .../kotlin/io/dico/parcels2/util/BukkitUtil.kt | 37 +- .../kotlin/io/dico/parcels2/util/PluginAware.kt | 18 + .../io/dico/parcels2/util/PluginScheduler.kt | 20 - .../kotlin/io/dico/parcels2/util/math/Vec3d.kt | 112 +-- .../kotlin/io/dico/parcels2/util/math/Vec3i.kt | 212 +++--- src/main/kotlin/io/dico/parcels2/util/parallel.kt | 9 + todo.md | 206 +++--- 25 files changed, 3953 insertions(+), 3554 deletions(-) create mode 100644 dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/ReflectiveCallFlags.java create mode 100644 src/main/kotlin/io/dico/parcels2/blockvisitor/Entities.kt create mode 100644 src/main/kotlin/io/dico/parcels2/util/PluginAware.kt delete mode 100644 src/main/kotlin/io/dico/parcels2/util/PluginScheduler.kt create mode 100644 src/main/kotlin/io/dico/parcels2/util/parallel.kt diff --git a/build.gradle.kts b/build.gradle.kts index 211a978..9194e56 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,7 +13,7 @@ version = "0.2" plugins { java - kotlin("jvm") version "1.3.0-rc-146" + kotlin("jvm") version "1.3.0" id("com.github.johnrengelman.plugin-shadow") version "2.0.3" } @@ -30,10 +30,11 @@ allprojects { maven("https://dl.bintray.com/kotlin/kotlin-dev/") maven("https://dl.bintray.com/kotlin/kotlin-eap/") maven("https://dl.bintray.com/kotlin/kotlinx/") + maven("http://maven.sk89q.com/repo") } dependencies { - val spigotVersion = "1.13.1-R0.1-SNAPSHOT" + val spigotVersion = "1.13.2-R0.1-SNAPSHOT" c.provided("org.bukkit:bukkit:$spigotVersion") { isTransitive = false } c.provided("org.spigotmc:spigot-api:$spigotVersion") { isTransitive = false } @@ -52,13 +53,15 @@ project(":dicore3:dicore3-core") { } } +val coroutinesCore = kotlinx("coroutines-core:0.26.1-eap13") + project(":dicore3:dicore3-command") { apply() dependencies { c.kotlinStd(kotlin("stdlib-jdk8")) c.kotlinStd(kotlin("reflect")) - c.kotlinStd(kotlinx("coroutines-core:0.26.1-eap13")) + c.kotlinStd(coroutinesCore) compile(project(":dicore3:dicore3-core")) compile("com.thoughtworks.paranamer:paranamer:2.8") @@ -72,12 +75,13 @@ dependencies { c.kotlinStd(kotlin("stdlib-jdk8")) c.kotlinStd(kotlin("reflect")) - c.kotlinStd(kotlinx("coroutines-core:0.26.1-eap13")) - c.kotlinStd("org.jetbrains.kotlinx:atomicfu-common:0.11.7-eap13") + c.kotlinStd(coroutinesCore) + c.kotlinStd("org.jetbrains.kotlinx:atomicfu-common:0.11.12") // not on sk89q maven repo yet - compileClasspath(files("$rootDir/debug/plugins/worldedit-bukkit-7.0.0-beta-01.jar")) - compileClasspath(files("$rootDir/debug/lib/spigot-1.13.1.jar")) + //compileClasspath(files("$rootDir/debug/plugins/worldedit-bukkit-7.0.0-beta-01.jar")) + // compileClasspath(files("$rootDir/debug/lib/spigot-1.13.2.jar")) + compileClasspath("com.sk89q.worldedit:worldedit-bukkit:7.0.0-SNAPSHOT") compile("org.jetbrains.exposed:exposed:0.10.5") { isTransitive = false } compile("joda-time:joda-time:2.10") @@ -167,7 +171,6 @@ val ConfigurationContainer.`kotlinStd`: Configuration get() = findByName("kotlinStd") ?: create("kotlinStd").let { compileClasspath.extendsFrom(it) } fun Jar.fromFiles(files: Iterable) { - return afterEvaluate { from(*files.map { if (it.isDirectory) it else zipTree(it) }.toTypedArray()) } } 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 2b70eaf..4df8c49 100644 --- a/dicore3/command/src/main/java/io/dico/dicore/command/RootCommandAddress.java +++ b/dicore3/command/src/main/java/io/dico/dicore/command/RootCommandAddress.java @@ -1,275 +1,281 @@ -package io.dico.dicore.command; - -import io.dico.dicore.command.parameter.ArgumentBuffer; -import io.dico.dicore.command.registration.BukkitCommand; -import org.bukkit.Location; -import org.bukkit.command.CommandSender; - -import java.util.*; - -public class RootCommandAddress extends ModifiableCommandAddress implements ICommandDispatcher { - @Deprecated - public static final RootCommandAddress INSTANCE = new RootCommandAddress(); - - public RootCommandAddress() { - } - - @Override - public Command getCommand() { - return null; - } - - @Override - public boolean isRoot() { - return true; - } - - @Override - public List getNames() { - return Collections.emptyList(); - } - - @Override - public ModifiableCommandAddress getParent() { - return null; - } - - @Override - public String getMainKey() { - return null; - } - - @Override - public String getAddress() { - return ""; - } - - @Override - public void registerToCommandMap(String fallbackPrefix, Map map, EOverridePolicy overridePolicy) { - Objects.requireNonNull(overridePolicy); - //debugChildren(this); - Map children = this.children; - Map wrappers = new IdentityHashMap<>(); - - for (ChildCommandAddress address : children.values()) { - if (!wrappers.containsKey(address)) { - wrappers.put(address, new BukkitCommand(address)); - } - } - - for (Map.Entry entry : children.entrySet()) { - String key = entry.getKey(); - ChildCommandAddress address = entry.getValue(); - boolean override = overridePolicy == EOverridePolicy.OVERRIDE_ALL; - if (!override && key.equals(address.getMainKey())) { - override = overridePolicy == EOverridePolicy.MAIN_KEY_ONLY || overridePolicy == EOverridePolicy.MAIN_AND_FALLBACK; - } - - registerMember(map, key, wrappers.get(address), override); - - if (fallbackPrefix != null) { - key = fallbackPrefix + key; - override = overridePolicy != EOverridePolicy.OVERRIDE_NONE && overridePolicy != EOverridePolicy.MAIN_KEY_ONLY; - registerMember(map, key, wrappers.get(address), override); - } - } - - } - - private static void debugChildren(ModifiableCommandAddress address) { - Collection keys = address.getChildrenMainKeys(); - for (String key : keys) { - ChildCommandAddress child = address.getChild(key); - System.out.println(child.getAddress()); - debugChildren(child); - } - } - - private static void registerMember(Map map, - String key, org.bukkit.command.Command value, boolean override) { - if (override) { - map.put(key, value); - } else { - map.putIfAbsent(key, value); - } - } - - @Override - public void unregisterFromCommandMap(Map map) { - Set children = new HashSet<>(this.children.values()); - Iterator> iterator = map.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); - org.bukkit.command.Command cmd = entry.getValue(); - if (cmd instanceof BukkitCommand && children.contains(((BukkitCommand) cmd).getOrigin())) { - iterator.remove(); - } - } - } - - @Override - public ModifiableCommandAddress getDeepChild(ArgumentBuffer buffer) { - ModifiableCommandAddress cur = this; - ChildCommandAddress child; - while (buffer.hasNext()) { - child = cur.getChild(buffer.next()); - if (child == null) { - buffer.rewind(); - return cur; - } - - cur = child; - } - return cur; - } - - @Override - public ModifiableCommandAddress getCommandTarget(CommandSender sender, ArgumentBuffer buffer) { - ModifiableCommandAddress cur = this; - ChildCommandAddress child; - while (buffer.hasNext()) { - child = cur.getChild(buffer.next()); - 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; - } - - @Override - public ModifiableCommandAddress getCommandTarget(ExecutionContext context, ArgumentBuffer buffer) throws CommandException { - CommandSender sender = context.getSender(); - ModifiableCommandAddress cur = this; - ChildCommandAddress child; - while (buffer.hasNext()) { - int cursor = buffer.getCursor(); - - child = cur.getChild(context, buffer); - - if (child == null - || (context.isTabComplete() && !buffer.hasNext()) - || (child.hasCommand() && !child.getCommand().isVisibleTo(sender)) - || (cur.hasCommand() && cur.getCommand().takePrecedenceOverSubcommand(buffer.peekPrevious(), buffer.getUnaffectingCopy()))) { - buffer.setCursor(cursor); - break; - } - - cur = child; - - context.setAddress(child); - if (child.hasCommand() && child.isCommandTrailing()) { - child.getCommand().initializeAndFilterContext(context); - child.getCommand().execute(context.getSender(), context); - } - } - - return cur; - } - - @Override - public boolean dispatchCommand(CommandSender sender, String[] command) { - return dispatchCommand(sender, new ArgumentBuffer(command)); - } - - @Override - public boolean dispatchCommand(CommandSender sender, String usedLabel, String[] args) { - return dispatchCommand(sender, new ArgumentBuffer(usedLabel, args)); - } - - @Override - public boolean dispatchCommand(CommandSender sender, ArgumentBuffer buffer) { - ExecutionContext context = new ExecutionContext(sender, buffer, false); - - ModifiableCommandAddress targetAddress = null; - - try { - targetAddress = getCommandTarget(context, buffer); - Command target = targetAddress.getCommand(); - - if (target == null) { - if (targetAddress.hasHelpCommand()) { - target = targetAddress.getHelpCommand().getCommand(); - } else { - return false; - } - } - - context.setCommand(target); - - if (!targetAddress.isCommandTrailing()) { - target.initializeAndFilterContext(context); - String message = target.execute(sender, context); - if (message != null && !message.isEmpty()) { - context.sendMessage(EMessageType.RESULT, message); - } - } - - } catch (Throwable t) { - if (targetAddress == null) { - targetAddress = this; - } - targetAddress.getChatHandler().handleException(sender, context, t); - } - - return true; - } - - @Override - public List getTabCompletions(CommandSender sender, Location location, String[] args) { - return getTabCompletions(sender, location, new ArgumentBuffer(args)); - } - - @Override - public List getTabCompletions(CommandSender sender, String usedLabel, Location location, String[] args) { - return getTabCompletions(sender, location, new ArgumentBuffer(usedLabel, args)); - } - - @Override - public List getTabCompletions(CommandSender sender, Location location, ArgumentBuffer buffer) { - ExecutionContext context = new ExecutionContext(sender, buffer, true); - - try { - ICommandAddress target = getCommandTarget(context, buffer); - - List out; - if (target.hasCommand()) { - context.setCommand(target.getCommand()); - target.getCommand().initializeAndFilterContext(context); - out = target.getCommand().tabComplete(sender, context, location); - } else { - out = 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.getChildrenMainKeys()) { - if (child.toLowerCase().startsWith(input)) { - if (!wrapped) { - out = new ArrayList<>(out); - wrapped = true; - } - out.add(child); - } - } - - return out; - - } catch (CommandException ex) { - return Collections.emptyList(); - } - - } -} +package io.dico.dicore.command; + +import io.dico.dicore.command.parameter.ArgumentBuffer; +import io.dico.dicore.command.registration.BukkitCommand; +import org.bukkit.Location; +import org.bukkit.command.CommandSender; + +import java.util.*; + +public class RootCommandAddress extends ModifiableCommandAddress implements ICommandDispatcher { + @Deprecated + public static final RootCommandAddress INSTANCE = new RootCommandAddress(); + + public RootCommandAddress() { + } + + @Override + public Command getCommand() { + return null; + } + + @Override + public boolean isRoot() { + return true; + } + + @Override + public List getNames() { + return Collections.emptyList(); + } + + @Override + public ModifiableCommandAddress getParent() { + return null; + } + + @Override + public String getMainKey() { + return null; + } + + @Override + public String getAddress() { + return ""; + } + + @Override + public void registerToCommandMap(String fallbackPrefix, Map map, EOverridePolicy overridePolicy) { + Objects.requireNonNull(overridePolicy); + //debugChildren(this); + Map children = this.children; + Map wrappers = new IdentityHashMap<>(); + + for (ChildCommandAddress address : children.values()) { + if (!wrappers.containsKey(address)) { + wrappers.put(address, new BukkitCommand(address)); + } + } + + for (Map.Entry entry : children.entrySet()) { + String key = entry.getKey(); + ChildCommandAddress address = entry.getValue(); + boolean override = overridePolicy == EOverridePolicy.OVERRIDE_ALL; + if (!override && key.equals(address.getMainKey())) { + override = overridePolicy == EOverridePolicy.MAIN_KEY_ONLY || overridePolicy == EOverridePolicy.MAIN_AND_FALLBACK; + } + + registerMember(map, key, wrappers.get(address), override); + + if (fallbackPrefix != null) { + key = fallbackPrefix + key; + override = overridePolicy != EOverridePolicy.OVERRIDE_NONE && overridePolicy != EOverridePolicy.MAIN_KEY_ONLY; + registerMember(map, key, wrappers.get(address), override); + } + } + + } + + private static void debugChildren(ModifiableCommandAddress address) { + Collection keys = address.getChildrenMainKeys(); + for (String key : keys) { + ChildCommandAddress child = address.getChild(key); + System.out.println(child.getAddress()); + debugChildren(child); + } + } + + private static void registerMember(Map map, + String key, org.bukkit.command.Command value, boolean override) { + if (override) { + map.put(key, value); + } else { + map.putIfAbsent(key, value); + } + } + + @Override + public void unregisterFromCommandMap(Map map) { + Set children = new HashSet<>(this.children.values()); + Iterator> iterator = map.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + org.bukkit.command.Command cmd = entry.getValue(); + if (cmd instanceof BukkitCommand && children.contains(((BukkitCommand) cmd).getOrigin())) { + iterator.remove(); + } + } + } + + @Override + public ModifiableCommandAddress getDeepChild(ArgumentBuffer buffer) { + ModifiableCommandAddress cur = this; + ChildCommandAddress child; + while (buffer.hasNext()) { + child = cur.getChild(buffer.next()); + if (child == null) { + buffer.rewind(); + return cur; + } + + cur = child; + } + return cur; + } + + @Override + public ModifiableCommandAddress getCommandTarget(CommandSender sender, ArgumentBuffer buffer) { + ModifiableCommandAddress cur = this; + ChildCommandAddress child; + while (buffer.hasNext()) { + child = cur.getChild(buffer.next()); + 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; + } + + @Override + public ModifiableCommandAddress getCommandTarget(ExecutionContext context, ArgumentBuffer buffer) throws CommandException { + CommandSender sender = context.getSender(); + ModifiableCommandAddress cur = this; + ChildCommandAddress child; + while (buffer.hasNext()) { + int cursor = buffer.getCursor(); + + child = cur.getChild(context, buffer); + + if (child == null + || (context.isTabComplete() && !buffer.hasNext()) + || (child.hasCommand() && !child.getCommand().isVisibleTo(sender)) + || (cur.hasCommand() && cur.getCommand().takePrecedenceOverSubcommand(buffer.peekPrevious(), buffer.getUnaffectingCopy()))) { + buffer.setCursor(cursor); + break; + } + + cur = child; + + context.setAddress(child); + if (child.hasCommand() && child.isCommandTrailing()) { + child.getCommand().initializeAndFilterContext(context); + child.getCommand().execute(context.getSender(), context); + } + } + + return cur; + } + + @Override + public boolean dispatchCommand(CommandSender sender, String[] command) { + return dispatchCommand(sender, new ArgumentBuffer(command)); + } + + @Override + public boolean dispatchCommand(CommandSender sender, String usedLabel, String[] args) { + return dispatchCommand(sender, new ArgumentBuffer(usedLabel, args)); + } + + @Override + public boolean dispatchCommand(CommandSender sender, ArgumentBuffer buffer) { + ExecutionContext context = new ExecutionContext(sender, buffer, false); + + ModifiableCommandAddress targetAddress = null; + + try { + targetAddress = getCommandTarget(context, buffer); + Command target = targetAddress.getCommand(); + + if (target == null) { + if (targetAddress.hasHelpCommand()) { + target = targetAddress.getHelpCommand().getCommand(); + } else { + return false; + } + } + + context.setCommand(target); + + if (!targetAddress.isCommandTrailing()) { + target.initializeAndFilterContext(context); + String message = target.execute(sender, context); + if (message != null && !message.isEmpty()) { + context.sendMessage(EMessageType.RESULT, message); + } + } + + } catch (Throwable t) { + if (targetAddress == null) { + targetAddress = this; + } + targetAddress.getChatHandler().handleException(sender, context, t); + } + + return true; + } + + @Override + public List getTabCompletions(CommandSender sender, Location location, String[] args) { + return getTabCompletions(sender, location, new ArgumentBuffer(args)); + } + + @Override + public List getTabCompletions(CommandSender sender, String usedLabel, Location location, String[] args) { + return getTabCompletions(sender, location, new ArgumentBuffer(usedLabel, args)); + } + + @Override + public List getTabCompletions(CommandSender sender, Location location, ArgumentBuffer buffer) { + ExecutionContext context = new ExecutionContext(sender, buffer, true); + long start = System.currentTimeMillis(); + + try { + ICommandAddress target = getCommandTarget(context, buffer); + + List out; + if (target.hasCommand()) { + context.setCommand(target.getCommand()); + target.getCommand().initializeAndFilterContext(context); + out = target.getCommand().tabComplete(sender, context, location); + } else { + out = 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.getChildrenMainKeys()) { + if (child.toLowerCase().startsWith(input)) { + if (!wrapped) { + out = new ArrayList<>(out); + wrapped = true; + } + out.add(child); + } + } + + return out; + + } catch (CommandException ex) { + return Collections.emptyList(); + } finally { + long duration = System.currentTimeMillis() - start; + if (duration > 2) { + System.out.println(String.format("Complete took %.3f seconds", duration / 1000.0)); + } + } + + } +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/parameter/ContextParser.java b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/ContextParser.java index f486f52..7418c4a 100644 --- a/dicore3/command/src/main/java/io/dico/dicore/command/parameter/ContextParser.java +++ b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/ContextParser.java @@ -1,276 +1,278 @@ -package io.dico.dicore.command.parameter; - -import io.dico.dicore.command.CommandException; -import io.dico.dicore.command.ExecutionContext; - -import java.lang.reflect.Array; -import java.util.*; - -public class ContextParser { - private final ExecutionContext m_context; - private final ArgumentBuffer m_buffer; - private final ParameterList m_paramList; - private final Parameter m_repeatedParam; - private final List> m_indexedParams; - private final int m_maxIndex; - private final int m_maxRequiredIndex; - - private Map m_valueMap; - private Set m_parsedKeys; - private int m_completionCursor = -1; - private Parameter m_completionTarget = null; - - public ContextParser(ExecutionContext context, - ParameterList parameterList, - Map valueMap, - Set keySet) { - m_context = context; - m_paramList = parameterList; - m_valueMap = valueMap; - m_parsedKeys = keySet; - - m_buffer = context.getBuffer(); - m_repeatedParam = m_paramList.getRepeatedParameter(); - m_indexedParams = m_paramList.getIndexedParameters(); - m_maxIndex = m_indexedParams.size() - 1; - m_maxRequiredIndex = m_paramList.getRequiredCount() - 1; - } - - public ExecutionContext getContext() { - return m_context; - } - - public Map getValueMap() { - return m_valueMap; - } - - public Set getParsedKeys() { - return m_parsedKeys; - } - - public void parse() throws CommandException { - parseAllParameters(); - } - - public int getCompletionCursor() { - if (!m_done) { - throw new IllegalStateException(); - } - return m_completionCursor; - } - - public Parameter getCompletionTarget() { - if (!m_done) { - throw new IllegalStateException(); - } - return m_completionTarget; - } - - // ################################ - // # PARSING METHODS # - // ################################ - - private boolean m_repeating = false; - private boolean m_done = false; - private int m_curParamIndex = -1; - private Parameter m_curParam = null; - private List m_curRepeatingList = null; - - private void parseAllParameters() throws CommandException { - try { - do { - prepareStateToParseParam(); - if (m_done) break; - parseCurParam(); - } while (!m_done); - - } finally { - m_curParam = null; - m_curRepeatingList = null; - assignDefaultValuesToUncomputedParams(); - arrayifyRepeatedParamValue(); - } - } - - private void prepareStateToParseParam() throws CommandException { - - boolean requireInput; - if (identifyFlag()) { - m_buffer.advance(); - prepareRepeatedParameterIfSet(); - requireInput = false; - - } else if (m_repeating) { - m_curParam = m_repeatedParam; - requireInput = false; - - } else if (m_curParamIndex < m_maxIndex) { - m_curParamIndex++; - m_curParam = m_indexedParams.get(m_curParamIndex); - prepareRepeatedParameterIfSet(); - requireInput = m_curParamIndex <= m_maxRequiredIndex; - - } else if (m_buffer.hasNext()) { - throw new CommandException("Too many arguments for /" + m_context.getAddress().getAddress()); - - } else { - m_done = true; - return; - } - - if (!m_buffer.hasNext()) { - if (requireInput) { - reportParameterRequired(m_curParam); - } - - if (m_repeating) { - m_done = true; - } - } - - } - - private boolean identifyFlag() { - String potentialFlag = m_buffer.peekNext(); - Parameter target; - if (potentialFlag != null - && potentialFlag.startsWith("-") - && (target = m_paramList.getParameterByName(potentialFlag)) != null - && target.isFlag() - && !m_valueMap.containsKey(potentialFlag) - -// Disabled because it's checked by {@link Parameter#parse(ExecutionContext, ArgumentBuffer)} -// && (target.getFlagPermission() == null || m_context.getSender().hasPermission(target.getFlagPermission())) - ) { - m_curParam = target; - return true; - } - - return false; - } - - private void prepareRepeatedParameterIfSet() throws CommandException { - if (m_curParam != null && m_curParam == m_repeatedParam) { - - if (m_curParam.isFlag() && m_curParamIndex < m_maxRequiredIndex) { - Parameter requiredParam = m_indexedParams.get(m_curParamIndex + 1); - reportParameterRequired(requiredParam); - } - - m_curRepeatingList = new ArrayList<>(); - assignValue(m_curRepeatingList); - m_repeating = true; - } - } - - private void reportParameterRequired(Parameter param) throws CommandException { - throw new CommandException("The argument '" + param.getName() + "' is required"); - } - - private void parseCurParam() throws CommandException { - if (!m_buffer.hasNext() && !m_curParam.isFlag()) { - assignDefaultValue(); - return; - } - - int cursorStart = m_buffer.getCursor(); - - if (m_context.isTabComplete() && "".equals(m_buffer.peekNext())) { - assignAsCompletionTarget(cursorStart); - return; - } - - Object parseResult; - try { - parseResult = m_curParam.parse(m_context, m_buffer); - } catch (CommandException e) { - assignAsCompletionTarget(cursorStart); - throw e; - } - - assignValue(parseResult); - m_parsedKeys.add(m_curParam.getName()); - } - - private void assignDefaultValue() throws CommandException { - assignValue(m_curParam.getDefaultValue(m_context, m_buffer)); - } - - private void assignAsCompletionTarget(int cursor) { - m_completionCursor = cursor; - m_completionTarget = m_curParam; - m_done = true; - } - - private void assignValue(Object value) { - if (m_repeating) { - m_curRepeatingList.add(value); - } else { - m_valueMap.put(m_curParam.getName(), value); - } - } - - private void assignDefaultValuesToUncomputedParams() throws CommandException { - // add default values for unset parameters - for (Map.Entry> entry : m_paramList.getParametersByName().entrySet()) { - String name = entry.getKey(); - if (!m_valueMap.containsKey(name)) { - if (m_repeatedParam == entry.getValue()) { - // below value will be turned into an array later - m_valueMap.put(name, Collections.emptyList()); - } else { - m_valueMap.put(name, entry.getValue().getDefaultValue(m_context, m_buffer)); - } - } - } - } - - private void arrayifyRepeatedParamValue() { - if (m_repeatedParam != null) { - m_valueMap.computeIfPresent(m_repeatedParam.getName(), (k, v) -> { - List list = (List) v; - Class returnType = m_repeatedParam.getType().getReturnType(); - Object array = Array.newInstance(returnType, list.size()); - ArraySetter setter = ArraySetter.getSetter(returnType); - for (int i = 0, n = list.size(); i < n; i++) { - setter.set(array, i, list.get(i)); - } - - return array; - }); - } - } - - private interface ArraySetter { - void set(Object array, int index, Object value); - - static ArraySetter getSetter(Class clazz) { - if (!clazz.isPrimitive()) { - return (array, index, value) -> ((Object[]) array)[index] = value; - } - - switch (clazz.getSimpleName()) { - case "boolean": - return (array, index, value) -> ((boolean[]) array)[index] = (boolean) value; - case "int": - return (array, index, value) -> ((int[]) array)[index] = (int) value; - case "double": - return (array, index, value) -> ((double[]) array)[index] = (double) value; - case "long": - return (array, index, value) -> ((long[]) array)[index] = (long) value; - case "short": - return (array, index, value) -> ((short[]) array)[index] = (short) value; - case "byte": - return (array, index, value) -> ((byte[]) array)[index] = (byte) value; - case "float": - return (array, index, value) -> ((float[]) array)[index] = (float) value; - case "char": - return (array, index, value) -> ((char[]) array)[index] = (char) value; - case "void": - default: - throw new InternalError("This should not happen"); - } - } - } - -} +package io.dico.dicore.command.parameter; + +import io.dico.dicore.command.CommandException; +import io.dico.dicore.command.ExecutionContext; + +import java.lang.reflect.Array; +import java.util.*; + +public class ContextParser { + private final ExecutionContext m_context; + private final ArgumentBuffer m_buffer; + private final ParameterList m_paramList; + private final Parameter m_repeatedParam; + private final List> m_indexedParams; + private final int m_maxIndex; + private final int m_maxRequiredIndex; + + private Map m_valueMap; + private Set m_parsedKeys; + private int m_completionCursor = -1; + private Parameter m_completionTarget = null; + + public ContextParser(ExecutionContext context, + ParameterList parameterList, + Map valueMap, + Set keySet) { + m_context = context; + m_paramList = parameterList; + m_valueMap = valueMap; + m_parsedKeys = keySet; + + m_buffer = context.getBuffer(); + m_repeatedParam = m_paramList.getRepeatedParameter(); + m_indexedParams = m_paramList.getIndexedParameters(); + m_maxIndex = m_indexedParams.size() - 1; + m_maxRequiredIndex = m_paramList.getRequiredCount() - 1; + } + + public ExecutionContext getContext() { + return m_context; + } + + public Map getValueMap() { + return m_valueMap; + } + + public Set getParsedKeys() { + return m_parsedKeys; + } + + public void parse() throws CommandException { + parseAllParameters(); + } + + public int getCompletionCursor() { + if (!m_done) { + throw new IllegalStateException(); + } + return m_completionCursor; + } + + public Parameter getCompletionTarget() { + if (!m_done) { + throw new IllegalStateException(); + } + return m_completionTarget; + } + + // ################################ + // # PARSING METHODS # + // ################################ + + private boolean m_repeating = false; + private boolean m_done = false; + private int m_curParamIndex = -1; + private Parameter m_curParam = null; + private List m_curRepeatingList = null; + + private void parseAllParameters() throws CommandException { + try { + do { + prepareStateToParseParam(); + if (m_done) break; + parseCurParam(); + } while (!m_done); + + } finally { + m_curParam = null; + m_curRepeatingList = null; + assignDefaultValuesToUncomputedParams(); + arrayifyRepeatedParamValue(); + + m_done = true; + } + } + + private void prepareStateToParseParam() throws CommandException { + + boolean requireInput; + if (identifyFlag()) { + m_buffer.advance(); + prepareRepeatedParameterIfSet(); + requireInput = false; + + } else if (m_repeating) { + m_curParam = m_repeatedParam; + requireInput = false; + + } else if (m_curParamIndex < m_maxIndex) { + m_curParamIndex++; + m_curParam = m_indexedParams.get(m_curParamIndex); + prepareRepeatedParameterIfSet(); + requireInput = m_curParamIndex <= m_maxRequiredIndex; + + } else if (m_buffer.hasNext()) { + throw new CommandException("Too many arguments for /" + m_context.getAddress().getAddress()); + + } else { + m_done = true; + return; + } + + if (!m_buffer.hasNext()) { + if (requireInput) { + reportParameterRequired(m_curParam); + } + + if (m_repeating) { + m_done = true; + } + } + + } + + private boolean identifyFlag() { + String potentialFlag = m_buffer.peekNext(); + Parameter target; + if (potentialFlag != null + && potentialFlag.startsWith("-") + && (target = m_paramList.getParameterByName(potentialFlag)) != null + && target.isFlag() + && !m_valueMap.containsKey(potentialFlag) + +// Disabled because it's checked by {@link Parameter#parse(ExecutionContext, ArgumentBuffer)} +// && (target.getFlagPermission() == null || m_context.getSender().hasPermission(target.getFlagPermission())) + ) { + m_curParam = target; + return true; + } + + return false; + } + + private void prepareRepeatedParameterIfSet() throws CommandException { + if (m_curParam != null && m_curParam == m_repeatedParam) { + + if (m_curParam.isFlag() && m_curParamIndex < m_maxRequiredIndex) { + Parameter requiredParam = m_indexedParams.get(m_curParamIndex + 1); + reportParameterRequired(requiredParam); + } + + m_curRepeatingList = new ArrayList<>(); + assignValue(m_curRepeatingList); + m_repeating = true; + } + } + + private void reportParameterRequired(Parameter param) throws CommandException { + throw new CommandException("The argument '" + param.getName() + "' is required"); + } + + private void parseCurParam() throws CommandException { + if (!m_buffer.hasNext() && !m_curParam.isFlag()) { + assignDefaultValue(); + return; + } + + int cursorStart = m_buffer.getCursor(); + + if (m_context.isTabComplete() && "".equals(m_buffer.peekNext())) { + assignAsCompletionTarget(cursorStart); + return; + } + + Object parseResult; + try { + parseResult = m_curParam.parse(m_context, m_buffer); + } catch (CommandException e) { + assignAsCompletionTarget(cursorStart); + throw e; + } + + assignValue(parseResult); + m_parsedKeys.add(m_curParam.getName()); + } + + private void assignDefaultValue() throws CommandException { + assignValue(m_curParam.getDefaultValue(m_context, m_buffer)); + } + + private void assignAsCompletionTarget(int cursor) { + m_completionCursor = cursor; + m_completionTarget = m_curParam; + m_done = true; + } + + private void assignValue(Object value) { + if (m_repeating) { + m_curRepeatingList.add(value); + } else { + m_valueMap.put(m_curParam.getName(), value); + } + } + + private void assignDefaultValuesToUncomputedParams() throws CommandException { + // add default values for unset parameters + for (Map.Entry> entry : m_paramList.getParametersByName().entrySet()) { + String name = entry.getKey(); + if (!m_valueMap.containsKey(name)) { + if (m_repeatedParam == entry.getValue()) { + // below value will be turned into an array later + m_valueMap.put(name, Collections.emptyList()); + } else { + m_valueMap.put(name, entry.getValue().getDefaultValue(m_context, m_buffer)); + } + } + } + } + + private void arrayifyRepeatedParamValue() { + if (m_repeatedParam != null) { + m_valueMap.computeIfPresent(m_repeatedParam.getName(), (k, v) -> { + List list = (List) v; + Class returnType = m_repeatedParam.getType().getReturnType(); + Object array = Array.newInstance(returnType, list.size()); + ArraySetter setter = ArraySetter.getSetter(returnType); + for (int i = 0, n = list.size(); i < n; i++) { + setter.set(array, i, list.get(i)); + } + + return array; + }); + } + } + + private interface ArraySetter { + void set(Object array, int index, Object value); + + static ArraySetter getSetter(Class clazz) { + if (!clazz.isPrimitive()) { + return (array, index, value) -> ((Object[]) array)[index] = value; + } + + switch (clazz.getSimpleName()) { + case "boolean": + return (array, index, value) -> ((boolean[]) array)[index] = (boolean) value; + case "int": + return (array, index, value) -> ((int[]) array)[index] = (int) value; + case "double": + return (array, index, value) -> ((double[]) array)[index] = (double) value; + case "long": + return (array, index, value) -> ((long[]) array)[index] = (long) value; + case "short": + return (array, index, value) -> ((short[]) array)[index] = (short) value; + case "byte": + return (array, index, value) -> ((byte[]) array)[index] = (byte) value; + case "float": + return (array, index, value) -> ((float[]) array)[index] = (float) value; + case "char": + return (array, index, value) -> ((char[]) array)[index] = (char) value; + case "void": + default: + throw new InternalError("This should not happen"); + } + } + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/ReflectiveCallFlags.java b/dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/ReflectiveCallFlags.java new file mode 100644 index 0000000..fa198c2 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/ReflectiveCallFlags.java @@ -0,0 +1,186 @@ +package io.dico.dicore.command.registration.reflect; + +import io.dico.dicore.command.CommandException; +import io.dico.dicore.command.ExecutionContext; +import io.dico.dicore.exceptions.checkedfunctions.CheckedSupplier; + +/** + * Call flags store which extra parameters the target function expects on top of command parameters. + * All 4 possible extra parameters are listed below. + *

+ * Extra parameters are ordered by the bit that represents them in the call flags. + * They can either be leading or trailing the command's parameters. + */ +public class ReflectiveCallFlags { + + /** + * Receiver ({@code this} in some kotlin functions - always first parameter) + * + * @see ICommandInterceptor#getReceiver(io.dico.dicore.command.ExecutionContext, java.lang.reflect.Method, String) + */ + public static final int RECEIVER_BIT = 1 << 0; + + /** + * CommandSender + * + * @see org.bukkit.command.CommandSender + */ + public static final int SENDER_BIT = 1 << 1; + + /** + * ExecutionContext + * + * @see io.dico.dicore.command.ExecutionContext + */ + public static final int CONTEXT_BIT = 1 << 2; + + /** + * Continuation (trailing parameters of kotlin suspended functions) + * + * @see kotlin.coroutines.Continuation + */ + public static final int CONTINUATION_BIT = 1 << 3; + + /** + * Mask of extra parameters that trail the command's parameters, instead of leading. + */ + public static final int TRAILING_MASK = CONTINUATION_BIT; + + /** + * Check if the call arg is trailing the command's parameters. + * + * @param bit the bit used for the call flag + * @return true if the call arg is trailing the command's parameters + */ + public static boolean isTrailingCallArg(int bit) { + return (bit & TRAILING_MASK) != 0; + } + + /** + * Number of call arguments leading the command parameters. + * + * @param flags the call flags + * @return the number of call arguments leading the command parameters + */ + public static int getLeadingCallArgNum(int flags) { + return Integer.bitCount(flags & ~TRAILING_MASK); + } + + /** + * Number of call arguments trailing the command parameters. + * + * @param flags the call flags + * @return the number of call arguments trailing the command parameters + */ + public static int getTrailingCallArgNum(int flags) { + return Integer.bitCount(flags & TRAILING_MASK); + } + + /** + * Check if the flags contain the call arg. + * + * @param flags the call flags + * @param bit the bit used for the call flag + * @return true if the flags contain the call arg + */ + public static boolean hasCallArg(int flags, int bit) { + return (flags & bit) != 0; + } + + /** + * Get the index used for the call arg when calling the reflective function + * + * @param flags the call flags + * @param bit the bit used for the call flag + * @param cmdParameterNum the number of parameters of the command + * @return the index used for the call arg + */ + public static int getCallArgIndex(int flags, int bit, int cmdParameterNum) { + if ((bit & TRAILING_MASK) == 0) { + // Leading. + + int preceding = precedingMaskFrom(bit); + int mask = flags & precedingMaskFrom(bit) & ~TRAILING_MASK; + + // Count the number of present call args that are leading and precede the given bit + return Integer.bitCount(flags & precedingMaskFrom(bit) & ~TRAILING_MASK); + } else { + // Trailing. + + // Count the number of present call args that are leading + // plus the number of present call args that are trailing and precede the given bit + // plus the command's parameters + + return Integer.bitCount(flags & ~TRAILING_MASK) + + Integer.bitCount(flags & precedingMaskFrom(bit) & TRAILING_MASK) + + cmdParameterNum; + } + } + + /** + * Get the mask for all bits trailing the given fromBit + * + *

+ * For example, if the bit is 00010000 + * This function returns 00001111 + *

+ * + * @param fromBit number with the bit set there the ones should stop. + * @return the mask for all bits trailing the given fromBit + */ + private static int precedingMaskFrom(int fromBit) { + int trailingZeros = Integer.numberOfTrailingZeros(fromBit); + if (trailingZeros == 0) return 0; + return -1 >>> -trailingZeros; + } + + /** + * Get the object array used to call the function. + * + * @param callFlags the call flags + * @param context the context + * @param parameterOrder the order of parameters in the function + * @param receiverFunction the function that will create the receiver for this call, if applicable + * @return the call args + */ + public static Object[] getCallArgs( + int callFlags, + ExecutionContext context, + String[] parameterOrder, + CheckedSupplier receiverFunction + ) throws CommandException { + int leadingParameterNum = getLeadingCallArgNum(callFlags); + int cmdParameterNum = parameterOrder.length; + int trailingParameterNum = getTrailingCallArgNum(callFlags); + + Object[] result = new Object[leadingParameterNum + cmdParameterNum + trailingParameterNum]; + + if (hasCallArg(callFlags, RECEIVER_BIT)) { + int index = getCallArgIndex(callFlags, RECEIVER_BIT, cmdParameterNum); + result[index] = receiverFunction.get(); + } + + if (hasCallArg(callFlags, SENDER_BIT)) { + int index = getCallArgIndex(callFlags, SENDER_BIT, cmdParameterNum); + result[index] = context.getSender(); + } + + if (hasCallArg(callFlags, CONTEXT_BIT)) { + int index = getCallArgIndex(callFlags, CONTEXT_BIT, cmdParameterNum); + result[index] = context; + } + + if (hasCallArg(callFlags, CONTINUATION_BIT)) { + int index = getCallArgIndex(callFlags, CONTINUATION_BIT, cmdParameterNum); + result[index] = null; // filled in later. + } + + for (int i = 0; i < parameterOrder.length; i++) { + String parameterName = parameterOrder[i]; + result[leadingParameterNum + i] = context.get(parameterName); + } + + return result; + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/ReflectiveCommand.java b/dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/ReflectiveCommand.java index 34ea8de..f4ddc70 100644 --- a/dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/ReflectiveCommand.java +++ b/dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/ReflectiveCommand.java @@ -1,187 +1,169 @@ -package io.dico.dicore.command.registration.reflect; - -import io.dico.dicore.command.*; -import io.dico.dicore.command.annotation.Cmd; -import io.dico.dicore.command.annotation.GenerateCommands; -import io.dico.dicore.command.parameter.type.IParameterTypeSelector; -import org.bukkit.command.CommandSender; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; - -public final class ReflectiveCommand extends Command { - private static final int continuationMask = 1 << 3; - private final Cmd cmdAnnotation; - private final Method method; - private final Object instance; - private String[] parameterOrder; - - // hasContinuation | hasContext | hasSender | hasReceiver - private final int flags; - - ReflectiveCommand(IParameterTypeSelector selector, Method method, Object instance) throws CommandParseException { - if (!method.isAnnotationPresent(Cmd.class)) { - throw new CommandParseException("No @Cmd present for the method " + method.toGenericString()); - } - cmdAnnotation = method.getAnnotation(Cmd.class); - - java.lang.reflect.Parameter[] parameters = method.getParameters(); - - if (!method.isAccessible()) try { - method.setAccessible(true); - } catch (Exception ex) { - throw new CommandParseException("Failed to make method accessible"); - } - - if (!Modifier.isStatic(method.getModifiers())) { - if (instance == null) { - try { - instance = method.getDeclaringClass().newInstance(); - } catch (Exception ex) { - throw new CommandParseException("No instance given for instance method, and failed to create new instance", ex); - } - } else if (!method.getDeclaringClass().isInstance(instance)) { - throw new CommandParseException("Given instance is not an instance of the method's declaring class"); - } - } - - this.method = method; - this.instance = instance; - this.flags = ReflectiveRegistration.parseCommandAttributes(selector, method, this, parameters); - } - - public Method getMethod() { - return method; - } - - public Object getInstance() { - return instance; - } - - public String getCmdName() { return cmdAnnotation.value(); } - - void setParameterOrder(String[] parameterOrder) { - this.parameterOrder = parameterOrder; - } - - ICommandAddress getAddress() { - ChildCommandAddress result = new ChildCommandAddress(); - result.setCommand(this); - - Cmd cmd = cmdAnnotation; - result.getNames().add(cmd.value()); - for (String alias : cmd.aliases()) { - result.getNames().add(alias); - } - result.finalizeNames(); - - GenerateCommands generateCommands = method.getAnnotation(GenerateCommands.class); - if (generateCommands != null) { - ReflectiveRegistration.generateCommands(result, generateCommands.value()); - } - - return result; - } - - @Override - public String execute(CommandSender sender, ExecutionContext context) throws CommandException { - String[] parameterOrder = this.parameterOrder; - int extraArgumentCount = Integer.bitCount(flags); - int parameterStartIndex = Integer.bitCount(flags & ~continuationMask); - - Object[] args = new Object[parameterOrder.length + extraArgumentCount]; - - int i = 0; - - int mask = 1; - if ((flags & mask) != 0) { - // Has receiver - try { - args[i++] = ((ICommandInterceptor) instance).getReceiver(context, method, getCmdName()); - } catch (Exception ex) { - handleException(ex); - return null; // unreachable - } - } - - mask <<= 1; - if ((flags & mask) != 0) { - // Has sender - args[i++] = sender; - } - - mask <<= 1; - if ((flags & mask) != 0) { - // Has context - args[i++] = context; - } - - mask <<= 1; - if ((flags & mask) != 0) { - // Has continuation - - extraArgumentCount--; - } - - for (int n = args.length; i < n; i++) { - args[i] = context.get(parameterOrder[i - extraArgumentCount]); - } - - if ((flags & mask) != 0) { - // Since it has continuation, call as coroutine - return callAsCoroutine(context, args); - } - - return callSynchronously(args); - } - - private boolean isSuspendFunction() { - try { - return KotlinReflectiveRegistrationKt.isSuspendFunction(method); - } catch (Throwable ex) { - return false; - } - } - - public String callSynchronously(Object[] args) throws CommandException { - try { - return getResult(method.invoke(instance, args), null); - } catch (Exception ex) { - return getResult(null, ex); - } - } - - public static String getResult(Object returned, Exception ex) throws CommandException { - if (ex != null) { - handleException(ex); - return null; // unreachable - } - - if (returned instanceof String) { - return (String) returned; - } - return null; - } - - public static void handleException(Exception ex) throws CommandException { - if (ex instanceof InvocationTargetException) { - if (ex.getCause() instanceof CommandException) { - throw (CommandException) ex.getCause(); - } - - ex.printStackTrace(); - throw new CommandException("An internal error occurred while executing this command.", ex); - } - if (ex instanceof CommandException) { - throw (CommandException) ex; - } - ex.printStackTrace(); - throw new CommandException("An internal error occurred while executing this command.", ex); - } - - private String callAsCoroutine(ExecutionContext context, Object[] args) { - return KotlinReflectiveRegistrationKt.callAsCoroutine(this, (ICommandInterceptor) instance, context, args); - } - -} +package io.dico.dicore.command.registration.reflect; + +import io.dico.dicore.command.*; +import io.dico.dicore.command.annotation.Cmd; +import io.dico.dicore.command.annotation.GenerateCommands; +import io.dico.dicore.command.parameter.type.IParameterTypeSelector; +import io.dico.dicore.exceptions.checkedfunctions.CheckedSupplier; +import kotlin.coroutines.CoroutineContext; +import org.bukkit.command.CommandSender; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +public final class ReflectiveCommand extends Command { + private final Cmd cmdAnnotation; + private final Method method; + private final Object instance; + private String[] parameterOrder; + private final int callFlags; + + ReflectiveCommand(IParameterTypeSelector selector, Method method, Object instance) throws CommandParseException { + if (!method.isAnnotationPresent(Cmd.class)) { + throw new CommandParseException("No @Cmd present for the method " + method.toGenericString()); + } + cmdAnnotation = method.getAnnotation(Cmd.class); + + java.lang.reflect.Parameter[] parameters = method.getParameters(); + + if (!method.isAccessible()) try { + method.setAccessible(true); + } catch (Exception ex) { + throw new CommandParseException("Failed to make method accessible"); + } + + if (!Modifier.isStatic(method.getModifiers())) { + if (instance == null) { + try { + instance = method.getDeclaringClass().newInstance(); + } catch (Exception ex) { + throw new CommandParseException("No instance given for instance method, and failed to create new instance", ex); + } + } else if (!method.getDeclaringClass().isInstance(instance)) { + throw new CommandParseException("Given instance is not an instance of the method's declaring class"); + } + } + + this.method = method; + this.instance = instance; + this.callFlags = ReflectiveRegistration.parseCommandAttributes(selector, method, this, parameters); + } + + public Method getMethod() { + return method; + } + + public Object getInstance() { + return instance; + } + + public String getCmdName() { + return cmdAnnotation.value(); + } + + public int getCallFlags() { + return callFlags; + } + + void setParameterOrder(String[] parameterOrder) { + this.parameterOrder = parameterOrder; + } + + public int getParameterNum() { + return parameterOrder.length; + } + + ICommandAddress getAddress() { + ChildCommandAddress result = new ChildCommandAddress(); + result.setCommand(this); + + Cmd cmd = cmdAnnotation; + result.getNames().add(cmd.value()); + for (String alias : cmd.aliases()) { + result.getNames().add(alias); + } + result.finalizeNames(); + + GenerateCommands generateCommands = method.getAnnotation(GenerateCommands.class); + if (generateCommands != null) { + ReflectiveRegistration.generateCommands(result, generateCommands.value()); + } + + return result; + } + + @Override + public String execute(CommandSender sender, ExecutionContext context) throws CommandException { + + CheckedSupplier receiverFunction = () -> { + try { + return ((ICommandInterceptor) instance).getReceiver(context, method, getCmdName()); + } catch (Exception ex) { + handleException(ex); + return null; // unreachable + } + }; + + Object[] callArgs = ReflectiveCallFlags.getCallArgs(callFlags, context, parameterOrder, receiverFunction); + + if (ReflectiveCallFlags.hasCallArg(callFlags, ReflectiveCallFlags.CONTINUATION_BIT)) { + // If it has a continuation, call as coroutine + return callAsCoroutine(context, callArgs); + } + + return callSynchronously(callArgs); + } + + private boolean isSuspendFunction() { + try { + return KotlinReflectiveRegistrationKt.isSuspendFunction(method); + } catch (Throwable ex) { + return false; + } + } + + public String callSynchronously(Object[] args) throws CommandException { + try { + return getResult(method.invoke(instance, args), null); + } catch (Exception ex) { + return getResult(null, ex); + } + } + + public static String getResult(Object returned, Exception ex) throws CommandException { + if (ex != null) { + handleException(ex); + return null; // unreachable + } + + if (returned instanceof String) { + return (String) returned; + } + return null; + } + + public static void handleException(Exception ex) throws CommandException { + if (ex instanceof InvocationTargetException) { + if (ex.getCause() instanceof CommandException) { + throw (CommandException) ex.getCause(); + } + + ex.printStackTrace(); + throw new CommandException("An internal error occurred while executing this command.", ex); + } + if (ex instanceof CommandException) { + throw (CommandException) ex; + } + ex.printStackTrace(); + throw new CommandException("An internal error occurred while executing this command.", ex); + } + + private String callAsCoroutine(ExecutionContext executionContext, Object[] args) throws CommandException { + ICommandInterceptor factory = (ICommandInterceptor) instance; + CoroutineContext coroutineContext = (CoroutineContext) factory.getCoroutineContext(executionContext, method, getCmdName()); + int continuationIndex = ReflectiveCallFlags.getCallArgIndex(callFlags, ReflectiveCallFlags.CONTINUATION_BIT, parameterOrder.length); + return KotlinReflectiveRegistrationKt.callCommandAsCoroutine(executionContext, coroutineContext, continuationIndex, method, instance, args); + } + +} 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 93ac0ee..ddd5420 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 @@ -1,415 +1,406 @@ -package io.dico.dicore.command.registration.reflect; - -import io.dico.dicore.command.*; -import io.dico.dicore.command.annotation.*; -import io.dico.dicore.command.annotation.GroupMatchedCommands.GroupEntry; -import io.dico.dicore.command.parameter.Parameter; -import io.dico.dicore.command.parameter.ParameterList; -import io.dico.dicore.command.parameter.type.IParameterTypeSelector; -import io.dico.dicore.command.parameter.type.MapBasedParameterTypeSelector; -import io.dico.dicore.command.parameter.type.ParameterType; -import io.dico.dicore.command.parameter.type.ParameterTypes; -import io.dico.dicore.command.predef.PredefinedCommand; -import org.bukkit.command.CommandSender; -import org.bukkit.command.ConsoleCommandSender; -import org.bukkit.entity.Player; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.*; -import java.util.function.Consumer; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; - -/** - * Takes care of turning a reflection {@link Method} into a command and more. - */ -public class ReflectiveRegistration { - /** - * This object provides names of the parameters. - * Oddly, the AnnotationParanamer extensions require a 'fallback' paranamer to function properly without - * requiring ALL parameters to have that flag. This is weird because it should just use the AdaptiveParanamer on an upper level to - * determine the name of each individual flag. Oddly this isn't how it works, so the fallback works the same way as the AdaptiveParanamer does. - * It's just linked instead of using an array for that part. Then we can use an AdaptiveParanamer for the latest fallback, to get bytecode names - * or, finally, to get the Jvm-provided parameter names. - */ - //private static final Paranamer paranamer = new CachingParanamer(new BytecodeReadingParanamer()); - @SuppressWarnings("StatementWithEmptyBody") - private static String[] lookupParameterNames(Method method, java.lang.reflect.Parameter[] parameters, int start) { - int n = parameters.length; - String[] out = new String[n - start]; - - //String[] bytecode; - //try { - // bytecode = paranamer.lookupParameterNames(method, false); - //} catch (Exception ex) { - // bytecode = new String[0]; - // System.err.println("ReflectiveRegistration.lookupParameterNames failed to read bytecode"); - // //ex.printStackTrace(); - //} - //int bn = bytecode.length; - - for (int i = start; i < n; i++) { - java.lang.reflect.Parameter parameter = parameters[i]; - Flag flag = parameter.getAnnotation(Flag.class); - NamedArg namedArg = parameter.getAnnotation(NamedArg.class); - - boolean isFlag = flag != null; - String name; - if (namedArg != null && !(name = namedArg.value()).isEmpty()) { - } else if (isFlag && !(name = flag.value()).isEmpty()) { - //} else if (i < bn && (name = bytecode[i]) != null && !name.isEmpty()) { - } else { - name = parameter.getName(); - } - - if (isFlag) { - name = '-' + name; - } else { - int idx = 0; - while (name.startsWith("-", idx)) { - idx++; - } - name = name.substring(idx); - } - - out[i - start] = name; - } - - return out; - } - - public static void parseCommandGroup(ICommandAddress address, Class clazz, Object instance) throws CommandParseException { - parseCommandGroup(address, ParameterTypes.getSelector(), clazz, instance); - } - - public static void parseCommandGroup(ICommandAddress address, IParameterTypeSelector selector, Class clazz, Object instance) throws CommandParseException { - boolean requireStatic = instance == null; - if (!requireStatic && !clazz.isInstance(instance)) { - throw new CommandParseException(); - } - - List methods = new LinkedList<>(Arrays.asList(clazz.getDeclaredMethods())); - - Iterator it = methods.iterator(); - for (Method method; it.hasNext(); ) { - method = it.next(); - - if (requireStatic && !Modifier.isStatic(method.getModifiers())) { - it.remove(); - continue; - } - - if (method.isAnnotationPresent(CmdParamType.class)) { - it.remove(); - - if (method.getReturnType() != ParameterType.class || method.getParameterCount() != 0) { - throw new CommandParseException("Invalid CmdParamType method: must return ParameterType and take no arguments"); - } - - ParameterType type; - try { - Object inst = Modifier.isStatic(method.getModifiers()) ? null : instance; - type = (ParameterType) method.invoke(inst); - Objects.requireNonNull(type, "ParameterType returned is null"); - } catch (Exception ex) { - throw new CommandParseException("Error occurred whilst getting ParameterType from CmdParamType method '" + method.toGenericString() + "'", ex); - } - - if (selector == ParameterTypes.getSelector()) { - selector = new MapBasedParameterTypeSelector(true); - } - - selector.addType(method.getAnnotation(CmdParamType.class).infolessAlias(), type); - } - } - - GroupMatcherCache groupMatcherCache = new GroupMatcherCache(clazz, address); - for (Method method : methods) { - if (method.isAnnotationPresent(Cmd.class)) { - ICommandAddress parsed = parseCommandMethod(selector, method, instance); - groupMatcherCache.getGroupFor(method).addChild(parsed); - } - } - - } - - private static final class GroupMatcherCache { - private ModifiableCommandAddress groupRootAddress; - private GroupEntry[] matchEntries; - private Pattern[] patterns; - private ModifiableCommandAddress[] addresses; - - GroupMatcherCache(Class clazz, ICommandAddress groupRootAddress) throws CommandParseException { - this.groupRootAddress = (ModifiableCommandAddress) groupRootAddress; - - GroupMatchedCommands groupMatchedCommands = clazz.getAnnotation(GroupMatchedCommands.class); - GroupEntry[] matchEntries = groupMatchedCommands == null ? new GroupEntry[0] : groupMatchedCommands.value(); - - Pattern[] patterns = new Pattern[matchEntries.length]; - for (int i = 0; i < matchEntries.length; i++) { - GroupEntry matchEntry = matchEntries[i]; - if (matchEntry.group().isEmpty() || matchEntry.regex().isEmpty()) { - throw new CommandParseException("Empty group or regex in GroupMatchedCommands entry"); - } - try { - patterns[i] = Pattern.compile(matchEntry.regex()); - } catch (PatternSyntaxException ex) { - throw new CommandParseException(ex); - } - } - - this.matchEntries = matchEntries; - this.patterns = patterns; - this.addresses = new ModifiableCommandAddress[this.matchEntries.length]; - } - - ModifiableCommandAddress getGroupFor(Method method) { - String name = method.getName(); - - GroupEntry[] matchEntries = this.matchEntries; - Pattern[] patterns = this.patterns; - ModifiableCommandAddress[] addresses = this.addresses; - - for (int i = 0; i < matchEntries.length; i++) { - GroupEntry matchEntry = matchEntries[i]; - if (patterns[i].matcher(name).matches()) { - if (addresses[i] == null) { - 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]; - } - } - - return groupRootAddress; - } - - } - - public static ICommandAddress parseCommandMethod(IParameterTypeSelector selector, Method method, Object instance) throws CommandParseException { - return new ReflectiveCommand(selector, method, instance).getAddress(); - } - - static int parseCommandAttributes(IParameterTypeSelector selector, Method method, ReflectiveCommand command, java.lang.reflect.Parameter[] parameters) throws CommandParseException { - ParameterList list = command.getParameterList(); - - boolean hasReceiverParameter = false; - boolean hasSenderParameter = false; - boolean hasContextParameter = false; - boolean hasContinuationParameter = false; - - int start = 0; - int end = parameters.length; - - Class senderParameterType = null; - - if (parameters.length > start - && command.getInstance() instanceof ICommandInterceptor - && ICommandReceiver.class.isAssignableFrom(parameters[start].getType())) { - hasReceiverParameter = true; - start++; - } - - if (parameters.length > start && CommandSender.class.isAssignableFrom(senderParameterType = parameters[start].getType())) { - hasSenderParameter = true; - start++; - } - - if (parameters.length > start && parameters[start].getType() == ExecutionContext.class) { - hasContextParameter = true; - start++; - } - - if (parameters.length > start && parameters[end - 1].getType().getName().equals("kotlin.coroutines.Continuation")) { - hasContinuationParameter = true; - end--; - } - - String[] parameterNames = lookupParameterNames(method, parameters, start); - for (int i = start, n = end; i < n; i++) { - Parameter parameter = parseParameter(selector, method, parameters[i], parameterNames[i - start]); - list.addParameter(parameter); - } - command.setParameterOrder(hasContinuationParameter ? Arrays.copyOfRange(parameterNames, 0, parameterNames.length - 1) : parameterNames); - - RequirePermissions cmdPermissions = method.getAnnotation(RequirePermissions.class); - if (cmdPermissions != null) { - for (String permission : cmdPermissions.value()) { - command.addContextFilter(IContextFilter.permission(permission)); - } - - if (cmdPermissions.inherit()) { - command.addContextFilter(IContextFilter.INHERIT_PERMISSIONS); - } - } else { - command.addContextFilter(IContextFilter.INHERIT_PERMISSIONS); - } - - RequireParameters reqPar = method.getAnnotation(RequireParameters.class); - if (reqPar != null) { - list.setRequiredCount(reqPar.value() < 0 ? Integer.MAX_VALUE : reqPar.value()); - } else { - list.setRequiredCount(list.getIndexedParameters().size()); - } - - /* - PreprocessArgs preprocessArgs = method.getAnnotation(PreprocessArgs.class); - if (preprocessArgs != null) { - IArgumentPreProcessor preProcessor = IArgumentPreProcessor.mergeOnTokens(preprocessArgs.tokens(), preprocessArgs.escapeChar()); - list.setArgumentPreProcessor(preProcessor); - }*/ - - Desc desc = method.getAnnotation(Desc.class); - if (desc != null) { - String[] array = desc.value(); - if (array.length == 0) { - command.setDescription(desc.shortVersion()); - } else { - command.setDescription(array); - } - } else { - command.setDescription(); - } - - if (hasSenderParameter && Player.class.isAssignableFrom(senderParameterType)) { - command.addContextFilter(IContextFilter.PLAYER_ONLY); - } else if (hasSenderParameter && ConsoleCommandSender.class.isAssignableFrom(senderParameterType)) { - command.addContextFilter(IContextFilter.CONSOLE_ONLY); - } else if (method.isAnnotationPresent(RequirePlayer.class)) { - command.addContextFilter(IContextFilter.PLAYER_ONLY); - } else if (method.isAnnotationPresent(RequireConsole.class)) { - command.addContextFilter(IContextFilter.CONSOLE_ONLY); - } - - list.setRepeatFinalParameter(parameters.length > start && parameters[parameters.length - 1].isVarArgs()); - list.setFinalParameterMayBeFlag(true); - - int flags = 0; - if (hasContinuationParameter) flags |= 1; - flags <<= 1; - if (hasContextParameter) flags |= 1; - flags <<= 1; - if (hasSenderParameter) flags |= 1; - flags <<= 1; - if (hasReceiverParameter) flags |= 1; - return flags; - } - - public static int parseCommandAttributes(IParameterTypeSelector selector, Method method, ReflectiveCommand command) throws CommandParseException { - return parseCommandAttributes(selector, method, command, method.getParameters()); - } - - public static Parameter parseParameter(IParameterTypeSelector selector, Method method, java.lang.reflect.Parameter parameter, String name) throws CommandParseException { - Class type = parameter.getType(); - if (parameter.isVarArgs()) { - type = type.getComponentType(); - } - - Annotation[] annotations = parameter.getAnnotations(); - Flag flag = null; - Annotation typeAnnotation = null; - Desc desc = null; - - for (Annotation annotation : annotations) { - //noinspection StatementWithEmptyBody - if (annotation instanceof NamedArg) { - // do nothing - } else if (annotation instanceof Flag) { - if (flag != null) { - throw new CommandParseException("Multiple flags for the same parameter"); - } - flag = (Flag) annotation; - } else if (annotation instanceof Desc) { - if (desc != null) { - throw new CommandParseException("Multiple descriptions for the same parameter"); - } - desc = (Desc) annotation; - } else { - if (typeAnnotation != null) { - throw new CommandParseException("Multiple parameter type annotations for the same parameter"); - } - typeAnnotation = annotation; - } - } - - if (flag == null && name.startsWith("-")) { - throw new CommandParseException("Non-flag parameter's name starts with -"); - } else if (flag != null && !name.startsWith("-")) { - throw new CommandParseException("Flag parameter's name doesn't start with -"); - } - - ParameterType parameterType = selector.selectAny(type, typeAnnotation == null ? null : typeAnnotation.getClass()); - if (parameterType == null) { - throw new CommandParseException("IParameter type not found for parameter " + name + " in method " + method.toString()); - } - - Object parameterInfo; - if (typeAnnotation == null) { - parameterInfo = null; - } else try { - parameterInfo = parameterType.getParameterConfig() == null ? null : parameterType.getParameterConfig().getParameterInfo(typeAnnotation); - } catch (Exception ex) { - throw new CommandParseException("Invalid parameter config", ex); - } - - String descString = desc == null ? null : CommandAnnotationUtils.getShortDescription(desc); - - try { - //noinspection unchecked - String flagPermission = flag == null || flag.permission().isEmpty() ? null : flag.permission(); - return new Parameter<>(name, descString, parameterType, parameterInfo, type.isPrimitive(), name.startsWith("-"), flagPermission); - } catch (Exception ex) { - throw new CommandParseException("Invalid parameter", ex); - } - } - - public static void generateCommands(ICommandAddress address, String[] input) { - for (String value : input) { - Consumer consumer = PredefinedCommand.getPredefinedCommandGenerator(value); - if (consumer == null) { - System.out.println("[Command Warning] generated command '" + value + "' could not be found"); - } else { - consumer.accept(address); - } - } - } - - /* - Desired format - - @Cmd({"tp", "tpto"}) - @RequirePermissions("teleport.self") - public (static) String|void onCommand(Player sender, Player target, @Flag("force", permission = "teleport.self.force") boolean force) { - Validate.isTrue(force || !hasTpToggledOff(target), "Target has teleportation disabled. Use -force to ignore"); - sender.teleport(target); - //return - } - - parser needs to: - - see the @Cmd and create a CommandTree for it - - see that it must be a Player executing the command - - add an indexed IParameter for a Player type - - add a flag parameter named force, that consumes no arguments. - - see that setting the force flag requires a permission - */ - - private static void setDescription(ICommandAddress address, String[] array, String shortVersion) { - if (!address.hasCommand()) { - return; - } - - if (array.length == 0) { - address.getCommand().setDescription(shortVersion); - } else { - address.getCommand().setDescription(array); - } - - } - -} +package io.dico.dicore.command.registration.reflect; + +import io.dico.dicore.command.*; +import io.dico.dicore.command.annotation.*; +import io.dico.dicore.command.annotation.GroupMatchedCommands.GroupEntry; +import io.dico.dicore.command.parameter.Parameter; +import io.dico.dicore.command.parameter.ParameterList; +import io.dico.dicore.command.parameter.type.IParameterTypeSelector; +import io.dico.dicore.command.parameter.type.MapBasedParameterTypeSelector; +import io.dico.dicore.command.parameter.type.ParameterType; +import io.dico.dicore.command.parameter.type.ParameterTypes; +import io.dico.dicore.command.predef.PredefinedCommand; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.entity.Player; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.function.Consumer; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import static io.dico.dicore.command.registration.reflect.ReflectiveCallFlags.*; + +/** + * Takes care of turning a reflection {@link Method} into a command and more. + */ +public class ReflectiveRegistration { + /** + * This object provides names of the parameters. + * Oddly, the AnnotationParanamer extensions require a 'fallback' paranamer to function properly without + * requiring ALL parameters to have that flag. This is weird because it should just use the AdaptiveParanamer on an upper level to + * determine the name of each individual flag. Oddly this isn't how it works, so the fallback works the same way as the AdaptiveParanamer does. + * It's just linked instead of using an array for that part. Then we can use an AdaptiveParanamer for the latest fallback, to get bytecode names + * or, finally, to get the Jvm-provided parameter names. + */ + //private static final Paranamer paranamer = new CachingParanamer(new BytecodeReadingParanamer()); + @SuppressWarnings("StatementWithEmptyBody") + private static String[] lookupParameterNames(Method method, java.lang.reflect.Parameter[] parameters, int start) { + int n = parameters.length; + String[] out = new String[n - start]; + + //String[] bytecode; + //try { + // bytecode = paranamer.lookupParameterNames(method, false); + //} catch (Exception ex) { + // bytecode = new String[0]; + // System.err.println("ReflectiveRegistration.lookupParameterNames failed to read bytecode"); + // //ex.printStackTrace(); + //} + //int bn = bytecode.length; + + for (int i = start; i < n; i++) { + java.lang.reflect.Parameter parameter = parameters[i]; + Flag flag = parameter.getAnnotation(Flag.class); + NamedArg namedArg = parameter.getAnnotation(NamedArg.class); + + boolean isFlag = flag != null; + String name; + if (namedArg != null && !(name = namedArg.value()).isEmpty()) { + } else if (isFlag && !(name = flag.value()).isEmpty()) { + //} else if (i < bn && (name = bytecode[i]) != null && !name.isEmpty()) { + } else { + name = parameter.getName(); + } + + if (isFlag) { + name = '-' + name; + } else { + int idx = 0; + while (name.startsWith("-", idx)) { + idx++; + } + name = name.substring(idx); + } + + out[i - start] = name; + } + + return out; + } + + public static void parseCommandGroup(ICommandAddress address, Class clazz, Object instance) throws CommandParseException { + parseCommandGroup(address, ParameterTypes.getSelector(), clazz, instance); + } + + public static void parseCommandGroup(ICommandAddress address, IParameterTypeSelector selector, Class clazz, Object instance) throws CommandParseException { + boolean requireStatic = instance == null; + if (!requireStatic && !clazz.isInstance(instance)) { + throw new CommandParseException(); + } + + List methods = new LinkedList<>(Arrays.asList(clazz.getDeclaredMethods())); + + Iterator it = methods.iterator(); + for (Method method; it.hasNext(); ) { + method = it.next(); + + if (requireStatic && !Modifier.isStatic(method.getModifiers())) { + it.remove(); + continue; + } + + if (method.isAnnotationPresent(CmdParamType.class)) { + it.remove(); + + if (method.getReturnType() != ParameterType.class || method.getParameterCount() != 0) { + throw new CommandParseException("Invalid CmdParamType method: must return ParameterType and take no arguments"); + } + + ParameterType type; + try { + Object inst = Modifier.isStatic(method.getModifiers()) ? null : instance; + type = (ParameterType) method.invoke(inst); + Objects.requireNonNull(type, "ParameterType returned is null"); + } catch (Exception ex) { + throw new CommandParseException("Error occurred whilst getting ParameterType from CmdParamType method '" + method.toGenericString() + "'", ex); + } + + if (selector == ParameterTypes.getSelector()) { + selector = new MapBasedParameterTypeSelector(true); + } + + selector.addType(method.getAnnotation(CmdParamType.class).infolessAlias(), type); + } + } + + GroupMatcherCache groupMatcherCache = new GroupMatcherCache(clazz, address); + for (Method method : methods) { + if (method.isAnnotationPresent(Cmd.class)) { + ICommandAddress parsed = parseCommandMethod(selector, method, instance); + groupMatcherCache.getGroupFor(method).addChild(parsed); + } + } + + } + + private static final class GroupMatcherCache { + private ModifiableCommandAddress groupRootAddress; + private GroupEntry[] matchEntries; + private Pattern[] patterns; + private ModifiableCommandAddress[] addresses; + + GroupMatcherCache(Class clazz, ICommandAddress groupRootAddress) throws CommandParseException { + this.groupRootAddress = (ModifiableCommandAddress) groupRootAddress; + + GroupMatchedCommands groupMatchedCommands = clazz.getAnnotation(GroupMatchedCommands.class); + GroupEntry[] matchEntries = groupMatchedCommands == null ? new GroupEntry[0] : groupMatchedCommands.value(); + + Pattern[] patterns = new Pattern[matchEntries.length]; + for (int i = 0; i < matchEntries.length; i++) { + GroupEntry matchEntry = matchEntries[i]; + if (matchEntry.group().isEmpty() || matchEntry.regex().isEmpty()) { + throw new CommandParseException("Empty group or regex in GroupMatchedCommands entry"); + } + try { + patterns[i] = Pattern.compile(matchEntry.regex()); + } catch (PatternSyntaxException ex) { + throw new CommandParseException(ex); + } + } + + this.matchEntries = matchEntries; + this.patterns = patterns; + this.addresses = new ModifiableCommandAddress[this.matchEntries.length]; + } + + ModifiableCommandAddress getGroupFor(Method method) { + String name = method.getName(); + + GroupEntry[] matchEntries = this.matchEntries; + Pattern[] patterns = this.patterns; + ModifiableCommandAddress[] addresses = this.addresses; + + for (int i = 0; i < matchEntries.length; i++) { + GroupEntry matchEntry = matchEntries[i]; + if (patterns[i].matcher(name).matches()) { + if (addresses[i] == null) { + 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]; + } + } + + return groupRootAddress; + } + + } + + public static ICommandAddress parseCommandMethod(IParameterTypeSelector selector, Method method, Object instance) throws CommandParseException { + return new ReflectiveCommand(selector, method, instance).getAddress(); + } + + static int parseCommandAttributes(IParameterTypeSelector selector, Method method, ReflectiveCommand command, java.lang.reflect.Parameter[] callParameters) throws CommandParseException { + ParameterList list = command.getParameterList(); + + Class senderParameterType = null; + int flags = 0; + int start = 0; + int end = callParameters.length; + + if (callParameters.length > start + && command.getInstance() instanceof ICommandInterceptor + && ICommandReceiver.class.isAssignableFrom(callParameters[start].getType())) { + flags |= RECEIVER_BIT; + ++start; + } + + if (callParameters.length > start && CommandSender.class.isAssignableFrom(senderParameterType = callParameters[start].getType())) { + flags |= SENDER_BIT; + ++start; + } + + if (callParameters.length > start && callParameters[start].getType() == ExecutionContext.class) { + flags |= CONTEXT_BIT; + ++start; + } + + if (callParameters.length > start && callParameters[end - 1].getType().getName().equals("kotlin.coroutines.Continuation")) { + flags |= CONTINUATION_BIT; + --end; + } + + String[] parameterNames = lookupParameterNames(method, callParameters, start); + for (int i = start, n = end; i < n; i++) { + Parameter parameter = parseParameter(selector, method, callParameters[i], parameterNames[i - start]); + list.addParameter(parameter); + } + + command.setParameterOrder(hasCallArg(flags, CONTINUATION_BIT) ? Arrays.copyOfRange(parameterNames, 0, parameterNames.length - 1) : parameterNames); + + RequirePermissions cmdPermissions = method.getAnnotation(RequirePermissions.class); + if (cmdPermissions != null) { + for (String permission : cmdPermissions.value()) { + command.addContextFilter(IContextFilter.permission(permission)); + } + + if (cmdPermissions.inherit()) { + command.addContextFilter(IContextFilter.INHERIT_PERMISSIONS); + } + } else { + command.addContextFilter(IContextFilter.INHERIT_PERMISSIONS); + } + + RequireParameters reqPar = method.getAnnotation(RequireParameters.class); + if (reqPar != null) { + list.setRequiredCount(reqPar.value() < 0 ? Integer.MAX_VALUE : reqPar.value()); + } else { + list.setRequiredCount(list.getIndexedParameters().size()); + } + + /* + PreprocessArgs preprocessArgs = method.getAnnotation(PreprocessArgs.class); + if (preprocessArgs != null) { + IArgumentPreProcessor preProcessor = IArgumentPreProcessor.mergeOnTokens(preprocessArgs.tokens(), preprocessArgs.escapeChar()); + list.setArgumentPreProcessor(preProcessor); + }*/ + + Desc desc = method.getAnnotation(Desc.class); + if (desc != null) { + String[] array = desc.value(); + if (array.length == 0) { + command.setDescription(desc.shortVersion()); + } else { + command.setDescription(array); + } + } else { + command.setDescription(); + } + + boolean hasSenderParameter = hasCallArg(flags, SENDER_BIT); + if (hasSenderParameter && Player.class.isAssignableFrom(senderParameterType)) { + command.addContextFilter(IContextFilter.PLAYER_ONLY); + } else if (hasSenderParameter && ConsoleCommandSender.class.isAssignableFrom(senderParameterType)) { + command.addContextFilter(IContextFilter.CONSOLE_ONLY); + } else if (method.isAnnotationPresent(RequirePlayer.class)) { + command.addContextFilter(IContextFilter.PLAYER_ONLY); + } else if (method.isAnnotationPresent(RequireConsole.class)) { + command.addContextFilter(IContextFilter.CONSOLE_ONLY); + } + + list.setRepeatFinalParameter(callParameters.length > start && callParameters[callParameters.length - 1].isVarArgs()); + list.setFinalParameterMayBeFlag(true); + + return flags; + } + + public static int parseCommandAttributes(IParameterTypeSelector selector, Method method, ReflectiveCommand command) throws CommandParseException { + return parseCommandAttributes(selector, method, command, method.getParameters()); + } + + public static Parameter parseParameter(IParameterTypeSelector selector, Method method, java.lang.reflect.Parameter parameter, String name) throws CommandParseException { + Class type = parameter.getType(); + if (parameter.isVarArgs()) { + type = type.getComponentType(); + } + + Annotation[] annotations = parameter.getAnnotations(); + Flag flag = null; + Annotation typeAnnotation = null; + Desc desc = null; + + for (Annotation annotation : annotations) { + //noinspection StatementWithEmptyBody + if (annotation instanceof NamedArg) { + // do nothing + } else if (annotation instanceof Flag) { + if (flag != null) { + throw new CommandParseException("Multiple flags for the same parameter"); + } + flag = (Flag) annotation; + } else if (annotation instanceof Desc) { + if (desc != null) { + throw new CommandParseException("Multiple descriptions for the same parameter"); + } + desc = (Desc) annotation; + } else { + if (typeAnnotation != null) { + throw new CommandParseException("Multiple parameter type annotations for the same parameter"); + } + typeAnnotation = annotation; + } + } + + if (flag == null && name.startsWith("-")) { + throw new CommandParseException("Non-flag parameter's name starts with -"); + } else if (flag != null && !name.startsWith("-")) { + throw new CommandParseException("Flag parameter's name doesn't start with -"); + } + + ParameterType parameterType = selector.selectAny(type, typeAnnotation == null ? null : typeAnnotation.getClass()); + if (parameterType == null) { + throw new CommandParseException("IParameter type not found for parameter " + name + " in method " + method.toString()); + } + + Object parameterInfo; + if (typeAnnotation == null) { + parameterInfo = null; + } else try { + parameterInfo = parameterType.getParameterConfig() == null ? null : parameterType.getParameterConfig().getParameterInfo(typeAnnotation); + } catch (Exception ex) { + throw new CommandParseException("Invalid parameter config", ex); + } + + String descString = desc == null ? null : CommandAnnotationUtils.getShortDescription(desc); + + try { + //noinspection unchecked + String flagPermission = flag == null || flag.permission().isEmpty() ? null : flag.permission(); + return new Parameter<>(name, descString, parameterType, parameterInfo, type.isPrimitive(), name.startsWith("-"), flagPermission); + } catch (Exception ex) { + throw new CommandParseException("Invalid parameter", ex); + } + } + + public static void generateCommands(ICommandAddress address, String[] input) { + for (String value : input) { + Consumer consumer = PredefinedCommand.getPredefinedCommandGenerator(value); + if (consumer == null) { + System.out.println("[Command Warning] generated command '" + value + "' could not be found"); + } else { + consumer.accept(address); + } + } + } + + /* + Desired format + + @Cmd({"tp", "tpto"}) + @RequirePermissions("teleport.self") + public (static) String|void onCommand(Player sender, Player target, @Flag("force", permission = "teleport.self.force") boolean force) { + Validate.isTrue(force || !hasTpToggledOff(target), "Target has teleportation disabled. Use -force to ignore"); + sender.teleport(target); + //return + } + + parser needs to: + - see the @Cmd and create a CommandTree for it + - see that it must be a Player executing the command + - add an indexed IParameter for a Player type + - add a flag parameter named force, that consumes no arguments. + - see that setting the force flag requires a permission + */ + + private static void setDescription(ICommandAddress address, String[] array, String shortVersion) { + if (!address.hasCommand()) { + return; + } + + if (array.length == 0) { + address.getCommand().setDescription(shortVersion); + } else { + address.getCommand().setDescription(array); + } + + } + +} diff --git a/dicore3/command/src/main/kotlin/io/dico/dicore/command/registration/reflect/KotlinReflectiveRegistration.kt b/dicore3/command/src/main/kotlin/io/dico/dicore/command/registration/reflect/KotlinReflectiveRegistration.kt index 1a6692b..273cadc 100644 --- a/dicore3/command/src/main/kotlin/io/dico/dicore/command/registration/reflect/KotlinReflectiveRegistration.kt +++ b/dicore3/command/src/main/kotlin/io/dico/dicore/command/registration/reflect/KotlinReflectiveRegistration.kt @@ -1,66 +1,69 @@ -package io.dico.dicore.command.registration.reflect - -import io.dico.dicore.command.* -import kotlinx.coroutines.CoroutineStart.UNDISPATCHED -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.async -import java.lang.reflect.Method -import java.util.concurrent.CancellationException -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.intrinsics.intercepted -import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn -import kotlin.reflect.jvm.kotlinFunction - -fun isSuspendFunction(method: Method): Boolean { - val func = method.kotlinFunction ?: return false - return func.isSuspend -} - -fun callAsCoroutine( - command: ReflectiveCommand, - factory: ICommandInterceptor, - context: ExecutionContext, - args: Array -): String? { - val coroutineContext = factory.getCoroutineContext(context, command.method, command.cmdName) as CoroutineContext - - // UNDISPATCHED causes the handler to run until the first suspension point on the current thread, - // meaning command handlers that don't have suspension points will run completely synchronously. - // Tasks that take time to compute should suspend the coroutine and resume on another thread. - val job = GlobalScope.async(context = coroutineContext, start = UNDISPATCHED) { - suspendCoroutineUninterceptedOrReturn { cont -> - command.method.invoke(command.instance, *args, cont.intercepted()) - } - } - - if (job.isCompleted) { - return job.getResult() - } - - job.invokeOnCompletion { - val chatHandler = context.address.chatHandler - try { - val result = job.getResult() - chatHandler.sendMessage(context.sender, EMessageType.RESULT, result) - } catch (ex: Throwable) { - chatHandler.handleException(context.sender, context, ex) - } - } - - return null -} - -@Throws(CommandException::class) -private fun Deferred.getResult(): String? { - getCompletionExceptionOrNull()?.let { ex -> - if (ex is CancellationException) { - System.err.println("An asynchronously dispatched command was cancelled unexpectedly") - ex.printStackTrace() - throw CommandException("The command was cancelled unexpectedly (see console)") - } - if (ex is Exception) return ReflectiveCommand.getResult(null, ex) - throw ex - } - return ReflectiveCommand.getResult(getCompleted(), null) -} +package io.dico.dicore.command.registration.reflect + +import io.dico.dicore.command.* +import kotlinx.coroutines.CoroutineStart.UNDISPATCHED +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import java.lang.reflect.Method +import java.util.concurrent.CancellationException +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.intrinsics.intercepted +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn +import kotlin.reflect.jvm.kotlinFunction + +fun isSuspendFunction(method: Method): Boolean { + val func = method.kotlinFunction ?: return false + return func.isSuspend +} + +@Throws(CommandException::class) +fun callCommandAsCoroutine( + executionContext: ExecutionContext, + coroutineContext: CoroutineContext, + continuationIndex: Int, + method: Method, + instance: Any?, + args: Array +): String? { + + // UNDISPATCHED causes the handler to run until the first suspension point on the current thread, + // meaning command handlers that don't have suspension points will run completely synchronously. + // Tasks that take time to compute should suspend the coroutine and resume on another thread. + val job = GlobalScope.async(context = coroutineContext, start = UNDISPATCHED) { + suspendCoroutineUninterceptedOrReturn { cont -> + args[continuationIndex] = cont.intercepted() + method.invoke(instance, *args) + } + } + + if (job.isCompleted) { + return job.getResult() + } + + job.invokeOnCompletion { + val chatHandler = executionContext.address.chatHandler + try { + val result = job.getResult() + chatHandler.sendMessage(executionContext.sender, EMessageType.RESULT, result) + } catch (ex: Throwable) { + chatHandler.handleException(executionContext.sender, executionContext, ex) + } + } + + return null +} + +@Throws(CommandException::class) +private fun Deferred.getResult(): String? { + getCompletionExceptionOrNull()?.let { ex -> + if (ex is CancellationException) { + System.err.println("An asynchronously dispatched command was cancelled unexpectedly") + ex.printStackTrace() + throw CommandException("The command was cancelled unexpectedly (see console)") + } + if (ex is Exception) return ReflectiveCommand.getResult(null, ex) + throw ex + } + return ReflectiveCommand.getResult(getCompleted(), null) +} diff --git a/src/main/kotlin/io/dico/parcels2/JobDispatcher.kt b/src/main/kotlin/io/dico/parcels2/JobDispatcher.kt index 12be89a..ebbe334 100644 --- a/src/main/kotlin/io/dico/parcels2/JobDispatcher.kt +++ b/src/main/kotlin/io/dico/parcels2/JobDispatcher.kt @@ -1,337 +1,368 @@ -package io.dico.parcels2 - -import io.dico.parcels2.util.math.clampMin -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart.LAZY -import kotlinx.coroutines.Job as CoroutineJob -import kotlinx.coroutines.launch -import org.bukkit.scheduler.BukkitTask -import java.lang.System.currentTimeMillis -import java.util.LinkedList -import kotlin.coroutines.Continuation -import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED -import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn -import kotlin.coroutines.resume - -typealias JobFunction = suspend JobScope.() -> Unit -typealias JobUpdateLister = Job.(Double, Long) -> Unit - -data class TickJobtimeOptions(var jobTime: Int, var tickInterval: Int) - -interface JobDispatcher { - /** - * Submit a [function] that should be run synchronously, but limited such that it does not stall the server - */ - fun dispatch(function: JobFunction): Job - - /** - * Get a list of all jobs - */ - val jobs: List - - /** - * Attempts to complete any remaining tasks immediately, without suspension. - */ - fun completeAllTasks() -} - -interface JobAndScopeMembersUnion { - /** - * The time that elapsed since this job was dispatched, in milliseconds - */ - val elapsedTime: Long - - /** - * A value indicating the progress of this job, in the range 0.0 <= progress <= 1.0 - * with no guarantees to its accuracy. - */ - val progress: Double -} - -interface Job : JobAndScopeMembersUnion { - /** - * The coroutine associated with this job - */ - val coroutine: CoroutineJob - - /** - * true if this job has completed - */ - val isComplete: Boolean - - /** - * If an exception was thrown during the execution of this task, - * returns that exception. Returns null otherwise. - */ - val completionException: Throwable? - - /** - * Calls the given [block] whenever the progress of this job is updated, - * if [minInterval] milliseconds expired since the last call. - * The first call occurs after at least [minDelay] milliseconds in a likewise manner. - * Repeated invocations of this method result in an [IllegalStateException] - * - * if [asCompletionListener] is true, [onCompleted] is called with the same [block] - */ - fun onProgressUpdate(minDelay: Int, minInterval: Int, asCompletionListener: Boolean = true, block: JobUpdateLister): Job - - /** - * Calls the given [block] when this job completes, with the progress value 1.0. - * Multiple listeners may be registered to this function. - */ - fun onCompleted(block: JobUpdateLister): Job - - /** - * Await completion of this job - */ - suspend fun awaitCompletion() -} - -interface JobScope : JobAndScopeMembersUnion { - /** - * A task should call this frequently during its execution, such that the timer can suspend it when necessary. - */ - suspend fun markSuspensionPoint() - - /** - * A task should call this method to indicate its progress - */ - fun setProgress(progress: Double) - - /** - * Indicate that this job is complete - */ - fun markComplete() = setProgress(1.0) - - /** - * Get a [JobScope] that is responsible for [portion] part of the progress - * If [portion] is negative, the remaining progress is used - */ - fun delegateProgress(portion: Double = -1.0): JobScope -} - -inline fun JobScope.delegateWork(portion: Double = -1.0, block: JobScope.() -> T): T { - delegateProgress(portion).apply { - val result = block() - markComplete() - return result - } -} - -interface JobInternal : Job, JobScope { - /** - * Start or resumes the execution of this job - * and returns true if the job completed - * - * [worktime] is the maximum amount of time, in milliseconds, - * that this job may run for until suspension. - * - * If [worktime] is not positive, the job will complete - * without suspension and this method will always return true. - */ - fun resume(worktime: Long): Boolean -} - -/** - * An object that controls one or more jobs, ensuring that they don't stall the server too much. - * There is a configurable maxiumum amount of milliseconds that can be allocated to all jobs together in each server tick - * This object attempts to split that maximum amount of milliseconds equally between all jobs - */ -class BukkitJobDispatcher(private val plugin: ParcelsPlugin, var options: TickJobtimeOptions) : JobDispatcher { - // The currently registered bukkit scheduler task - private var bukkitTask: BukkitTask? = null - // The jobs. - private val _jobs = LinkedList() - override val jobs: List = _jobs - - override fun dispatch(function: JobFunction): Job { - val job: JobInternal = JobImpl(plugin, function) - - if (bukkitTask == null) { - val completed = job.resume(options.jobTime.toLong()) - if (completed) return job - bukkitTask = plugin.scheduleRepeating(0, options.tickInterval) { tickCoroutineJobs() } - } - _jobs.addFirst(job) - return job - } - - private fun tickCoroutineJobs() { - val jobs = _jobs - if (jobs.isEmpty()) return - val tickStartTime = System.currentTimeMillis() - - val iterator = jobs.listIterator(index = 0) - while (iterator.hasNext()) { - val time = System.currentTimeMillis() - val timeElapsed = time - tickStartTime - val timeLeft = options.jobTime - timeElapsed - if (timeLeft <= 0) return - - val count = jobs.size - iterator.nextIndex() - val timePerJob = (timeLeft + count - 1) / count - val job = iterator.next() - val completed = job.resume(timePerJob) - if (completed) { - iterator.remove() - } - } - - if (jobs.isEmpty()) { - bukkitTask?.cancel() - bukkitTask = null - } - } - - override fun completeAllTasks() { - _jobs.forEach { - it.resume(-1) - } - _jobs.clear() - bukkitTask?.cancel() - bukkitTask = null - } - -} - -private class JobImpl(scope: CoroutineScope, task: JobFunction) : JobInternal { - override val coroutine: CoroutineJob = scope.launch(start = LAZY) { task() } - - private var continuation: Continuation? = null - private var nextSuspensionTime: Long = 0L - private var completeForcefully = false - private var isStarted = false - - override val elapsedTime - get() = - if (coroutine.isCompleted) startTimeOrElapsedTime - else currentTimeMillis() - startTimeOrElapsedTime - - override val isComplete get() = coroutine.isCompleted - - private var _progress = 0.0 - override val progress get() = _progress - override var completionException: Throwable? = null; private set - - private var startTimeOrElapsedTime: Long = 0L // startTime before completed, elapsed time otherwise - private var onProgressUpdate: JobUpdateLister? = null - private var progressUpdateInterval: Int = 0 - private var lastUpdateTime: Long = 0L - private var onCompleted: JobUpdateLister? = null - - init { - coroutine.invokeOnCompletion { exception -> - // report any error that occurred - completionException = exception?.also { - if (it !is CancellationException) - logger.error("JobFunction generated an exception", it) - } - - // convert to elapsed time here - startTimeOrElapsedTime = System.currentTimeMillis() - startTimeOrElapsedTime - onCompleted?.let { it(1.0, elapsedTime) } - - onCompleted = null - onProgressUpdate = { prog, el -> } - } - } - - override fun onProgressUpdate(minDelay: Int, minInterval: Int, asCompletionListener: Boolean, block: JobUpdateLister): Job { - onProgressUpdate?.let { throw IllegalStateException() } - if (asCompletionListener) onCompleted(block) - if (isComplete) return this - onProgressUpdate = block - progressUpdateInterval = minInterval - lastUpdateTime = System.currentTimeMillis() + minDelay - minInterval - - return this - } - - override fun onCompleted(block: JobUpdateLister): Job { - if (isComplete) { - block(1.0, startTimeOrElapsedTime) - return this - } - - val cur = onCompleted - onCompleted = if (cur == null) { - block - } else { - fun Job.(prog: Double, el: Long) { - cur(prog, el) - block(prog, el) - } - } - return this - } - - override suspend fun markSuspensionPoint() { - if (System.currentTimeMillis() >= nextSuspensionTime && !completeForcefully) - suspendCoroutineUninterceptedOrReturn { cont: Continuation -> - continuation = cont - COROUTINE_SUSPENDED - } - } - - override fun setProgress(progress: Double) { - this._progress = progress - val onProgressUpdate = onProgressUpdate ?: return - val time = System.currentTimeMillis() - if (time > lastUpdateTime + progressUpdateInterval) { - onProgressUpdate(progress, elapsedTime) - lastUpdateTime = time - } - } - - override fun resume(worktime: Long): Boolean { - if (isComplete) return true - - if (worktime > 0) { - nextSuspensionTime = currentTimeMillis() + worktime - } else { - completeForcefully = true - } - - if (isStarted) { - continuation?.let { - continuation = null - it.resume(Unit) - return continuation == null - } - return true - } - - isStarted = true - startTimeOrElapsedTime = System.currentTimeMillis() - coroutine.start() - - return continuation == null - } - - override suspend fun awaitCompletion() { - coroutine.join() - } - - private fun delegateProgress(curPortion: Double, portion: Double): JobScope = - DelegateScope(progress, curPortion * (if (portion < 0) 1.0 - progress else portion).clampMin(0.0)) - - override fun delegateProgress(portion: Double): JobScope = delegateProgress(1.0, portion) - - private inner class DelegateScope(val progressStart: Double, val portion: Double) : JobScope { - override val elapsedTime: Long - get() = this@JobImpl.elapsedTime - - override suspend fun markSuspensionPoint() = - this@JobImpl.markSuspensionPoint() - - override val progress: Double - get() = (this@JobImpl.progress - progressStart) / portion - - override fun setProgress(progress: Double) = - this@JobImpl.setProgress(progressStart + progress * portion) - - override fun delegateProgress(portion: Double): JobScope = - this@JobImpl.delegateProgress(this.portion, portion) - } -} +package io.dico.parcels2 + +import io.dico.parcels2.util.PluginAware +import io.dico.parcels2.util.math.clampMin +import io.dico.parcels2.util.scheduleRepeating +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart.LAZY +import kotlinx.coroutines.Job as CoroutineJob +import kotlinx.coroutines.launch +import org.bukkit.scheduler.BukkitTask +import java.lang.System.currentTimeMillis +import java.util.LinkedList +import kotlin.coroutines.Continuation +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn +import kotlin.coroutines.resume + +typealias JobFunction = suspend JobScope.() -> Unit +typealias JobUpdateLister = Job.(Double, Long) -> Unit + +data class TickJobtimeOptions(var jobTime: Int, var tickInterval: Int) + +interface JobDispatcher { + /** + * Submit a [function] that should be run synchronously, but limited such that it does not stall the server + */ + fun dispatch(function: JobFunction): Job + + /** + * Get a list of all jobs + */ + val jobs: List + + /** + * Attempts to complete any remaining tasks immediately, without suspension. + */ + fun completeAllTasks() +} + +interface JobAndScopeMembersUnion { + /** + * The time that elapsed since this job was dispatched, in milliseconds + */ + val elapsedTime: Long + + /** + * A value indicating the progress of this job, in the range 0.0 <= progress <= 1.0 + * with no guarantees to its accuracy. + */ + val progress: Double +} + +interface Job : JobAndScopeMembersUnion { + /** + * The coroutine associated with this job + */ + val coroutine: CoroutineJob + + /** + * true if this job has completed + */ + val isComplete: Boolean + + /** + * If an exception was thrown during the execution of this task, + * returns that exception. Returns null otherwise. + */ + val completionException: Throwable? + + /** + * Calls the given [block] whenever the progress of this job is updated, + * if [minInterval] milliseconds expired since the last call. + * The first call occurs after at least [minDelay] milliseconds in a likewise manner. + * Repeated invocations of this method result in an [IllegalStateException] + * + * if [asCompletionListener] is true, [onCompleted] is called with the same [block] + */ + fun onProgressUpdate( + minDelay: Int, + minInterval: Int, + asCompletionListener: Boolean = true, + block: JobUpdateLister + ): Job + + /** + * Calls the given [block] when this job completes, with the progress value 1.0. + * Multiple listeners may be registered to this function. + */ + fun onCompleted(block: JobUpdateLister): Job + + /** + * Await completion of this job + */ + suspend fun awaitCompletion() +} + +interface JobScope : JobAndScopeMembersUnion { + /** + * A task should call this frequently during its execution, such that the timer can suspend it when necessary. + */ + suspend fun markSuspensionPoint() + + /** + * A task should call this method to indicate its progress + */ + fun setProgress(progress: Double) + + /** + * Indicate that this job is complete + */ + fun markComplete() = setProgress(1.0) + + /** + * Get a [JobScope] that is responsible for [portion] part of the progress + * If [portion] is negative, the remaining progress is used + */ + fun delegateProgress(portion: Double = -1.0): JobScope +} + +inline fun JobScope.delegateWork(portion: Double = -1.0, block: JobScope.() -> T): T { + delegateProgress(portion).apply { + val result = block() + markComplete() + return result + } +} + +interface JobInternal : Job, JobScope { + /** + * Start or resumes the execution of this job + * and returns true if the job completed + * + * [worktime] is the maximum amount of time, in milliseconds, + * that this job may run for until suspension. + * + * If [worktime] is not positive, the job will complete + * without suspension and this method will always return true. + */ + fun resume(worktime: Long): Boolean +} + +/** + * An object that controls one or more jobs, ensuring that they don't stall the server too much. + * There is a configurable maxiumum amount of milliseconds that can be allocated to all jobs together in each server tick + * This object attempts to split that maximum amount of milliseconds equally between all jobs + */ +class BukkitJobDispatcher( + private val plugin: PluginAware, + private val scope: CoroutineScope, + var options: TickJobtimeOptions +) : JobDispatcher { + // The currently registered bukkit scheduler task + private var bukkitTask: BukkitTask? = null + // The jobs. + private val _jobs = LinkedList() + override val jobs: List = _jobs + + override fun dispatch(function: JobFunction): Job { + val job: JobInternal = JobImpl(scope, function) + + if (bukkitTask == null) { + val completed = job.resume(options.jobTime.toLong()) + if (completed) return job + bukkitTask = plugin.scheduleRepeating(options.tickInterval) { tickJobs() } + } + _jobs.addFirst(job) + return job + } + + private fun tickJobs() { + val jobs = _jobs + if (jobs.isEmpty()) return + val tickStartTime = System.currentTimeMillis() + + val iterator = jobs.listIterator(index = 0) + while (iterator.hasNext()) { + val time = System.currentTimeMillis() + val timeElapsed = time - tickStartTime + val timeLeft = options.jobTime - timeElapsed + if (timeLeft <= 0) return + + val count = jobs.size - iterator.nextIndex() + val timePerJob = (timeLeft + count - 1) / count + val job = iterator.next() + val completed = job.resume(timePerJob) + if (completed) { + iterator.remove() + } + } + + if (jobs.isEmpty()) { + bukkitTask?.cancel() + bukkitTask = null + } + } + + override fun completeAllTasks() { + _jobs.forEach { + it.resume(-1) + } + _jobs.clear() + bukkitTask?.cancel() + bukkitTask = null + } + +} + +private class JobImpl(scope: CoroutineScope, task: JobFunction) : JobInternal { + override val coroutine: CoroutineJob = scope.launch(start = LAZY) { task() } + + private var continuation: Continuation? = null + private var nextSuspensionTime: Long = 0L + private var completeForcefully = false + private var isStarted = false + + override val elapsedTime + get() = + if (coroutine.isCompleted) startTimeOrElapsedTime + else currentTimeMillis() - startTimeOrElapsedTime + + override val isComplete get() = coroutine.isCompleted + + private var _progress = 0.0 + override val progress get() = _progress + override var completionException: Throwable? = null; private set + + private var startTimeOrElapsedTime: Long = 0L // startTime before completed, elapsed time otherwise + private var onProgressUpdate: JobUpdateLister? = null + private var progressUpdateInterval: Int = 0 + private var lastUpdateTime: Long = 0L + private var onCompleted: JobUpdateLister? = null + + init { + coroutine.invokeOnCompletion { exception -> + // report any error that occurred + completionException = exception?.also { + if (it !is CancellationException) + logger.error("JobFunction generated an exception", it) + } + + // convert to elapsed time here + startTimeOrElapsedTime = System.currentTimeMillis() - startTimeOrElapsedTime + onCompleted?.let { it(1.0, elapsedTime) } + + onCompleted = null + onProgressUpdate = { prog, el -> } + } + } + + override fun onProgressUpdate( + minDelay: Int, + minInterval: Int, + asCompletionListener: Boolean, + block: JobUpdateLister + ): Job { + onProgressUpdate?.let { throw IllegalStateException() } + if (asCompletionListener) onCompleted(block) + if (isComplete) return this + onProgressUpdate = block + progressUpdateInterval = minInterval + lastUpdateTime = System.currentTimeMillis() + minDelay - minInterval + + return this + } + + override fun onCompleted(block: JobUpdateLister): Job { + if (isComplete) { + block(1.0, startTimeOrElapsedTime) + return this + } + + val cur = onCompleted + onCompleted = if (cur == null) { + block + } else { + fun Job.(prog: Double, el: Long) { + cur(prog, el) + block(prog, el) + } + } + return this + } + + override suspend fun markSuspensionPoint() { + if (System.currentTimeMillis() >= nextSuspensionTime && !completeForcefully) + suspendCoroutineUninterceptedOrReturn { cont: Continuation -> + continuation = cont + COROUTINE_SUSPENDED + } + } + + override fun setProgress(progress: Double) { + this._progress = progress + val onProgressUpdate = onProgressUpdate ?: return + val time = System.currentTimeMillis() + if (time > lastUpdateTime + progressUpdateInterval) { + onProgressUpdate(progress, elapsedTime) + lastUpdateTime = time + } + } + + override fun resume(worktime: Long): Boolean { + if (isComplete) return true + + if (worktime > 0) { + nextSuspensionTime = currentTimeMillis() + worktime + } else { + completeForcefully = true + } + + if (isStarted) { + continuation?.let { + continuation = null + + wrapExternalCall { + it.resume(Unit) + } + + return continuation == null + } + return true + } + + isStarted = true + startTimeOrElapsedTime = System.currentTimeMillis() + + wrapExternalCall { + coroutine.start() + } + + return continuation == null + } + + private inline fun wrapExternalCall(block: () -> Unit) { + try { + block() + } catch (ex: Throwable) { + logger.error("Job $coroutine generated an exception", ex) + } + } + + override suspend fun awaitCompletion() { + coroutine.join() + } + + private fun delegateProgress(curPortion: Double, portion: Double): JobScope = + DelegateScope(this, progress, curPortion * (if (portion < 0) 1.0 - progress else portion).clampMin(0.0)) + + override fun delegateProgress(portion: Double): JobScope = delegateProgress(1.0, portion) + + private class DelegateScope(val parent: JobImpl, val progressStart: Double, val portion: Double) : JobScope { + override val elapsedTime: Long + get() = parent.elapsedTime + + override suspend fun markSuspensionPoint() = + parent.markSuspensionPoint() + + override val progress: Double + get() = (parent.progress - progressStart) / portion + + override fun setProgress(progress: Double) = + parent.setProgress(progressStart + progress * portion) + + override fun delegateProgress(portion: Double): JobScope = + parent.delegateProgress(this.portion, portion) + } +} diff --git a/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt b/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt index 63ec02c..ada6d12 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelGenerator.kt @@ -1,103 +1,102 @@ -package io.dico.parcels2 - -import io.dico.parcels2.blockvisitor.RegionTraverser -import io.dico.parcels2.util.math.Region -import io.dico.parcels2.util.math.Vec2i -import io.dico.parcels2.util.math.Vec3i -import io.dico.parcels2.util.math.get -import kotlinx.coroutines.CoroutineScope -import org.bukkit.Chunk -import org.bukkit.Location -import org.bukkit.Material -import org.bukkit.World -import org.bukkit.block.Biome -import org.bukkit.block.Block -import org.bukkit.block.BlockFace -import org.bukkit.entity.Entity -import org.bukkit.generator.BlockPopulator -import org.bukkit.generator.ChunkGenerator -import java.util.Random - -abstract class ParcelGenerator : ChunkGenerator() { - abstract val worldName: String - - abstract val world: World - - abstract override fun generateChunkData(world: World?, random: Random?, chunkX: Int, chunkZ: Int, biome: BiomeGrid?): ChunkData - - abstract fun populate(world: World?, random: Random?, chunk: Chunk?) - - abstract override fun getFixedSpawnLocation(world: World?, random: Random?): Location - - override fun getDefaultPopulators(world: World?): MutableList { - return mutableListOf(object : BlockPopulator() { - override fun populate(world: World?, random: Random?, chunk: Chunk?) { - this@ParcelGenerator.populate(world, random, chunk) - } - }) - } - - abstract fun makeParcelLocatorAndBlockManager( - parcelProvider: ParcelProvider, - container: ParcelContainer, - coroutineScope: CoroutineScope, - jobDispatcher: JobDispatcher - ): Pair -} - -interface ParcelBlockManager { - val world: World - val jobDispatcher: JobDispatcher - val parcelTraverser: RegionTraverser - - fun getRegionOrigin(parcel: ParcelId) = getRegion(parcel).origin.toVec2i() - - fun getHomeLocation(parcel: ParcelId): Location - - fun getRegion(parcel: ParcelId): Region - - fun getEntities(parcel: ParcelId): Collection - - fun isParcelInfoSectionLoaded(parcel: ParcelId): Boolean - - fun updateParcelInfo(parcel: ParcelId, owner: PlayerProfile?) - - fun getParcelForInfoBlockInteraction(block: Vec3i, type: Material, face: BlockFace): Parcel? - - fun setBiome(parcel: ParcelId, biome: Biome): Job? - - fun clearParcel(parcel: ParcelId): Job? - - /** - * Used to update owner blocks in the corner of the parcel - */ - fun getParcelsWithOwnerBlockIn(chunk: Chunk): Collection -} - -inline fun ParcelBlockManager.tryDoBlockOperation( - parcelProvider: ParcelProvider, - parcel: ParcelId, - traverser: RegionTraverser, - crossinline operation: suspend JobScope.(Block) -> Unit -) = parcelProvider.trySubmitBlockVisitor(Permit(), arrayOf(parcel)) { - val region = getRegion(parcel) - val blockCount = region.blockCount.toDouble() - val blocks = traverser.traverseRegion(region) - for ((index, vec) in blocks.withIndex()) { - markSuspensionPoint() - operation(world[vec]) - setProgress((index + 1) / blockCount) - } -} - -abstract class ParcelBlockManagerBase : ParcelBlockManager { - - override fun getEntities(parcel: ParcelId): Collection { - val region = getRegion(parcel) - val center = region.center - val centerLoc = Location(world, center.x, center.y, center.z) - val centerDist = (center - region.origin).add(0.2, 0.2, 0.2) - return world.getNearbyEntities(centerLoc, centerDist.x, centerDist.y, centerDist.z) - } - -} +package io.dico.parcels2 + +import io.dico.parcels2.blockvisitor.RegionTraverser +import io.dico.parcels2.util.math.Region +import io.dico.parcels2.util.math.Vec2i +import io.dico.parcels2.util.math.Vec3i +import io.dico.parcels2.util.math.get +import kotlinx.coroutines.CoroutineScope +import org.bukkit.Chunk +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.World +import org.bukkit.block.Biome +import org.bukkit.block.Block +import org.bukkit.block.BlockFace +import org.bukkit.entity.Entity +import org.bukkit.generator.BlockPopulator +import org.bukkit.generator.ChunkGenerator +import java.util.Random + +abstract class ParcelGenerator : ChunkGenerator() { + abstract val worldName: String + + abstract val world: World + + abstract override fun generateChunkData(world: World?, random: Random?, chunkX: Int, chunkZ: Int, biome: BiomeGrid?): ChunkData + + abstract fun populate(world: World?, random: Random?, chunk: Chunk?) + + abstract override fun getFixedSpawnLocation(world: World?, random: Random?): Location + + override fun getDefaultPopulators(world: World?): MutableList { + return mutableListOf(object : BlockPopulator() { + override fun populate(world: World?, random: Random?, chunk: Chunk?) { + this@ParcelGenerator.populate(world, random, chunk) + } + }) + } + + abstract fun makeParcelLocatorAndBlockManager( + parcelProvider: ParcelProvider, + container: ParcelContainer, + coroutineScope: CoroutineScope, + jobDispatcher: JobDispatcher + ): Pair +} + +interface ParcelBlockManager { + val world: World + val jobDispatcher: JobDispatcher + val parcelTraverser: RegionTraverser + + fun getRegionOrigin(parcel: ParcelId) = getRegion(parcel).origin.toVec2i() + + fun getHomeLocation(parcel: ParcelId): Location + + fun getRegion(parcel: ParcelId): Region + + fun getEntities(region: Region): Collection + + fun isParcelInfoSectionLoaded(parcel: ParcelId): Boolean + + fun updateParcelInfo(parcel: ParcelId, owner: PlayerProfile?) + + fun getParcelForInfoBlockInteraction(block: Vec3i, type: Material, face: BlockFace): Parcel? + + fun setBiome(parcel: ParcelId, biome: Biome): Job? + + fun clearParcel(parcel: ParcelId): Job? + + /** + * Used to update owner blocks in the corner of the parcel + */ + fun getParcelsWithOwnerBlockIn(chunk: Chunk): Collection +} + +inline fun ParcelBlockManager.tryDoBlockOperation( + parcelProvider: ParcelProvider, + parcel: ParcelId, + traverser: RegionTraverser, + crossinline operation: suspend JobScope.(Block) -> Unit +) = parcelProvider.trySubmitBlockVisitor(Permit(), arrayOf(parcel)) { + val region = getRegion(parcel) + val blockCount = region.blockCount.toDouble() + val blocks = traverser.traverseRegion(region) + for ((index, vec) in blocks.withIndex()) { + markSuspensionPoint() + operation(world[vec]) + setProgress((index + 1) / blockCount) + } +} + +abstract class ParcelBlockManagerBase : ParcelBlockManager { + + override fun getEntities(region: Region): Collection { + val center = region.center + val centerLoc = Location(world, center.x, center.y, center.z) + val centerDist = (center - region.origin).add(0.2, 0.2, 0.2) + return world.getNearbyEntities(centerLoc, centerDist.x, centerDist.y, centerDist.z) + } + +} diff --git a/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt b/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt index 36dfe1c..054d9d6 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelWorld.kt @@ -1,103 +1,103 @@ -package io.dico.parcels2 - -import io.dico.parcels2.options.RuntimeWorldOptions -import io.dico.parcels2.storage.Storage -import io.dico.parcels2.util.math.Vec2i -import io.dico.parcels2.util.math.floor -import org.bukkit.Location -import org.bukkit.World -import org.bukkit.block.Block -import org.bukkit.entity.Entity -import org.joda.time.DateTime -import java.lang.IllegalStateException -import java.util.UUID - -class Permit - -interface ParcelProvider { - val worlds: Map - - fun getWorldById(id: ParcelWorldId): ParcelWorld? - - fun getParcelById(id: ParcelId): Parcel? - - fun getWorld(name: String): ParcelWorld? - - fun getWorld(world: World): ParcelWorld? = getWorld(world.name) - - fun getWorld(block: Block): ParcelWorld? = getWorld(block.world) - - fun getWorld(loc: Location): ParcelWorld? = getWorld(loc.world) - - fun getWorld(entity: Entity): ParcelWorld? = getWorld(entity.location) - - fun getParcelAt(worldName: String, x: Int, z: Int): Parcel? = getWorld(worldName)?.locator?.getParcelAt(x, z) - - fun getParcelAt(world: World, x: Int, z: Int): Parcel? = getParcelAt(world.name, x, z) - - fun getParcelAt(world: World, vec: Vec2i): Parcel? = getParcelAt(world, vec.x, vec.z) - - fun getParcelAt(loc: Location): Parcel? = getParcelAt(loc.world, loc.x.floor(), loc.z.floor()) - - fun getParcelAt(entity: Entity): Parcel? = getParcelAt(entity.location) - - fun getParcelAt(block: Block): Parcel? = getParcelAt(block.world, block.x, block.z) - - fun getWorldGenerator(worldName: String): ParcelGenerator? - - fun loadWorlds() - - fun acquireBlockVisitorPermit(parcelId: ParcelId, with: Permit): Boolean - - @Throws(IllegalStateException::class) - fun releaseBlockVisitorPermit(parcelId: ParcelId, with: Permit) - - fun trySubmitBlockVisitor(permit: Permit, parcelIds: Array, function: JobFunction): Job? - - fun swapParcels(parcelId1: ParcelId, parcelId2: ParcelId): Job? -} - -interface ParcelLocator { - val world: World - - fun getParcelIdAt(x: Int, z: Int): ParcelId? - - fun getParcelAt(x: Int, z: Int): Parcel? - - fun getParcelAt(vec: Vec2i): Parcel? = getParcelAt(vec.x, vec.z) - - fun getParcelAt(loc: Location): Parcel? = getParcelAt(loc.x.floor(), loc.z.floor()).takeIf { loc.world == world } - - fun getParcelAt(entity: Entity): Parcel? = getParcelAt(entity.location).takeIf { entity.world == world } - - fun getParcelAt(block: Block): Parcel? = getParcelAt(block.x, block.z).takeIf { block.world == world } -} - -typealias ParcelContainerFactory = (ParcelWorld) -> ParcelContainer - -interface ParcelContainer { - - fun getParcelById(x: Int, z: Int): Parcel? - - fun getParcelById(id: Vec2i): Parcel? = getParcelById(id.x, id.z) - - fun getParcelById(id: ParcelId): Parcel? - - fun nextEmptyParcel(): Parcel? - -} - -interface ParcelWorld : ParcelLocator, ParcelContainer { - val id: ParcelWorldId - val name: String - val uid: UUID? - val options: RuntimeWorldOptions - val generator: ParcelGenerator - val storage: Storage - val container: ParcelContainer - val locator: ParcelLocator - val blockManager: ParcelBlockManager - val globalPrivileges: GlobalPrivilegesManager - - val creationTime: DateTime? -} +package io.dico.parcels2 + +import io.dico.parcels2.options.RuntimeWorldOptions +import io.dico.parcels2.storage.Storage +import io.dico.parcels2.util.math.Vec2i +import io.dico.parcels2.util.math.floor +import org.bukkit.Location +import org.bukkit.World +import org.bukkit.block.Block +import org.bukkit.entity.Entity +import org.joda.time.DateTime +import java.lang.IllegalStateException +import java.util.UUID + +class Permit + +interface ParcelProvider { + val worlds: Map + + fun getWorldById(id: ParcelWorldId): ParcelWorld? + + fun getParcelById(id: ParcelId): Parcel? + + fun getWorld(name: String): ParcelWorld? + + fun getWorld(world: World): ParcelWorld? = getWorld(world.name) + + fun getWorld(block: Block): ParcelWorld? = getWorld(block.world) + + fun getWorld(loc: Location): ParcelWorld? = getWorld(loc.world) + + fun getWorld(entity: Entity): ParcelWorld? = getWorld(entity.location) + + fun getParcelAt(worldName: String, x: Int, z: Int): Parcel? = getWorld(worldName)?.locator?.getParcelAt(x, z) + + fun getParcelAt(world: World, x: Int, z: Int): Parcel? = getParcelAt(world.name, x, z) + + fun getParcelAt(world: World, vec: Vec2i): Parcel? = getParcelAt(world, vec.x, vec.z) + + fun getParcelAt(loc: Location): Parcel? = getParcelAt(loc.world, loc.x.floor(), loc.z.floor()) + + fun getParcelAt(entity: Entity): Parcel? = getParcelAt(entity.location) + + fun getParcelAt(block: Block): Parcel? = getParcelAt(block.world, block.x, block.z) + + fun getWorldGenerator(worldName: String): ParcelGenerator? + + fun loadWorlds() + + fun acquireBlockVisitorPermit(parcelId: ParcelId, with: Permit): Boolean + + @Throws(IllegalStateException::class) + fun releaseBlockVisitorPermit(parcelId: ParcelId, with: Permit) + + fun trySubmitBlockVisitor(permit: Permit, parcelIds: Array, function: JobFunction): Job? + + fun swapParcels(parcelId1: ParcelId, parcelId2: ParcelId): Job? +} + +interface ParcelLocator { + val world: World + + fun getParcelIdAt(x: Int, z: Int): ParcelId? + + fun getParcelAt(x: Int, z: Int): Parcel? + + fun getParcelAt(vec: Vec2i): Parcel? = getParcelAt(vec.x, vec.z) + + fun getParcelAt(loc: Location): Parcel? = getParcelAt(loc.x.floor(), loc.z.floor()).takeIf { loc.world == world } + + fun getParcelAt(entity: Entity): Parcel? = getParcelAt(entity.location).takeIf { entity.world == world } + + fun getParcelAt(block: Block): Parcel? = getParcelAt(block.x, block.z).takeIf { block.world == world } +} + +typealias ParcelContainerFactory = (ParcelWorld) -> ParcelContainer + +interface ParcelContainer { + + fun getParcelById(x: Int, z: Int): Parcel? + + fun getParcelById(id: Vec2i): Parcel? = getParcelById(id.x, id.z) + + fun getParcelById(id: ParcelId): Parcel? + + suspend fun nextEmptyParcel(): Parcel? + +} + +interface ParcelWorld : ParcelLocator, ParcelContainer { + val id: ParcelWorldId + val name: String + val uid: UUID? + val options: RuntimeWorldOptions + val generator: ParcelGenerator + val storage: Storage + val container: ParcelContainer + val locator: ParcelLocator + val blockManager: ParcelBlockManager + val globalPrivileges: GlobalPrivilegesManager + + val creationTime: DateTime? +} diff --git a/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt b/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt index b2d52a9..2ffef06 100644 --- a/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt +++ b/src/main/kotlin/io/dico/parcels2/ParcelsPlugin.kt @@ -1,153 +1,154 @@ -package io.dico.parcels2 - -import io.dico.dicore.Registrator -import io.dico.dicore.command.EOverridePolicy -import io.dico.dicore.command.ICommandDispatcher -import io.dico.parcels2.command.getParcelCommands -import io.dico.parcels2.defaultimpl.GlobalPrivilegesManagerImpl -import io.dico.parcels2.defaultimpl.ParcelProviderImpl -import io.dico.parcels2.listener.ParcelEntityTracker -import io.dico.parcels2.listener.ParcelListeners -import io.dico.parcels2.listener.WorldEditListener -import io.dico.parcels2.options.Options -import io.dico.parcels2.options.optionsMapper -import io.dico.parcels2.storage.Storage -import io.dico.parcels2.util.MainThreadDispatcher -import io.dico.parcels2.util.PluginScheduler -import io.dico.parcels2.util.ext.tryCreate -import io.dico.parcels2.util.isServerThread -import kotlinx.coroutines.CoroutineScope -import org.bukkit.Bukkit -import org.bukkit.generator.ChunkGenerator -import org.bukkit.plugin.Plugin -import org.bukkit.plugin.java.JavaPlugin -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.io.File -import kotlin.coroutines.CoroutineContext - -val logger: Logger = LoggerFactory.getLogger("ParcelsPlugin") -private inline val plogger get() = logger - -class ParcelsPlugin : JavaPlugin(), CoroutineScope, PluginScheduler { - lateinit var optionsFile: File; private set - lateinit var options: Options; private set - lateinit var parcelProvider: ParcelProvider; private set - lateinit var storage: Storage; private set - lateinit var globalPrivileges: GlobalPrivilegesManager; private set - - val registrator = Registrator(this) - lateinit var entityTracker: ParcelEntityTracker; private set - private var listeners: ParcelListeners? = null - private var cmdDispatcher: ICommandDispatcher? = null - - override val coroutineContext: CoroutineContext = MainThreadDispatcher(this) - override val plugin: Plugin get() = this - val jobDispatcher: JobDispatcher by lazy { BukkitJobDispatcher(this, options.tickJobtime) } - - override fun onEnable() { - plogger.info("Is server thread: ${isServerThread()}") - plogger.info("Debug enabled: ${plogger.isDebugEnabled}") - plogger.debug(System.getProperty("user.dir")) - if (!init()) { - Bukkit.getPluginManager().disablePlugin(this) - } - } - - override fun onDisable() { - val hasWorkers = jobDispatcher.jobs.isNotEmpty() - if (hasWorkers) { - plogger.warn("Parcels is attempting to complete all ${jobDispatcher.jobs.size} remaining jobs before shutdown...") - } - jobDispatcher.completeAllTasks() - if (hasWorkers) { - plogger.info("Parcels has completed the remaining jobs.") - } - - cmdDispatcher?.unregisterFromCommandMap() - } - - private fun init(): Boolean { - optionsFile = File(dataFolder, "options.yml") - options = Options() - parcelProvider = ParcelProviderImpl(this) - - try { - if (!loadOptions()) return false - - try { - storage = options.storage.newInstance() - storage.init() - } catch (ex: Exception) { - plogger.error("Failed to connect to database", ex) - return false - } - - globalPrivileges = GlobalPrivilegesManagerImpl(this) - entityTracker = ParcelEntityTracker(parcelProvider) - } catch (ex: Exception) { - plogger.error("Error loading options", ex) - return false - } - - registerListeners() - registerCommands() - - parcelProvider.loadWorlds() - return true - } - - fun loadOptions(): Boolean { - when { - optionsFile.exists() -> optionsMapper.readerForUpdating(options).readValue(optionsFile) - else -> run { - options.addWorld("parcels") - if (saveOptions()) { - plogger.warn("Created options file with a world template. Please review it before next start.") - } else { - plogger.error("Failed to save options file ${optionsFile.canonicalPath}") - } - return false - } - } - return true - } - - fun saveOptions(): Boolean { - if (optionsFile.tryCreate()) { - try { - optionsMapper.writeValue(optionsFile, options) - } catch (ex: Throwable) { - optionsFile.delete() - throw ex - } - return true - } - return false - } - - override fun getDefaultWorldGenerator(worldName: String, generatorId: String?): ChunkGenerator? { - return parcelProvider.getWorldGenerator(worldName) - } - - private fun registerCommands() { - cmdDispatcher = getParcelCommands(this).apply { - registerToCommandMap("parcels:", EOverridePolicy.FALLBACK_ONLY) - } - } - - private fun registerListeners() { - if (listeners == null) { - listeners = ParcelListeners(parcelProvider, entityTracker, storage) - registrator.registerListeners(listeners!!) - - val worldEditPlugin = server.pluginManager.getPlugin("WorldEdit") - if (worldEditPlugin != null) { - WorldEditListener.register(this, worldEditPlugin) - } - } - - scheduleRepeating(100, 5, entityTracker::tick) - } - +package io.dico.parcels2 + +import io.dico.dicore.Registrator +import io.dico.dicore.command.EOverridePolicy +import io.dico.dicore.command.ICommandDispatcher +import io.dico.parcels2.command.getParcelCommands +import io.dico.parcels2.defaultimpl.GlobalPrivilegesManagerImpl +import io.dico.parcels2.defaultimpl.ParcelProviderImpl +import io.dico.parcels2.listener.ParcelEntityTracker +import io.dico.parcels2.listener.ParcelListeners +import io.dico.parcels2.listener.WorldEditListener +import io.dico.parcels2.options.Options +import io.dico.parcels2.options.optionsMapper +import io.dico.parcels2.storage.Storage +import io.dico.parcels2.util.MainThreadDispatcher +import io.dico.parcels2.util.PluginAware +import io.dico.parcels2.util.ext.tryCreate +import io.dico.parcels2.util.isServerThread +import io.dico.parcels2.util.scheduleRepeating +import kotlinx.coroutines.CoroutineScope +import org.bukkit.Bukkit +import org.bukkit.generator.ChunkGenerator +import org.bukkit.plugin.Plugin +import org.bukkit.plugin.java.JavaPlugin +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.File +import kotlin.coroutines.CoroutineContext + +val logger: Logger = LoggerFactory.getLogger("ParcelsPlugin") +private inline val plogger get() = logger + +class ParcelsPlugin : JavaPlugin(), CoroutineScope, PluginAware { + lateinit var optionsFile: File; private set + lateinit var options: Options; private set + lateinit var parcelProvider: ParcelProvider; private set + lateinit var storage: Storage; private set + lateinit var globalPrivileges: GlobalPrivilegesManager; private set + + val registrator = Registrator(this) + lateinit var entityTracker: ParcelEntityTracker; private set + private var listeners: ParcelListeners? = null + private var cmdDispatcher: ICommandDispatcher? = null + + override val coroutineContext: CoroutineContext = MainThreadDispatcher(this) + override val plugin: Plugin get() = this + val jobDispatcher: JobDispatcher by lazy { BukkitJobDispatcher(this, this, options.tickJobtime) } + + override fun onEnable() { + plogger.info("Is server thread: ${isServerThread()}") + plogger.info("Debug enabled: ${plogger.isDebugEnabled}") + plogger.debug(System.getProperty("user.dir")) + if (!init()) { + Bukkit.getPluginManager().disablePlugin(this) + } + } + + override fun onDisable() { + val hasWorkers = jobDispatcher.jobs.isNotEmpty() + if (hasWorkers) { + plogger.warn("Parcels is attempting to complete all ${jobDispatcher.jobs.size} remaining jobs before shutdown...") + } + jobDispatcher.completeAllTasks() + if (hasWorkers) { + plogger.info("Parcels has completed the remaining jobs.") + } + + cmdDispatcher?.unregisterFromCommandMap() + } + + private fun init(): Boolean { + optionsFile = File(dataFolder, "options.yml") + options = Options() + parcelProvider = ParcelProviderImpl(this) + + try { + if (!loadOptions()) return false + + try { + storage = options.storage.newInstance() + storage.init() + } catch (ex: Exception) { + plogger.error("Failed to connect to database", ex) + return false + } + + globalPrivileges = GlobalPrivilegesManagerImpl(this) + entityTracker = ParcelEntityTracker(parcelProvider) + } catch (ex: Exception) { + plogger.error("Error loading options", ex) + return false + } + + registerListeners() + registerCommands() + + parcelProvider.loadWorlds() + return true + } + + fun loadOptions(): Boolean { + when { + optionsFile.exists() -> optionsMapper.readerForUpdating(options).readValue(optionsFile) + else -> run { + options.addWorld("parcels") + if (saveOptions()) { + plogger.warn("Created options file with a world template. Please review it before next start.") + } else { + plogger.error("Failed to save options file ${optionsFile.canonicalPath}") + } + return false + } + } + return true + } + + fun saveOptions(): Boolean { + if (optionsFile.tryCreate()) { + try { + optionsMapper.writeValue(optionsFile, options) + } catch (ex: Throwable) { + optionsFile.delete() + throw ex + } + return true + } + return false + } + + override fun getDefaultWorldGenerator(worldName: String, generatorId: String?): ChunkGenerator? { + return parcelProvider.getWorldGenerator(worldName) + } + + private fun registerCommands() { + cmdDispatcher = getParcelCommands(this).apply { + registerToCommandMap("parcels:", EOverridePolicy.FALLBACK_ONLY) + } + } + + private fun registerListeners() { + if (listeners == null) { + listeners = ParcelListeners(parcelProvider, entityTracker, storage) + registrator.registerListeners(listeners!!) + + val worldEditPlugin = server.pluginManager.getPlugin("WorldEdit") + if (worldEditPlugin != null) { + WorldEditListener.register(this, worldEditPlugin) + } + } + + scheduleRepeating(5, delay = 100, task = entityTracker::tick) + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/PlayerProfile.kt b/src/main/kotlin/io/dico/parcels2/PlayerProfile.kt index 6c30c27..b73f7ba 100644 --- a/src/main/kotlin/io/dico/parcels2/PlayerProfile.kt +++ b/src/main/kotlin/io/dico/parcels2/PlayerProfile.kt @@ -1,184 +1,208 @@ -@file:Suppress("unused", "UsePropertyAccessSyntax", "DEPRECATION") - -package io.dico.parcels2 - -import io.dico.parcels2.storage.Storage -import io.dico.parcels2.util.ext.PLAYER_NAME_PLACEHOLDER -import io.dico.parcels2.util.ext.isValid -import io.dico.parcels2.util.ext.uuid -import io.dico.parcels2.util.getOfflinePlayer -import io.dico.parcels2.util.getPlayerName -import org.bukkit.Bukkit -import org.bukkit.OfflinePlayer -import java.util.UUID - -interface PlayerProfile { - val uuid: UUID? get() = null - val name: String? - val nameOrBukkitName: String? - val notNullName: String - val isStar: Boolean get() = this is Star - val exists: Boolean get() = this is RealImpl - - fun matches(player: OfflinePlayer, allowNameMatch: Boolean = false): Boolean - - fun equals(other: PlayerProfile): Boolean - - override fun equals(other: Any?): Boolean - override fun hashCode(): Int - - val isFake: Boolean get() = this is Fake - val isReal: Boolean get() = this is Real - - companion object { - fun safe(uuid: UUID?, name: String?): PlayerProfile? { - if (uuid != null) return Real(uuid, name) - if (name != null) return invoke(name) - return null - } - - operator fun invoke(uuid: UUID?, name: String?): PlayerProfile { - return safe(uuid, name) ?: throw IllegalArgumentException("One of uuid and name must not be null") - } - - operator fun invoke(uuid: UUID): Real { - if (uuid == Star.uuid) return Star - return RealImpl(uuid, null) - } - - operator fun invoke(name: String): PlayerProfile { - if (name == Star.name) return Star - return Fake(name) - } - - operator fun invoke(player: OfflinePlayer): PlayerProfile { - return if (player.isValid) Real(player.uuid, player.name) else Fake(player.name) - } - - fun nameless(player: OfflinePlayer): Real { - if (!player.isValid) throw IllegalArgumentException("The given OfflinePlayer is not valid") - return RealImpl(player.uuid, null) - } - - fun byName(input: String, allowReal: Boolean = true, allowFake: Boolean = false): PlayerProfile { - if (!allowReal) { - if (!allowFake) throw IllegalArgumentException("at least one of allowReal and allowFake must be true") - return Fake(input) - } - - if (input == Star.name) return Star - - return getOfflinePlayer(input)?.let { PlayerProfile(it) } ?: Unresolved(input) - } - } - - interface Real : PlayerProfile { - override val uuid: UUID - override val nameOrBukkitName: String? - // If a player is online, their name is prioritized to get name changes right immediately - get() = Bukkit.getPlayer(uuid)?.name ?: name ?: getPlayerName(uuid) - override val notNullName: String - get() = nameOrBukkitName ?: PLAYER_NAME_PLACEHOLDER - - val player: OfflinePlayer? get() = getOfflinePlayer(uuid) - - override fun matches(player: OfflinePlayer, allowNameMatch: Boolean): Boolean { - return uuid == player.uuid || (allowNameMatch && name?.let { it == player.name } == true) - } - - override fun equals(other: PlayerProfile): Boolean { - return other is Real && uuid == other.uuid - } - - companion object { - fun byName(name: String): PlayerProfile { - if (name == Star.name) return Star - return Unresolved(name) - } - - operator fun invoke(uuid: UUID, name: String?): Real { - if (name == Star.name || uuid == Star.uuid) return Star - return RealImpl(uuid, name) - } - - fun safe(uuid: UUID?, name: String?): Real? { - if (name == Star.name || uuid == Star.uuid) return Star - if (uuid == null) return null - return RealImpl(uuid, name) - } - - } - } - - object Star : BaseImpl(), Real { - override val name get() = "*" - override val nameOrBukkitName get() = name - override val notNullName get() = name - - // hopefully nobody will have this random UUID :) - override val uuid: UUID = UUID.fromString("7d09c4c6-117d-4f36-9778-c4d24618cee1") - - override fun matches(player: OfflinePlayer, allowNameMatch: Boolean): Boolean { - return true - } - - override fun toString() = "Star" - } - - abstract class NameOnly(override val name: String) : BaseImpl() { - override val notNullName get() = name - override val nameOrBukkitName: String get() = name - - override fun matches(player: OfflinePlayer, allowNameMatch: Boolean): Boolean { - return allowNameMatch && player.name == name - } - - override fun toString() = "${javaClass.simpleName}($name)" - } - - class Fake(name: String) : NameOnly(name) { - override fun equals(other: PlayerProfile): Boolean { - return other is Fake && other.name == name - } - } - - class Unresolved(name: String) : NameOnly(name) { - override fun equals(other: PlayerProfile): Boolean { - return other is Unresolved && name == other.name - } - - suspend fun tryResolveSuspendedly(storage: Storage): Real? { - return storage.getPlayerUuidForName(name).await()?.let { resolve(it) } - } - - fun resolve(uuid: UUID): Real { - return RealImpl(uuid, name) - } - - fun throwException(): Nothing { - throw IllegalArgumentException("A UUID for the player $name can not be found") - } - } - - abstract class BaseImpl : PlayerProfile { - override fun equals(other: Any?): Boolean { - return this === other || (other is PlayerProfile && equals(other)) - } - - override fun hashCode(): Int { - return uuid?.hashCode() ?: name!!.hashCode() - } - } - - private class RealImpl(override val uuid: UUID, override val name: String?) : BaseImpl(), Real { - override fun toString() = "Real($notNullName)" - } - -} - -suspend fun PlayerProfile.resolved(storage: Storage, resolveToFake: Boolean = false): PlayerProfile? = - when (this) { - is PlayerProfile.Unresolved -> tryResolveSuspendedly(storage) - ?: if (resolveToFake) PlayerProfile.Fake(name) else null - else -> this +@file:Suppress("unused", "UsePropertyAccessSyntax", "DEPRECATION") + +package io.dico.parcels2 + +import io.dico.parcels2.storage.Storage +import io.dico.parcels2.util.checkPlayerNameValid +import io.dico.parcels2.util.ext.PLAYER_NAME_PLACEHOLDER +import io.dico.parcels2.util.ext.isValid +import io.dico.parcels2.util.ext.uuid +import io.dico.parcels2.util.getOfflinePlayer +import io.dico.parcels2.util.getPlayerName +import io.dico.parcels2.util.isPlayerNameValid +import org.bukkit.Bukkit +import org.bukkit.OfflinePlayer +import java.util.UUID + +interface PlayerProfile { + val uuid: UUID? get() = null + val name: String? + val nameOrBukkitName: String? + val notNullName: String + val isStar: Boolean get() = this is Star + val exists: Boolean get() = this is RealImpl + + fun matches(player: OfflinePlayer, allowNameMatch: Boolean = false): Boolean + + fun equals(other: PlayerProfile): Boolean + + override fun equals(other: Any?): Boolean + override fun hashCode(): Int + + val isFake: Boolean get() = this is Fake + val isReal: Boolean get() = this is Real + + companion object { + fun safe(uuid: UUID?, name: String?): PlayerProfile? { + if (uuid != null) return Real(uuid, if (name != null && !isPlayerNameValid(name)) null else name) + if (name != null) return invoke(name) + return null + } + + operator fun invoke(uuid: UUID?, name: String?): PlayerProfile { + return safe(uuid, name) ?: throw IllegalArgumentException("One of uuid and name must not be null") + } + + operator fun invoke(uuid: UUID): Real { + if (uuid == Star.uuid) return Star + return RealImpl(uuid, null) + } + + operator fun invoke(name: String): PlayerProfile { + if (name equalsIgnoreCase Star.name) return Star + return Fake(name) + } + + operator fun invoke(player: OfflinePlayer): PlayerProfile { + return if (player.isValid) Real(player.uuid, player.name) else Fake(player.name) + } + + fun nameless(player: OfflinePlayer): Real { + if (!player.isValid) throw IllegalArgumentException("The given OfflinePlayer is not valid") + return RealImpl(player.uuid, null) + } + + fun byName(input: String, allowReal: Boolean = true, allowFake: Boolean = false): PlayerProfile? { + if (!allowReal) { + if (!allowFake) throw IllegalArgumentException("at least one of allowReal and allowFake must be true") + return Fake(input) + } + + if (!isPlayerNameValid(input)) { + if (!allowFake) return null + return Fake(input) + } + + if (input == Star.name) return Star + + return getOfflinePlayer(input)?.let { PlayerProfile(it) } ?: Unresolved(input) + } + } + + interface Real : PlayerProfile { + override val uuid: UUID + override val nameOrBukkitName: String? + // If a player is online, their name is prioritized to get name changes right immediately + get() = Bukkit.getPlayer(uuid)?.name ?: name ?: getPlayerName(uuid) + override val notNullName: String + get() = nameOrBukkitName ?: PLAYER_NAME_PLACEHOLDER + + val player: OfflinePlayer? get() = getOfflinePlayer(uuid) + + override fun matches(player: OfflinePlayer, allowNameMatch: Boolean): Boolean { + return uuid == player.uuid || (allowNameMatch && name?.let { it == player.name } == true) + } + + override fun equals(other: PlayerProfile): Boolean { + return other is Real && uuid == other.uuid + } + + companion object { + fun byName(name: String): PlayerProfile { + if (name equalsIgnoreCase Star.name) return Star + return Unresolved(name) + } + + operator fun invoke(uuid: UUID, name: String?): Real { + if (name equalsIgnoreCase Star.name || uuid == Star.uuid) return Star + return RealImpl(uuid, name) + } + + fun safe(uuid: UUID?, name: String?): Real? { + if (name equalsIgnoreCase Star.name || uuid == Star.uuid) return Star + if (uuid == null) return null + return RealImpl(uuid, if (name != null && !isPlayerNameValid(name)) null else name) + } + + } + } + + object Star : BaseImpl(), Real { + override val name get() = "*" + override val nameOrBukkitName get() = name + override val notNullName get() = name + + // hopefully nobody will have this random UUID :) + override val uuid: UUID = UUID.fromString("7d09c4c6-117d-4f36-9778-c4d24618cee1") + + override fun matches(player: OfflinePlayer, allowNameMatch: Boolean): Boolean { + return true + } + + override fun toString() = "Star" + } + + abstract class NameOnly(override val name: String) : BaseImpl() { + override val notNullName get() = name + override val nameOrBukkitName: String get() = name + + override fun matches(player: OfflinePlayer, allowNameMatch: Boolean): Boolean { + return allowNameMatch && player.name equalsIgnoreCase name + } + + override fun toString() = "${javaClass.simpleName}($name)" + } + + class Fake(name: String) : NameOnly(name) { + override fun equals(other: PlayerProfile): Boolean { + return other is Fake && other.name equalsIgnoreCase name + } + } + + class Unresolved(name: String) : NameOnly(name) { + init { + checkPlayerNameValid(name) + } + + override fun equals(other: PlayerProfile): Boolean { + return other is Unresolved && name equalsIgnoreCase other.name + } + + suspend fun tryResolveSuspendedly(storage: Storage): Real? { + return storage.getPlayerUuidForName(name).await()?.let { resolve(it) } + } + + fun resolve(uuid: UUID): Real { + return RealImpl(uuid, name) + } + + fun throwException(): Nothing { + throw IllegalArgumentException("A UUID for the player $name can not be found") + } + } + + abstract class BaseImpl : PlayerProfile { + override fun equals(other: Any?): Boolean { + return this === other || (other is PlayerProfile && equals(other)) + } + + override fun hashCode(): Int { + return uuid?.hashCode() ?: name!!.hashCode() + } + } + + private class RealImpl(override val uuid: UUID, override val name: String?) : BaseImpl(), Real { + init { + name?.let { checkPlayerNameValid(it) } + } + + override fun toString() = "Real($notNullName)" + } + +} + +private infix fun String?.equalsIgnoreCase(other: String): Boolean { + if (this == null) return false + if (length != other.length) return false + repeat(length) { i -> + if (this[i].toLowerCase() != other[i].toLowerCase()) return false + } + return true +} + +suspend fun PlayerProfile.resolved(storage: Storage, resolveToFake: Boolean = false): PlayerProfile? = + when (this) { + is PlayerProfile.Unresolved -> tryResolveSuspendedly(storage) + ?: if (resolveToFake) PlayerProfile.Fake(name) else null + else -> this } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/blockvisitor/Entities.kt b/src/main/kotlin/io/dico/parcels2/blockvisitor/Entities.kt new file mode 100644 index 0000000..d9ea09f --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/blockvisitor/Entities.kt @@ -0,0 +1,38 @@ +package io.dico.parcels2.blockvisitor + +import io.dico.parcels2.util.math.Vec3d +import org.bukkit.Location +import org.bukkit.World +import org.bukkit.entity.Entity +import org.bukkit.entity.Minecart + +/* +open class EntityCopy(entity: T) { + val type = entity.type + + @Suppress("UNCHECKED_CAST") + fun spawn(world: World, position: Vec3d): T { + val entity = world.spawnEntity(Location(null, position.x, position.y, position.z), type) as T + setAttributes(entity) + return entity + } + + open fun setAttributes(entity: T) {} +} + +open class MinecartCopy(entity: T) : EntityCopy(entity) { + val damage = entity.damage + val maxSpeed = entity.maxSpeed + val isSlowWhenEmpty = entity.isSlowWhenEmpty + val flyingVelocityMod = entity.flyingVelocityMod + val derailedVelocityMod = entity.derailedVelocityMod + val displayBlockData = entity.displayBlockData + val displayBlockOffset = entity.displayBlockOffset + + override fun setAttributes(entity: T) { + super.setAttributes(entity) + entity.damage = damage + entity.displayBlockData = displayBlockData + entity.displayBlockOffset = displayBlockOffset + } +}*/ \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/command/ParcelParameterTypes.kt b/src/main/kotlin/io/dico/parcels2/command/ParcelParameterTypes.kt index 730625e..1b20f72 100644 --- a/src/main/kotlin/io/dico/parcels2/command/ParcelParameterTypes.kt +++ b/src/main/kotlin/io/dico/parcels2/command/ParcelParameterTypes.kt @@ -1,83 +1,90 @@ -package io.dico.parcels2.command - -import io.dico.dicore.command.CommandException -import io.dico.dicore.command.parameter.ArgumentBuffer -import io.dico.dicore.command.parameter.Parameter -import io.dico.dicore.command.parameter.type.ParameterConfig -import io.dico.dicore.command.parameter.type.ParameterType -import io.dico.parcels2.* -import io.dico.parcels2.command.ProfileKind.Companion.ANY -import io.dico.parcels2.command.ProfileKind.Companion.FAKE -import io.dico.parcels2.command.ProfileKind.Companion.REAL -import org.bukkit.Location -import org.bukkit.command.CommandSender -import org.bukkit.entity.Player - -fun invalidInput(parameter: Parameter<*, *>, message: String): Nothing { - throw CommandException("invalid input for ${parameter.name}: $message") -} - -fun ParcelProvider.getTargetWorld(input: String?, sender: CommandSender, parameter: Parameter<*, *>): ParcelWorld { - val worldName = input - ?.takeUnless { it.isEmpty() } - ?: (sender as? Player)?.world?.name - ?: invalidInput(parameter, "console cannot omit the world name") - - return getWorld(worldName) - ?: invalidInput(parameter, "$worldName is not a parcel world") -} - -class ParcelParameterType(val parcelProvider: ParcelProvider) : ParameterType(Parcel::class.java) { - val regex = Regex.fromLiteral("((.+)->)?([0-9]+):([0-9]+)") - - override fun parse(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): Parcel { - val matchResult = regex.matchEntire(buffer.next()!!) - ?: invalidInput(parameter, "must match (w->)?a:b (/${regex.pattern}/)") - - val world = parcelProvider.getTargetWorld(matchResult.groupValues[2], sender, parameter) - - val x = matchResult.groupValues[3].toIntOrNull() - ?: invalidInput(parameter, "couldn't parse int") - - val z = matchResult.groupValues[4].toIntOrNull() - ?: invalidInput(parameter, "couldn't parse int") - - return world.getParcelById(x, z) - ?: invalidInput(parameter, "parcel id is out of range") - } - -} - -annotation class ProfileKind(val kind: Int) { - companion object : ParameterConfig(ProfileKind::class.java) { - const val REAL = 1 - const val FAKE = 2 - const val ANY = REAL or FAKE - - override fun toParameterInfo(annotation: ProfileKind): Int { - return annotation.kind - } - } -} - -class ProfileParameterType : ParameterType(PlayerProfile::class.java, ProfileKind) { - - override fun parse(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): PlayerProfile { - val info = parameter.paramInfo ?: REAL - val allowReal = (info and REAL) != 0 - val allowFake = (info and FAKE) != 0 - - val input = buffer.next()!! - return PlayerProfile.byName(input, allowReal, allowFake) - } - - override fun complete( - parameter: Parameter, - sender: CommandSender, - location: Location?, - buffer: ArgumentBuffer - ): MutableList { - logger.info("Completing PlayerProfile: ${buffer.next()}") - return super.complete(parameter, sender, location, buffer) - } -} +package io.dico.parcels2.command + +import io.dico.dicore.command.CommandException +import io.dico.dicore.command.parameter.ArgumentBuffer +import io.dico.dicore.command.parameter.Parameter +import io.dico.dicore.command.parameter.type.ParameterConfig +import io.dico.dicore.command.parameter.type.ParameterType +import io.dico.parcels2.* +import io.dico.parcels2.command.ProfileKind.Companion.FAKE +import io.dico.parcels2.command.ProfileKind.Companion.REAL +import org.bukkit.Location +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + +fun invalidInput(parameter: Parameter<*, *>, message: String): Nothing { + throw CommandException("invalid input for ${parameter.name}: $message") +} + +fun ParcelProvider.getTargetWorld(input: String?, sender: CommandSender, parameter: Parameter<*, *>): ParcelWorld { + val worldName = input + ?.takeUnless { it.isEmpty() } + ?: (sender as? Player)?.world?.name + ?: invalidInput(parameter, "console cannot omit the world name") + + return getWorld(worldName) + ?: invalidInput(parameter, "$worldName is not a parcel world") +} + +class ParcelParameterType(val parcelProvider: ParcelProvider) : ParameterType(Parcel::class.java) { + val regex = Regex.fromLiteral("((.+)->)?([0-9]+):([0-9]+)") + + override fun parse(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): Parcel { + val matchResult = regex.matchEntire(buffer.next()!!) + ?: invalidInput(parameter, "must match (w->)?a:b (/${regex.pattern}/)") + + val world = parcelProvider.getTargetWorld(matchResult.groupValues[2], sender, parameter) + + val x = matchResult.groupValues[3].toIntOrNull() + ?: invalidInput(parameter, "couldn't parse int") + + val z = matchResult.groupValues[4].toIntOrNull() + ?: invalidInput(parameter, "couldn't parse int") + + return world.getParcelById(x, z) + ?: invalidInput(parameter, "parcel id is out of range") + } + +} + +annotation class ProfileKind(val kind: Int) { + companion object : ParameterConfig(ProfileKind::class.java) { + const val REAL = 1 + const val FAKE = 2 + const val ANY = REAL or FAKE + const val ALLOW_INVALID = 4 + + override fun toParameterInfo(annotation: ProfileKind): Int { + return annotation.kind + } + } +} + +class ProfileParameterType : ParameterType(PlayerProfile::class.java, ProfileKind) { + + override fun parse(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): PlayerProfile? { + val info = parameter.paramInfo ?: REAL + val allowReal = (info and REAL) != 0 + val allowFake = (info and FAKE) != 0 + + val input = buffer.next()!! + + val profile = PlayerProfile.byName(input, allowReal, allowFake) + + if (profile == null && (info and ProfileKind.ALLOW_INVALID) == 0) { + invalidInput(parameter, "\'$input\' is not a valid player name") + } + + return profile + } + + override fun complete( + parameter: Parameter, + sender: CommandSender, + location: Location?, + buffer: ArgumentBuffer + ): MutableList { + logger.info("Completing PlayerProfile: ${buffer.next()}") + return super.complete(parameter, sender, location, buffer) + } +} diff --git a/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt b/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt index c39c4b6..934a993 100644 --- a/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt +++ b/src/main/kotlin/io/dico/parcels2/command/ParcelTarget.kt @@ -1,191 +1,215 @@ -package io.dico.parcels2.command - -import io.dico.dicore.command.parameter.ArgumentBuffer -import io.dico.dicore.command.parameter.Parameter -import io.dico.dicore.command.parameter.type.ParameterConfig -import io.dico.dicore.command.parameter.type.ParameterType -import io.dico.parcels2.Parcel -import io.dico.parcels2.ParcelProvider -import io.dico.parcels2.ParcelWorld -import io.dico.parcels2.PlayerProfile -import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.DEFAULT_KIND -import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.ID -import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.OWNER -import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.OWNER_FAKE -import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.OWNER_REAL -import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.PREFER_OWNED_FOR_DEFAULT -import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.REAL -import io.dico.parcels2.storage.Storage -import io.dico.parcels2.util.math.Vec2i -import io.dico.parcels2.util.math.floor -import org.bukkit.command.CommandSender -import org.bukkit.entity.Player - -sealed class ParcelTarget(val world: ParcelWorld, val parsedKind: Int, val isDefault: Boolean) { - - abstract suspend fun getParcelSuspend(storage: Storage): Parcel? - - class ByID(world: ParcelWorld, val id: Vec2i?, parsedKind: Int, isDefault: Boolean) : ParcelTarget(world, parsedKind, isDefault) { - override suspend fun getParcelSuspend(storage: Storage): Parcel? = getParcel() - fun getParcel() = id?.let { world.getParcelById(it) } - val isPath: Boolean get() = id == null - } - - class ByOwner( - world: ParcelWorld, - owner: PlayerProfile, - val index: Int, - parsedKind: Int, - isDefault: Boolean, - val onResolveFailure: (() -> Unit)? = null - ) : ParcelTarget(world, parsedKind, isDefault) { - init { - if (index < 0) throw IllegalArgumentException("Invalid parcel home index: $index") - } - - var owner = owner; private set - - suspend fun resolveOwner(storage: Storage): Boolean { - val owner = owner - if (owner is PlayerProfile.Unresolved) { - this.owner = owner.tryResolveSuspendedly(storage) ?: if (parsedKind and OWNER_FAKE != 0) PlayerProfile.Fake(owner.name) - else run { onResolveFailure?.invoke(); return false } - } - return true - } - - override suspend fun getParcelSuspend(storage: Storage): Parcel? { - onResolveFailure?.let { resolveOwner(storage) } - - val ownedParcelsSerialized = storage.getOwnedParcels(owner).await() - val ownedParcels = ownedParcelsSerialized - .filter { it.worldId.equals(world.id) } - .map { world.getParcelById(it.x, it.z) } - - return ownedParcels.getOrNull(index) - } - } - - annotation class TargetKind(val kind: Int) { - companion object : ParameterConfig(TargetKind::class.java) { - const val ID = 1 // ID - const val OWNER_REAL = 2 // an owner backed by a UUID - const val OWNER_FAKE = 4 // an owner not backed by a UUID - - const val OWNER = OWNER_REAL or OWNER_FAKE // any owner - const val ANY = ID or OWNER_REAL or OWNER_FAKE // any - const val REAL = ID or OWNER_REAL // no owner not backed by a UUID - - const val DEFAULT_KIND = REAL - - const val PREFER_OWNED_FOR_DEFAULT = 8 // if the kind can be ID and OWNER_REAL, prefer OWNER_REAL for default - // instead of parcel that the player is in - - override fun toParameterInfo(annotation: TargetKind): Int { - return annotation.kind - } - } - } - - class PType(val parcelProvider: ParcelProvider, val parcelAddress: SpecialCommandAddress? = null) : - ParameterType(ParcelTarget::class.java, TargetKind) { - - override fun parse(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): ParcelTarget { - var input = buffer.next()!! - val worldString = input.substringBefore("/", missingDelimiterValue = "") - input = input.substringAfter("/") - - val world = if (worldString.isEmpty()) { - val player = requirePlayer(sender, parameter, "the world") - parcelProvider.getWorld(player.world) - ?: invalidInput(parameter, "You cannot omit the world if you're not in a parcel world") - } else { - parcelProvider.getWorld(worldString) ?: invalidInput(parameter, "$worldString is not a parcel world") - } - - val kind = parameter.paramInfo ?: DEFAULT_KIND - if (input.contains(',')) { - if (kind and ID == 0) invalidInput(parameter, "You must specify a parcel by OWNER, that is, an owner and index") - return ByID(world, getId(parameter, input), kind, false) - } - - if (kind and OWNER == 0) invalidInput(parameter, "You must specify a parcel by ID, that is, the x and z component separated by a comma") - val (owner, index) = getHomeIndex(parameter, kind, sender, input) - return ByOwner(world, owner, index, kind, false, onResolveFailure = { invalidInput(parameter, "The player $input does not exist") }) - } - - private fun getId(parameter: Parameter<*, *>, input: String): Vec2i { - val x = input.substringBefore(',').run { - toIntOrNull() ?: invalidInput(parameter, "ID(x) must be an integer, $this is not an integer") - } - val z = input.substringAfter(',').run { - toIntOrNull() ?: invalidInput(parameter, "ID(z) must be an integer, $this is not an integer") - } - return Vec2i(x, z) - } - - private fun getHomeIndex(parameter: Parameter<*, *>, kind: Int, sender: CommandSender, input: String): Pair { - val splitIdx = input.indexOf(':') - val ownerString: String - val index: Int? - - val speciallyParsedIndex = parcelAddress?.speciallyParsedIndex - - if (splitIdx == -1) { - - if (speciallyParsedIndex == null) { - // just the index. - index = input.toIntOrNull() - ownerString = if (index == null) input else "" - } else { - // just the owner. - index = speciallyParsedIndex - ownerString = input - } - - } else { - if (speciallyParsedIndex != null) { - invalidInput(parameter, "Duplicate home index") - } - - ownerString = input.substring(0, splitIdx) - - val indexString = input.substring(splitIdx + 1) - index = indexString.toIntOrNull() - ?: invalidInput(parameter, "The home index must be an integer, $indexString is not an integer") - } - - val owner = if (ownerString.isEmpty()) - PlayerProfile(requirePlayer(sender, parameter, "the player")) - else - PlayerProfile.byName(ownerString, allowReal = kind and OWNER_REAL != 0, allowFake = kind and OWNER_FAKE != 0) - - return owner to (index ?: 0) - } - - private fun requirePlayer(sender: CommandSender, parameter: Parameter<*, *>, objName: String): Player { - if (sender !is Player) invalidInput(parameter, "console cannot omit the $objName") - return sender - } - - override fun getDefaultValue(parameter: Parameter, sender: CommandSender, buffer: ArgumentBuffer): ParcelTarget? { - val kind = parameter.paramInfo ?: DEFAULT_KIND - val useLocation = when { - kind and REAL == REAL -> kind and PREFER_OWNED_FOR_DEFAULT == 0 - kind and ID != 0 -> true - kind and OWNER_REAL != 0 -> false - else -> return null - } - - val player = requirePlayer(sender, parameter, "the parcel") - val world = parcelProvider.getWorld(player.world) ?: invalidInput(parameter, "You must be in a parcel world to omit the parcel") - if (useLocation) { - val id = player.location.let { world.getParcelIdAt(it.x.floor(), it.z.floor())?.pos } - return ByID(world, id, kind, true) - } - - return ByOwner(world, PlayerProfile(player), parcelAddress?.speciallyParsedIndex ?: 0, kind, true) - } - } - -} +package io.dico.parcels2.command + +import io.dico.dicore.command.parameter.ArgumentBuffer +import io.dico.dicore.command.parameter.Parameter +import io.dico.dicore.command.parameter.type.ParameterConfig +import io.dico.dicore.command.parameter.type.ParameterType +import io.dico.parcels2.Parcel +import io.dico.parcels2.ParcelProvider +import io.dico.parcels2.ParcelWorld +import io.dico.parcels2.PlayerProfile +import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.DEFAULT_KIND +import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.ID +import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.OWNER +import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.OWNER_FAKE +import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.OWNER_REAL +import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.PREFER_OWNED_FOR_DEFAULT +import io.dico.parcels2.command.ParcelTarget.TargetKind.Companion.REAL +import io.dico.parcels2.storage.Storage +import io.dico.parcels2.util.math.Vec2i +import io.dico.parcels2.util.math.floor +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + +sealed class ParcelTarget(val world: ParcelWorld, val parsedKind: Int, val isDefault: Boolean) { + + abstract suspend fun getParcelSuspend(storage: Storage): Parcel? + + class ByID(world: ParcelWorld, val id: Vec2i?, parsedKind: Int, isDefault: Boolean) : + ParcelTarget(world, parsedKind, isDefault) { + override suspend fun getParcelSuspend(storage: Storage): Parcel? = getParcel() + fun getParcel() = id?.let { world.getParcelById(it) } + val isPath: Boolean get() = id == null + } + + class ByOwner( + world: ParcelWorld, + owner: PlayerProfile, + val index: Int, + parsedKind: Int, + isDefault: Boolean, + val onResolveFailure: (() -> Unit)? = null + ) : ParcelTarget(world, parsedKind, isDefault) { + init { + if (index < 0) throw IllegalArgumentException("Invalid parcel home index: $index") + } + + var owner = owner; private set + + suspend fun resolveOwner(storage: Storage): Boolean { + val owner = owner + if (owner is PlayerProfile.Unresolved) { + this.owner = owner.tryResolveSuspendedly(storage) ?: if (parsedKind and OWNER_FAKE != 0) PlayerProfile.Fake(owner.name) + else run { onResolveFailure?.invoke(); return false } + } + return true + } + + override suspend fun getParcelSuspend(storage: Storage): Parcel? { + onResolveFailure?.let { resolveOwner(storage) } + + val ownedParcelsSerialized = storage.getOwnedParcels(owner).await() + val ownedParcels = ownedParcelsSerialized + .filter { it.worldId.equals(world.id) } + .map { world.getParcelById(it.x, it.z) } + + return ownedParcels.getOrNull(index) + } + } + + annotation class TargetKind(val kind: Int) { + companion object : ParameterConfig(TargetKind::class.java) { + const val ID = 1 // ID + const val OWNER_REAL = 2 // an owner backed by a UUID + const val OWNER_FAKE = 4 // an owner not backed by a UUID + + const val OWNER = OWNER_REAL or OWNER_FAKE // any owner + const val ANY = ID or OWNER_REAL or OWNER_FAKE // any + const val REAL = ID or OWNER_REAL // no owner not backed by a UUID + + const val DEFAULT_KIND = REAL + + const val PREFER_OWNED_FOR_DEFAULT = + 8 // if the kind can be ID and OWNER_REAL, prefer OWNER_REAL for default + // instead of parcel that the player is in + + override fun toParameterInfo(annotation: TargetKind): Int { + return annotation.kind + } + } + } + + class PType(val parcelProvider: ParcelProvider, val parcelAddress: SpecialCommandAddress? = null) : + ParameterType(ParcelTarget::class.java, TargetKind) { + + override fun parse( + parameter: Parameter, + sender: CommandSender, + buffer: ArgumentBuffer + ): ParcelTarget { + var input = buffer.next()!! + val worldString = input.substringBefore("/", missingDelimiterValue = "") + input = input.substringAfter("/") + + val world = if (worldString.isEmpty()) { + val player = requirePlayer(sender, parameter, "the world") + parcelProvider.getWorld(player.world) + ?: invalidInput(parameter, "You cannot omit the world if you're not in a parcel world") + } else { + parcelProvider.getWorld(worldString) ?: invalidInput(parameter, "$worldString is not a parcel world") + } + + val kind = parameter.paramInfo ?: DEFAULT_KIND + if (input.contains(',')) { + if (kind and ID == 0) invalidInput(parameter, + "You must specify a parcel by OWNER, that is, an owner and index") + return ByID(world, getId(parameter, input), kind, false) + } + + if (kind and OWNER == 0) invalidInput(parameter, + "You must specify a parcel by ID, that is, the x and z component separated by a comma") + val (owner, index) = getHomeIndex(parameter, kind, sender, input) + return ByOwner(world, + owner, + index, + kind, + false, + onResolveFailure = { invalidInput(parameter, "The player $input does not exist") }) + } + + private fun getId(parameter: Parameter<*, *>, input: String): Vec2i { + val x = input.substringBefore(',').run { + toIntOrNull() ?: invalidInput(parameter, "ID(x) must be an integer, $this is not an integer") + } + val z = input.substringAfter(',').run { + toIntOrNull() ?: invalidInput(parameter, "ID(z) must be an integer, $this is not an integer") + } + return Vec2i(x, z) + } + + private fun getHomeIndex( + parameter: Parameter<*, *>, + kind: Int, + sender: CommandSender, + input: String + ): Pair { + val splitIdx = input.indexOf(':') + val ownerString: String + val index: Int? + + val speciallyParsedIndex = parcelAddress?.speciallyParsedIndex + + if (splitIdx == -1) { + + if (speciallyParsedIndex == null) { + // just the index. + index = input.toIntOrNull() + ownerString = if (index == null) input else "" + } else { + // just the owner. + index = speciallyParsedIndex + ownerString = input + } + + } else { + if (speciallyParsedIndex != null) { + invalidInput(parameter, "Duplicate home index") + } + + ownerString = input.substring(0, splitIdx) + + val indexString = input.substring(splitIdx + 1) + index = indexString.toIntOrNull() + ?: invalidInput(parameter, "The home index must be an integer, $indexString is not an integer") + } + + val owner = (if (ownerString.isEmpty()) + PlayerProfile(requirePlayer(sender, parameter, "the player")) + else + PlayerProfile.byName(ownerString, allowReal = kind and OWNER_REAL != 0, allowFake = kind and OWNER_FAKE != 0)) + ?: invalidInput(parameter, "\'$ownerString\' is not a valid player name") + + return owner to (index ?: 0) + } + + private fun requirePlayer(sender: CommandSender, parameter: Parameter<*, *>, objName: String): Player { + if (sender !is Player) invalidInput(parameter, "console cannot omit the $objName") + return sender + } + + override fun getDefaultValue( + parameter: Parameter, + sender: CommandSender, + buffer: ArgumentBuffer + ): ParcelTarget? { + val kind = parameter.paramInfo ?: DEFAULT_KIND + val useLocation = when { + kind and REAL == REAL -> kind and PREFER_OWNED_FOR_DEFAULT == 0 + kind and ID != 0 -> true + kind and OWNER_REAL != 0 -> false + else -> return null + } + + val player = requirePlayer(sender, parameter, "the parcel") + val world = parcelProvider.getWorld(player.world) ?: invalidInput(parameter, + "You must be in a parcel world to omit the parcel") + if (useLocation) { + val id = player.location.let { world.getParcelIdAt(it.x.floor(), it.z.floor())?.pos } + return ByID(world, id, kind, true) + } + + return ByOwner(world, PlayerProfile(player), parcelAddress?.speciallyParsedIndex ?: 0, kind, true) + } + } + +} diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt index caa3f1f..73b6b4d 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt @@ -1,378 +1,391 @@ -package io.dico.parcels2.defaultimpl - -import io.dico.parcels2.* -import io.dico.parcels2.blockvisitor.RegionTraverser -import io.dico.parcels2.options.DefaultGeneratorOptions -import io.dico.parcels2.util.math.* -import kotlinx.coroutines.CoroutineScope -import org.bukkit.* -import org.bukkit.block.Biome -import org.bukkit.block.BlockFace -import org.bukkit.block.Skull -import org.bukkit.block.data.type.Slab -import org.bukkit.block.data.type.WallSign -import java.util.Random - -private val airType = Bukkit.createBlockData(Material.AIR) - -private const val chunkSize = 16 - -class DefaultParcelGenerator( - override val worldName: String, - private val o: DefaultGeneratorOptions -) : ParcelGenerator() { - private var _world: World? = null - override val world: World - get() { - if (_world == null) { - val world = Bukkit.getWorld(worldName) - maxHeight = world.maxHeight - _world = world - return world - } - return _world!! - } - - private var maxHeight = 0 - val sectionSize = o.parcelSize + o.pathSize - val pathOffset = (if (o.pathSize % 2 == 0) o.pathSize + 2 else o.pathSize + 1) / 2 - val makePathMain = o.pathSize > 2 - val makePathAlt = o.pathSize > 4 - - private inline fun generate( - chunkX: Int, - chunkZ: Int, - floor: T, wall: - T, pathMain: T, - pathAlt: T, - fill: T, - setter: (Int, Int, Int, T) -> Unit - ) { - - val floorHeight = o.floorHeight - val parcelSize = o.parcelSize - val sectionSize = sectionSize - val pathOffset = pathOffset - val makePathMain = makePathMain - val makePathAlt = makePathAlt - - // parcel bottom x and z - // umod is unsigned %: the result is always >= 0 - val pbx = ((chunkX shl 4) - o.offsetX) umod sectionSize - val pbz = ((chunkZ shl 4) - o.offsetZ) umod sectionSize - - var curHeight: Int - var x: Int - var z: Int - for (cx in 0..15) { - for (cz in 0..15) { - x = (pbx + cx) % sectionSize - pathOffset - z = (pbz + cz) % sectionSize - pathOffset - curHeight = floorHeight - - val type = when { - (x in 0 until parcelSize && z in 0 until parcelSize) -> floor - (x in -1..parcelSize && z in -1..parcelSize) -> { - curHeight++ - wall - } - (makePathAlt && x in -2 until parcelSize + 2 && z in -2 until parcelSize + 2) -> pathAlt - (makePathMain) -> pathMain - else -> { - curHeight++ - wall - } - } - - for (y in 0 until curHeight) { - setter(cx, y, cz, fill) - } - setter(cx, curHeight, cz, type) - } - } - } - - override fun generateChunkData(world: World?, random: Random?, chunkX: Int, chunkZ: Int, biome: BiomeGrid?): ChunkData { - val out = Bukkit.createChunkData(world) - generate(chunkX, chunkZ, o.floorType, o.wallType, o.pathMainType, o.pathAltType, o.fillType) { x, y, z, type -> - out.setBlock(x, y, z, type) - } - return out - } - - override fun populate(world: World?, random: Random?, chunk: Chunk?) { - // do nothing - } - - override fun getFixedSpawnLocation(world: World?, random: Random?): Location { - val fix = if (o.parcelSize.even) 0.5 else 0.0 - return Location(world, o.offsetX + fix, o.floorHeight + 1.0, o.offsetZ + fix) - } - - override fun makeParcelLocatorAndBlockManager( - parcelProvider: ParcelProvider, - container: ParcelContainer, - coroutineScope: CoroutineScope, - jobDispatcher: JobDispatcher - ): Pair { - val impl = ParcelLocatorAndBlockManagerImpl(parcelProvider, container, coroutineScope, jobDispatcher) - return impl to impl - } - - private inline fun convertBlockLocationToId(x: Int, z: Int, mapper: (Int, Int) -> T): T? { - val sectionSize = sectionSize - val parcelSize = o.parcelSize - val absX = x - o.offsetX - pathOffset - val absZ = z - o.offsetZ - pathOffset - val modX = absX umod sectionSize - val modZ = absZ umod sectionSize - if (modX in 0 until parcelSize && modZ in 0 until parcelSize) { - return mapper((absX - modX) / sectionSize + 1, (absZ - modZ) / sectionSize + 1) - } - return null - } - - @Suppress("DEPRECATION") - private inner class ParcelLocatorAndBlockManagerImpl( - val parcelProvider: ParcelProvider, - val container: ParcelContainer, - coroutineScope: CoroutineScope, - override val jobDispatcher: JobDispatcher - ) : ParcelBlockManagerBase(), ParcelLocator, CoroutineScope by coroutineScope { - - override val world: World get() = this@DefaultParcelGenerator.world - val worldId = parcelProvider.getWorld(world)?.id ?: ParcelWorldId(world) - override val parcelTraverser: RegionTraverser = RegionTraverser.convergingTo(o.floorHeight) - - private val cornerWallType = when { - o.wallType is Slab -> (o.wallType.clone() as Slab).apply { type = Slab.Type.DOUBLE } - o.wallType.material.name.endsWith("CARPET") -> { - Bukkit.createBlockData(Material.getMaterial(o.wallType.material.name.substringBefore("CARPET") + "WOOL")) - } - else -> null - } - - override fun getParcelAt(x: Int, z: Int): Parcel? { - return convertBlockLocationToId(x, z, container::getParcelById) - } - - override fun getParcelIdAt(x: Int, z: Int): ParcelId? { - return convertBlockLocationToId(x, z) { idx, idz -> ParcelId(worldId, idx, idz) } - } - - - private fun checkParcelId(parcel: ParcelId): ParcelId { - if (!parcel.worldId.equals(worldId)) { - throw IllegalArgumentException() - } - return parcel - } - - override fun getRegionOrigin(parcel: ParcelId): Vec2i { - checkParcelId(parcel) - return Vec2i( - sectionSize * (parcel.x - 1) + pathOffset + o.offsetX, - sectionSize * (parcel.z - 1) + pathOffset + o.offsetZ - ) - } - - override fun getRegion(parcel: ParcelId): Region { - val origin = getRegionOrigin(parcel) - return Region( - Vec3i(origin.x, 0, origin.z), - Vec3i(o.parcelSize, maxHeight, o.parcelSize) - ) - } - - override fun getHomeLocation(parcel: ParcelId): Location { - val origin = getRegionOrigin(parcel) - val x = origin.x + (o.parcelSize - 1) / 2.0 - val z = origin.z - 2 - return Location(world, x + 0.5, o.floorHeight + 1.0, z + 0.5, 0F, 0F) - } - - override fun getParcelForInfoBlockInteraction(block: Vec3i, type: Material, face: BlockFace): Parcel? { - if (block.y != o.floorHeight + 1) return null - - val expectedParcelOrigin = when (type) { - Material.WALL_SIGN -> Vec2i(block.x + 1, block.z + 2) - o.wallType.material, cornerWallType?.material -> { - if (face != BlockFace.NORTH || world[block + Vec3i.convert(BlockFace.NORTH)].type == Material.WALL_SIGN) { - return null - } - - Vec2i(block.x + 1, block.z + 1) - } - else -> return null - } - - return getParcelAt(expectedParcelOrigin.x, expectedParcelOrigin.z) - ?.takeIf { expectedParcelOrigin == getRegionOrigin(it.id) } - ?.also { parcel -> - if (type != Material.WALL_SIGN && parcel.owner != null) { - updateParcelInfo(parcel.id, parcel.owner) - parcel.isOwnerSignOutdated = false - } - } - } - - override fun isParcelInfoSectionLoaded(parcel: ParcelId): Boolean { - val wallBlockChunk = getRegionOrigin(parcel).add(-1, -1).toChunk() - return world.isChunkLoaded(wallBlockChunk.x, wallBlockChunk.z) - } - - override fun updateParcelInfo(parcel: ParcelId, owner: PlayerProfile?) { - val b = getRegionOrigin(parcel) - - val wallBlock = world.getBlockAt(b.x - 1, o.floorHeight + 1, b.z - 1) - val signBlock = world.getBlockAt(b.x - 1, o.floorHeight + 1, b.z - 2) - val skullBlock = world.getBlockAt(b.x - 1, o.floorHeight + 2, b.z - 1) - - if (owner == null) { - wallBlock.blockData = o.wallType - signBlock.type = Material.AIR - skullBlock.type = Material.AIR - - } else { - cornerWallType?.let { wallBlock.blockData = it } - signBlock.blockData = (Bukkit.createBlockData(Material.WALL_SIGN) as WallSign).apply { facing = BlockFace.NORTH } - - val sign = signBlock.state as org.bukkit.block.Sign - sign.setLine(0, "${parcel.x},${parcel.z}") - sign.setLine(2, owner.name ?: "") - sign.update() - - skullBlock.type = Material.AIR - skullBlock.type = Material.PLAYER_HEAD - val skull = skullBlock.state as Skull - if (owner is PlayerProfile.Real) { - skull.owningPlayer = Bukkit.getOfflinePlayer(owner.uuid) - - } else if (!skull.setOwner(owner.name)) { - skullBlock.type = Material.AIR - return - } - - skull.rotation = BlockFace.SOUTH - skull.update() - } - } - - private fun trySubmitBlockVisitor(vararg parcels: ParcelId, function: JobFunction): Job? { - parcels.forEach { checkParcelId(it) } - return parcelProvider.trySubmitBlockVisitor(Permit(), parcels, function) - } - - override fun setBiome(parcel: ParcelId, biome: Biome) = trySubmitBlockVisitor(checkParcelId(parcel)) { - val world = world - val b = getRegionOrigin(parcel) - val parcelSize = o.parcelSize - for (x in b.x until b.x + parcelSize) { - for (z in b.z until b.z + parcelSize) { - markSuspensionPoint() - world.setBiome(x, z, biome) - } - } - } - - override fun clearParcel(parcel: ParcelId) = trySubmitBlockVisitor(checkParcelId(parcel)) { - val region = getRegion(parcel) - val blocks = parcelTraverser.traverseRegion(region) - val blockCount = region.blockCount.toDouble() - val world = world - val floorHeight = o.floorHeight - val airType = airType - val floorType = o.floorType - val fillType = o.fillType - - for ((index, vec) in blocks.withIndex()) { - markSuspensionPoint() - val y = vec.y - val blockType = when { - y > floorHeight -> airType - y == floorHeight -> floorType - else -> fillType - } - world[vec].blockData = blockType - setProgress((index + 1) / blockCount) - } - } - - override fun getParcelsWithOwnerBlockIn(chunk: Chunk): Collection { - /* - * Get the offsets for the world out of the way - * to simplify the calculation that follows. - */ - - val x = chunk.x.shl(4) - (o.offsetX + pathOffset) - val z = chunk.z.shl(4) - (o.offsetZ + pathOffset) - - /* Locations of wall corners (where owner blocks are placed) are defined as: - * - * x umod sectionSize == sectionSize-1 - * - * This check needs to be made for all 16 slices of the chunk in 2 dimensions - * How to optimize this? - * Let's take the expression - * - * x umod sectionSize - * - * And call it modX - * x can be shifted (chunkSize -1) times to attempt to get a modX of 0. - * This means that if the modX is 1, and sectionSize == (chunkSize-1), there would be a match at the last shift. - * To check that there are any matches, we can see if the following holds: - * - * modX >= ((sectionSize-1) - (chunkSize-1)) - * - * Which can be simplified to: - * modX >= sectionSize - chunkSize - * - * if sectionSize == chunkSize, this expression can be simplified to - * modX >= 0 - * which is always true. This is expected. - * To get the total number of matches on a dimension, we can evaluate the following: - * - * (modX - (sectionSize - chunkSize) + sectionSize) / sectionSize - * - * We add sectionSize to the lhs because, if the other part of the lhs is 0, we need at least 1. - * This can be simplified to: - * - * (modX + chunkSize) / sectionSize - */ - - val sectionSize = sectionSize - - val modX = x umod sectionSize - val matchesOnDimensionX = (modX + chunkSize) / sectionSize - if (matchesOnDimensionX <= 0) return emptyList() - - val modZ = z umod sectionSize - val matchesOnDimensionZ = (modZ + chunkSize) / sectionSize - if (matchesOnDimensionZ <= 0) return emptyList() - - /* - * Now we need to find the first id within the matches, - * and then return the subsequent matches in a rectangle following it. - * - * On each dimension, get the distance to the first match, which is equal to (sectionSize-1 - modX) - * and add it to the coordinate value - */ - val firstX = x + (sectionSize - 1 - modX) - val firstZ = z + (sectionSize - 1 - modZ) - - val firstIdX = (firstX + 1) / sectionSize + 1 - val firstIdZ = (firstZ + 1) / sectionSize + 1 - - if (matchesOnDimensionX == 1 && matchesOnDimensionZ == 1) { - // fast-path optimization - return listOf(Vec2i(firstIdX, firstIdZ)) - } - - return (0 until matchesOnDimensionX).flatMap { idOffsetX -> - (0 until matchesOnDimensionZ).map { idOffsetZ -> Vec2i(firstIdX + idOffsetX, firstIdZ + idOffsetZ) } - } - } - - } - +package io.dico.parcels2.defaultimpl + +import io.dico.parcels2.* +import io.dico.parcels2.blockvisitor.RegionTraverser +import io.dico.parcels2.options.DefaultGeneratorOptions +import io.dico.parcels2.util.math.* +import kotlinx.coroutines.CoroutineScope +import org.bukkit.* +import org.bukkit.block.Biome +import org.bukkit.block.BlockFace +import org.bukkit.block.Skull +import org.bukkit.block.data.type.Slab +import org.bukkit.block.data.type.WallSign +import org.bukkit.entity.Player +import java.util.Random + +private val airType = Bukkit.createBlockData(Material.AIR) + +private const val chunkSize = 16 + +class DefaultParcelGenerator( + override val worldName: String, + private val o: DefaultGeneratorOptions +) : ParcelGenerator() { + private var _world: World? = null + override val world: World + get() { + if (_world == null) { + val world = Bukkit.getWorld(worldName) + maxHeight = world.maxHeight + _world = world + return world + } + return _world!! + } + + private var maxHeight = 0 + val sectionSize = o.parcelSize + o.pathSize + val pathOffset = (if (o.pathSize % 2 == 0) o.pathSize + 2 else o.pathSize + 1) / 2 + val makePathMain = o.pathSize > 2 + val makePathAlt = o.pathSize > 4 + + private inline fun generate( + chunkX: Int, + chunkZ: Int, + floor: T, wall: + T, pathMain: T, + pathAlt: T, + fill: T, + setter: (Int, Int, Int, T) -> Unit + ) { + + val floorHeight = o.floorHeight + val parcelSize = o.parcelSize + val sectionSize = sectionSize + val pathOffset = pathOffset + val makePathMain = makePathMain + val makePathAlt = makePathAlt + + // parcel bottom x and z + // umod is unsigned %: the result is always >= 0 + val pbx = ((chunkX shl 4) - o.offsetX) umod sectionSize + val pbz = ((chunkZ shl 4) - o.offsetZ) umod sectionSize + + var curHeight: Int + var x: Int + var z: Int + for (cx in 0..15) { + for (cz in 0..15) { + x = (pbx + cx) % sectionSize - pathOffset + z = (pbz + cz) % sectionSize - pathOffset + curHeight = floorHeight + + val type = when { + (x in 0 until parcelSize && z in 0 until parcelSize) -> floor + (x in -1..parcelSize && z in -1..parcelSize) -> { + curHeight++ + wall + } + (makePathAlt && x in -2 until parcelSize + 2 && z in -2 until parcelSize + 2) -> pathAlt + (makePathMain) -> pathMain + else -> { + curHeight++ + wall + } + } + + for (y in 0 until curHeight) { + setter(cx, y, cz, fill) + } + setter(cx, curHeight, cz, type) + } + } + } + + override fun generateChunkData(world: World?, random: Random?, chunkX: Int, chunkZ: Int, biome: BiomeGrid?): ChunkData { + val out = Bukkit.createChunkData(world) + generate(chunkX, chunkZ, o.floorType, o.wallType, o.pathMainType, o.pathAltType, o.fillType) { x, y, z, type -> + out.setBlock(x, y, z, type) + } + return out + } + + override fun populate(world: World?, random: Random?, chunk: Chunk?) { + // do nothing + } + + override fun getFixedSpawnLocation(world: World?, random: Random?): Location { + val fix = if (o.parcelSize.even) 0.5 else 0.0 + return Location(world, o.offsetX + fix, o.floorHeight + 1.0, o.offsetZ + fix) + } + + override fun makeParcelLocatorAndBlockManager( + parcelProvider: ParcelProvider, + container: ParcelContainer, + coroutineScope: CoroutineScope, + jobDispatcher: JobDispatcher + ): Pair { + val impl = ParcelLocatorAndBlockManagerImpl(parcelProvider, container, coroutineScope, jobDispatcher) + return impl to impl + } + + private inline fun convertBlockLocationToId(x: Int, z: Int, mapper: (Int, Int) -> T): T? { + val sectionSize = sectionSize + val parcelSize = o.parcelSize + val absX = x - o.offsetX - pathOffset + val absZ = z - o.offsetZ - pathOffset + val modX = absX umod sectionSize + val modZ = absZ umod sectionSize + if (modX in 0 until parcelSize && modZ in 0 until parcelSize) { + return mapper((absX - modX) / sectionSize + 1, (absZ - modZ) / sectionSize + 1) + } + return null + } + + @Suppress("DEPRECATION") + private inner class ParcelLocatorAndBlockManagerImpl( + val parcelProvider: ParcelProvider, + val container: ParcelContainer, + coroutineScope: CoroutineScope, + override val jobDispatcher: JobDispatcher + ) : ParcelBlockManagerBase(), ParcelLocator, CoroutineScope by coroutineScope { + + override val world: World get() = this@DefaultParcelGenerator.world + val worldId = parcelProvider.getWorld(world)?.id ?: ParcelWorldId(world) + override val parcelTraverser: RegionTraverser = RegionTraverser.convergingTo(o.floorHeight) + + private val cornerWallType = when { + o.wallType is Slab -> (o.wallType.clone() as Slab).apply { type = Slab.Type.DOUBLE } + o.wallType.material.name.endsWith("CARPET") -> { + Bukkit.createBlockData(Material.getMaterial(o.wallType.material.name.substringBefore("CARPET") + "WOOL")) + } + else -> null + } + + override fun getParcelAt(x: Int, z: Int): Parcel? { + return convertBlockLocationToId(x, z, container::getParcelById) + } + + override fun getParcelIdAt(x: Int, z: Int): ParcelId? { + return convertBlockLocationToId(x, z) { idx, idz -> ParcelId(worldId, idx, idz) } + } + + + private fun checkParcelId(parcel: ParcelId): ParcelId { + if (!parcel.worldId.equals(worldId)) { + throw IllegalArgumentException() + } + return parcel + } + + override fun getRegionOrigin(parcel: ParcelId): Vec2i { + checkParcelId(parcel) + return Vec2i( + sectionSize * (parcel.x - 1) + pathOffset + o.offsetX, + sectionSize * (parcel.z - 1) + pathOffset + o.offsetZ + ) + } + + override fun getRegion(parcel: ParcelId): Region { + val origin = getRegionOrigin(parcel) + return Region( + Vec3i(origin.x, 0, origin.z), + Vec3i(o.parcelSize, maxHeight, o.parcelSize) + ) + } + + override fun getHomeLocation(parcel: ParcelId): Location { + val origin = getRegionOrigin(parcel) + val x = origin.x + (o.parcelSize - 1) / 2.0 + val z = origin.z - 2 + return Location(world, x + 0.5, o.floorHeight + 1.0, z + 0.5, 0F, 0F) + } + + override fun getParcelForInfoBlockInteraction(block: Vec3i, type: Material, face: BlockFace): Parcel? { + if (block.y != o.floorHeight + 1) return null + + val expectedParcelOrigin = when (type) { + Material.WALL_SIGN -> Vec2i(block.x + 1, block.z + 2) + o.wallType.material, cornerWallType?.material -> { + if (face != BlockFace.NORTH || world[block + Vec3i.convert(BlockFace.NORTH)].type == Material.WALL_SIGN) { + return null + } + + Vec2i(block.x + 1, block.z + 1) + } + else -> return null + } + + return getParcelAt(expectedParcelOrigin.x, expectedParcelOrigin.z) + ?.takeIf { expectedParcelOrigin == getRegionOrigin(it.id) } + ?.also { parcel -> + if (type != Material.WALL_SIGN && parcel.owner != null) { + updateParcelInfo(parcel.id, parcel.owner) + parcel.isOwnerSignOutdated = false + } + } + } + + override fun isParcelInfoSectionLoaded(parcel: ParcelId): Boolean { + val wallBlockChunk = getRegionOrigin(parcel).add(-1, -1).toChunk() + return world.isChunkLoaded(wallBlockChunk.x, wallBlockChunk.z) + } + + override fun updateParcelInfo(parcel: ParcelId, owner: PlayerProfile?) { + val b = getRegionOrigin(parcel) + + val wallBlock = world.getBlockAt(b.x - 1, o.floorHeight + 1, b.z - 1) + val signBlock = world.getBlockAt(b.x - 1, o.floorHeight + 1, b.z - 2) + val skullBlock = world.getBlockAt(b.x - 1, o.floorHeight + 2, b.z - 1) + + if (owner == null) { + wallBlock.blockData = o.wallType + signBlock.type = Material.AIR + skullBlock.type = Material.AIR + + } else { + cornerWallType?.let { wallBlock.blockData = it } + signBlock.blockData = (Bukkit.createBlockData(Material.WALL_SIGN) as WallSign).apply { facing = BlockFace.NORTH } + + val sign = signBlock.state as org.bukkit.block.Sign + sign.setLine(0, "${parcel.x},${parcel.z}") + sign.setLine(2, owner.name ?: "") + sign.update() + + skullBlock.type = Material.AIR + skullBlock.type = Material.PLAYER_HEAD + val skull = skullBlock.state as Skull + if (owner is PlayerProfile.Real) { + skull.owningPlayer = Bukkit.getOfflinePlayer(owner.uuid) + + } else if (!skull.setOwner(owner.name)) { + skullBlock.type = Material.AIR + return + } + + skull.rotation = BlockFace.SOUTH + skull.update() + } + } + + private fun trySubmitBlockVisitor(vararg parcels: ParcelId, function: JobFunction): Job? { + parcels.forEach { checkParcelId(it) } + return parcelProvider.trySubmitBlockVisitor(Permit(), parcels, function) + } + + override fun setBiome(parcel: ParcelId, biome: Biome) = trySubmitBlockVisitor(checkParcelId(parcel)) { + val world = world + val b = getRegionOrigin(parcel) + val parcelSize = o.parcelSize + for (x in b.x until b.x + parcelSize) { + for (z in b.z until b.z + parcelSize) { + markSuspensionPoint() + world.setBiome(x, z, biome) + } + } + } + + override fun clearParcel(parcel: ParcelId) = trySubmitBlockVisitor(checkParcelId(parcel)) { + val region = getRegion(parcel) + val blocks = parcelTraverser.traverseRegion(region) + val blockCount = region.blockCount.toDouble() + val world = world + val floorHeight = o.floorHeight + val airType = airType + val floorType = o.floorType + val fillType = o.fillType + + delegateWork(0.95) { + for ((index, vec) in blocks.withIndex()) { + markSuspensionPoint() + val y = vec.y + val blockType = when { + y > floorHeight -> airType + y == floorHeight -> floorType + else -> fillType + } + world[vec].blockData = blockType + setProgress((index + 1) / blockCount) + } + } + + delegateWork { + val entities = getEntities(region) + for ((index, entity) in entities.withIndex()) { + if (entity is Player) continue + entity.remove() + setProgress((index + 1) / entities.size.toDouble()) + } + } + + } + + override fun getParcelsWithOwnerBlockIn(chunk: Chunk): Collection { + /* + * Get the offsets for the world out of the way + * to simplify the calculation that follows. + */ + + val x = chunk.x.shl(4) - (o.offsetX + pathOffset) + val z = chunk.z.shl(4) - (o.offsetZ + pathOffset) + + /* Locations of wall corners (where owner blocks are placed) are defined as: + * + * x umod sectionSize == sectionSize-1 + * + * This check needs to be made for all 16 slices of the chunk in 2 dimensions + * How to optimize this? + * Let's take the expression + * + * x umod sectionSize + * + * And call it modX + * x can be shifted (chunkSize -1) times to attempt to get a modX of 0. + * This means that if the modX is 1, and sectionSize == (chunkSize-1), there would be a match at the last shift. + * To check that there are any matches, we can see if the following holds: + * + * modX >= ((sectionSize-1) - (chunkSize-1)) + * + * Which can be simplified to: + * modX >= sectionSize - chunkSize + * + * if sectionSize == chunkSize, this expression can be simplified to + * modX >= 0 + * which is always true. This is expected. + * To get the total number of matches on a dimension, we can evaluate the following: + * + * (modX - (sectionSize - chunkSize) + sectionSize) / sectionSize + * + * We add sectionSize to the lhs because, if the other part of the lhs is 0, we need at least 1. + * This can be simplified to: + * + * (modX + chunkSize) / sectionSize + */ + + val sectionSize = sectionSize + + val modX = x umod sectionSize + val matchesOnDimensionX = (modX + chunkSize) / sectionSize + if (matchesOnDimensionX <= 0) return emptyList() + + val modZ = z umod sectionSize + val matchesOnDimensionZ = (modZ + chunkSize) / sectionSize + if (matchesOnDimensionZ <= 0) return emptyList() + + /* + * Now we need to find the first id within the matches, + * and then return the subsequent matches in a rectangle following it. + * + * On each dimension, get the distance to the first match, which is equal to (sectionSize-1 - modX) + * and add it to the coordinate value + */ + val firstX = x + (sectionSize - 1 - modX) + val firstZ = z + (sectionSize - 1 - modZ) + + val firstIdX = (firstX + 1) / sectionSize + 1 + val firstIdZ = (firstZ + 1) / sectionSize + 1 + + if (matchesOnDimensionX == 1 && matchesOnDimensionZ == 1) { + // fast-path optimization + return listOf(Vec2i(firstIdX, firstIdZ)) + } + + return (0 until matchesOnDimensionX).flatMap { idOffsetX -> + (0 until matchesOnDimensionZ).map { idOffsetZ -> Vec2i(firstIdX + idOffsetX, firstIdZ + idOffsetZ) } + } + } + + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt index da004d6..7748fc7 100644 --- a/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt +++ b/src/main/kotlin/io/dico/parcels2/defaultimpl/ParcelProviderImpl.kt @@ -1,223 +1,284 @@ -package io.dico.parcels2.defaultimpl - -import io.dico.parcels2.* -import io.dico.parcels2.blockvisitor.Schematic -import io.dico.parcels2.util.schedule -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import org.bukkit.Bukkit -import org.bukkit.WorldCreator -import org.joda.time.DateTime - -class ParcelProviderImpl(val plugin: ParcelsPlugin) : ParcelProvider { - inline val options get() = plugin.options - override val worlds: Map get() = _worlds - private val _worlds: MutableMap = hashMapOf() - private val _generators: MutableMap = hashMapOf() - private var _worldsLoaded = false - private var _dataIsLoaded = false - - // disabled while !_dataIsLoaded. getParcelById() will work though for data loading. - override fun getWorld(name: String): ParcelWorld? = _worlds[name]?.takeIf { _dataIsLoaded } - - override fun getWorldById(id: ParcelWorldId): ParcelWorld? { - if (id is ParcelWorld) return id - return _worlds[id.name] ?: id.bukkitWorld?.let { getWorld(it) } - } - - override fun getParcelById(id: ParcelId): Parcel? { - if (id is Parcel) return id - return getWorldById(id.worldId)?.container?.getParcelById(id.x, id.z) - } - - override fun getWorldGenerator(worldName: String): ParcelGenerator? { - return _worlds[worldName]?.generator - ?: _generators[worldName] - ?: options.worlds[worldName]?.generator?.newInstance(worldName)?.also { _generators[worldName] = it } - } - - override fun loadWorlds() { - if (_worldsLoaded) throw IllegalStateException() - _worldsLoaded = true - loadWorlds0() - } - - private fun loadWorlds0() { - if (Bukkit.getWorlds().isEmpty()) { - plugin.schedule(::loadWorlds0) - plugin.logger.warning("Scheduling to load worlds in the next tick because no bukkit worlds are loaded yet") - return - } - - val newlyCreatedWorlds = mutableListOf() - for ((worldName, worldOptions) in options.worlds.entries) { - var parcelWorld = _worlds[worldName] - if (parcelWorld != null) continue - - val generator: ParcelGenerator = getWorldGenerator(worldName)!! - val worldExists = Bukkit.getWorld(worldName) != null - val bukkitWorld = - if (worldExists) Bukkit.getWorld(worldName)!! - else { - logger.info("Creating world $worldName") - WorldCreator(worldName).generator(generator).createWorld() - } - - parcelWorld = ParcelWorldImpl(plugin, bukkitWorld, generator, worldOptions.runtime,::DefaultParcelContainer) - - if (!worldExists) { - val time = DateTime.now() - plugin.storage.setWorldCreationTime(parcelWorld.id, time) - parcelWorld.creationTime = time - newlyCreatedWorlds.add(parcelWorld) - } else { - GlobalScope.launch(context = Dispatchers.Unconfined) { - parcelWorld.creationTime = plugin.storage.getWorldCreationTime(parcelWorld.id).await() ?: DateTime.now() - } - } - - _worlds[worldName] = parcelWorld - } - - loadStoredData(newlyCreatedWorlds.toSet()) - } - - private fun loadStoredData(newlyCreatedWorlds: Collection = emptyList()) { - plugin.launch(Dispatchers.Default) { - val migration = plugin.options.migration - if (migration.enabled) { - migration.instance?.newInstance()?.apply { - logger.warn("Migrating database now...") - migrateTo(plugin.storage).join() - logger.warn("Migration completed") - - if (migration.disableWhenComplete) { - migration.enabled = false - plugin.saveOptions() - } - } - } - - logger.info("Loading all parcel data...") - - val job1 = launch { - val channel = plugin.storage.transmitAllParcelData() - while (true) { - val (id, data) = channel.receiveOrNull() ?: break - val parcel = getParcelById(id) ?: continue - data?.let { parcel.copyData(it, callerIsDatabase = true) } - } - } - - val channel2 = plugin.storage.transmitAllGlobalPrivileges() - while (true) { - val (profile, data) = channel2.receiveOrNull() ?: break - if (profile !is PrivilegeKey) { - logger.error("Received profile that is not a privilege key: ${profile.javaClass}, $profile") - continue - } - (plugin.globalPrivileges[profile] as PrivilegesHolder).copyPrivilegesFrom(data) - } - - job1.join() - - logger.info("Loading data completed") - _dataIsLoaded = true - } - } - - override fun acquireBlockVisitorPermit(parcelId: ParcelId, with: Permit): Boolean { - val parcel = getParcelById(parcelId) as? ParcelImpl ?: return true - return parcel.acquireBlockVisitorPermit(with) - } - - override fun releaseBlockVisitorPermit(parcelId: ParcelId, with: Permit) { - val parcel = getParcelById(parcelId) as? ParcelImpl ?: return - parcel.releaseBlockVisitorPermit(with) - } - - override fun trySubmitBlockVisitor(permit: Permit, vararg parcelIds: ParcelId, function: JobFunction): Job? { - val withPermit = parcelIds.filter { acquireBlockVisitorPermit(it, permit) } - if (withPermit.size != parcelIds.size) { - withPermit.forEach { releaseBlockVisitorPermit(it, permit) } - return null - } - - val job = plugin.jobDispatcher.dispatch(function) - - plugin.launch { - job.awaitCompletion() - withPermit.forEach { releaseBlockVisitorPermit(it, permit) } - } - - return job - } - - override fun swapParcels(parcelId1: ParcelId, parcelId2: ParcelId): Job? { - val blockManager1 = getWorldById(parcelId1.worldId)?.blockManager ?: return null - val blockManager2 = getWorldById(parcelId2.worldId)?.blockManager ?: return null - - return trySubmitBlockVisitor(Permit(), parcelId1, parcelId2) { - var region1 = blockManager1.getRegion(parcelId1) - var region2 = blockManager2.getRegion(parcelId2) - - val size = region1.size.clampMax(region2.size) - if (size != region1.size) { - region1 = region1.withSize(size) - region2 = region2.withSize(size) - } - - val schematicOf1 = delegateWork(0.25) { Schematic().apply { load(blockManager1.world, region1) } } - val schematicOf2 = delegateWork(0.25) { Schematic().apply { load(blockManager2.world, region2) } } - delegateWork(0.25) { with(schematicOf1) { paste(blockManager2.world, region2.origin) } } - delegateWork(0.25) { with(schematicOf2) { paste(blockManager1.world, region1.origin) } } - } - } - - /* - fun loadWorlds(options: Options) { - for ((worldName, worldOptions) in options.worlds.entries) { - val world: ParcelWorld - try { - - world = ParcelWorldImpl( - worldName, - worldOptions, - worldOptions.generator.newGenerator(this, worldName), - plugin.storage, - plugin.globalPrivileges, - ::DefaultParcelContainer) - - } catch (ex: Exception) { - ex.printStackTrace() - continue - } - - _worlds[worldName] = world - } - - plugin.functionHelper.schedule(10) { - println("Parcels generating parcelProvider now") - for ((name, world) in _worlds) { - if (Bukkit.getWorld(name) == null) { - val bworld = WorldCreator(name).generator(world.generator).createWorld() - val spawn = world.generator.getFixedSpawnLocation(bworld, null) - bworld.setSpawnLocation(spawn.x.floor(), spawn.y.floor(), spawn.z.floor()) - } - } - - val channel = plugin.storage.transmitAllParcelData() - val job = plugin.functionHelper.launchLazilyOnMainThread { - do { - val pair = channel.receiveOrNull() ?: break - val parcel = getParcelById(pair.first) ?: continue - pair.second?.let { parcel.copyDataIgnoringDatabase(it) } - } while (true) - } - job.start() - } - - } - */ +package io.dico.parcels2.defaultimpl + +import io.dico.parcels2.* +import io.dico.parcels2.blockvisitor.Schematic +import io.dico.parcels2.util.math.Region +import io.dico.parcels2.util.math.Vec3d +import io.dico.parcels2.util.math.Vec3i +import io.dico.parcels2.util.schedule +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.bukkit.Bukkit +import org.bukkit.World +import org.bukkit.WorldCreator +import org.bukkit.entity.Entity +import org.bukkit.util.Vector +import org.joda.time.DateTime + +class ParcelProviderImpl(val plugin: ParcelsPlugin) : ParcelProvider { + inline val options get() = plugin.options + override val worlds: Map get() = _worlds + private val _worlds: MutableMap = hashMapOf() + private val _generators: MutableMap = hashMapOf() + private var _worldsLoaded = false + private var _dataIsLoaded = false + + // disabled while !_dataIsLoaded. getParcelById() will work though for data loading. + override fun getWorld(name: String): ParcelWorld? = _worlds[name]?.takeIf { _dataIsLoaded } + + override fun getWorldById(id: ParcelWorldId): ParcelWorld? { + if (id is ParcelWorld) return id + return _worlds[id.name] ?: id.bukkitWorld?.let { getWorld(it) } + } + + override fun getParcelById(id: ParcelId): Parcel? { + if (id is Parcel) return id + return getWorldById(id.worldId)?.container?.getParcelById(id.x, id.z) + } + + override fun getWorldGenerator(worldName: String): ParcelGenerator? { + return _worlds[worldName]?.generator + ?: _generators[worldName] + ?: options.worlds[worldName]?.generator?.newInstance(worldName)?.also { _generators[worldName] = it } + } + + override fun loadWorlds() { + if (_worldsLoaded) throw IllegalStateException() + _worldsLoaded = true + loadWorlds0() + } + + private fun loadWorlds0() { + if (Bukkit.getWorlds().isEmpty()) { + plugin.schedule { loadWorlds0() } + plugin.logger.warning("Scheduling to load worlds in the next tick because no bukkit worlds are loaded yet") + return + } + + val newlyCreatedWorlds = mutableListOf() + for ((worldName, worldOptions) in options.worlds.entries) { + var parcelWorld = _worlds[worldName] + if (parcelWorld != null) continue + + val generator: ParcelGenerator = getWorldGenerator(worldName)!! + val worldExists = Bukkit.getWorld(worldName) != null + val bukkitWorld = + if (worldExists) Bukkit.getWorld(worldName)!! + else { + logger.info("Creating world $worldName") + WorldCreator(worldName).generator(generator).createWorld() + } + + parcelWorld = + ParcelWorldImpl(plugin, bukkitWorld, generator, worldOptions.runtime, ::DefaultParcelContainer) + + if (!worldExists) { + val time = DateTime.now() + plugin.storage.setWorldCreationTime(parcelWorld.id, time) + parcelWorld.creationTime = time + newlyCreatedWorlds.add(parcelWorld) + } else { + GlobalScope.launch(context = Dispatchers.Unconfined) { + parcelWorld.creationTime = plugin.storage.getWorldCreationTime(parcelWorld.id).await() ?: + DateTime.now() + } + } + + _worlds[worldName] = parcelWorld + } + + loadStoredData(newlyCreatedWorlds.toSet()) + } + + private fun loadStoredData(newlyCreatedWorlds: Collection = emptyList()) { + plugin.launch { + val migration = plugin.options.migration + if (migration.enabled) { + migration.instance?.newInstance()?.apply { + logger.warn("Migrating database now...") + migrateTo(plugin.storage).join() + logger.warn("Migration completed") + + if (migration.disableWhenComplete) { + migration.enabled = false + plugin.saveOptions() + } + } + } + + logger.info("Loading all parcel data...") + + val job1 = launch { + val channel = plugin.storage.transmitAllParcelData() + while (true) { + val (id, data) = channel.receiveOrNull() ?: break + val parcel = getParcelById(id) ?: continue + data?.let { parcel.copyData(it, callerIsDatabase = true) } + } + } + + val channel2 = plugin.storage.transmitAllGlobalPrivileges() + while (true) { + val (profile, data) = channel2.receiveOrNull() ?: break + if (profile !is PrivilegeKey) { + logger.error("Received profile that is not a privilege key: ${profile.javaClass}, $profile") + continue + } + (plugin.globalPrivileges[profile] as PrivilegesHolder).copyPrivilegesFrom(data) + } + + job1.join() + + logger.info("Loading data completed") + _dataIsLoaded = true + } + } + + override fun acquireBlockVisitorPermit(parcelId: ParcelId, with: Permit): Boolean { + val parcel = getParcelById(parcelId) as? ParcelImpl ?: return true + return parcel.acquireBlockVisitorPermit(with) + } + + override fun releaseBlockVisitorPermit(parcelId: ParcelId, with: Permit) { + val parcel = getParcelById(parcelId) as? ParcelImpl ?: return + parcel.releaseBlockVisitorPermit(with) + } + + override fun trySubmitBlockVisitor(permit: Permit, vararg parcelIds: ParcelId, function: JobFunction): Job? { + val withPermit = parcelIds.filter { acquireBlockVisitorPermit(it, permit) } + if (withPermit.size != parcelIds.size) { + withPermit.forEach { releaseBlockVisitorPermit(it, permit) } + return null + } + + val job = plugin.jobDispatcher.dispatch(function) + + plugin.launch { + job.awaitCompletion() + withPermit.forEach { releaseBlockVisitorPermit(it, permit) } + } + + return job + } + + override fun swapParcels(parcelId1: ParcelId, parcelId2: ParcelId): Job? { + val world1 = getWorldById(parcelId1.worldId) ?: return null + val world2 = getWorldById(parcelId2.worldId) ?: return null + val blockManager1 = world1.blockManager + val blockManager2 = world2.blockManager + + class CopyTarget(val world: World, val region: Region) + class CopySource(val origin: Vec3i, val schematic: Schematic, val entities: Collection) + + suspend fun JobScope.copy(source: CopySource, target: CopyTarget) { + with(source.schematic) { paste(target.world, target.region.origin) } + + for (entity in source.entities) { + entity.velocity = Vector(0, 0, 0) + val location = entity.location + location.world = target.world + val coords = target.region.origin + (Vec3d(entity.location) - source.origin) + coords.copyInto(location) + entity.teleport(location) + } + } + + return trySubmitBlockVisitor(Permit(), parcelId1, parcelId2) { + val temporaryParcel = world1.nextEmptyParcel() + ?: world2.nextEmptyParcel() + ?: return@trySubmitBlockVisitor + + var region1 = blockManager1.getRegion(parcelId1) + var region2 = blockManager2.getRegion(parcelId2) + + val size = region1.size.clampMax(region2.size) + if (size != region1.size) { + region1 = region1.withSize(size) + region2 = region2.withSize(size) + } + + // Teleporting entities safely requires a different approach: + // * Copy schematic1 into temporary location + // * Teleport entities1 into temporary location + // * Copy schematic2 into parcel1 + // * Teleport entities2 into parcel1 + // * Copy schematic1 into parcel2 + // * Teleport entities1 into parcel2 + // * Clear temporary location + + lateinit var source1: CopySource + lateinit var source2: CopySource + + delegateWork(0.30) { + val schematicOf1 = delegateWork(0.50) { Schematic().apply { load(blockManager1.world, region1) } } + val schematicOf2 = delegateWork(0.50) { Schematic().apply { load(blockManager2.world, region2) } } + + source1 = CopySource(region1.origin, schematicOf1, blockManager1.getEntities(region1)) + source2 = CopySource(region2.origin, schematicOf2, blockManager2.getEntities(region2)) + } + + val target1 = CopyTarget(blockManager1.world, region1) + val target2 = CopyTarget(blockManager2.world, region2) + val targetTemp = CopyTarget( + temporaryParcel.world.world, + temporaryParcel.world.blockManager.getRegion(temporaryParcel.id) + ) + + delegateWork { + delegateWork(1.0 / 3.0) { copy(source1, targetTemp) } + delegateWork(1.0 / 3.0) { copy(source2, target1) } + delegateWork(1.0 / 3.0) { copy(source1, target2) } + } + + // Separate job. Whatever + temporaryParcel.world.blockManager.clearParcel(temporaryParcel.id) + } + } + + /* + fun loadWorlds(options: Options) { + for ((worldName, worldOptions) in options.worlds.entries) { + val world: ParcelWorld + try { + + world = ParcelWorldImpl( + worldName, + worldOptions, + worldOptions.generator.newGenerator(this, worldName), + plugin.storage, + plugin.globalPrivileges, + ::DefaultParcelContainer) + + } catch (ex: Exception) { + ex.printStackTrace() + continue + } + + _worlds[worldName] = world + } + + plugin.functionHelper.schedule(10) { + println("Parcels generating parcelProvider now") + for ((name, world) in _worlds) { + if (Bukkit.getWorld(name) == null) { + val bworld = WorldCreator(name).generator(world.generator).createWorld() + val spawn = world.generator.getFixedSpawnLocation(bworld, null) + bworld.setSpawnLocation(spawn.x.floor(), spawn.y.floor(), spawn.z.floor()) + } + } + + val channel = plugin.storage.transmitAllParcelData() + val job = plugin.functionHelper.launchLazilyOnMainThread { + do { + val pair = channel.receiveOrNull() ?: break + val parcel = getParcelById(pair.first) ?: continue + pair.second?.let { parcel.copyDataIgnoringDatabase(it) } + } while (true) + } + job.start() + } + + } + */ } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedBacking.kt b/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedBacking.kt index 32065bc..d9e3071 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedBacking.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/exposed/ExposedBacking.kt @@ -1,282 +1,284 @@ -@file:Suppress("NOTHING_TO_INLINE", "PARAMETER_NAME_CHANGED_ON_OVERRIDE", "LocalVariableName", "UNUSED_EXPRESSION") - -package io.dico.parcels2.storage.exposed - -import com.zaxxer.hikari.HikariDataSource -import io.dico.parcels2.* -import io.dico.parcels2.PlayerProfile.Star.name -import io.dico.parcels2.storage.* -import io.dico.parcels2.util.math.clampMax -import io.dico.parcels2.util.ext.synchronized -import kotlinx.coroutines.* -import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.ArrayChannel -import kotlinx.coroutines.channels.LinkedListChannel -import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.coroutines.channels.SendChannel -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.SchemaUtils.create -import org.jetbrains.exposed.sql.transactions.transaction -import org.jetbrains.exposed.sql.vendors.DatabaseDialect -import org.joda.time.DateTime -import java.util.UUID -import javax.sql.DataSource - -class ExposedDatabaseException(message: String? = null) : Exception(message) - -class ExposedBacking(private val dataSourceFactory: () -> DataSource, val poolSize: Int) : Backing, CoroutineScope { - override val name get() = "Exposed" - override val coroutineContext = Job() + newFixedThreadPoolContext(poolSize, "Parcels StorageThread") - private var dataSource: DataSource? = null - private var database: Database? = null - private var isShutdown: Boolean = false - override val isConnected get() = database != null - - override fun launchJob(job: Backing.() -> Unit): Job = launch { transaction { job() } } - override fun launchFuture(future: Backing.() -> T): Deferred = async { transaction { future() } } - - override fun openChannel(future: Backing.(SendChannel) -> Unit): ReceiveChannel { - val channel = LinkedListChannel() - launchJob { future(channel) } - return channel - } - - override fun openChannelForWriting(action: Backing.(T) -> Unit): SendChannel { - val channel = ArrayChannel(poolSize * 2) - - repeat(poolSize.clampMax(3)) { - launch { - try { - while (true) { - action(channel.receive()) - } - } catch (ex: Exception) { - // channel closed - } - } - } - - return channel - } - - private fun transaction(statement: Transaction.() -> T) = transaction(database!!, statement) - - companion object { - init { - Database.registerDialect("mariadb") { - Class.forName("org.jetbrains.exposed.sql.vendors.MysqlDialect").newInstance() as DatabaseDialect - } - } - } - - override fun init() { - synchronized { - if (isShutdown || isConnected) throw IllegalStateException() - dataSource = dataSourceFactory() - database = Database.connect(dataSource!!) - transaction(database!!) { - create(WorldsT, ProfilesT, ParcelsT, ParcelOptionsT, PrivilegesLocalT, PrivilegesGlobalT) - } - } - } - - override fun shutdown() { - synchronized { - if (isShutdown) throw IllegalStateException() - isShutdown = true - coroutineContext[Job]!!.cancel(CancellationException("ExposedBacking shutdown")) - dataSource?.let { - (it as? HikariDataSource)?.close() - } - database = null - } - } - - @Suppress("RedundantObjectTypeCheck") - private fun PlayerProfile.toOwnerProfile(): PlayerProfile { - if (this is PlayerProfile.Star) return PlayerProfile.Fake(name) - return this - } - - private fun PlayerProfile.Unresolved.toResolvedProfile(): PlayerProfile.Real { - return resolve(getPlayerUuidForName(name) ?: throwException()) - } - - private fun PlayerProfile.toResolvedProfile(): PlayerProfile { - if (this is PlayerProfile.Unresolved) return toResolvedProfile() - return this - } - - private fun PlayerProfile.toRealProfile(): PlayerProfile.Real = when (this) { - is PlayerProfile.Real -> this - is PlayerProfile.Fake -> throw IllegalArgumentException("Fake profiles are not accepted") - is PlayerProfile.Unresolved -> toResolvedProfile() - else -> throw InternalError("Case should not be reached") - } - - - override fun getWorldCreationTime(worldId: ParcelWorldId): DateTime? { - return WorldsT.getWorldCreationTime(worldId) - } - - override fun setWorldCreationTime(worldId: ParcelWorldId, time: DateTime) { - WorldsT.setWorldCreationTime(worldId, time) - } - - override fun getPlayerUuidForName(name: String): UUID? { - return ProfilesT.slice(ProfilesT.uuid).select { ProfilesT.name.upperCase() eq name.toUpperCase() } - .firstOrNull()?.let { it[ProfilesT.uuid]?.toUUID() } - } - - override fun updatePlayerName(uuid: UUID, name: String) { - val binaryUuid = uuid.toByteArray() - ProfilesT.upsert(ProfilesT.uuid) { - it[ProfilesT.uuid] = binaryUuid - it[ProfilesT.name] = name - } - } - - override fun transmitParcelData(channel: SendChannel, parcels: Sequence) { - for (parcel in parcels) { - val data = readParcelData(parcel) - channel.offer(parcel to data) - } - channel.close() - } - - override fun transmitAllParcelData(channel: SendChannel) { - ParcelsT.selectAll().forEach { row -> - val parcel = ParcelsT.getItem(row) ?: return@forEach - val data = rowToParcelData(row) - channel.offer(parcel to data) - } - channel.close() - } - - override fun readParcelData(parcel: ParcelId): ParcelDataHolder? { - val row = ParcelsT.getRow(parcel) ?: return null - return rowToParcelData(row) - } - - override fun getOwnedParcels(user: PlayerProfile): List { - val user_id = ProfilesT.getId(user.toOwnerProfile()) ?: return emptyList() - return ParcelsT.select { ParcelsT.owner_id eq user_id } - .orderBy(ParcelsT.claim_time, isAsc = true) - .mapNotNull(ParcelsT::getItem) - .toList() - } - - override fun setParcelData(parcel: ParcelId, data: ParcelDataHolder?) { - if (data == null) { - transaction { - ParcelsT.getId(parcel)?.let { id -> - ParcelsT.deleteIgnoreWhere { ParcelsT.id eq id } - - // Below should cascade automatically - /* - PrivilegesLocalT.deleteIgnoreWhere { PrivilegesLocalT.parcel_id eq id } - ParcelOptionsT.deleteIgnoreWhere(limit = 1) { ParcelOptionsT.parcel_id eq id } - */ - } - - } - return - } - - transaction { - val id = ParcelsT.getOrInitId(parcel) - PrivilegesLocalT.deleteIgnoreWhere { PrivilegesLocalT.attach_id eq id } - } - - setParcelOwner(parcel, data.owner) - - for ((profile, privilege) in data.privilegeMap) { - PrivilegesLocalT.setPrivilege(parcel, profile, privilege) - } - - data.privilegeOfStar.takeIf { it != Privilege.DEFAULT }?.let { privilege -> - PrivilegesLocalT.setPrivilege(parcel, PlayerProfile.Star, privilege) - } - - setParcelOptionsInteractConfig(parcel, data.interactableConfig) - } - - override fun setParcelOwner(parcel: ParcelId, owner: PlayerProfile?) { - val id = if (owner == null) - ParcelsT.getId(parcel) ?: return - else - ParcelsT.getOrInitId(parcel) - - val owner_id = owner?.let { ProfilesT.getOrInitId(it.toOwnerProfile()) } - val time = owner?.let { DateTime.now() } - - ParcelsT.update({ ParcelsT.id eq id }) { - it[ParcelsT.owner_id] = owner_id - it[claim_time] = time - it[sign_oudated] = false - } - } - - override fun setParcelOwnerSignOutdated(parcel: ParcelId, outdated: Boolean) { - val id = ParcelsT.getId(parcel) ?: return - ParcelsT.update({ ParcelsT.id eq id }) { - it[sign_oudated] = outdated - } - } - - override fun setLocalPrivilege(parcel: ParcelId, player: PlayerProfile, privilege: Privilege) { - PrivilegesLocalT.setPrivilege(parcel, player.toRealProfile(), privilege) - } - - override fun setParcelOptionsInteractConfig(parcel: ParcelId, config: InteractableConfiguration) { - val bitmaskArray = (config as? BitmaskInteractableConfiguration ?: return).bitmaskArray - val isAllZero = !bitmaskArray.fold(false) { cur, elem -> cur || elem != 0 } - - if (isAllZero) { - val id = ParcelsT.getId(parcel) ?: return - ParcelOptionsT.deleteWhere { ParcelOptionsT.parcel_id eq id } - return - } - - if (bitmaskArray.size != 1) throw IllegalArgumentException() - val array = bitmaskArray.toByteArray() - val id = ParcelsT.getOrInitId(parcel) - ParcelOptionsT.upsert(ParcelOptionsT.parcel_id) { - it[parcel_id] = id - it[interact_bitmask] = array - } - } - - override fun transmitAllGlobalPrivileges(channel: SendChannel>) { - PrivilegesGlobalT.sendAllPrivilegesH(channel) - channel.close() - } - - override fun readGlobalPrivileges(owner: PlayerProfile): PrivilegesHolder? { - return PrivilegesGlobalT.readPrivileges(ProfilesT.getId(owner.toOwnerProfile()) ?: return null) - } - - override fun setGlobalPrivilege(owner: PlayerProfile, player: PlayerProfile, privilege: Privilege) { - PrivilegesGlobalT.setPrivilege(owner, player.toRealProfile(), privilege) - } - - private fun rowToParcelData(row: ResultRow) = ParcelDataHolder().apply { - owner = row[ParcelsT.owner_id]?.let { ProfilesT.getItem(it) } - lastClaimTime = row[ParcelsT.claim_time] - isOwnerSignOutdated = row[ParcelsT.sign_oudated] - - val id = row[ParcelsT.id] - ParcelOptionsT.select { ParcelOptionsT.parcel_id eq id }.firstOrNull()?.let { optrow -> - val source = optrow[ParcelOptionsT.interact_bitmask].toIntArray() - val target = (interactableConfig as? BitmaskInteractableConfiguration ?: return@let).bitmaskArray - System.arraycopy(source, 0, target, 0, source.size.clampMax(target.size)) - } - - val privileges = PrivilegesLocalT.readPrivileges(id) - if (privileges != null) { - copyPrivilegesFrom(privileges) - } - } - -} - +@file:Suppress("NOTHING_TO_INLINE", "PARAMETER_NAME_CHANGED_ON_OVERRIDE", "LocalVariableName", "UNUSED_EXPRESSION") + +package io.dico.parcels2.storage.exposed + +import com.zaxxer.hikari.HikariDataSource +import io.dico.parcels2.* +import io.dico.parcels2.PlayerProfile.Star.name +import io.dico.parcels2.storage.* +import io.dico.parcels2.util.math.clampMax +import io.dico.parcels2.util.ext.synchronized +import kotlinx.coroutines.* +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.ArrayChannel +import kotlinx.coroutines.channels.LinkedListChannel +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SchemaUtils.create +import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.vendors.DatabaseDialect +import org.joda.time.DateTime +import java.util.UUID +import javax.sql.DataSource + +class ExposedDatabaseException(message: String? = null) : Exception(message) + +class ExposedBacking(private val dataSourceFactory: () -> DataSource, val poolSize: Int) : Backing, CoroutineScope { + override val name get() = "Exposed" + override val coroutineContext = Job() + newFixedThreadPoolContext(poolSize, "Parcels StorageThread") + private var dataSource: DataSource? = null + private var database: Database? = null + private var isShutdown: Boolean = false + override val isConnected get() = database != null + + override fun launchJob(job: Backing.() -> Unit): Job = launch { transaction { job() } } + override fun launchFuture(future: Backing.() -> T): Deferred = async { transaction { future() } } + + override fun openChannel(future: Backing.(SendChannel) -> Unit): ReceiveChannel { + val channel = LinkedListChannel() + launchJob { future(channel) } + return channel + } + + override fun openChannelForWriting(action: Backing.(T) -> Unit): SendChannel { + val channel = ArrayChannel(poolSize * 2) + + repeat(poolSize.clampMax(3)) { + launch { + try { + while (true) { + action(channel.receive()) + } + } catch (ex: Exception) { + // channel closed + } + } + } + + return channel + } + + private fun transaction(statement: Transaction.() -> T) = transaction(database!!, statement) + + companion object { + init { + Database.registerDialect("mariadb") { + Class.forName("org.jetbrains.exposed.sql.vendors.MysqlDialect").newInstance() as DatabaseDialect + } + } + } + + override fun init() { + synchronized { + if (isShutdown || isConnected) throw IllegalStateException() + val dataSource = dataSourceFactory() + this.dataSource = dataSource + val database = Database.connect(dataSource) + this.database = database + transaction(database) { + create(WorldsT, ProfilesT, ParcelsT, ParcelOptionsT, PrivilegesLocalT, PrivilegesGlobalT) + } + } + } + + override fun shutdown() { + synchronized { + if (isShutdown) throw IllegalStateException() + isShutdown = true + coroutineContext.cancel(CancellationException("ExposedBacking shutdown")) + dataSource?.let { + (it as? HikariDataSource)?.close() + } + database = null + } + } + + @Suppress("RedundantObjectTypeCheck") + private fun PlayerProfile.toOwnerProfile(): PlayerProfile { + if (this is PlayerProfile.Star) return PlayerProfile.Fake(name) + return this + } + + private fun PlayerProfile.Unresolved.toResolvedProfile(): PlayerProfile.Real { + return resolve(getPlayerUuidForName(name) ?: throwException()) + } + + private fun PlayerProfile.toResolvedProfile(): PlayerProfile { + if (this is PlayerProfile.Unresolved) return toResolvedProfile() + return this + } + + private fun PlayerProfile.toRealProfile(): PlayerProfile.Real = when (this) { + is PlayerProfile.Real -> this + is PlayerProfile.Fake -> throw IllegalArgumentException("Fake profiles are not accepted") + is PlayerProfile.Unresolved -> toResolvedProfile() + else -> throw InternalError("Case should not be reached") + } + + + override fun getWorldCreationTime(worldId: ParcelWorldId): DateTime? { + return WorldsT.getWorldCreationTime(worldId) + } + + override fun setWorldCreationTime(worldId: ParcelWorldId, time: DateTime) { + WorldsT.setWorldCreationTime(worldId, time) + } + + override fun getPlayerUuidForName(name: String): UUID? { + return ProfilesT.slice(ProfilesT.uuid).select { ProfilesT.name.upperCase() eq name.toUpperCase() } + .firstOrNull()?.let { it[ProfilesT.uuid]?.toUUID() } + } + + override fun updatePlayerName(uuid: UUID, name: String) { + val binaryUuid = uuid.toByteArray() + ProfilesT.upsert(ProfilesT.uuid) { + it[ProfilesT.uuid] = binaryUuid + it[ProfilesT.name] = name + } + } + + override fun transmitParcelData(channel: SendChannel, parcels: Sequence) { + for (parcel in parcels) { + val data = readParcelData(parcel) + channel.offer(parcel to data) + } + channel.close() + } + + override fun transmitAllParcelData(channel: SendChannel) { + ParcelsT.selectAll().forEach { row -> + val parcel = ParcelsT.getItem(row) ?: return@forEach + val data = rowToParcelData(row) + channel.offer(parcel to data) + } + channel.close() + } + + override fun readParcelData(parcel: ParcelId): ParcelDataHolder? { + val row = ParcelsT.getRow(parcel) ?: return null + return rowToParcelData(row) + } + + override fun getOwnedParcels(user: PlayerProfile): List { + val user_id = ProfilesT.getId(user.toOwnerProfile()) ?: return emptyList() + return ParcelsT.select { ParcelsT.owner_id eq user_id } + .orderBy(ParcelsT.claim_time, isAsc = true) + .mapNotNull(ParcelsT::getItem) + .toList() + } + + override fun setParcelData(parcel: ParcelId, data: ParcelDataHolder?) { + if (data == null) { + transaction { + ParcelsT.getId(parcel)?.let { id -> + ParcelsT.deleteIgnoreWhere { ParcelsT.id eq id } + + // Below should cascade automatically + /* + PrivilegesLocalT.deleteIgnoreWhere { PrivilegesLocalT.parcel_id eq id } + ParcelOptionsT.deleteIgnoreWhere(limit = 1) { ParcelOptionsT.parcel_id eq id } + */ + } + + } + return + } + + transaction { + val id = ParcelsT.getOrInitId(parcel) + PrivilegesLocalT.deleteIgnoreWhere { PrivilegesLocalT.attach_id eq id } + } + + setParcelOwner(parcel, data.owner) + + for ((profile, privilege) in data.privilegeMap) { + PrivilegesLocalT.setPrivilege(parcel, profile, privilege) + } + + data.privilegeOfStar.takeIf { it != Privilege.DEFAULT }?.let { privilege -> + PrivilegesLocalT.setPrivilege(parcel, PlayerProfile.Star, privilege) + } + + setParcelOptionsInteractConfig(parcel, data.interactableConfig) + } + + override fun setParcelOwner(parcel: ParcelId, owner: PlayerProfile?) { + val id = if (owner == null) + ParcelsT.getId(parcel) ?: return + else + ParcelsT.getOrInitId(parcel) + + val owner_id = owner?.let { ProfilesT.getOrInitId(it.toOwnerProfile()) } + val time = owner?.let { DateTime.now() } + + ParcelsT.update({ ParcelsT.id eq id }) { + it[ParcelsT.owner_id] = owner_id + it[claim_time] = time + it[sign_oudated] = false + } + } + + override fun setParcelOwnerSignOutdated(parcel: ParcelId, outdated: Boolean) { + val id = ParcelsT.getId(parcel) ?: return + ParcelsT.update({ ParcelsT.id eq id }) { + it[sign_oudated] = outdated + } + } + + override fun setLocalPrivilege(parcel: ParcelId, player: PlayerProfile, privilege: Privilege) { + PrivilegesLocalT.setPrivilege(parcel, player.toRealProfile(), privilege) + } + + override fun setParcelOptionsInteractConfig(parcel: ParcelId, config: InteractableConfiguration) { + val bitmaskArray = (config as? BitmaskInteractableConfiguration ?: return).bitmaskArray + val isAllZero = !bitmaskArray.fold(false) { cur, elem -> cur || elem != 0 } + + if (isAllZero) { + val id = ParcelsT.getId(parcel) ?: return + ParcelOptionsT.deleteWhere { ParcelOptionsT.parcel_id eq id } + return + } + + if (bitmaskArray.size != 1) throw IllegalArgumentException() + val array = bitmaskArray.toByteArray() + val id = ParcelsT.getOrInitId(parcel) + ParcelOptionsT.upsert(ParcelOptionsT.parcel_id) { + it[parcel_id] = id + it[interact_bitmask] = array + } + } + + override fun transmitAllGlobalPrivileges(channel: SendChannel>) { + PrivilegesGlobalT.sendAllPrivilegesH(channel) + channel.close() + } + + override fun readGlobalPrivileges(owner: PlayerProfile): PrivilegesHolder? { + return PrivilegesGlobalT.readPrivileges(ProfilesT.getId(owner.toOwnerProfile()) ?: return null) + } + + override fun setGlobalPrivilege(owner: PlayerProfile, player: PlayerProfile, privilege: Privilege) { + PrivilegesGlobalT.setPrivilege(owner, player.toRealProfile(), privilege) + } + + private fun rowToParcelData(row: ResultRow) = ParcelDataHolder().apply { + owner = row[ParcelsT.owner_id]?.let { ProfilesT.getItem(it) } + lastClaimTime = row[ParcelsT.claim_time] + isOwnerSignOutdated = row[ParcelsT.sign_oudated] + + val id = row[ParcelsT.id] + ParcelOptionsT.select { ParcelOptionsT.parcel_id eq id }.firstOrNull()?.let { optrow -> + val source = optrow[ParcelOptionsT.interact_bitmask].toIntArray() + val target = (interactableConfig as? BitmaskInteractableConfiguration ?: return@let).bitmaskArray + System.arraycopy(source, 0, target, 0, source.size.clampMax(target.size)) + } + + val privileges = PrivilegesLocalT.readPrivileges(id) + if (privileges != null) { + copyPrivilegesFrom(privileges) + } + } + +} + diff --git a/src/main/kotlin/io/dico/parcels2/util/BukkitUtil.kt b/src/main/kotlin/io/dico/parcels2/util/BukkitUtil.kt index a4a6da9..c7d813b 100644 --- a/src/main/kotlin/io/dico/parcels2/util/BukkitUtil.kt +++ b/src/main/kotlin/io/dico/parcels2/util/BukkitUtil.kt @@ -1,14 +1,23 @@ -package io.dico.parcels2.util - -import io.dico.parcels2.util.ext.isValid -import org.bukkit.Bukkit -import org.bukkit.OfflinePlayer -import java.util.UUID - -fun getPlayerName(uuid: UUID): String? = getOfflinePlayer(uuid)?.name - -fun getOfflinePlayer(uuid: UUID): OfflinePlayer? = Bukkit.getOfflinePlayer(uuid).takeIf { it.isValid } - -fun getOfflinePlayer(name: String): OfflinePlayer? = Bukkit.getOfflinePlayer(name).takeIf { it.isValid } - -fun isServerThread(): Boolean = Thread.currentThread().name == "Server thread" +package io.dico.parcels2.util + +import io.dico.parcels2.util.ext.isValid +import org.bukkit.Bukkit +import org.bukkit.OfflinePlayer +import java.lang.IllegalArgumentException +import java.util.UUID + +fun getPlayerName(uuid: UUID): String? = getOfflinePlayer(uuid)?.name + +fun getOfflinePlayer(uuid: UUID): OfflinePlayer? = Bukkit.getOfflinePlayer(uuid).takeIf { it.isValid } + +fun getOfflinePlayer(name: String): OfflinePlayer? = Bukkit.getOfflinePlayer(name).takeIf { it.isValid } + +fun isServerThread(): Boolean = Thread.currentThread().name == "Server thread" + +fun isPlayerNameValid(name: String): Boolean = + name.length in 3..16 + && name.find { it !in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" } == null + +fun checkPlayerNameValid(name: String) { + if (!isPlayerNameValid(name)) throw IllegalArgumentException("Invalid player name: $name") +} diff --git a/src/main/kotlin/io/dico/parcels2/util/PluginAware.kt b/src/main/kotlin/io/dico/parcels2/util/PluginAware.kt new file mode 100644 index 0000000..de75519 --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/util/PluginAware.kt @@ -0,0 +1,18 @@ +package io.dico.parcels2.util + +import org.bukkit.plugin.Plugin +import org.bukkit.scheduler.BukkitTask + +interface PluginAware { + val plugin: Plugin +} + +inline fun PluginAware.schedule(delay: Int = 0, crossinline task: () -> Unit): BukkitTask { + return plugin.server.scheduler.runTaskLater(plugin, { task() }, delay.toLong()) +} + +inline fun PluginAware.scheduleRepeating(interval: Int, delay: Int = 0, crossinline task: () -> Unit): BukkitTask { + return plugin.server.scheduler.runTaskTimer(plugin, { task() }, delay.toLong(), interval.toLong()) +} + + diff --git a/src/main/kotlin/io/dico/parcels2/util/PluginScheduler.kt b/src/main/kotlin/io/dico/parcels2/util/PluginScheduler.kt deleted file mode 100644 index 268a083..0000000 --- a/src/main/kotlin/io/dico/parcels2/util/PluginScheduler.kt +++ /dev/null @@ -1,20 +0,0 @@ -package io.dico.parcels2.util - -import org.bukkit.plugin.Plugin -import org.bukkit.scheduler.BukkitTask - -interface PluginScheduler { - val plugin: Plugin - - fun schedule(delay: Int, task: () -> Unit): BukkitTask { - return plugin.server.scheduler.runTaskLater(plugin, task, delay.toLong()) - } - - fun scheduleRepeating(delay: Int, interval: Int, task: () -> Unit): BukkitTask { - return plugin.server.scheduler.runTaskTimer(plugin, task, delay.toLong(), interval.toLong()) - } -} - -@Suppress("NOTHING_TO_INLINE") -inline fun PluginScheduler.schedule(noinline task: () -> Unit) = schedule(0, task) - diff --git a/src/main/kotlin/io/dico/parcels2/util/math/Vec3d.kt b/src/main/kotlin/io/dico/parcels2/util/math/Vec3d.kt index 72b6dcd..2c3512f 100644 --- a/src/main/kotlin/io/dico/parcels2/util/math/Vec3d.kt +++ b/src/main/kotlin/io/dico/parcels2/util/math/Vec3d.kt @@ -1,53 +1,61 @@ -package io.dico.parcels2.util.math - -import org.bukkit.Location -import kotlin.math.sqrt - -data class Vec3d( - val x: Double, - val y: Double, - val z: Double -) { - constructor(loc: Location) : this(loc.x, loc.y, loc.z) - - operator fun plus(o: Vec3d) = Vec3d(x + o.x, y + o.y, z + o.z) - operator fun minus(o: Vec3i) = Vec3d(x - o.x, y - o.y, z - o.z) - infix fun addX(o: Double) = Vec3d(x + o, y, z) - infix fun addY(o: Double) = Vec3d(x, y + o, z) - infix fun addZ(o: Double) = Vec3d(x, y, z + o) - infix fun withX(o: Double) = Vec3d(o, y, z) - infix fun withY(o: Double) = Vec3d(x, o, z) - infix fun withZ(o: Double) = Vec3d(x, y, o) - fun add(ox: Double, oy: Double, oz: Double) = Vec3d(x + ox, y + oy, z + oz) - fun toVec3i() = Vec3i(x.floor(), y.floor(), z.floor()) - - fun distanceSquared(o: Vec3d): Double { - val dx = o.x - x - val dy = o.y - y - val dz = o.z - z - return dx * dx + dy * dy + dz * dz - } - - fun distance(o: Vec3d) = sqrt(distanceSquared(o)) - - operator fun get(dimension: Dimension) = - when (dimension) { - Dimension.X -> x - Dimension.Y -> y - Dimension.Z -> z - } - - fun with(dimension: Dimension, value: Double) = - when (dimension) { - Dimension.X -> withX(value) - Dimension.Y -> withY(value) - Dimension.Z -> withZ(value) - } - - fun add(dimension: Dimension, value: Double) = - when (dimension) { - Dimension.X -> addX(value) - Dimension.Y -> addY(value) - Dimension.Z -> addZ(value) - } +package io.dico.parcels2.util.math + +import org.bukkit.Location +import kotlin.math.sqrt + +data class Vec3d( + val x: Double, + val y: Double, + val z: Double +) { + constructor(loc: Location) : this(loc.x, loc.y, loc.z) + + operator fun plus(o: Vec3d) = Vec3d(x + o.x, y + o.y, z + o.z) + operator fun plus(o: Vec3i) = Vec3d(x + o.x, y + o.y, z + o.z) + operator fun minus(o: Vec3d) = Vec3d(x - o.x, y - o.y, z - o.z) + operator fun minus(o: Vec3i) = Vec3d(x - o.x, y - o.y, z - o.z) + infix fun addX(o: Double) = Vec3d(x + o, y, z) + infix fun addY(o: Double) = Vec3d(x, y + o, z) + infix fun addZ(o: Double) = Vec3d(x, y, z + o) + infix fun withX(o: Double) = Vec3d(o, y, z) + infix fun withY(o: Double) = Vec3d(x, o, z) + infix fun withZ(o: Double) = Vec3d(x, y, o) + fun add(ox: Double, oy: Double, oz: Double) = Vec3d(x + ox, y + oy, z + oz) + fun toVec3i() = Vec3i(x.floor(), y.floor(), z.floor()) + + fun distanceSquared(o: Vec3d): Double { + val dx = o.x - x + val dy = o.y - y + val dz = o.z - z + return dx * dx + dy * dy + dz * dz + } + + fun distance(o: Vec3d) = sqrt(distanceSquared(o)) + + operator fun get(dimension: Dimension) = + when (dimension) { + Dimension.X -> x + Dimension.Y -> y + Dimension.Z -> z + } + + fun with(dimension: Dimension, value: Double) = + when (dimension) { + Dimension.X -> withX(value) + Dimension.Y -> withY(value) + Dimension.Z -> withZ(value) + } + + fun add(dimension: Dimension, value: Double) = + when (dimension) { + Dimension.X -> addX(value) + Dimension.Y -> addY(value) + Dimension.Z -> addZ(value) + } + + fun copyInto(loc: Location) { + loc.x = x + loc.y = y + loc.z = z + } } \ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/util/math/Vec3i.kt b/src/main/kotlin/io/dico/parcels2/util/math/Vec3i.kt index 484ad13..b3ba169 100644 --- a/src/main/kotlin/io/dico/parcels2/util/math/Vec3i.kt +++ b/src/main/kotlin/io/dico/parcels2/util/math/Vec3i.kt @@ -1,105 +1,107 @@ -package io.dico.parcels2.util.math - -import org.bukkit.Location -import org.bukkit.World -import org.bukkit.block.Block -import org.bukkit.block.BlockFace - -data class Vec3i( - val x: Int, - val y: Int, - val z: Int -) { - constructor(loc: Location) : this(loc.blockX, loc.blockY, loc.blockZ) - constructor(block: Block) : this(block.x, block.y, block.z) - - fun toVec2i() = Vec2i(x, z) - operator fun plus(o: Vec3i) = Vec3i(x + o.x, y + o.y, z + o.z) - operator fun minus(o: Vec3i) = Vec3i(x - o.x, y - o.y, z - o.z) - infix fun addX(o: Int) = Vec3i(x + o, y, z) - infix fun addY(o: Int) = Vec3i(x, y + o, z) - infix fun addZ(o: Int) = Vec3i(x, y, z + o) - infix fun withX(o: Int) = Vec3i(o, y, z) - infix fun withY(o: Int) = Vec3i(x, o, z) - infix fun withZ(o: Int) = Vec3i(x, y, o) - fun add(ox: Int, oy: Int, oz: Int) = Vec3i(x + ox, y + oy, z + oz) - fun neg() = Vec3i(-x, -y, -z) - fun clampMax(o: Vec3i) = Vec3i(x.clampMax(o.x), y.clampMax(o.y), z.clampMax(o.z)) - - operator fun get(dimension: Dimension) = - when (dimension) { - Dimension.X -> x - Dimension.Y -> y - Dimension.Z -> z - } - - fun with(dimension: Dimension, value: Int) = - when (dimension) { - Dimension.X -> withX(value) - Dimension.Y -> withY(value) - Dimension.Z -> withZ(value) - } - - fun add(dimension: Dimension, value: Int) = - when (dimension) { - Dimension.X -> addX(value) - Dimension.Y -> addY(value) - Dimension.Z -> addZ(value) - } - - companion object { - private operator fun invoke(face: BlockFace) = Vec3i(face.modX, face.modY, face.modZ) - val down = Vec3i(BlockFace.DOWN) - val up = Vec3i(BlockFace.UP) - val north = Vec3i(BlockFace.NORTH) - val east = Vec3i(BlockFace.EAST) - val south = Vec3i(BlockFace.SOUTH) - val west = Vec3i(BlockFace.WEST) - - fun convert(face: BlockFace) = when (face) { - BlockFace.DOWN -> down - BlockFace.UP -> up - BlockFace.NORTH -> north - BlockFace.EAST -> east - BlockFace.SOUTH -> south - BlockFace.WEST -> west - else -> Vec3i(face) - } - } -} - -@Suppress("NOTHING_TO_INLINE") -inline operator fun World.get(vec: Vec3i): Block = getBlockAt(vec.x, vec.y, vec.z) - -/* -private /*inline */class IVec3i(private val data: Long) { - - private companion object { - const val mask = 0x001F_FFFF - const val max: Int = 0x000F_FFFF // +1048575 - const val min: Int = -max - 1 // -1048575 // 0xFFF0_0000 - - @Suppress("NOTHING_TO_INLINE") - inline fun Int.compressIntoLong(offset: Int): Long { - if (this !in min..max) throw IllegalArgumentException() - return and(mask).toLong().shl(offset) - } - - @Suppress("NOTHING_TO_INLINE") - inline fun Long.extractInt(offset: Int): Int { - val result = ushr(offset).toInt().and(mask) - return if (result > max) result or mask.inv() else result - } - } - - constructor(x: Int, y: Int, z: Int) : this( - x.compressIntoLong(42) - or y.compressIntoLong(21) - or z.compressIntoLong(0)) - - val x: Int get() = data.extractInt(42) - val y: Int get() = data.extractInt(21) - val z: Int get() = data.extractInt(0) - -} -*/ +package io.dico.parcels2.util.math + +import org.bukkit.Location +import org.bukkit.World +import org.bukkit.block.Block +import org.bukkit.block.BlockFace + +data class Vec3i( + val x: Int, + val y: Int, + val z: Int +) { + constructor(loc: Location) : this(loc.blockX, loc.blockY, loc.blockZ) + constructor(block: Block) : this(block.x, block.y, block.z) + + fun toVec2i() = Vec2i(x, z) + operator fun plus(o: Vec3i) = Vec3i(x + o.x, y + o.y, z + o.z) + operator fun plus(o: Vec3d) = Vec3d(x + o.x, y + o.y, z + o.z) + operator fun minus(o: Vec3i) = Vec3i(x - o.x, y - o.y, z - o.z) + operator fun minus(o: Vec3d) = Vec3d(x - o.x, y - o.y, z - o.z) + infix fun addX(o: Int) = Vec3i(x + o, y, z) + infix fun addY(o: Int) = Vec3i(x, y + o, z) + infix fun addZ(o: Int) = Vec3i(x, y, z + o) + infix fun withX(o: Int) = Vec3i(o, y, z) + infix fun withY(o: Int) = Vec3i(x, o, z) + infix fun withZ(o: Int) = Vec3i(x, y, o) + fun add(ox: Int, oy: Int, oz: Int) = Vec3i(x + ox, y + oy, z + oz) + fun neg() = Vec3i(-x, -y, -z) + fun clampMax(o: Vec3i) = Vec3i(x.clampMax(o.x), y.clampMax(o.y), z.clampMax(o.z)) + + operator fun get(dimension: Dimension) = + when (dimension) { + Dimension.X -> x + Dimension.Y -> y + Dimension.Z -> z + } + + fun with(dimension: Dimension, value: Int) = + when (dimension) { + Dimension.X -> withX(value) + Dimension.Y -> withY(value) + Dimension.Z -> withZ(value) + } + + fun add(dimension: Dimension, value: Int) = + when (dimension) { + Dimension.X -> addX(value) + Dimension.Y -> addY(value) + Dimension.Z -> addZ(value) + } + + companion object { + private operator fun invoke(face: BlockFace) = Vec3i(face.modX, face.modY, face.modZ) + val down = Vec3i(BlockFace.DOWN) + val up = Vec3i(BlockFace.UP) + val north = Vec3i(BlockFace.NORTH) + val east = Vec3i(BlockFace.EAST) + val south = Vec3i(BlockFace.SOUTH) + val west = Vec3i(BlockFace.WEST) + + fun convert(face: BlockFace) = when (face) { + BlockFace.DOWN -> down + BlockFace.UP -> up + BlockFace.NORTH -> north + BlockFace.EAST -> east + BlockFace.SOUTH -> south + BlockFace.WEST -> west + else -> Vec3i(face) + } + } +} + +@Suppress("NOTHING_TO_INLINE") +inline operator fun World.get(vec: Vec3i): Block = getBlockAt(vec.x, vec.y, vec.z) + +/* +private /*inline */class IVec3i(private val data: Long) { + + private companion object { + const val mask = 0x001F_FFFF + const val max: Int = 0x000F_FFFF // +1048575 + const val min: Int = -max - 1 // -1048575 // 0xFFF0_0000 + + @Suppress("NOTHING_TO_INLINE") + inline fun Int.compressIntoLong(offset: Int): Long { + if (this !in min..max) throw IllegalArgumentException() + return and(mask).toLong().shl(offset) + } + + @Suppress("NOTHING_TO_INLINE") + inline fun Long.extractInt(offset: Int): Int { + val result = ushr(offset).toInt().and(mask) + return if (result > max) result or mask.inv() else result + } + } + + constructor(x: Int, y: Int, z: Int) : this( + x.compressIntoLong(42) + or y.compressIntoLong(21) + or z.compressIntoLong(0)) + + val x: Int get() = data.extractInt(42) + val y: Int get() = data.extractInt(21) + val z: Int get() = data.extractInt(0) + +} +*/ diff --git a/src/main/kotlin/io/dico/parcels2/util/parallel.kt b/src/main/kotlin/io/dico/parcels2/util/parallel.kt new file mode 100644 index 0000000..a4edc3c --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/util/parallel.kt @@ -0,0 +1,9 @@ +package io.dico.parcels2.util + +fun doParallel() { + + val array = IntArray(1000) + IntRange(0, 1000).chunked() + + +} \ No newline at end of file diff --git a/todo.md b/todo.md index cb073df..74a1dca 100644 --- a/todo.md +++ b/todo.md @@ -1,103 +1,103 @@ -# Parcels Todo list - -Commands -- -Basically all admin commands. -* ~~setowner~~ -* ~~dispose~~ -* ~~reset~~ -* ~~swap~~ -* New admin commands that I can't think of right now. - -Also -* ~~setbiome~~ -* random - -~~Modify home command:~~ -* ~~Make `:` not be required if prior component cannot be parsed to an int~~ -* ~~Listen for command events that use plotme-style argument, and transform the command~~ - -~~Add permissions to commands (replace or fix `IContextFilter` from command lib -to allow inheriting permissions properly).~~ - -Parcel Options -- - -Parcel options apply to any player with `DEFAULT` added status. -They affect what their permissions might be within the parcel. - -Apart from `/p option inputs`, `/p option inventory`, the following might be considered. - -~~Move existing options to "interact" namespace (`/p o interact`) -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.~~ -This could become optional. - -* Option to control spreading and/or forming of blocks such as grass and ice within the parcel.~~ - -Block Management -- -~~Update the parcel corner with owner info when a player flies into the parcel (after migrations). -Parcels has a player head in that corner in addition to the sign that PlotMe uses.~~ - -~~Commands that modify parcel blocks must be kept track of to prevent multiple -from running simultaneously in the same parcel. `hasBlockVisitors` field must be updated. -In general, spamming the commands must be caught at all cost to avoid lots of lag.~~ - -~~Swap - schematic is in place, but proper placement order must be enforced to make sure that attachable -blocks are placed properly. Alternatively, if a block change method can be found that doesn't -cause block updates, that would be preferred subject to having good performance.~~ - -~~Change `RegionTraversal` to allow traversing different parts of a region in a different order. -This could apply to clearing of plots, for example. It would be better if the bottom 64 (floor height) -layers are done upwards, and the rest downwards.~~ - -Events -- -Prevent block spreading subject to conditions. - -Scan through blocks that were added since original Parcels implementation, -that might introduce things that need to be checked or listened for. - -~~WorldEdit Listener.~~ - -Limit number of beacons in a parcel and/or avoid potion effects being applied outside the parcel. - -Database -- -Find and patch ways to add new useless entries (for regular players at least) - -Prevent invalid player names from being saved to the database. -Here, invalid player names mean names that contain invalid characters. - -Use an atomic GET OR INSERT query so that parallel execution doesn't cause problems -(as is currently the case when migrating). - -Implement a container that doesn't require loading all parcel data on startup (Complex). - -~~Update player profiles in the database on join to account for name changes.~~ - -~~Store player status on parcel (allowed, default banned) as a number to allow for future additions to this set of possibilities~~ - - -After testing on Redstoner -- - -Clear (and swap) entities on /p clear etc -Fix command lag -Chorus fruit can grow outside plots -Vines can grow outside plots -Ghasts, bats, phantoms and magma cubes can be spawned with eggs -ParcelTarget doesn't report a world that wasn't found correctly -Jumping on turtle eggs is considered as interacting with pressure plates -Setbiome internal error when progress reporting is attached -Unclaim doesn't clear the plot. It probably should. -Players can shoot boats and minecarts. -You can use disabled items by rightclicking air. -Tab complete isn't working correctly. -~~Bed use in nether and end might not have to be blocked.~~ - +# Parcels Todo list + +Commands +- +Basically all admin commands. +* ~~setowner~~ +* ~~dispose~~ +* ~~reset~~ +* ~~swap~~ +* New admin commands that I can't think of right now. + +Also +* ~~setbiome~~ +* random + +~~Modify home command:~~ +* ~~Make `:` not be required if prior component cannot be parsed to an int~~ +* ~~Listen for command events that use plotme-style argument, and transform the command~~ + +~~Add permissions to commands (replace or fix `IContextFilter` from command lib +to allow inheriting permissions properly).~~ + +Parcel Options +- + +Parcel options apply to any player with `DEFAULT` added status. +They affect what their permissions might be within the parcel. + +Apart from `/p option inputs`, `/p option inventory`, the following might be considered. + +~~Move existing options to "interact" namespace (`/p o interact`) +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.~~ +This could become optional. + +* Option to control spreading and/or forming of blocks such as grass and ice within the parcel.~~ + +Block Management +- +~~Update the parcel corner with owner info when a player flies into the parcel (after migrations). +Parcels has a player head in that corner in addition to the sign that PlotMe uses.~~ + +~~Commands that modify parcel blocks must be kept track of to prevent multiple +from running simultaneously in the same parcel. `hasBlockVisitors` field must be updated. +In general, spamming the commands must be caught at all cost to avoid lots of lag.~~ + +~~Swap - schematic is in place, but proper placement order must be enforced to make sure that attachable +blocks are placed properly. Alternatively, if a block change method can be found that doesn't +cause block updates, that would be preferred subject to having good performance.~~ + +~~Change `RegionTraversal` to allow traversing different parts of a region in a different order. +This could apply to clearing of plots, for example. It would be better if the bottom 64 (floor height) +layers are done upwards, and the rest downwards.~~ + +Events +- +Prevent block spreading subject to conditions. + +Scan through blocks that were added since original Parcels implementation, +that might introduce things that need to be checked or listened for. + +~~WorldEdit Listener.~~ + +Limit number of beacons in a parcel and/or avoid potion effects being applied outside the parcel. + +Database +- +Find and patch ways to add new useless entries (for regular players at least) + +~~Prevent invalid player names from being saved to the database. +Here, invalid player names mean names that contain invalid characters.~~ + +Use an atomic GET OR INSERT query so that parallel execution doesn't cause problems +(as is currently the case when migrating). + +Implement a container that doesn't require loading all parcel data on startup (Complex). + +~~Update player profiles in the database on join to account for name changes.~~ + +~~Store player status on parcel (allowed, default banned) as a number to allow for future additions to this set of possibilities~~ + + +After testing on Redstoner +- + +~~Clear (and swap) entities on /p clear etc~~ +Fix command lag +Chorus fruit can grow outside plots +Vines can grow outside plots +Ghasts, bats, phantoms and magma cubes can be spawned with eggs +ParcelTarget doesn't report a world that wasn't found correctly +Jumping on turtle eggs is considered as interacting with pressure plates +Setbiome internal error when progress reporting is attached +Unclaim doesn't clear the plot. It probably should. +Players can shoot boats and minecarts. +You can use disabled items by rightclicking air. +Tab complete isn't working correctly. +~~Bed use in nether and end might not have to be blocked.~~ + -- cgit v1.2.3