Tweak some command stuff, clear/swap entities
This commit is contained in:
@@ -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()) }
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
38
src/main/kotlin/io/dico/parcels2/blockvisitor/Entities.kt
Normal file
38
src/main/kotlin/io/dico/parcels2/blockvisitor/Entities.kt
Normal file
@@ -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
|
||||
}
|
||||
}*/
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
18
src/main/kotlin/io/dico/parcels2/util/PluginAware.kt
Normal file
18
src/main/kotlin/io/dico/parcels2/util/PluginAware.kt
Normal file
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
9
src/main/kotlin/io/dico/parcels2/util/parallel.kt
Normal file
9
src/main/kotlin/io/dico/parcels2/util/parallel.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package io.dico.parcels2.util
|
||||
|
||||
fun doParallel() {
|
||||
|
||||
val array = IntArray(1000)
|
||||
IntRange(0, 1000).chunked()
|
||||
|
||||
|
||||
}
|
||||
206
todo.md
206
todo.md
@@ -1,103 +1,103 @@
|
||||
# Parcels Todo list
|
||||
|
||||
Commands
|
||||
-
|
||||
Basically all admin commands.
|
||||
* ~~setowner~~
|
||||
* ~~dispose~~
|
||||
* ~~reset~~
|
||||
* ~~swap~~
|
||||
* New admin commands that I can't think of right now.
|
||||
|
||||
Also
|
||||
* ~~setbiome~~
|
||||
* random
|
||||
|
||||
~~Modify home command:~~
|
||||
* ~~Make `:` not be required if prior component cannot be parsed to an int~~
|
||||
* ~~Listen for command events that use plotme-style argument, and transform the command~~
|
||||
|
||||
~~Add permissions to commands (replace or fix `IContextFilter` from command lib
|
||||
to allow inheriting permissions properly).~~
|
||||
|
||||
Parcel Options
|
||||
-
|
||||
|
||||
Parcel options apply to any player with `DEFAULT` added status.
|
||||
They affect what their permissions might be within the parcel.
|
||||
|
||||
Apart from `/p option inputs`, `/p option inventory`, the following might be considered.
|
||||
|
||||
~~Move existing options to "interact" namespace (`/p o interact`)
|
||||
Add classes for different things you can interact with~~
|
||||
|
||||
~~Then,~~
|
||||
~~* Split `/p option interact inputs` into a list of interactible block types.~~
|
||||
~~The list could include container blocks, merging the existing inventory option.~~
|
||||
* Players cannot launch projectiles in locations where they can't build.~~
|
||||
This could become optional.
|
||||
|
||||
* Option to control spreading and/or forming of blocks such as grass and ice within the parcel.~~
|
||||
|
||||
Block Management
|
||||
-
|
||||
~~Update the parcel corner with owner info when a player flies into the parcel (after migrations).
|
||||
Parcels has a player head in that corner in addition to the sign that PlotMe uses.~~
|
||||
|
||||
~~Commands that modify parcel blocks must be kept track of to prevent multiple
|
||||
from running simultaneously in the same parcel. `hasBlockVisitors` field must be updated.
|
||||
In general, spamming the commands must be caught at all cost to avoid lots of lag.~~
|
||||
|
||||
~~Swap - schematic is in place, but proper placement order must be enforced to make sure that attachable
|
||||
blocks are placed properly. Alternatively, if a block change method can be found that doesn't
|
||||
cause block updates, that would be preferred subject to having good performance.~~
|
||||
|
||||
~~Change `RegionTraversal` to allow traversing different parts of a region in a different order.
|
||||
This could apply to clearing of plots, for example. It would be better if the bottom 64 (floor height)
|
||||
layers are done upwards, and the rest downwards.~~
|
||||
|
||||
Events
|
||||
-
|
||||
Prevent block spreading subject to conditions.
|
||||
|
||||
Scan through blocks that were added since original Parcels implementation,
|
||||
that might introduce things that need to be checked or listened for.
|
||||
|
||||
~~WorldEdit Listener.~~
|
||||
|
||||
Limit number of beacons in a parcel and/or avoid potion effects being applied outside the parcel.
|
||||
|
||||
Database
|
||||
-
|
||||
Find and patch ways to add new useless entries (for regular players at least)
|
||||
|
||||
Prevent invalid player names from being saved to the database.
|
||||
Here, invalid player names mean names that contain invalid characters.
|
||||
|
||||
Use an atomic GET OR INSERT query so that parallel execution doesn't cause problems
|
||||
(as is currently the case when migrating).
|
||||
|
||||
Implement a container that doesn't require loading all parcel data on startup (Complex).
|
||||
|
||||
~~Update player profiles in the database on join to account for name changes.~~
|
||||
|
||||
~~Store player status on parcel (allowed, default banned) as a number to allow for future additions to this set of possibilities~~
|
||||
|
||||
|
||||
After testing on Redstoner
|
||||
-
|
||||
|
||||
Clear (and swap) entities on /p clear etc
|
||||
Fix command lag
|
||||
Chorus fruit can grow outside plots
|
||||
Vines can grow outside plots
|
||||
Ghasts, bats, phantoms and magma cubes can be spawned with eggs
|
||||
ParcelTarget doesn't report a world that wasn't found correctly
|
||||
Jumping on turtle eggs is considered as interacting with pressure plates
|
||||
Setbiome internal error when progress reporting is attached
|
||||
Unclaim doesn't clear the plot. It probably should.
|
||||
Players can shoot boats and minecarts.
|
||||
You can use disabled items by rightclicking air.
|
||||
Tab complete isn't working correctly.
|
||||
~~Bed use in nether and end might not have to be blocked.~~
|
||||
|
||||
# Parcels Todo list
|
||||
|
||||
Commands
|
||||
-
|
||||
Basically all admin commands.
|
||||
* ~~setowner~~
|
||||
* ~~dispose~~
|
||||
* ~~reset~~
|
||||
* ~~swap~~
|
||||
* New admin commands that I can't think of right now.
|
||||
|
||||
Also
|
||||
* ~~setbiome~~
|
||||
* random
|
||||
|
||||
~~Modify home command:~~
|
||||
* ~~Make `:` not be required if prior component cannot be parsed to an int~~
|
||||
* ~~Listen for command events that use plotme-style argument, and transform the command~~
|
||||
|
||||
~~Add permissions to commands (replace or fix `IContextFilter` from command lib
|
||||
to allow inheriting permissions properly).~~
|
||||
|
||||
Parcel Options
|
||||
-
|
||||
|
||||
Parcel options apply to any player with `DEFAULT` added status.
|
||||
They affect what their permissions might be within the parcel.
|
||||
|
||||
Apart from `/p option inputs`, `/p option inventory`, the following might be considered.
|
||||
|
||||
~~Move existing options to "interact" namespace (`/p o interact`)
|
||||
Add classes for different things you can interact with~~
|
||||
|
||||
~~Then,~~
|
||||
~~* Split `/p option interact inputs` into a list of interactible block types.~~
|
||||
~~The list could include container blocks, merging the existing inventory option.~~
|
||||
* Players cannot launch projectiles in locations where they can't build.~~
|
||||
This could become optional.
|
||||
|
||||
* Option to control spreading and/or forming of blocks such as grass and ice within the parcel.~~
|
||||
|
||||
Block Management
|
||||
-
|
||||
~~Update the parcel corner with owner info when a player flies into the parcel (after migrations).
|
||||
Parcels has a player head in that corner in addition to the sign that PlotMe uses.~~
|
||||
|
||||
~~Commands that modify parcel blocks must be kept track of to prevent multiple
|
||||
from running simultaneously in the same parcel. `hasBlockVisitors` field must be updated.
|
||||
In general, spamming the commands must be caught at all cost to avoid lots of lag.~~
|
||||
|
||||
~~Swap - schematic is in place, but proper placement order must be enforced to make sure that attachable
|
||||
blocks are placed properly. Alternatively, if a block change method can be found that doesn't
|
||||
cause block updates, that would be preferred subject to having good performance.~~
|
||||
|
||||
~~Change `RegionTraversal` to allow traversing different parts of a region in a different order.
|
||||
This could apply to clearing of plots, for example. It would be better if the bottom 64 (floor height)
|
||||
layers are done upwards, and the rest downwards.~~
|
||||
|
||||
Events
|
||||
-
|
||||
Prevent block spreading subject to conditions.
|
||||
|
||||
Scan through blocks that were added since original Parcels implementation,
|
||||
that might introduce things that need to be checked or listened for.
|
||||
|
||||
~~WorldEdit Listener.~~
|
||||
|
||||
Limit number of beacons in a parcel and/or avoid potion effects being applied outside the parcel.
|
||||
|
||||
Database
|
||||
-
|
||||
Find and patch ways to add new useless entries (for regular players at least)
|
||||
|
||||
~~Prevent invalid player names from being saved to the database.
|
||||
Here, invalid player names mean names that contain invalid characters.~~
|
||||
|
||||
Use an atomic GET OR INSERT query so that parallel execution doesn't cause problems
|
||||
(as is currently the case when migrating).
|
||||
|
||||
Implement a container that doesn't require loading all parcel data on startup (Complex).
|
||||
|
||||
~~Update player profiles in the database on join to account for name changes.~~
|
||||
|
||||
~~Store player status on parcel (allowed, default banned) as a number to allow for future additions to this set of possibilities~~
|
||||
|
||||
|
||||
After testing on Redstoner
|
||||
-
|
||||
|
||||
~~Clear (and swap) entities on /p clear etc~~
|
||||
Fix command lag
|
||||
Chorus fruit can grow outside plots
|
||||
Vines can grow outside plots
|
||||
Ghasts, bats, phantoms and magma cubes can be spawned with eggs
|
||||
ParcelTarget doesn't report a world that wasn't found correctly
|
||||
Jumping on turtle eggs is considered as interacting with pressure plates
|
||||
Setbiome internal error when progress reporting is attached
|
||||
Unclaim doesn't clear the plot. It probably should.
|
||||
Players can shoot boats and minecarts.
|
||||
You can use disabled items by rightclicking air.
|
||||
Tab complete isn't working correctly.
|
||||
~~Bed use in nether and end might not have to be blocked.~~
|
||||
|
||||
|
||||
Reference in New Issue
Block a user