Archived
0

Tweak some command stuff, clear/swap entities

This commit is contained in:
Dico Karssiens
2018-11-17 21:32:43 +00:00
parent 0f196f59c6
commit 5ef2584fdb
25 changed files with 3953 additions and 3554 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
}
}*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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())
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
package io.dico.parcels2.util
fun doParallel() {
val array = IntArray(1000)
IntRange(0, 1000).chunked()
}

206
todo.md
View File

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