diff options
author | Dico Karssiens <dico.karssiens@gmail.com> | 2018-11-17 21:32:43 +0000 |
---|---|---|
committer | Dico Karssiens <dico.karssiens@gmail.com> | 2018-11-17 21:32:43 +0000 |
commit | 5ef2584fdb6e4db482aa4c57e6ecf0202c67a48d (patch) | |
tree | 7954c8fb9048c81246d5c53a4cba5b7646d4b6e1 | |
parent | 0f196f59c6a4cb76ab8409da62ff1f35505f94a8 (diff) |
Tweak some command stuff, clear/swap entities
25 files changed, 3953 insertions, 3554 deletions
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<KotlinPlatformJvmPlugin>() 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<File>) { - 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<String> 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<String, org.bukkit.command.Command> map, EOverridePolicy overridePolicy) {
- Objects.requireNonNull(overridePolicy);
- //debugChildren(this);
- Map<String, ChildCommandAddress> children = this.children;
- Map<ChildCommandAddress, BukkitCommand> wrappers = new IdentityHashMap<>();
-
- for (ChildCommandAddress address : children.values()) {
- if (!wrappers.containsKey(address)) {
- wrappers.put(address, new BukkitCommand(address));
- }
- }
-
- for (Map.Entry<String, ChildCommandAddress> 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<String> keys = address.getChildrenMainKeys();
- for (String key : keys) {
- ChildCommandAddress child = address.getChild(key);
- System.out.println(child.getAddress());
- debugChildren(child);
- }
- }
-
- private static void registerMember(Map<String, org.bukkit.command.Command> 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<String, org.bukkit.command.Command> map) {
- Set<ICommandAddress> children = new HashSet<>(this.children.values());
- Iterator<Map.Entry<String, org.bukkit.command.Command>> iterator = map.entrySet().iterator();
- while (iterator.hasNext()) {
- Map.Entry<String, org.bukkit.command.Command> 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<String> getTabCompletions(CommandSender sender, Location location, String[] args) {
- return getTabCompletions(sender, location, new ArgumentBuffer(args));
- }
-
- @Override
- public List<String> getTabCompletions(CommandSender sender, String usedLabel, Location location, String[] args) {
- return getTabCompletions(sender, location, new ArgumentBuffer(usedLabel, args));
- }
-
- @Override
- public List<String> getTabCompletions(CommandSender sender, Location location, ArgumentBuffer buffer) {
- ExecutionContext context = new ExecutionContext(sender, buffer, true);
-
- try {
- ICommandAddress target = getCommandTarget(context, buffer);
-
- List<String> 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<String> 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<String, org.bukkit.command.Command> map, EOverridePolicy overridePolicy) { + Objects.requireNonNull(overridePolicy); + //debugChildren(this); + Map<String, ChildCommandAddress> children = this.children; + Map<ChildCommandAddress, BukkitCommand> wrappers = new IdentityHashMap<>(); + + for (ChildCommandAddress address : children.values()) { + if (!wrappers.containsKey(address)) { + wrappers.put(address, new BukkitCommand(address)); + } + } + + for (Map.Entry<String, ChildCommandAddress> 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<String> keys = address.getChildrenMainKeys(); + for (String key : keys) { + ChildCommandAddress child = address.getChild(key); + System.out.println(child.getAddress()); + debugChildren(child); + } + } + + private static void registerMember(Map<String, org.bukkit.command.Command> 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<String, org.bukkit.command.Command> map) { + Set<ICommandAddress> children = new HashSet<>(this.children.values()); + Iterator<Map.Entry<String, org.bukkit.command.Command>> iterator = map.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry<String, org.bukkit.command.Command> 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<String> getTabCompletions(CommandSender sender, Location location, String[] args) { + return getTabCompletions(sender, location, new ArgumentBuffer(args)); + } + + @Override + public List<String> getTabCompletions(CommandSender sender, String usedLabel, Location location, String[] args) { + return getTabCompletions(sender, location, new ArgumentBuffer(usedLabel, args)); + } + + @Override + public List<String> 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<String> 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<Parameter<?, ?>> m_indexedParams;
- private final int m_maxIndex;
- private final int m_maxRequiredIndex;
-
- private Map<String, Object> m_valueMap;
- private Set<String> m_parsedKeys;
- private int m_completionCursor = -1;
- private Parameter<?, ?> m_completionTarget = null;
-
- public ContextParser(ExecutionContext context,
- ParameterList parameterList,
- Map<String, Object> valueMap,
- Set<String> 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<String, Object> getValueMap() {
- return m_valueMap;
- }
-
- public Set<String> 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<Object> 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<String, Parameter<?, ?>> 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<Parameter<?, ?>> m_indexedParams; + private final int m_maxIndex; + private final int m_maxRequiredIndex; + + private Map<String, Object> m_valueMap; + private Set<String> m_parsedKeys; + private int m_completionCursor = -1; + private Parameter<?, ?> m_completionTarget = null; + + public ContextParser(ExecutionContext context, + ParameterList parameterList, + Map<String, Object> valueMap, + Set<String> 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<String, Object> getValueMap() { + return m_valueMap; + } + + public Set<String> 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<Object> 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<String, Parameter<?, ?>> 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. + * <p> + * 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 + * + * <p> + * For example, if the bit is 00010000 + * This function returns 00001111 + * <p> + * + * @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<Object, CommandException> 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<Object, CommandException> 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<Method> methods = new LinkedList<>(Arrays.asList(clazz.getDeclaredMethods()));
-
- Iterator<Method> 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<Object, Object> 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<ICommandAddress> 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<Method> methods = new LinkedList<>(Arrays.asList(clazz.getDeclaredMethods())); + + Iterator<Method> 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<Object, Object> 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<ICommandAddress> 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<Any?>
-): 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<Any?> { 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<Any?>.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<Any?> +): String? { + + // UNDISPATCHED causes the handler to run until the first suspension point on the current thread, + // meaning command handlers that don't have suspension points will run completely synchronously. + // Tasks that take time to compute should suspend the coroutine and resume on another thread. + val job = GlobalScope.async(context = coroutineContext, start = UNDISPATCHED) { + suspendCoroutineUninterceptedOrReturn<Any?> { 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<Any?>.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<Job>
-
- /**
- * 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 <T> 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<JobInternal>()
- override val jobs: List<Job> = _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<Unit>? = 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<Unit> ->
- 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<Job> + + /** + * 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 <T> 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<JobInternal>() + override val jobs: List<Job> = _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<Unit>? = 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<Unit> -> + 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<BlockPopulator> {
- 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<ParcelLocator, ParcelBlockManager>
-}
-
-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<Entity>
-
- 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<Vec2i>
-}
-
-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<Entity> {
- val region = getRegion(parcel)
- val center = region.center
- val centerLoc = Location(world, center.x, center.y, center.z)
- val centerDist = (center - region.origin).add(0.2, 0.2, 0.2)
- return world.getNearbyEntities(centerLoc, centerDist.x, centerDist.y, centerDist.z)
- }
-
-}
+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<BlockPopulator> { + 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<ParcelLocator, ParcelBlockManager> +} + +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<Entity> + + 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<Vec2i> +} + +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<Entity> { + 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<String, ParcelWorld>
-
- 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<out ParcelId>, 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<String, ParcelWorld> + + 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<out ParcelId>, 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<Options>(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<Options>(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<T : Entity>(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<T : Minecart>(entity: T) : EntityCopy<T>(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, Void>(Parcel::class.java) {
- val regex = Regex.fromLiteral("((.+)->)?([0-9]+):([0-9]+)")
-
- override fun parse(parameter: Parameter<Parcel, Void>, 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, Int>(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, Int>(PlayerProfile::class.java, ProfileKind) {
-
- override fun parse(parameter: Parameter<PlayerProfile, Int>, 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<PlayerProfile, Int>,
- sender: CommandSender,
- location: Location?,
- buffer: ArgumentBuffer
- ): MutableList<String> {
- 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, Void>(Parcel::class.java) { + val regex = Regex.fromLiteral("((.+)->)?([0-9]+):([0-9]+)") + + override fun parse(parameter: Parameter<Parcel, Void>, 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, Int>(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, Int>(PlayerProfile::class.java, ProfileKind) { + + override fun parse(parameter: Parameter<PlayerProfile, Int>, 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<PlayerProfile, Int>, + sender: CommandSender, + location: Location?, + buffer: ArgumentBuffer + ): MutableList<String> { + 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, Int>(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, Int>(ParcelTarget::class.java, TargetKind) {
-
- override fun parse(parameter: Parameter<ParcelTarget, Int>, 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<PlayerProfile, Int> {
- 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<ParcelTarget, Int>, 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, Int>(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, Int>(ParcelTarget::class.java, TargetKind) { + + override fun parse( + parameter: Parameter<ParcelTarget, Int>, + 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<PlayerProfile, Int> { + 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<ParcelTarget, Int>, + 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 <T> generate(
- chunkX: Int,
- chunkZ: Int,
- floor: T, wall:
- T, pathMain: T,
- pathAlt: T,
- fill: T,
- setter: (Int, Int, Int, T) -> Unit
- ) {
-
- val floorHeight = o.floorHeight
- val parcelSize = o.parcelSize
- 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<ParcelLocator, ParcelBlockManager> {
- val impl = ParcelLocatorAndBlockManagerImpl(parcelProvider, container, coroutineScope, jobDispatcher)
- return impl to impl
- }
-
- private inline fun <T> 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<Vec2i> {
- /*
- * Get the offsets for the world out of the way
- * to simplify the calculation that follows.
- */
-
- val x = chunk.x.shl(4) - (o.offsetX + pathOffset)
- val z = chunk.z.shl(4) - (o.offsetZ + pathOffset)
-
- /* Locations of wall corners (where owner blocks are placed) are defined as:
- *
- * x umod sectionSize == sectionSize-1
- *
- * This check needs to be made for all 16 slices of the chunk in 2 dimensions
- * How to optimize this?
- * Let's take the expression
- *
- * x umod sectionSize
- *
- * And call it modX
- * x can be shifted (chunkSize -1) times to attempt to get a modX of 0.
- * This means that if the modX is 1, and sectionSize == (chunkSize-1), there would be a match at the last shift.
- * To check that there are any matches, we can see if the following holds:
- *
- * modX >= ((sectionSize-1) - (chunkSize-1))
- *
- * Which can be simplified to:
- * modX >= sectionSize - chunkSize
- *
- * if sectionSize == chunkSize, this expression can be simplified to
- * modX >= 0
- * which is always true. This is expected.
- * To get the total number of matches on a dimension, we can evaluate the following:
- *
- * (modX - (sectionSize - chunkSize) + sectionSize) / sectionSize
- *
- * We add sectionSize to the lhs because, if the other part of the lhs is 0, we need at least 1.
- * This can be simplified to:
- *
- * (modX + chunkSize) / sectionSize
- */
-
- val sectionSize = sectionSize
-
- val modX = x umod sectionSize
- val matchesOnDimensionX = (modX + chunkSize) / sectionSize
- if (matchesOnDimensionX <= 0) return emptyList()
-
- val modZ = z umod sectionSize
- val matchesOnDimensionZ = (modZ + chunkSize) / sectionSize
- if (matchesOnDimensionZ <= 0) return emptyList()
-
- /*
- * Now we need to find the first id within the matches,
- * and then return the subsequent matches in a rectangle following it.
- *
- * On each dimension, get the distance to the first match, which is equal to (sectionSize-1 - modX)
- * and add it to the coordinate value
- */
- val firstX = x + (sectionSize - 1 - modX)
- val firstZ = z + (sectionSize - 1 - modZ)
-
- val firstIdX = (firstX + 1) / sectionSize + 1
- val firstIdZ = (firstZ + 1) / sectionSize + 1
-
- if (matchesOnDimensionX == 1 && matchesOnDimensionZ == 1) {
- // fast-path optimization
- return listOf(Vec2i(firstIdX, firstIdZ))
- }
-
- 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 <T> generate( + chunkX: Int, + chunkZ: Int, + floor: T, wall: + T, pathMain: T, + pathAlt: T, + fill: T, + setter: (Int, Int, Int, T) -> Unit + ) { + + val floorHeight = o.floorHeight + val parcelSize = o.parcelSize + 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<ParcelLocator, ParcelBlockManager> { + val impl = ParcelLocatorAndBlockManagerImpl(parcelProvider, container, coroutineScope, jobDispatcher) + return impl to impl + } + + private inline fun <T> 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<Vec2i> { + /* + * Get the offsets for the world out of the way + * to simplify the calculation that follows. + */ + + val x = chunk.x.shl(4) - (o.offsetX + pathOffset) + val z = chunk.z.shl(4) - (o.offsetZ + pathOffset) + + /* Locations of wall corners (where owner blocks are placed) are defined as: + * + * x umod sectionSize == sectionSize-1 + * + * This check needs to be made for all 16 slices of the chunk in 2 dimensions + * How to optimize this? + * Let's take the expression + * + * x umod sectionSize + * + * And call it modX + * x can be shifted (chunkSize -1) times to attempt to get a modX of 0. + * This means that if the modX is 1, and sectionSize == (chunkSize-1), there would be a match at the last shift. + * To check that there are any matches, we can see if the following holds: + * + * modX >= ((sectionSize-1) - (chunkSize-1)) + * + * Which can be simplified to: + * modX >= sectionSize - chunkSize + * + * if sectionSize == chunkSize, this expression can be simplified to + * modX >= 0 + * which is always true. This is expected. + * To get the total number of matches on a dimension, we can evaluate the following: + * + * (modX - (sectionSize - chunkSize) + sectionSize) / sectionSize + * + * We add sectionSize to the lhs because, if the other part of the lhs is 0, we need at least 1. + * This can be simplified to: + * + * (modX + chunkSize) / sectionSize + */ + + val sectionSize = sectionSize + + val modX = x umod sectionSize + val matchesOnDimensionX = (modX + chunkSize) / sectionSize + if (matchesOnDimensionX <= 0) return emptyList() + + val modZ = z umod sectionSize + val matchesOnDimensionZ = (modZ + chunkSize) / sectionSize + if (matchesOnDimensionZ <= 0) return emptyList() + + /* + * Now we need to find the first id within the matches, + * and then return the subsequent matches in a rectangle following it. + * + * On each dimension, get the distance to the first match, which is equal to (sectionSize-1 - modX) + * and add it to the coordinate value + */ + val firstX = x + (sectionSize - 1 - modX) + val firstZ = z + (sectionSize - 1 - modZ) + + val firstIdX = (firstX + 1) / sectionSize + 1 + val firstIdZ = (firstZ + 1) / sectionSize + 1 + + if (matchesOnDimensionX == 1 && matchesOnDimensionZ == 1) { + // fast-path optimization + return listOf(Vec2i(firstIdX, firstIdZ)) + } + + 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<String, ParcelWorld> get() = _worlds
- private val _worlds: MutableMap<String, ParcelWorld> = hashMapOf()
- private val _generators: MutableMap<String, ParcelGenerator> = 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<ParcelWorld>()
- 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<ParcelWorld> = 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<String, ParcelWorld> get() = _worlds + private val _worlds: MutableMap<String, ParcelWorld> = hashMapOf() + private val _generators: MutableMap<String, ParcelGenerator> = 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<ParcelWorld>() + 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<ParcelWorld> = 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<Entity>) + + 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 <T> launchFuture(future: Backing.() -> T): Deferred<T> = async { transaction { future() } }
-
- override fun <T> openChannel(future: Backing.(SendChannel<T>) -> Unit): ReceiveChannel<T> {
- val channel = LinkedListChannel<T>()
- launchJob { future(channel) }
- return channel
- }
-
- override fun <T> openChannelForWriting(action: Backing.(T) -> Unit): SendChannel<T> {
- val channel = ArrayChannel<T>(poolSize * 2)
-
- repeat(poolSize.clampMax(3)) {
- launch {
- try {
- while (true) {
- action(channel.receive())
- }
- } catch (ex: Exception) {
- // channel closed
- }
- }
- }
-
- return channel
- }
-
- private fun <T> 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<DataPair>, parcels: Sequence<ParcelId>) {
- for (parcel in parcels) {
- val data = readParcelData(parcel)
- channel.offer(parcel to data)
- }
- channel.close()
- }
-
- override fun transmitAllParcelData(channel: SendChannel<DataPair>) {
- 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<ParcelId> {
- 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<PrivilegePair<PlayerProfile>>) {
- 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 <T> launchFuture(future: Backing.() -> T): Deferred<T> = async { transaction { future() } } + + override fun <T> openChannel(future: Backing.(SendChannel<T>) -> Unit): ReceiveChannel<T> { + val channel = LinkedListChannel<T>() + launchJob { future(channel) } + return channel + } + + override fun <T> openChannelForWriting(action: Backing.(T) -> Unit): SendChannel<T> { + val channel = ArrayChannel<T>(poolSize * 2) + + repeat(poolSize.clampMax(3)) { + launch { + try { + while (true) { + action(channel.receive()) + } + } catch (ex: Exception) { + // channel closed + } + } + } + + return channel + } + + private fun <T> 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<DataPair>, parcels: Sequence<ParcelId>) { + for (parcel in parcels) { + val data = readParcelData(parcel) + channel.offer(parcel to data) + } + channel.close() + } + + override fun transmitAllParcelData(channel: SendChannel<DataPair>) { + 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<ParcelId> { + 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<PrivilegePair<PlayerProfile>>) { + 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 @@ -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.~~ + |