From 42789e012bf5a0c165781ac395aa1638fb3ed9f4 Mon Sep 17 00:00:00 2001 From: Gabriel Tofvesson Date: Tue, 6 Jul 2021 21:04:43 +0200 Subject: [PATCH] Initial commit --- .gitignore | 3 + .idea/.gitignore | 8 + .idea/compiler.xml | 19 ++ .idea/gradle.xml | 20 ++ .idea/jarRepositories.xml | 75 ++++++ .idea/kotlinc.xml | 6 + .idea/libraries-with-intellij-classes.xml | 65 ++++++ .idea/libraries/KotlinJavaRuntime.xml | 15 ++ .idea/libraries/SpigotWizCompat.xml | 9 + .idea/misc.xml | 10 + .idea/uiDesigner.xml | 124 ++++++++++ README.md | 139 +++++++++++ build.gradle | 195 ++++++++++++++++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 185 +++++++++++++++ gradlew.bat | 89 +++++++ .../chunkprotector/ChunkProtectorPlugin.kt | 86 +++++++ .../spigot/chunkprotector/claim/Claim.kt | 217 ++++++++++++++++++ .../spigot/chunkprotector/claim/ClaimChunk.kt | 27 +++ .../chunkprotector/claim/ClaimManager.kt | 132 +++++++++++ .../chunkprotector/collection/BinaryCache.kt | 112 +++++++++ .../chunkprotector/collection/BinaryList.kt | 82 +++++++ .../chunkprotector/command/ClaimCommand.kt | 32 +++ .../command/ClaimOptionCommand.kt | 61 +++++ .../command/ClaimOwnerCommand.kt | 24 ++ .../chunkprotector/command/InviteCommand.kt | 38 +++ .../command/ListClaimsCommand.kt | 34 +++ .../command/ShowClaimCommand.kt | 79 +++++++ .../command/TabCompletionCommandExecutor.kt | 8 + .../chunkprotector/command/UnClaimCommand.kt | 29 +++ .../chunkprotector/command/UnInviteCommand.kt | 38 +++ .../chunkprotector/freecam/FreeCamManager.kt | 101 ++++++++ .../chunkprotector/freecam/FreeCammer.kt | 20 ++ .../spigot/chunkprotector/kotlin/Assert.kt | 15 ++ .../listener/PlayerActionListener.kt | 143 ++++++++++++ .../listener/TabCompleteListener.kt | 34 +++ 37 files changed, 2279 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/compiler.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/kotlinc.xml create mode 100644 .idea/libraries-with-intellij-classes.xml create mode 100644 .idea/libraries/KotlinJavaRuntime.xml create mode 100644 .idea/libraries/SpigotWizCompat.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/uiDesigner.xml create mode 100644 README.md create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 src/main/java/dev/w1zzrd/spigot/chunkprotector/ChunkProtectorPlugin.kt create mode 100644 src/main/java/dev/w1zzrd/spigot/chunkprotector/claim/Claim.kt create mode 100644 src/main/java/dev/w1zzrd/spigot/chunkprotector/claim/ClaimChunk.kt create mode 100644 src/main/java/dev/w1zzrd/spigot/chunkprotector/claim/ClaimManager.kt create mode 100644 src/main/java/dev/w1zzrd/spigot/chunkprotector/collection/BinaryCache.kt create mode 100644 src/main/java/dev/w1zzrd/spigot/chunkprotector/collection/BinaryList.kt create mode 100644 src/main/java/dev/w1zzrd/spigot/chunkprotector/command/ClaimCommand.kt create mode 100644 src/main/java/dev/w1zzrd/spigot/chunkprotector/command/ClaimOptionCommand.kt create mode 100644 src/main/java/dev/w1zzrd/spigot/chunkprotector/command/ClaimOwnerCommand.kt create mode 100644 src/main/java/dev/w1zzrd/spigot/chunkprotector/command/InviteCommand.kt create mode 100644 src/main/java/dev/w1zzrd/spigot/chunkprotector/command/ListClaimsCommand.kt create mode 100644 src/main/java/dev/w1zzrd/spigot/chunkprotector/command/ShowClaimCommand.kt create mode 100644 src/main/java/dev/w1zzrd/spigot/chunkprotector/command/TabCompletionCommandExecutor.kt create mode 100644 src/main/java/dev/w1zzrd/spigot/chunkprotector/command/UnClaimCommand.kt create mode 100644 src/main/java/dev/w1zzrd/spigot/chunkprotector/command/UnInviteCommand.kt create mode 100644 src/main/java/dev/w1zzrd/spigot/chunkprotector/freecam/FreeCamManager.kt create mode 100644 src/main/java/dev/w1zzrd/spigot/chunkprotector/freecam/FreeCammer.kt create mode 100644 src/main/java/dev/w1zzrd/spigot/chunkprotector/kotlin/Assert.kt create mode 100644 src/main/java/dev/w1zzrd/spigot/chunkprotector/listener/PlayerActionListener.kt create mode 100644 src/main/java/dev/w1zzrd/spigot/chunkprotector/listener/TabCompleteListener.kt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b740e72 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Project exclude paths +/.gradle/ +/build/ \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..9cf288a --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..ce3a0dc --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..dd0babe --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..9110b64 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/libraries-with-intellij-classes.xml b/.idea/libraries-with-intellij-classes.xml new file mode 100644 index 0000000..9fa3156 --- /dev/null +++ b/.idea/libraries-with-intellij-classes.xml @@ -0,0 +1,65 @@ + + + + + + \ No newline at end of file diff --git a/.idea/libraries/KotlinJavaRuntime.xml b/.idea/libraries/KotlinJavaRuntime.xml new file mode 100644 index 0000000..9fbfb0d --- /dev/null +++ b/.idea/libraries/KotlinJavaRuntime.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/SpigotWizCompat.xml b/.idea/libraries/SpigotWizCompat.xml new file mode 100644 index 0000000..6418489 --- /dev/null +++ b/.idea/libraries/SpigotWizCompat.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..6680db9 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..e96534f --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..052159c --- /dev/null +++ b/README.md @@ -0,0 +1,139 @@ +# Spigot Chunk Protector +by Gabriel Tofvesson (IKEA_Jesus) + +## Index + +* [Permissions](#Permissions) + +* [Commands](#Commands) + + * [claim](#claim) + + * [unclaim](#unclaim) + + * [claims](#claims) + + * [invite](#invite) + + * [uninvite](#uninvite) + + * [showclaim](#showclaim) + + * [claimowner](#claimowner) + + * [claimoption](#claimoption) + + * [Options](#Options) + +## Permissions + +|Permission Node|Description|Applies to (by default)| +|:---|:---|:---:| +|`chunkprotector.claim`|Allows access to [`/claim`](#claim)|All players| +|`chunkprotector.unclaim`|Allows access to [`/unclaim`](#unclaim)|All players| +|`chunkprotector.invite`|Allows access to [`/invite`](#invite)|All players| +|`chunkprotector.uninvite`|Allows access to [`/uninvite`](#uninvite)|All players| +|`chunkprotector.listclaims`|Allows access to [`/claims`](#claims)|All players| +|`chunkprotector.claimowner`|Allows access to [`/claimowner`](#claimowner)|All players| +|`chunkprotector.showclaim`|Allows access to [`/showclaim`](#showclaim)|All players| +|`chunkprotector.claimoption`|Allows access to [`/claimoption`](#claimoption)|All players| +|`chunkprotector.bypass`|Allows for modification of otherp player's claims|Operators| +|`chunkprotector.ignore`|Allows bypassing of all physical restrictions|Operators| + +## Commands + +### claim + +*Claim an area and give it a specified name* + +| Action |Command| +|:--- | :---: | +|Select area|`/claim [name]`| +|Cancel selection|`/claim`| + + +### unclaim + +*Remove a claim with a given name for a specific player* + +|Who|Command| +|:--- | :---:| +|All players|`/unclaim [claim]`| +|Bypass permission + Console|`/unclaim [claim] [player]`| + + +### claims + +*List all claimed areas for a player* + +|Who|Command| +|:--- | :---:| +|All players|`/claims`| +|Bypass permission + Console|`/claims [player]`| + + +### invite + +*Invite a player to a claim* + +|Who|Command|Context| +|:--- | :---:|:---| +|All players|`/invite [player]`|Invite player to claim that owner is standing in| +|All players|`/invite [player] [claim]`|Invite player to given claim| +|Bypass permission + Console|`/invite [player] [claim] [claimOwner]`|Invite player to another players claim| + + +### uninvite + +*Un-invite a player to a claim* + +|Who|Command|Context| +|:--- | :---:|:---| +|All players|`/uninvite [player]`|Un-invite player to claim that owner is standing in| +|All players|`/uninvite [player] [claim]`|Un-invite player to given claim| +|Bypass permission + Console|`/uninvite [player] [claim] [claimOwner]`|Un-invite player to another players claim| + + +### showclaim + +*Show the boundaries of a claimed area* + +|Action|Command| +|:--- | :---:| +|Current claim|`/claims`| +|Specific claim|`/claims [claim]`| +|Specific claim|`/claims [claim] [player]`| + + +### claimowner + +*Get the name and owner of the claim the command sender is standing in* + +|Where|Command| +|:--- | :---:| +|Current claim|`/claimowner`| + + +### claimoption + +*Set a configuration option for a given claim. Values will always be either `true` or `false`* + +|Who|Command|Context| +|:--- | :---:|:---| +|All players|`/claimoption [option] [value]`|Set option for the claim the command sender is standing in| +|All players|`/claimoption [claim] [option] [value]`|Set option for the given claim| +|Bypass permission + Console|`/claimoption [player] [claim] [option] [value]`|Set option for the claim of the given player| + + +### Options + +|Option name|Description| +|:---|:---| +|`allowAllLiquids`|Allow all liquids to enter a claimed area| +|`allowEntityInteract`|Allow entities to interact with the environment (e.g. creepers break blocks, mobs can hurt each other). When this is false, mobs will act as if players that are not invited to a claim do not exist| +|`allowGuestLiquids`|Allow liquids to enter claimed area if they originate from a claim owned by an invited player| +|`allowPlayerEntityInteract`|Allow all players (invited or not) to interact with non-hostile mobs in a claim| +|`allowTNT`|Allow TNT explosions to break blocks| +|`disablePVP`|Disable player-versus-player interactions| + +**NOTE:** Regarding `allowPlayerEntityInteract`, players can always interact with hostile mobs, so long as said mobs do not have a custom name. \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..e422ae8 --- /dev/null +++ b/build.gradle @@ -0,0 +1,195 @@ +plugins { + id 'java' + id 'kr.entree.spigradle' version '2.2.4' + id 'org.jetbrains.kotlin.jvm' version '1.5.20' +} + +group 'dev.w1zzrd.spigot.chunkprotector' +version '1.0-SNAPSHOT' + +repositories { + maven { + url = 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' + + // As of Gradle 5.1, you can limit this to only those + // dependencies you expect from it + content { + includeGroup 'org.bukkit' + includeGroup 'org.spigotmc' + } + } + /* + As Spigot-API depends on the Bungeecord ChatComponent-API, + we need to add the Sonatype OSS repository, as Gradle, + in comparison to maven, doesn't want to understand the ~/.m2 + directory unless added using mavenLocal(). Maven usually just gets + it from there, as most people have run the BuildTools at least once. + This is therefore not needed if you're using the full Spigot/CraftBukkit, + or if you're using the Bukkit API. + */ + maven { + url = "https://papermc.io/repo/repository/maven-public" + + content { + includeGroup 'io.papermc.paper' + } + } + maven { url = 'https://oss.sonatype.org/content/repositories/snapshots' } + maven { url = 'https://oss.sonatype.org/content/repositories/central' } + mavenLocal() // This is needed for CraftBukkit and Spigot. + + mavenCentral() + protocolLib() + jitpack() // For vault +} + +dependencies { + // Pick only one of these and read the comment in the repositories block. + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" // The Spigot API with no shadowing. Requires the OSS repo. + implementation files('lib/SpigotWizCompat.jar') + + compileOnly spigot('1.17') // Or spigot() + //compileOnly protocolLib() + //compileOnly vault() +} + +spigot { + authors = ['IKEAJesus'] + depends = ['WizCompat', 'Kotlin'] + apiVersion = '1.16' + load = STARTUP + + commands { + claim { + description = 'Start claiming land' + permission = 'chunkprotector.claim' + permissionMessage = 'You are not allowed to claim land!' + usage = '/ [claim]' + } + + unclaim { + description = 'Un-claim a claimed region' + permission = 'chunkprotector.unclaim' + permissionMessage = 'You are not allowed to unclaim land!' + usage = '/ [claim]' + } + + invite { + description = 'Invite a player to a claimed area' + permission = 'chunkprotector.invite' + permissionMessage = 'You are not allowed to invite players!' + usage = '/ [player] [claim]' + } + + uninvite { + description = 'Un-invite a player from a claimed area' + permission = 'chunkprotector.uninvite' + permissionMessage = 'You are not allowed to un-invite players!' + usage = '/ [player] [claim]' + } + + claims { + description = 'Get a list of all owned claims' + permission = 'chunkprotector.listclaims' + permissionMessage = 'You are not allowed get a list of your claims!' + usage = '/' + } + + claimowner { + description = 'Get the owner of the current chunk' + permission = 'chunkprotector.claimowner' + permissionMessage = 'You are not allowed to check who owns this chunk!' + usage = '/' + } + + showclaim { + description = 'Show the outline of a claimed region' + permission = 'chunkprotector.showclaim' + permissionMessage = 'You are not allowed to see the claimed region!' + usage = '/ {claim}' + } + + claimoption { + description = 'Change properties of a claimed area' + permission = 'chunkprotector.claimoption' + permissionMessage = 'You are not allowed to change the properties of a claimed area!' + usage = '/ {claim} [claimOption] [true/false]' + } + } + + permissions { + 'chunkprotector.claim' { + description = 'Allows access to /claim' + defaults = 'true' + } + 'chunkprotector.unclaim' { + description = 'Allows access to /unclaim' + defaults = 'true' + } + 'chunkprotector.invite' { + description = 'Allows access to /invite' + defaults = 'true' + } + 'chunkprotector.uninvite' { + description = 'Allows access to /uninvite' + defaults = 'true' + } + 'chunkprotector.listclaims' { + description = 'Allows access to /claims' + defaults = 'true' + } + 'chunkprotector.bypass' { + description = 'Allows bypassing of all command-related claim restrictions' + defaults = 'op' + } + 'chunkprotector.ignore' { + description = 'Allows bypassing of all physical claim restrictions' + defaults = 'op' + } + 'chunkprotector.claimowner' { + description = 'Allows access to /claimowner' + defaults = 'true' + } + 'chunkprotector.showclaim' { + description = 'Allows access to /showclaim' + defaults = 'true' + } + 'chunkprotector.claimoption' { + description = 'Allows access to /claimoption' + defaults = 'true' + } + } + + /* + commands { + give { + aliases = ['i'] + description = 'Give command.' + permission = 'test.foo' + permissionMessage = 'You do not have permission!' + usage = '/ [test|stop]' + } + } + permissions { + 'test.foo' { + description = 'Allows foo command' + defaults = 'true' + } + 'test.*' { + description = 'Wildcard permission' + defaults = 'op' + children = ['test.foo': true] + } + } + */ +} +compileKotlin { + kotlinOptions { + jvmTarget = "16" + } +} +compileTestKotlin { + kotlinOptions { + jvmTarget = "16" + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..69a9715 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/dev/w1zzrd/spigot/chunkprotector/ChunkProtectorPlugin.kt b/src/main/java/dev/w1zzrd/spigot/chunkprotector/ChunkProtectorPlugin.kt new file mode 100644 index 0000000..011a0b1 --- /dev/null +++ b/src/main/java/dev/w1zzrd/spigot/chunkprotector/ChunkProtectorPlugin.kt @@ -0,0 +1,86 @@ +package dev.w1zzrd.spigot.chunkprotector + +import dev.w1zzrd.spigot.chunkprotector.claim.Claim +import dev.w1zzrd.spigot.chunkprotector.claim.ClaimChunk +import dev.w1zzrd.spigot.chunkprotector.claim.ClaimManager +import dev.w1zzrd.spigot.chunkprotector.collection.SerializableBinaryList +import dev.w1zzrd.spigot.chunkprotector.command.* +import dev.w1zzrd.spigot.chunkprotector.freecam.FreeCamManager +import dev.w1zzrd.spigot.chunkprotector.listener.PlayerActionListener +import dev.w1zzrd.spigot.chunkprotector.listener.TabCompleteListener +import dev.w1zzrd.spigot.wizcompat.serialization.PersistentData +import dev.w1zzrd.spigot.wizcompat.serialization.UUIDList +import kr.entree.spigradle.annotations.SpigotPlugin +import org.bukkit.configuration.serialization.ConfigurationSerialization +import org.bukkit.event.HandlerList +import org.bukkit.plugin.java.JavaPlugin + +@SpigotPlugin +class ChunkProtectorPlugin: JavaPlugin() { + private val freeCamManager = FreeCamManager() + + private val tabCompleteListener = TabCompleteListener() + + // Properties that should be initialized only after serializers are enabled + private val persistentData: PersistentData by lazy { PersistentData("data", this) } + private val claimManager: ClaimManager by lazy { ClaimManager(freeCamManager, persistentData) } + private val actionListener: PlayerActionListener by lazy { + PlayerActionListener(claimManager) + } + + override fun onEnable() { + super.onEnable() + enableSerializers() + freeCamManager.onEnable(this) + claimManager.onEnable(this) + enableListeners() + enableCommands() + } + + private fun enableSerializers() { + ConfigurationSerialization.registerClass(UUIDList::class.java) + ConfigurationSerialization.registerClass(SerializableBinaryList::class.java) + ConfigurationSerialization.registerClass(ClaimChunk::class.java) + ConfigurationSerialization.registerClass(Claim::class.java) + } + + private fun enableListeners() { + server.pluginManager.registerEvents(tabCompleteListener, this) + server.pluginManager.registerEvents(actionListener, this) + } + + private fun enableCommands() { + getCommand("claim")!!.setExecutor(ClaimCommand(claimManager)) + getCommand("unclaim")!!.setExecutor(UnClaimCommand(claimManager)) + getCommand("invite")!!.setExecutor(InviteCommand(claimManager)) + getCommand("uninvite")!!.setExecutor(UnInviteCommand(claimManager)) + getCommand("claims")!!.setExecutor(ListClaimsCommand(claimManager)) + getCommand("claimowner")!!.setExecutor(ClaimOwnerCommand(claimManager)) + getCommand("showclaim")!!.setExecutor(ShowClaimCommand(claimManager, this)) + getCommand("claimoption")!!.setExecutor( + ClaimOptionCommand(claimManager).also { tabCompleteListener.registerTabCompleter("claimoption", it.completionProcessor) } + ) + } + + override fun onDisable() { + disableListeners() + claimManager.onDisable() + freeCamManager.onDisable() + persistentData.saveData() + disableSerializers() + super.onDisable() + } + + private fun disableSerializers() { + ConfigurationSerialization.unregisterClass(Claim::class.java) + ConfigurationSerialization.unregisterClass(ClaimChunk::class.java) + ConfigurationSerialization.unregisterClass(SerializableBinaryList::class.java) + ConfigurationSerialization.unregisterClass(UUIDList::class.java) + } + + private fun disableListeners() { + HandlerList.unregisterAll(actionListener) + tabCompleteListener.unRegisterAll() + HandlerList.unregisterAll(tabCompleteListener) + } +} \ No newline at end of file diff --git a/src/main/java/dev/w1zzrd/spigot/chunkprotector/claim/Claim.kt b/src/main/java/dev/w1zzrd/spigot/chunkprotector/claim/Claim.kt new file mode 100644 index 0000000..0a17ad6 --- /dev/null +++ b/src/main/java/dev/w1zzrd/spigot/chunkprotector/claim/Claim.kt @@ -0,0 +1,217 @@ +package dev.w1zzrd.spigot.chunkprotector.claim + +import dev.w1zzrd.spigot.chunkprotector.collection.BinaryList +import dev.w1zzrd.spigot.wizcompat.packet.EntityCreator.* +import dev.w1zzrd.spigot.wizcompat.serialization.SimpleReflectiveConfigItem +import dev.w1zzrd.spigot.wizcompat.serialization.UUIDList +import org.bukkit.Chunk +import org.bukkit.Location +import org.bukkit.OfflinePlayer +import org.bukkit.World +import org.bukkit.entity.Player +import java.lang.Integer.max +import java.lang.Integer.min +import java.util.* + +class ClaimBuilder(val who: Player, val world: World, val name: String): Comparable { + private val first = CornerSelection() + private val second = CornerSelection() + + val isValidRegion: Boolean + get() = second.corner != null + + val built: Claim + get() = Claim(who.uniqueId, world.uid, name, ClaimChunk(first.corner!!.x, first.corner!!.z), ClaimChunk(second.corner!!.x, second.corner!!.z)) + + fun toggleCorner(corner: Chunk) { + when (corner) { + first.corner -> first.corner = null + second.corner -> second.corner = null + else -> { + if ((second.corner != null && first.corner != null) || first.corner == null) { + second.corner = null + first.corner = corner + } else { + second.corner = corner + } + } + } + + first.render() + second.render() + } + + fun clearRenders() { + first.clearRender() + second.clearRender() + } + + override fun compareTo(other: ClaimBuilder) = who.uniqueId.compareTo(other.who.uniqueId) + override fun equals(other: Any?) = other is ClaimBuilder && hashCode() == other.hashCode() + override fun hashCode() = who.hashCode() + + private inner class CornerSelection { + private var render = -1 + private var location: Location? = null + + var isDirty: Boolean = true + private set + var corner: Chunk? = null + set(value) { + isDirty = isDirty || (field != value) + field = value + + adjustHeight() + } + + fun adjustHeight() { + val currentCorner = corner + + if (currentCorner != null) { + location = Location( + who.world, + (currentCorner.x shl 4) + 8.5, + kotlin.math.max(who.location.y - 3.0, 0.0), + (currentCorner.z shl 4) + 8.5, + 0f, + 0f + ) + } + } + + fun render() { + if (isDirty) { + clearRender() + + if (corner != null) { + val renderShulker = createFakeShulker(who) + setEntityInvisible(renderShulker, true) + setEntityInvulnerable(renderShulker, true) + setEntityLocation( + renderShulker, + location!!.x, + location!!.y, + location!!.z, + location!!.yaw, + location!!.pitch + ) + setEntityCollision(renderShulker, false) + setEntityGlowing(renderShulker, true) + + sendEntitySpawnPacket(who, renderShulker) + sendEntityMetadataPacket(who, renderShulker) + + render = getEntityID(renderShulker) + } + + isDirty = false + } + } + + fun clearRender() { + if (render != -1) { + sendEntityDespawnPacket(who, render) + render = -1 + } + } + } +} + +class Claim: SimpleReflectiveConfigItem, Comparable { + @Transient + var owner: UUID + private set + private lateinit var ownerString: String + + @Transient + var world: UUID + private set + private lateinit var worldString: String + + lateinit var name: String + private set + + lateinit var topLeft: ClaimChunk + private set + + lateinit var bottomRight: ClaimChunk + private set + + private lateinit var guests: UUIDList + + + // Per-claim settings + var allowPlayerEntityInteract = false + var allowTNT = false + var allowEntityInteract = false + var allowAllLiquids = false + var allowGuestLiquids = true + var disablePVP = false + + constructor(map: Map): super(map) { + owner = UUID.fromString(ownerString) + world = UUID.fromString(worldString) + } + constructor(owner: UUID, world: UUID, name: String, corner1: ClaimChunk, corner2: ClaimChunk): super(Collections.emptyMap()) { + this.owner = owner + ownerString = owner.toString() + this.world = world + worldString = world.toString() + this.name = name + this.topLeft = ClaimChunk(min(corner1.chunkX, corner2.chunkX), max(corner1.chunkZ, corner2.chunkZ)) + this.bottomRight = ClaimChunk(max(corner1.chunkX, corner2.chunkX), min(corner1.chunkZ, corner2.chunkZ)) + guests = UUIDList(BinaryList.newBinaryList()) + } + + fun addGuest(player: OfflinePlayer) = guests.uuids.add(player.uniqueId) + fun removeGuest(player: OfflinePlayer) = guests.uuids.remove(player.uniqueId) + + fun hasGuest(player: OfflinePlayer) = guests.uuids.contains(player.uniqueId) + + fun isAccessible(player: OfflinePlayer) = owner == player.uniqueId || guests.uuids.contains(player.uniqueId) + + fun overlaps(other: Claim) = + !(topLeft.chunkX > other.bottomRight.chunkX || other.topLeft.chunkX > bottomRight.chunkZ) && + !(bottomRight.chunkZ > other.topLeft.chunkZ || other.bottomRight.chunkZ > topLeft.chunkZ) + + fun contains(chunk: Chunk) = contains(chunk.x, chunk.z) + fun contains(chunkX: Int, chunkZ: Int) = + !(topLeft.chunkX > chunkX || chunkX > bottomRight.chunkX) && + !(bottomRight.chunkZ > chunkZ || chunkZ > topLeft.chunkZ) + + override fun compareTo(other: Claim) = compareRaw(other.owner, other.name) + + fun compareRaw(owner: UUID, name: String): Int { + val comp1 = this.owner.compareTo(owner) + return if (comp1 == 0) this.name.compareTo(name) else comp1 + } + + fun compareFindFirst(owner: UUID): Int { + val comp1 = this.owner.compareTo(owner) + return if (comp1 == 0) 1 else comp1 + } + + fun compareFindLast(owner: UUID): Int { + val comp1 = this.owner.compareTo(owner) + return if (comp1 == 0) -1 else comp1 + } +} + +fun BinaryList.getByName(owner: UUID, name: String): Claim? { + val index = binarySearch { it.compareRaw(owner, name) } + + if (index >= 0) + return this[index] + + return null +} + +fun BinaryList.getAllForOwner(owner: UUID): List { + val startIndex = -(binarySearch { it.compareFindFirst(owner) } + 1) + val endIndex = -(binarySearch { it.compareFindLast(owner) } + 1) + + if (startIndex >= endIndex) + return emptyList() + + return subList(startIndex, endIndex) +} \ No newline at end of file diff --git a/src/main/java/dev/w1zzrd/spigot/chunkprotector/claim/ClaimChunk.kt b/src/main/java/dev/w1zzrd/spigot/chunkprotector/claim/ClaimChunk.kt new file mode 100644 index 0000000..4707fc1 --- /dev/null +++ b/src/main/java/dev/w1zzrd/spigot/chunkprotector/claim/ClaimChunk.kt @@ -0,0 +1,27 @@ +package dev.w1zzrd.spigot.chunkprotector.claim + +import dev.w1zzrd.spigot.wizcompat.serialization.SimpleReflectiveConfigItem +import java.util.* + +class ClaimChunk: SimpleReflectiveConfigItem, Comparable { + var chunkX: Int = 0 + private set + + var chunkZ: Int = 0 + private set + + constructor(map: Map): super(map) + + constructor(chunkX: Int, chunkZ: Int): super(Collections.emptyMap()) { + this.chunkX = chunkX + this.chunkZ = chunkZ + } + + private val longCoordinate: Long + get() = chunkX.toLong().shl(32) or chunkZ.toLong().and(0xFFFFFFFFL) + + override fun compareTo(other: ClaimChunk) = + longCoordinate.compareTo(other.longCoordinate) + + override fun equals(other: Any?) = other is ClaimChunk && compareTo(other) == 0 +} \ No newline at end of file diff --git a/src/main/java/dev/w1zzrd/spigot/chunkprotector/claim/ClaimManager.kt b/src/main/java/dev/w1zzrd/spigot/chunkprotector/claim/ClaimManager.kt new file mode 100644 index 0000000..807dbfa --- /dev/null +++ b/src/main/java/dev/w1zzrd/spigot/chunkprotector/claim/ClaimManager.kt @@ -0,0 +1,132 @@ +package dev.w1zzrd.spigot.chunkprotector.claim + +import dev.w1zzrd.spigot.chunkprotector.collection.BinaryCache +import dev.w1zzrd.spigot.chunkprotector.collection.BinaryList.Companion.newBinaryList +import dev.w1zzrd.spigot.chunkprotector.collection.SerializableBinaryList +import dev.w1zzrd.spigot.chunkprotector.freecam.FreeCamManager +import dev.w1zzrd.spigot.wizcompat.command.CommandUtils +import dev.w1zzrd.spigot.wizcompat.command.CommandUtils.assertTrue +import dev.w1zzrd.spigot.wizcompat.command.CommandUtils.errorMessage +import dev.w1zzrd.spigot.wizcompat.serialization.PersistentData +import org.bukkit.Chunk +import org.bukkit.OfflinePlayer +import org.bukkit.World +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.HandlerList +import org.bukkit.event.Listener +import org.bukkit.event.block.Action +import org.bukkit.event.player.PlayerChangedWorldEvent +import org.bukkit.event.player.PlayerInteractEvent +import org.bukkit.plugin.Plugin + +private val Chunk.longChunkCoordinate: Long + get() = (x.toLong() shl 32) or (z.toLong() and 0xFFFFFFFFL) + +private fun Chunk.compareTo(other: Chunk): Int { + val comp1 = world.uid.compareTo(other.world.uid) + return if (comp1 == 0) longChunkCoordinate.compareTo(other.longChunkCoordinate) else comp1 +} + +private data class ClaimNameCacheEntry(val player: OfflinePlayer, val name: String): Comparable { + override fun compareTo(other: ClaimNameCacheEntry): Int { + val comp1 = player.uniqueId.compareTo(player.uniqueId) + return if (comp1 == 0) name.compareTo(other.name) else comp1 + } +} + +class ClaimManager(private val freeCamManager: FreeCamManager, persistentData: PersistentData) { + private val claimSelectListener = ClaimSelectListener() + private val claimBuilders = newBinaryList() + private val claims: SerializableBinaryList = persistentData.loadData("claims") { SerializableBinaryList(newBinaryList()) } + private val chunkPermCache = BinaryCache.makeCache(512, Chunk::compareTo) { chunk -> + claims.list.find { it.world == chunk.world.uid && it.contains(chunk) } + } + private val namePermCache = BinaryCache.makeCache(64, ClaimNameCacheEntry::compareTo) { + claims.list.getByName(it.player.uniqueId, it.name) + } + + init { + freeCamManager.addOnPlayerExitFreeCam { player -> + val builderIndex = claimBuilders.binarySearch { it.who.uniqueId.compareTo(player.uniqueId) } + if (builderIndex >= 0) { + val builder = claimBuilders.removeAt(builderIndex) + builder.clearRenders() + } + } + } + + fun onEnable(plugin: Plugin) = plugin.server.pluginManager.registerEvents(claimSelectListener, plugin) + fun onDisable() = HandlerList.unregisterAll(claimSelectListener) + + fun toggleClaim(player: Player, name: String) = + if (isClaiming(player)) { + freeCamManager.disableFreeCam(player) + false + } else { + freeCamManager.enableFreeCam(player) + claimBuilders.add(ClaimBuilder(player, player.world, name)) + true + } + + fun isClaiming(player: Player) = claimBuilders.contains(player.uniqueId) { who.uniqueId } + + fun addClaim(claim: Claim): Boolean { + if (claims.list.any { it.overlaps(claim) }) + return false + + claims.list.add(claim) + return true + } + + fun removeClaim(claim: Claim): Boolean { + chunkPermCache.clearValues(claim) + namePermCache.clearValues(claim) + return claims.list.remove(claim) + } + + fun invitePlayer(owner: Player, name: String, invitedPlayer: Player) = + claims.list.getByName(owner.uniqueId, name)?.addGuest(invitedPlayer) ?: false + + fun unInvitePlayer(owner: Player, name: String, unInvitedPlayer: Player) = + claims.list.getByName(owner.uniqueId, name)?.removeGuest(unInvitedPlayer) ?: false + + // This is slow, because a sequential search must be done + fun getClaimAt(chunk: Chunk) = chunkPermCache[chunk] + fun getClaimByName(owner: OfflinePlayer, name: String) = namePermCache[ClaimNameCacheEntry(owner, name)] + fun getClaimsForOwner(owner: OfflinePlayer) = claims.list.getAllForOwner(owner.uniqueId) + + private inner class ClaimSelectListener: Listener { + @EventHandler + fun onFreeCamHit(event: PlayerInteractEvent) { + if ((event.action == Action.LEFT_CLICK_AIR || event.action == Action.LEFT_CLICK_BLOCK) && isClaiming(event.player)) { + val claimBuilder = claimBuilders.find { it.who == event.player }!! + + claimBuilder.toggleCorner(event.player.location.chunk) + + if (claimBuilder.isValidRegion) { + event.player.spigot() + .sendMessage(CommandUtils.successMessage("Region selected! Right-click a block to confirm selections")) + } + } else if (event.action == Action.RIGHT_CLICK_BLOCK && isClaiming(event.player)) { + val claimBuilder = claimBuilders.find { it.who == event.player }!! + + if (assertTrue(claimBuilder.isValidRegion, "You have not selected a region", event.player) || + assertTrue(addClaim(claimBuilder.built), "This claim overlaps with another claim", event.player)) + return + + freeCamManager.disableFreeCam(event.player) + + event.player.spigot().sendMessage(CommandUtils.successMessage("Region claimed!")) + } + } + + @EventHandler + fun onPlayerWorldChange(event: PlayerChangedWorldEvent) { + if (isClaiming(event.player)) { + freeCamManager.disableFreeCam(event.player) + event.player.spigot().sendMessage(errorMessage("Cancelled claiming!")) + } + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/w1zzrd/spigot/chunkprotector/collection/BinaryCache.kt b/src/main/java/dev/w1zzrd/spigot/chunkprotector/collection/BinaryCache.kt new file mode 100644 index 0000000..9591cea --- /dev/null +++ b/src/main/java/dev/w1zzrd/spigot/chunkprotector/collection/BinaryCache.kt @@ -0,0 +1,112 @@ +package dev.w1zzrd.spigot.chunkprotector.collection + +import java.util.* +import kotlin.Comparator + +class BinaryCache (cacheSize: Int, private val comparator: Comparator, private val cacheMiss: (K) -> V?, private val keyType: Class) { + + companion object { + inline fun makeCache(cacheSize: Int, comparator: Comparator, noinline cacheMiss: (K) -> V?) = + BinaryCache(cacheSize, comparator, cacheMiss, K::class.java) + + inline fun , V> makeCache(cacheSize: Int, noinline cacheMiss: (K) -> V?) = + BinaryCache(cacheSize, Comparable::compareTo, cacheMiss, K::class.java) + } + + private val keys = java.lang.reflect.Array.newInstance(keyType, cacheSize) as Array + private val values = Array(cacheSize) { null } + private val ages = java.lang.reflect.Array.newInstance(keyType, cacheSize) as Array // Essentially a fifo queue + + private var entryCount = 0 + private var oldest = 0 + + fun clearValues(value: V) { + val scratch1 = java.lang.reflect.Array.newInstance(keyType, ages.size) as Array + + // Since ages softly depend on key/value entries, it's easiest to process them first + var copyIndex = 0 + for (index in oldest until entryCount) + if (values[indexOf(ages[index])] != value) + scratch1[copyIndex++] = ages[index] + + if (entryCount == ages.size) { + for (index in 0 until oldest) + if (values[indexOf(ages[index])] != value) + scratch1[copyIndex++] = ages[index] + } + + // No change + if (copyIndex == entryCount) + return + + // Just re-index the queue so that the oldest entry lies at index 0 + System.arraycopy(scratch1, 0, ages, 0, copyIndex) + oldest = 0 + + copyIndex = 0 + + val scratch2 = Array(values.size){ null } + + for (index in 0 until entryCount) + if (values[index] != value) { + scratch1[copyIndex] = keys[index] + scratch2[copyIndex++] = values[index] + } + + System.arraycopy(scratch1, 0, keys, 0, copyIndex) + System.arraycopy(scratch2, 0, values, 0, copyIndex) + + entryCount -= copyIndex + } + + operator fun get(key: K): V? { + var index = indexOf(key) + + // Cache hit + if (index >= 0) + return values[index] as V + + // Cache miss + index = -(index + 1) + + val value = cacheMiss(key) ?: return null + + if (entryCount < keys.size) { + System.arraycopy(keys, index, keys, index + 1, entryCount - index) + System.arraycopy(values, index, values, index + 1, entryCount - index) + + ages[(oldest + entryCount).rem(ages.size)] = key + + ++entryCount + } else { + // We're out of spaces. This works + if (index > 0) + --index + + // Find oldest entry + val oldestIndex = indexOf(ages[oldest]) + + if (oldestIndex > index) { + System.arraycopy(keys, index, keys, index + 1, oldestIndex - index) + System.arraycopy(values, index, values, index + 1, oldestIndex - index) + } else if (oldestIndex < index) { + System.arraycopy(keys, oldestIndex + 1, keys, oldestIndex, index - oldestIndex) + System.arraycopy(values, oldestIndex + 1, values, oldestIndex, index - oldestIndex) + } + + // Overwrite oldest entry with new entry + ages[oldest] = key + + // Re-index age list so that current oldest entry becomes youngest + oldest = (oldest + 1).rem(ages.size) + } + + keys[index] = key + values[index] = value + + return value + } + + private fun indexOf(key: K) = + keys.binarySearch(key, comparator, 0, entryCount) +} \ No newline at end of file diff --git a/src/main/java/dev/w1zzrd/spigot/chunkprotector/collection/BinaryList.kt b/src/main/java/dev/w1zzrd/spigot/chunkprotector/collection/BinaryList.kt new file mode 100644 index 0000000..2e19b5e --- /dev/null +++ b/src/main/java/dev/w1zzrd/spigot/chunkprotector/collection/BinaryList.kt @@ -0,0 +1,82 @@ +package dev.w1zzrd.spigot.chunkprotector.collection + +import dev.w1zzrd.spigot.chunkprotector.collection.BinaryList.Companion.wrapSortedList +import org.bukkit.configuration.serialization.ConfigurationSerializable + +class SerializableBinaryList: ConfigurationSerializable { + lateinit var list: BinaryList + private set + + constructor(map: Map) { + if (map.containsKey("list")) + list = wrapSortedList({ a, b -> (a as Comparable).compareTo(b) }, map["list"] as MutableList) + } + + constructor(list: BinaryList) { + this.list = list + } + + override fun serialize() = mutableMapOf("list" to list.toMutableList()) +} + +class BinaryList private constructor(private val backing: MutableList, private val comparator: Comparator): MutableList by backing { + + companion object { + fun newBinaryList(comparator: Comparator, backingFactory: () -> MutableList = ::ArrayList) = + BinaryList(backingFactory(), comparator) + + fun > newBinaryList(backingFactory: () -> MutableList = ::ArrayList) = + newBinaryList({ a, b -> a.compareTo(b) }, backingFactory) + + fun wrapSortedList(comparator: Comparator, backingList: MutableList) = + BinaryList(backingList, comparator) + + fun > wrapSortedList(backingList: MutableList) = + wrapSortedList({ a, b -> a.compareTo(b) }, backingList) + } + + override fun add(element: T): Boolean { + val index = binarySearch(element, comparator) + if (index >= 0) + return false + + backing.add(-(index + 1), element) + return true + } + + override fun addAll(elements: Collection) = elements.map(this::add).reduce(Boolean::or) + override fun addAll(index: Int, elements: Collection): Boolean { + throw UnsupportedOperationException("Sorted list does not support inserting elements at a specific index") + } + + override fun remove(element: T): Boolean { + val index = binarySearch(element, comparator) + if (index < 0) + return false + + backing.removeAt(index) + return true + } + + override fun removeAll(elements: Collection) = elements.map(this::remove).reduce(Boolean::or) + + override fun contains(element: T) = binarySearch(element, comparator) >= 0 + fun > contains(element: K, convert: T.() -> K) = binarySearch { it.convert().compareTo(element) } >= 0 + override fun indexOf(element: T): Int { + val index = binarySearch(element, comparator) + if (index < 0) + return -1 + + return index + } + + fun getOrAdd(element: T): T { + val index = binarySearch(element, comparator) + return if (index >= 0) + backing[index] + else { + backing.add(-(index + 1), element) + element + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/ClaimCommand.kt b/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/ClaimCommand.kt new file mode 100644 index 0000000..e5072eb --- /dev/null +++ b/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/ClaimCommand.kt @@ -0,0 +1,32 @@ +package dev.w1zzrd.spigot.chunkprotector.command + +import dev.w1zzrd.spigot.chunkprotector.claim.ClaimManager +import dev.w1zzrd.spigot.chunkprotector.listener.TabCompleteListener +import dev.w1zzrd.spigot.wizcompat.command.CommandUtils +import dev.w1zzrd.spigot.wizcompat.command.CommandUtils.assertTrue +import org.bukkit.command.Command +import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + +class ClaimCommand(private val claimManager: ClaimManager): CommandExecutor { + + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + if (assertTrue(sender is Player, "Only players can claim land", sender)) + return true + + val isClaiming = claimManager.isClaiming(sender as Player) + + if (assertTrue(isClaiming || args.size == 1, "Expected exactly one argument", sender) || + assertTrue(!isClaiming || args.isEmpty(), "You are already claiming a region!", sender) || + assertTrue(isClaiming || claimManager.getClaimByName(sender, args[0]) == null, "You already have a claim with that name", sender)) + return true + + if (claimManager.toggleClaim(sender, if(isClaiming) "" else args[0])) + sender.spigot().sendMessage(CommandUtils.successMessage("Enabled claim mode")) + else + sender.spigot().sendMessage(CommandUtils.successMessage("Cancelled claim!")) + + return true + } +} \ No newline at end of file diff --git a/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/ClaimOptionCommand.kt b/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/ClaimOptionCommand.kt new file mode 100644 index 0000000..acc0484 --- /dev/null +++ b/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/ClaimOptionCommand.kt @@ -0,0 +1,61 @@ +package dev.w1zzrd.spigot.chunkprotector.command + +import dev.w1zzrd.spigot.chunkprotector.claim.Claim +import dev.w1zzrd.spigot.chunkprotector.claim.ClaimManager +import dev.w1zzrd.spigot.chunkprotector.kotlin.assertNotNull +import dev.w1zzrd.spigot.chunkprotector.listener.CompletionProcessor +import dev.w1zzrd.spigot.wizcompat.OfflinePlayers +import dev.w1zzrd.spigot.wizcompat.command.CommandUtils.assertTrue +import dev.w1zzrd.spigot.wizcompat.command.CommandUtils.successMessage +import org.bukkit.Bukkit +import org.bukkit.OfflinePlayer +import org.bukkit.command.Command +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + +private val OPTIONS = arrayOf( + Claim::allowAllLiquids, + Claim::allowEntityInteract, + Claim::allowGuestLiquids, + Claim::allowPlayerEntityInteract, + Claim::allowTNT, + Claim::disablePVP +) + +class ClaimOptionCommand(private val claimManager: ClaimManager): TabCompletionCommandExecutor() { + override val completionProcessor: CompletionProcessor + get() = { sender, args -> + if (args.isEmpty() || (args.size == 1 && args[0].isBlank())) + if(sender is Player && !sender.hasPermission("chunkprotector.bypass")) claimManager.getClaimsForOwner(sender).map(Claim::name) + else OfflinePlayers.getAllKnownPlayers( + Bukkit.getServer(), + true + ).mapNotNull { Bukkit.getOfflinePlayer(it).name } + else emptyList() + } + + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + assertTrue(sender is Player || args.size == 4, "Command can only be run by players", sender) && return true + assertTrue( + (sender is Player && (args.size in 2..3 || (sender.hasPermission("chunkprotector.bypass") && args.size in 2..4))) || (sender !is Player && args.size == 4), + "Wrong number of arguments", + sender + ) && return true + + // It has been written. Let it nevermore be read + (assertNotNull(OPTIONS.firstOrNull { it.name.equals(args[args.size - 2], ignoreCase = true) }, "Unknown option", sender) ?: return true).set( + if(args.size == 2) + assertNotNull(claimManager.getClaimAt((sender as Player).location.chunk), "This location is unclaimed", sender) ?: return true + else + assertNotNull(claimManager.getClaimByName(assertNotNull( + if(args.size == 3) sender as Player + else OfflinePlayers.getKnownPlayer(Bukkit.getServer(), args[0]), "Could not find a player with the given name", sender + ) ?: return true, args[1]), "No claim with the given name could be found", sender) ?: return true, + args[args.size - 1].lowercase().toBoolean() + ) + + sender.spigot().sendMessage(successMessage("Option has been updated!")) + + return true + } +} \ No newline at end of file diff --git a/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/ClaimOwnerCommand.kt b/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/ClaimOwnerCommand.kt new file mode 100644 index 0000000..7ad5338 --- /dev/null +++ b/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/ClaimOwnerCommand.kt @@ -0,0 +1,24 @@ +package dev.w1zzrd.spigot.chunkprotector.command + +import dev.w1zzrd.spigot.chunkprotector.claim.ClaimManager +import dev.w1zzrd.spigot.wizcompat.command.CommandUtils.* +import org.bukkit.Bukkit +import org.bukkit.command.Command +import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + +class ClaimOwnerCommand(private val claimManager: ClaimManager): CommandExecutor { + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + assertTrue(sender is Player, "Only players can check the owner of a chunk", sender) && return true + assertTrue(args.isEmpty(), "No arguments expected for this command", sender) && return true + sender as Player + + val claim = claimManager.getClaimAt(sender.location.chunk) + assertTrue(claim != null, "No one owns this chunk", sender) && return true + claim!! + + sender.spigot().sendMessage(successMessage("Chunk is owned by ${if(claim.owner == sender.uniqueId) "you" else Bukkit.getOfflinePlayer(claim.owner).name} (name: ${claim.name})")) + return true + } +} \ No newline at end of file diff --git a/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/InviteCommand.kt b/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/InviteCommand.kt new file mode 100644 index 0000000..1719506 --- /dev/null +++ b/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/InviteCommand.kt @@ -0,0 +1,38 @@ +package dev.w1zzrd.spigot.chunkprotector.command + +import dev.w1zzrd.spigot.chunkprotector.claim.ClaimManager +import dev.w1zzrd.spigot.wizcompat.OfflinePlayers +import dev.w1zzrd.spigot.wizcompat.command.CommandUtils.assertTrue +import dev.w1zzrd.spigot.wizcompat.command.CommandUtils.successMessage +import org.bukkit.Bukkit +import org.bukkit.command.Command +import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + +class InviteCommand(private val claimManager: ClaimManager): CommandExecutor { + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + assertTrue(sender is Player || args.size == 3, "Command can only be issued by players", sender) && true + assertTrue( + (sender is Player && args.size in 1..2) || ((sender !is Player || sender.hasPermission("chunkprotector.bypass")) && args.size == 3), + "Wrong number of arguments supplied!", + sender + ) && return true + + val targetPlayer = if(args.size == 3) OfflinePlayers.getKnownPlayer(Bukkit.getServer(), args[2]) else sender as Player + assertTrue(targetPlayer != null, "Could not find given player", sender) && return true + assertTrue(args[0] != targetPlayer!!.name, "The owner of a claim cannot be invited to said claim", sender) && return true + + val claim = if(args.size == 1) claimManager.getClaimAt((targetPlayer as Player).location.chunk) else claimManager.getClaimByName(targetPlayer, args[1]) + assertTrue(claim != null && claim.owner == targetPlayer.uniqueId, "Could not find a claim with that name", sender) && return true + + val guest = OfflinePlayers.getKnownPlayer(Bukkit.getServer(), args[0]) + assertTrue(guest != null, "Cannot find a player with that name", sender) && return true + + if (claim!!.addGuest(guest!!) && guest.isOnline) + (guest as Player).spigot().sendMessage(successMessage("You have been invited to \"${claim.name}\" (owned by ${targetPlayer.name})")) + sender.spigot().sendMessage(successMessage("${guest.name} has been invited to ${claim.name}")) + + return true + } +} \ No newline at end of file diff --git a/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/ListClaimsCommand.kt b/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/ListClaimsCommand.kt new file mode 100644 index 0000000..df204c4 --- /dev/null +++ b/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/ListClaimsCommand.kt @@ -0,0 +1,34 @@ +package dev.w1zzrd.spigot.chunkprotector.command + +import dev.w1zzrd.spigot.chunkprotector.claim.Claim +import dev.w1zzrd.spigot.chunkprotector.claim.ClaimManager +import dev.w1zzrd.spigot.wizcompat.OfflinePlayers +import dev.w1zzrd.spigot.wizcompat.command.CommandUtils.assertTrue +import dev.w1zzrd.spigot.wizcompat.command.CommandUtils.successMessage +import org.bukkit.Bukkit +import org.bukkit.command.Command +import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + +class ListClaimsCommand(private val claimManager: ClaimManager): CommandExecutor { + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + assertTrue(sender is Player || args.size == 1, "Command can only be issued by players", sender) && return true + assertTrue( + ((sender !is Player || sender.hasPermission("chunkprotector.bypass")) && args.size == 1) || (sender is Player && args.isEmpty()), + "This command expects no arguments", + sender + ) && return true + + val targetPlayer = if(args.isNotEmpty()) OfflinePlayers.getKnownPlayer(Bukkit.getServer(), args[0]) else sender as Player + assertTrue(targetPlayer != null, "Could not find given player", sender) && return true + + val owned = claimManager.getClaimsForOwner(targetPlayer!!) + if (owned.isEmpty()) + sender.spigot().sendMessage(successMessage("${if(targetPlayer != sender) "${targetPlayer.name} has" else "You have" } no claims")) + else + sender.spigot().sendMessage(successMessage("Claims: ${owned.map(Claim::name).joinToString(", ")}")) + + return true + } +} \ No newline at end of file diff --git a/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/ShowClaimCommand.kt b/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/ShowClaimCommand.kt new file mode 100644 index 0000000..b867685 --- /dev/null +++ b/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/ShowClaimCommand.kt @@ -0,0 +1,79 @@ +package dev.w1zzrd.spigot.chunkprotector.command + +import dev.w1zzrd.spigot.chunkprotector.claim.ClaimChunk +import dev.w1zzrd.spigot.chunkprotector.claim.ClaimManager +import dev.w1zzrd.spigot.wizcompat.OfflinePlayers +import dev.w1zzrd.spigot.wizcompat.command.CommandUtils.assertTrue +import org.bukkit.Bukkit +import org.bukkit.Color +import org.bukkit.Particle +import org.bukkit.command.Command +import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player +import org.bukkit.plugin.Plugin +import org.bukkit.scheduler.BukkitTask + +private val inaccessible = Particle.DustOptions(Color.fromRGB(255, 0, 0), 10.0F) +private val accessible = Particle.DustOptions(Color.fromRGB(0, 255, 0), 10.0F) + +class ShowClaimCommand(private val claimManager: ClaimManager, private val plugin: Plugin): CommandExecutor { + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + assertTrue(sender is Player, "Claims can only be visualised by players", sender) && return true + assertTrue(args.size <= 2, "Too many arguments supplied", sender) && return true + sender as Player + + val targetPlayer = if(args.size == 2) OfflinePlayers.getKnownPlayer(Bukkit.getServer(), args[1]) else sender + assertTrue(targetPlayer != null, "Specified player could not be found", sender) && return true + targetPlayer!! + + val targetRegion = if(args.isEmpty()) claimManager.getClaimAt(sender.location.chunk) else claimManager.getClaimByName(targetPlayer, args[0]) + assertTrue(targetRegion != null, "Region is either not claimed, or a claim with the given name does not exist", sender) && return true + targetRegion!! + + fun ClaimChunk.asCoords() = (chunkX shl 4).toDouble() to (chunkZ shl 4).toDouble() + fun Pair.lerp(other: Pair) = (first + other.first).div(2.0) to (second + other.second).div(2.0) + + val baseVerts = arrayOf( + ClaimChunk(targetRegion.topLeft.chunkX, targetRegion.topLeft.chunkZ + 1).asCoords(), + ClaimChunk(targetRegion.topLeft.chunkX, targetRegion.bottomRight.chunkZ).asCoords(), + ClaimChunk(targetRegion.bottomRight.chunkX + 1, targetRegion.bottomRight.chunkZ).asCoords(), + ClaimChunk(targetRegion.bottomRight.chunkX + 1, targetRegion.topLeft.chunkZ + 1).asCoords() + ) + + val allVerts = arrayOf( + *baseVerts, + baseVerts[0].lerp(baseVerts[1]), + baseVerts[1].lerp(baseVerts[2]), + baseVerts[2].lerp(baseVerts[3]), + baseVerts[3].lerp(baseVerts[0]) + ) + var count = 0 + var task: BukkitTask? = null + + val particleData = if (targetRegion.isAccessible(sender)) accessible else inaccessible + + task = Bukkit.getScheduler().runTaskTimer( + plugin, + Runnable { + if (count == 40) + task!!.cancel() + + count += 1 + for (vert in allVerts) + sender.spawnParticle( + Particle.REDSTONE, + vert.first, + sender.location.y + 1.0, + vert.second, + 10, + particleData + ) + }, + 0, + 10 + ) + + return true + } +} \ No newline at end of file diff --git a/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/TabCompletionCommandExecutor.kt b/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/TabCompletionCommandExecutor.kt new file mode 100644 index 0000000..82a9a09 --- /dev/null +++ b/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/TabCompletionCommandExecutor.kt @@ -0,0 +1,8 @@ +package dev.w1zzrd.spigot.chunkprotector.command + +import dev.w1zzrd.spigot.chunkprotector.listener.CompletionProcessor +import org.bukkit.command.CommandExecutor + +abstract class TabCompletionCommandExecutor: CommandExecutor { + abstract val completionProcessor: CompletionProcessor +} \ No newline at end of file diff --git a/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/UnClaimCommand.kt b/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/UnClaimCommand.kt new file mode 100644 index 0000000..0de64fd --- /dev/null +++ b/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/UnClaimCommand.kt @@ -0,0 +1,29 @@ +package dev.w1zzrd.spigot.chunkprotector.command + +import dev.w1zzrd.spigot.chunkprotector.claim.ClaimManager +import dev.w1zzrd.spigot.wizcompat.OfflinePlayers +import dev.w1zzrd.spigot.wizcompat.command.CommandUtils.assertTrue +import dev.w1zzrd.spigot.wizcompat.command.CommandUtils.successMessage +import org.bukkit.Bukkit +import org.bukkit.command.Command +import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + +class UnClaimCommand(private val claimManager: ClaimManager): CommandExecutor { + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + assertTrue(sender is Player || args.size == 2, "Command can only be run by players", sender) && return true + assertTrue((sender is Player && args.size in 0..1) || ((sender !is Player || sender.hasPermission("chunkprotector.bypass")) && args.size == 2), "Wrong number of arguments", sender) && return true + + val targetPlayer = if (args.size == 2) OfflinePlayers.getKnownPlayer(Bukkit.getServer(), args[1]) else sender as Player + assertTrue(targetPlayer != null, "Could not find target player", sender) && return true + + val claim = if(args.isEmpty()) claimManager.getClaimAt((targetPlayer!! as Player).location.chunk) else claimManager.getClaimByName(targetPlayer!!, args[0]) + assertTrue(claim != null, "Could not find a claim with that name", sender) && return true + + claimManager.removeClaim(claim!!) + sender.spigot().sendMessage(successMessage("Unclaimed ${args[0]}")) + + return true + } +} \ No newline at end of file diff --git a/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/UnInviteCommand.kt b/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/UnInviteCommand.kt new file mode 100644 index 0000000..5afd88a --- /dev/null +++ b/src/main/java/dev/w1zzrd/spigot/chunkprotector/command/UnInviteCommand.kt @@ -0,0 +1,38 @@ +package dev.w1zzrd.spigot.chunkprotector.command + +import dev.w1zzrd.spigot.chunkprotector.claim.ClaimManager +import dev.w1zzrd.spigot.wizcompat.OfflinePlayers +import dev.w1zzrd.spigot.wizcompat.command.CommandUtils.* +import org.bukkit.Bukkit +import org.bukkit.command.Command +import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + + +class UnInviteCommand(private val claimManager: ClaimManager): CommandExecutor { + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + assertTrue(sender is Player || args.size == 3, "Command can only be issued by players", sender) && return true + assertTrue( + args.size in 1..2 || ((sender !is Player || sender.hasPermission("chunkprotector.bypass")) && args.size == 3), + "Wrong number of arguments supplied!", + sender + ) && return true + + val targetPlayer = if(args.size == 3) OfflinePlayers.getKnownPlayer(Bukkit.getServer(), args[2]) else sender as Player + assertTrue(targetPlayer != null, "Could not find given player", sender) && return true + assertTrue(args[0] != targetPlayer!!.name, "The owner of a claim cannot be uninvited from said claim", sender) && return true + + val claim = if(args.size == 1) claimManager.getClaimAt((targetPlayer as Player).location.chunk) else claimManager.getClaimByName(targetPlayer, args[1]) + assertTrue(claim != null && claim.owner == targetPlayer.uniqueId, "Could not find a claim with that name", sender) && return true + + val guest = OfflinePlayers.getKnownPlayer(Bukkit.getServer(), args[0]) + assertTrue(guest != null, "Cannot find a player with that name", sender) && return true + + if (claim!!.removeGuest(guest!!) && guest.isOnline) + (guest as Player).spigot().sendMessage(errorMessage("You have been uninvited from \"${claim.name}\" (owned by ${targetPlayer.name})")) + sender.spigot().sendMessage(successMessage("${guest.name} is no longer invited to ${claim.name}")) + + return true + } +} \ No newline at end of file diff --git a/src/main/java/dev/w1zzrd/spigot/chunkprotector/freecam/FreeCamManager.kt b/src/main/java/dev/w1zzrd/spigot/chunkprotector/freecam/FreeCamManager.kt new file mode 100644 index 0000000..6a32198 --- /dev/null +++ b/src/main/java/dev/w1zzrd/spigot/chunkprotector/freecam/FreeCamManager.kt @@ -0,0 +1,101 @@ +package dev.w1zzrd.spigot.chunkprotector.freecam + +import dev.w1zzrd.spigot.wizcompat.packet.Players +import org.bukkit.GameMode +import org.bukkit.Location +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.HandlerList +import org.bukkit.event.Listener +import org.bukkit.event.player.PlayerQuitEvent +import org.bukkit.event.player.PlayerTeleportEvent +import org.bukkit.plugin.Plugin + +class FreeCamManager { + private val freeCammers = ArrayList() + private val freeCamEventListener = FreeCamEventListener() + private val freeCamEnableListeners = ArrayList<(Player) -> Unit>() + private val freeCamDisableListeners = ArrayList<(Player) -> Unit>() + + fun disableFreeCam(player: Player): Boolean { + val index = freeCammers.binarySearch(FreeCammer(player)) + + if (index < 0) + return false + + // Disable before removal to prevent TOCTOU (in case I decide to multi-thread this) + freeCammers[index].disable() + freeCammers.removeAt(index) + + return true + } + + fun enableFreeCam(player: Player): Boolean { + val freeCammer = FreeCammer(player, player.location.clone(), player.gameMode) + + val index = freeCammers.binarySearch(freeCammer) + + if (index >= 0) + return false + + // Enable after insertion to prevent TOCTOU (in case I decide to multi-thread this) + freeCammers.add(-(index + 1), freeCammer) + freeCammer.enable() + + return true + } + + fun toggleFreeCam(player: Player): Boolean { + val freeCammer = FreeCammer(player, player.location.clone(), player.gameMode) + + val index = freeCammers.binarySearch(freeCammer) + + return if (index >= 0) { + freeCammers[index].disable() + freeCammers.removeAt(index) + false + } else { + freeCammers.add(-(index + 1), freeCammer) + freeCammer.enable() + true + } + } + + fun isFreeCamming(player: Player) = freeCammers.binarySearch(FreeCammer(player)) >= 0 + + fun addOnPlayerEnterFreeCam(callback: (Player) -> Unit) = freeCamEnableListeners.add(callback) + fun addOnPlayerExitFreeCam(callback: (Player) -> Unit) = freeCamDisableListeners.add(callback) + + fun onEnable(plugin: Plugin) = plugin.server.pluginManager.registerEvents(freeCamEventListener, plugin) + fun onDisable() { + HandlerList.unregisterAll(freeCamEventListener) + + freeCammers.forEach(FreeCammer::disable) + freeCammers.clear() + } + + private inner class FreeCamEventListener: Listener { + @EventHandler + fun onPlayerQuit(event: PlayerQuitEvent) = disableFreeCam(event.player) + } + + inner class FreeCammer(val player: Player, val startLocation: Location? = null, val originalMode: GameMode? = null): Comparable { + + fun enable() { + player.gameMode = GameMode.SPECTATOR + Players.sendPlayerGameModePacket(player, GameMode.CREATIVE) + + freeCamEnableListeners.forEach { it(player) } + } + + fun disable() { + player.teleport(startLocation!!, PlayerTeleportEvent.TeleportCause.PLUGIN) + player.gameMode = originalMode!! + Players.sendPlayerGameModePacket(player, originalMode) + + freeCamDisableListeners.forEach { it(player) } + } + + override fun compareTo(other: FreeCammer) = player.uniqueId.compareTo(other.player.uniqueId) + } +} \ No newline at end of file diff --git a/src/main/java/dev/w1zzrd/spigot/chunkprotector/freecam/FreeCammer.kt b/src/main/java/dev/w1zzrd/spigot/chunkprotector/freecam/FreeCammer.kt new file mode 100644 index 0000000..002e7a6 --- /dev/null +++ b/src/main/java/dev/w1zzrd/spigot/chunkprotector/freecam/FreeCammer.kt @@ -0,0 +1,20 @@ +package dev.w1zzrd.spigot.chunkprotector.freecam + +import org.bukkit.GameMode +import org.bukkit.Location +import org.bukkit.entity.Player +import org.bukkit.event.player.PlayerTeleportEvent + +data class FreeCammer(val player: Player, val startLocation: Location? = null, val originalMode: GameMode? = null): Comparable { + + fun enable() { + player.gameMode = GameMode.SPECTATOR + } + + fun disable() { + player.teleport(startLocation!!, PlayerTeleportEvent.TeleportCause.PLUGIN) + player.gameMode = originalMode!! + } + + override fun compareTo(other: FreeCammer) = player.uniqueId.compareTo(other.player.uniqueId) +} \ No newline at end of file diff --git a/src/main/java/dev/w1zzrd/spigot/chunkprotector/kotlin/Assert.kt b/src/main/java/dev/w1zzrd/spigot/chunkprotector/kotlin/Assert.kt new file mode 100644 index 0000000..3acaa90 --- /dev/null +++ b/src/main/java/dev/w1zzrd/spigot/chunkprotector/kotlin/Assert.kt @@ -0,0 +1,15 @@ +package dev.w1zzrd.spigot.chunkprotector.kotlin + +import dev.w1zzrd.spigot.wizcompat.command.CommandUtils.assertTrue +import org.bukkit.command.CommandSender +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +@OptIn(ExperimentalContracts::class) +fun assertNotNull(value: T?, errorMessage: String, sender: CommandSender): T? { + contract { returnsNotNull() implies (value != null) } + + assertTrue(value != null, errorMessage, sender) + + return value +} \ No newline at end of file diff --git a/src/main/java/dev/w1zzrd/spigot/chunkprotector/listener/PlayerActionListener.kt b/src/main/java/dev/w1zzrd/spigot/chunkprotector/listener/PlayerActionListener.kt new file mode 100644 index 0000000..2fdc293 --- /dev/null +++ b/src/main/java/dev/w1zzrd/spigot/chunkprotector/listener/PlayerActionListener.kt @@ -0,0 +1,143 @@ +package dev.w1zzrd.spigot.chunkprotector.listener + +import dev.w1zzrd.spigot.chunkprotector.claim.Claim +import dev.w1zzrd.spigot.chunkprotector.claim.ClaimManager +import org.bukkit.Bukkit +import org.bukkit.Chunk +import org.bukkit.Location +import org.bukkit.block.Block +import org.bukkit.entity.Entity +import org.bukkit.entity.Monster +import org.bukkit.entity.Player +import org.bukkit.event.Cancellable +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.block.BlockBreakEvent +import org.bukkit.event.block.BlockExplodeEvent +import org.bukkit.event.block.BlockFromToEvent +import org.bukkit.event.block.BlockPlaceEvent +import org.bukkit.event.entity.EntityDamageByEntityEvent +import org.bukkit.event.entity.EntityExplodeEvent +import org.bukkit.event.entity.EntityTargetEvent +import org.bukkit.event.entity.PlayerLeashEntityEvent +import org.bukkit.event.player.PlayerInteractEntityEvent +import org.bukkit.event.player.PlayerInteractEvent +import org.bukkit.event.player.PlayerShearEntityEvent + +@Suppress("unused") +class PlayerActionListener(private val claimManager: ClaimManager): Listener { + @EventHandler + fun onPlayerInteract(event: PlayerInteractEvent) = + doConditionalCancellation(event.player, event) + + @EventHandler + fun onPlayerBreakBlock(event: BlockBreakEvent) = + doConditionalCancellation(event.player, event, event.block.location) + + @EventHandler + fun onPlayerPlaceBlock(event: BlockPlaceEvent) = + doConditionalCancellation(event.player, event, event.blockPlaced.location) + + @EventHandler + fun onEntityExplode(event: EntityExplodeEvent) { + if (event.isCancelled) + return + + filterProtectedBlocks(event.blockList(), Claim::allowEntityInteract) + } + + @EventHandler + fun onBlockExplode(event: BlockExplodeEvent) { + if (event.isCancelled) + return + + filterProtectedBlocks(event.blockList(), Claim::allowTNT) + } + + @EventHandler + fun onLiquidMove(event: BlockFromToEvent) { + if (event.isCancelled) + return + + // Cancel the event if a liquid is moving into a claimed area from an area that is not owned by the claimer + val toClaim = claimManager.getClaimAt(event.toBlock.chunk) ?: return + val fromClaim = claimManager.getClaimAt(event.toBlock.location.subtract(event.face.direction).chunk) + if (fromClaim == null) { + event.isCancelled = true + return + } + + if (toClaim.allowAllLiquids || toClaim.owner == fromClaim.owner || (toClaim.allowGuestLiquids && toClaim.hasGuest(Bukkit.getPlayer(fromClaim.owner)!!))) + return + + event.isCancelled = true + } + + @EventHandler + fun onPlayerInteractEntity(event: PlayerInteractEntityEvent) { + if (!event.isCancelled && checkEntityInteraction(event.player, event.rightClicked, Claim::allowPlayerEntityInteract)) + event.isCancelled = true + } + + @EventHandler + fun onPlayerShearEntity(event: PlayerShearEntityEvent) { + if (!event.isCancelled && checkEntityInteraction(event.player, event.entity, Claim::allowPlayerEntityInteract)) + event.isCancelled = true + } + + @EventHandler + fun onPlayerLeashEntity(event: PlayerLeashEntityEvent) { + if (!event.isCancelled && checkEntityInteraction(event.player, event.entity, Claim::allowPlayerEntityInteract)) + event.isCancelled = true + } + + @EventHandler + fun onPlayerAttractAnimal(event: EntityTargetEvent) { + if (!event.isCancelled && event.target is Player && event.reason == EntityTargetEvent.TargetReason.TEMPT && checkEntityInteraction(event.target as Player, event.entity, Claim::allowPlayerEntityInteract)) + event.isCancelled = true + } + + @EventHandler + fun onPlayerAttackMob(event: EntityDamageByEntityEvent) { + if (!event.isCancelled && checkEntityInteraction( + event.damager, + event.entity, + if(event.damager is Player) Claim::allowPlayerEntityInteract + else Claim::allowEntityInteract + )) + event.isCancelled = true + } + + private fun checkEntityInteraction(source: Entity, entity: Entity, permissionType: Claim.() -> Boolean): Boolean { + if (entity is Monster && entity.customName == null) + return false + + val claim = claimManager.getClaimAt(entity.location.chunk) + + return (claim != null && claim.disablePVP && source is Player && entity is Player) || !(entity is Player || claim == null || claim.permissionType() || (source is Player && (claim.isAccessible(source) || source.hasPermission("chunkprotector.ignore")))) + } + + private fun filterProtectedBlocks(changedBlocks: MutableList, configCheck: Claim.() -> Boolean) { + val chunks = HashMap>() + + for (block in changedBlocks) { + if (block.chunk !in chunks) + chunks[block.chunk] = ArrayList() + chunks[block.chunk]!!.add(block) + } + + for ((chunk, blocks) in chunks) { + val claim = claimManager.getClaimAt(chunk) + if (claim != null && !claim.configCheck()) + changedBlocks.removeAll(blocks) + } + } + + private fun doConditionalCancellation(player: Player, event: Cancellable, eventLocation: Location = player.location) { + if (event.isCancelled || player.hasPermission("chunkprotector.ignore")) + return + + if (claimManager.getClaimAt(eventLocation.chunk)?.isAccessible(player) == false) + event.isCancelled = true + } +} \ No newline at end of file diff --git a/src/main/java/dev/w1zzrd/spigot/chunkprotector/listener/TabCompleteListener.kt b/src/main/java/dev/w1zzrd/spigot/chunkprotector/listener/TabCompleteListener.kt new file mode 100644 index 0000000..73270d1 --- /dev/null +++ b/src/main/java/dev/w1zzrd/spigot/chunkprotector/listener/TabCompleteListener.kt @@ -0,0 +1,34 @@ +package dev.w1zzrd.spigot.chunkprotector.listener + +import org.bukkit.command.CommandSender +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.server.TabCompleteEvent + +typealias CompletionProcessor = (CommandSender, List) -> Collection +private val SPLITTER = Regex(" +") + +class TabCompleteListener: Listener { + + private val tabCompleters = HashMap() + + @EventHandler + fun onTabComplete(event: TabCompleteEvent) { + if (!event.isCancelled) { + val split = event.buffer.split(SPLITTER) + event.completions.addAll((tabCompleters[split[0]] ?: return).invoke(event.sender, if(split.size > 1) split.subList(1, split.size) else emptyList())) + } + } + + fun registerTabCompleter(command: String, processor: CompletionProcessor) { + tabCompleters[command] = processor + } + + fun unRegisterTabCompleter(command: String) { + tabCompleters.remove(command) + } + + fun unRegisterAll() { + tabCompleters.clear() + } +} \ No newline at end of file