feat: implement initial hcu Items plugin with command and event handling for special items
This commit is contained in:
commit
b19d9cf17a
9
.gitattributes
vendored
Normal file
9
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
#
|
||||
# Linux start script should use lf
|
||||
/gradlew text eol=lf
|
||||
|
||||
# These are Windows script files and should use crlf
|
||||
*.bat text eol=crlf
|
||||
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Ignore Gradle project-specific cache directory
|
||||
.gradle
|
||||
.vscode
|
||||
|
||||
# Ignore Gradle build output directory
|
||||
build
|
||||
|
||||
.direnv
|
||||
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
[submodule "hcu-core"]
|
||||
path = hcu-core
|
||||
url = git@gitea.hareworks.net:hcu/hcu-core.git
|
||||
5
README.md
Normal file
5
README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
Kotlin + Gradle kts
|
||||
|
||||
# How to Build
|
||||
|
||||
```./gradlew shadowJar```:
|
||||
66
build.gradle.kts
Normal file
66
build.gradle.kts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import net.minecrell.pluginyml.paper.PaperPluginDescription
|
||||
|
||||
|
||||
plugins {
|
||||
kotlin("jvm") version "2.2.21"
|
||||
id("de.eldoria.plugin-yml.paper") version "0.8.0"
|
||||
id("com.gradleup.shadow") version "9.2.2"
|
||||
}
|
||||
|
||||
group = "com.github.kaaariyaaa"
|
||||
version = "1.0"
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven("https://repo.papermc.io/repository/maven-public/")
|
||||
}
|
||||
|
||||
val exposedVersion = "1.0.0-rc-4"
|
||||
|
||||
dependencies {
|
||||
compileOnly("io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib")
|
||||
|
||||
implementation("net.kyori:adventure-api:4.25.0")
|
||||
implementation("net.kyori:adventure-text-minimessage:4.25.0")
|
||||
|
||||
compileOnly("net.hareworks.hcu:hcu-core")
|
||||
compileOnly("net.hareworks:kommand-lib")
|
||||
compileOnly("net.hareworks:permits-lib")
|
||||
|
||||
implementation("org.postgresql:postgresql:42.7.8")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
|
||||
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
|
||||
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
|
||||
implementation("org.jetbrains.exposed:exposed-kotlin-datetime:$exposedVersion")
|
||||
implementation("com.michael-bull.kotlin-result:kotlin-result:2.1.0")
|
||||
}
|
||||
tasks {
|
||||
withType<Jar> {
|
||||
archiveBaseName.set("hcu-items")
|
||||
}
|
||||
shadowJar {
|
||||
archiveClassifier.set("min")
|
||||
minimize()
|
||||
}
|
||||
}
|
||||
|
||||
paper {
|
||||
main = "net.hareworks.hcu.items.App"
|
||||
name = "hcu-items"
|
||||
description = "hcu-items plugin"
|
||||
version = getVersion().toString()
|
||||
apiVersion = "1.21.10"
|
||||
serverDependencies {
|
||||
register("hcu-core") {
|
||||
required = true
|
||||
load = PaperPluginDescription.RelativeLoadOrder.BEFORE
|
||||
}
|
||||
}
|
||||
authors =
|
||||
listOf(
|
||||
"Hare-K02",
|
||||
"Kaariyaaa"
|
||||
)
|
||||
}
|
||||
61
flake.lock
Normal file
61
flake.lock
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1763948260,
|
||||
"narHash": "sha256-dY9qLD0H0zOUgU3vWacPY6Qc421BeQAfm8kBuBtPVE0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "1c8ba8d3f7634acac4a2094eef7c32ad9106532c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-25.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
45
flake.nix
Normal file
45
flake.nix
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
description = "Minecraft dev environment with JDK 21 and Gradle";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
jdk21
|
||||
gradle
|
||||
git
|
||||
unzip
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
export JAVA_HOME=${pkgs.jdk21}/lib/openjdk
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
|
||||
export GRADLE_USER_HOME="$PWD/.gradle"
|
||||
|
||||
echo "Loaded Minecraft dev env (JDK 21 + Gradle)"
|
||||
java -version || true
|
||||
gradle --version || true
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
244
gradlew
vendored
Executable file
244
gradlew
vendored
Executable file
|
|
@ -0,0 +1,244 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original 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 POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# 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 "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# 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 ;; #(
|
||||
MSYS* | 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" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
92
gradlew.bat
vendored
Normal file
92
gradlew.bat
vendored
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
@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=.
|
||||
@rem This is normally unused
|
||||
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% equ 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% equ 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!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
1
hcu-core/.envrc
Normal file
1
hcu-core/.envrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
use flake
|
||||
12
hcu-core/.gitattributes
vendored
Normal file
12
hcu-core/.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
#
|
||||
# Linux start script should use lf
|
||||
/gradlew text eol=lf
|
||||
|
||||
# These are Windows script files and should use crlf
|
||||
*.bat text eol=crlf
|
||||
|
||||
# Binary files should be left untouched
|
||||
*.jar binary
|
||||
|
||||
5
hcu-core/.gitignore
vendored
Normal file
5
hcu-core/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
.direnv
|
||||
.gradle
|
||||
.kotlin
|
||||
build
|
||||
bin
|
||||
4
hcu-core/.gitmodules
vendored
Normal file
4
hcu-core/.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[submodule "kommand-lib"]
|
||||
path = kommand-lib
|
||||
url = git@gitea.hareworks.net:Hare/kommand-lib.git
|
||||
branch = master
|
||||
64
hcu-core/build.gradle.kts
Normal file
64
hcu-core/build.gradle.kts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
group = "net.hareworks.hcu"
|
||||
version = "1.3"
|
||||
|
||||
plugins {
|
||||
kotlin("jvm") version "2.2.21"
|
||||
id("de.eldoria.plugin-yml.paper") version "0.8.0"
|
||||
id("com.gradleup.shadow") version "9.2.2"
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven("https://repo.papermc.io/repository/maven-public/")
|
||||
}
|
||||
|
||||
val exposedVersion = "1.0.0-rc-4"
|
||||
|
||||
dependencies {
|
||||
compileOnly("io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT")
|
||||
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib")
|
||||
implementation("com.michael-bull.kotlin-result:kotlin-result:2.1.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1")
|
||||
|
||||
implementation("net.kyori:adventure-api:4.25.0")
|
||||
implementation("net.kyori:adventure-text-minimessage:4.25.0")
|
||||
implementation("net.hareworks:kommand-lib")
|
||||
implementation("net.hareworks:permits-lib")
|
||||
|
||||
implementation("org.postgresql:postgresql:42.7.8")
|
||||
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
|
||||
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
|
||||
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
|
||||
implementation("org.jetbrains.exposed:exposed-kotlin-datetime:$exposedVersion")
|
||||
implementation("com.zaxxer:HikariCP:6.2.1")
|
||||
|
||||
}
|
||||
tasks {
|
||||
withType<Jar> {
|
||||
archiveBaseName.set("hcu-core")
|
||||
}
|
||||
shadowJar {
|
||||
archiveClassifier.set("min")
|
||||
minimize {
|
||||
exclude("net.hareworks:kommand-lib")
|
||||
exclude("net.hareworks:permits-lib")
|
||||
exclude(dependency("org.jetbrains.exposed:exposed-core"))
|
||||
exclude(dependency("org.jetbrains.exposed:exposed-dao"))
|
||||
exclude(dependency("org.jetbrains.exposed:exposed-jdbc"))
|
||||
exclude(dependency("org.jetbrains.exposed:exposed-kotlin-datetime"))
|
||||
exclude(dependency("org.postgresql:postgresql"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paper {
|
||||
main = "net.hareworks.hcu.core.Main"
|
||||
name = "hcu-core"
|
||||
description = "libraries and implementations for Hare's civilized universe"
|
||||
version = getVersion().toString()
|
||||
apiVersion = "1.21.10"
|
||||
authors = listOf(
|
||||
"Hare-K02",
|
||||
)
|
||||
}
|
||||
61
hcu-core/flake.lock
Normal file
61
hcu-core/flake.lock
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1763948260,
|
||||
"narHash": "sha256-dY9qLD0H0zOUgU3vWacPY6Qc421BeQAfm8kBuBtPVE0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "1c8ba8d3f7634acac4a2094eef7c32ad9106532c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-25.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
45
hcu-core/flake.nix
Normal file
45
hcu-core/flake.nix
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
description = "Minecraft dev environment with JDK 21 and Gradle";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
jdk21
|
||||
gradle
|
||||
git
|
||||
unzip
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
export JAVA_HOME=${pkgs.jdk21}/lib/openjdk
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
|
||||
export GRADLE_USER_HOME="$PWD/.gradle"
|
||||
|
||||
echo "Loaded Minecraft dev env (JDK 21 + Gradle)"
|
||||
java -version || true
|
||||
gradle --version || true
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
7
hcu-core/gradle.properties
Normal file
7
hcu-core/gradle.properties
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
org.gradle.configuration-cache=true
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.daemon=false
|
||||
|
||||
org.gradle.configuration-cache=true
|
||||
|
||||
2
hcu-core/gradle/libs.versions.toml
Normal file
2
hcu-core/gradle/libs.versions.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# This file was generated by the Gradle 'init' task.
|
||||
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
|
||||
BIN
hcu-core/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
hcu-core/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
hcu-core/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
hcu-core/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
251
hcu-core/gradlew
vendored
Executable file
251
hcu-core/gradlew
vendored
Executable file
|
|
@ -0,0 +1,251 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original 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.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# 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 ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# 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
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
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
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# 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"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
94
hcu-core/gradlew.bat
vendored
Normal file
94
hcu-core/gradlew.bat
vendored
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
@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
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@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=.
|
||||
@rem This is normally unused
|
||||
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% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 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!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
1
hcu-core/kommand-lib/.envrc
Normal file
1
hcu-core/kommand-lib/.envrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
use flake
|
||||
12
hcu-core/kommand-lib/.gitattributes
vendored
Normal file
12
hcu-core/kommand-lib/.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
#
|
||||
# Linux start script should use lf
|
||||
/gradlew text eol=lf
|
||||
|
||||
# These are Windows script files and should use crlf
|
||||
*.bat text eol=crlf
|
||||
|
||||
# Binary files should be left untouched
|
||||
*.jar binary
|
||||
|
||||
5
hcu-core/kommand-lib/.gitignore
vendored
Normal file
5
hcu-core/kommand-lib/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
.direnv
|
||||
.kotlin
|
||||
.gradle
|
||||
build
|
||||
bin
|
||||
4
hcu-core/kommand-lib/.gitmodules
vendored
Normal file
4
hcu-core/kommand-lib/.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[submodule "permits-lib"]
|
||||
path = permits-lib
|
||||
url = git@gitea.hareworks.net:Hare/permits-lib.git
|
||||
branch = master
|
||||
118
hcu-core/kommand-lib/MIGRATION_GUIDE.md
Normal file
118
hcu-core/kommand-lib/MIGRATION_GUIDE.md
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# kommand-lib マイグレーションガイド
|
||||
|
||||
旧バージョンから最新の Brigadier ネイティブ対応バージョンへの移行方法。
|
||||
|
||||
---
|
||||
|
||||
## 変更の概要
|
||||
|
||||
### 主な変更点
|
||||
|
||||
1. **`coordinates()` の型変更** (破壊的変更)
|
||||
- `Coordinates3` → `io.papermc.paper.math.Position`
|
||||
- `coords.resolve(base)` → `position.toLocation(world)`
|
||||
|
||||
2. **内部処理の改善**
|
||||
- Player/Entity セレクターの安定性向上
|
||||
- Bukkit CommandMap → Brigadier Lifecycle API
|
||||
|
||||
---
|
||||
|
||||
## マイグレーション手順
|
||||
|
||||
### 1. 依存関係の確認
|
||||
|
||||
Paper API 1.21 以降が必要です。
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
compileOnly("io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT")
|
||||
}
|
||||
```
|
||||
|
||||
### 2. coordinates の修正
|
||||
|
||||
#### Before
|
||||
```kotlin
|
||||
coordinates("point") {
|
||||
executes {
|
||||
val coords = argument<Coordinates3>("point")
|
||||
val location = coords.resolve(player.location)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### After
|
||||
```kotlin
|
||||
import io.papermc.paper.math.Position
|
||||
|
||||
coordinates("point") {
|
||||
executes {
|
||||
val position = argument<Position>("point")
|
||||
val location = position.toLocation(player.world)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. ビルド確認
|
||||
|
||||
```bash
|
||||
./gradlew build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Position API
|
||||
|
||||
```kotlin
|
||||
val position = argument<Position>("pos")
|
||||
|
||||
// 座標取得
|
||||
val x = position.x()
|
||||
val y = position.y()
|
||||
val z = position.z()
|
||||
|
||||
// Location 変換
|
||||
val location = position.toLocation(world)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## トラブルシューティング
|
||||
|
||||
### `Coordinates3` が見つからない
|
||||
|
||||
`Coordinates3` は存在しません。`Position` を使用してください。
|
||||
|
||||
```kotlin
|
||||
// ❌ 間違い
|
||||
argument<Coordinates3>("pos")
|
||||
|
||||
// ✅ 正しい
|
||||
argument<Position>("pos")
|
||||
```
|
||||
|
||||
### `resolve()` メソッドが見つからない
|
||||
|
||||
`Position` には `resolve()` はありません。`toLocation(world)` を使用してください。
|
||||
|
||||
```kotlin
|
||||
// ❌ 間違い
|
||||
position.resolve(baseLocation)
|
||||
|
||||
// ✅ 正しい
|
||||
position.toLocation(world)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: 相対座標 (`~`) は使えますか?**
|
||||
A: はい、`Position` は相対座標を完全にサポートしています。
|
||||
|
||||
**Q: 旧バージョンとの互換性は?**
|
||||
A: `coordinates()` の型が変更されているため互換性はありません。他の引数(`player()`, `players()` など)は互換性があります。
|
||||
|
||||
**Q: 段階的な移行は可能?**
|
||||
A: `coordinates()` を使用している場合は一度にすべて移行する必要があります。
|
||||
201
hcu-core/kommand-lib/README.md
Normal file
201
hcu-core/kommand-lib/README.md
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
# Kommand Lib
|
||||
|
||||
Paper/Bukkit サーバー向けのコマンド定義を DSL で記述するためのライブラリです。ルート定義から引数の型、補完、パーミッション伝播までを宣言的に表現でき、手続き的な `CommandExecutor` 実装を大きく簡略化します。
|
||||
|
||||
## 特徴
|
||||
|
||||
- Kotlin DSL で `command { literal { argument { ... } } }` のようにネストを表現
|
||||
- 型付き引数 (`string`, `integer`, `float`, `player`, `selector`, `coordinates` など) と検証ロジックを組み込み
|
||||
- 1 つの定義から実行とタブ補完の両方を生成
|
||||
- パーミッションや条件をノード単位で宣言し、子ノードへ自動伝播
|
||||
- `suggests {}` で引数ごとの補完候補を柔軟に制御
|
||||
- Brigadier (Paper 1.21 Lifecycle API) 対応により、クライアント側で `<speed> <count>` のような構文ヒントや、数値範囲の検証エラー(赤文字)が表示されます
|
||||
- `permits-lib` との連携により、コマンドツリーから Bukkit パーミッションを自動生成し、`compileOnly` 依存として参照可能
|
||||
|
||||
## バージョン情報
|
||||
|
||||
**現在のバージョン**: 1.1 (Brigadier ネイティブ対応)
|
||||
|
||||
### 🔄 旧バージョンからの移行
|
||||
|
||||
旧バージョン (Brigadier 対応前) から移行する場合は、[マイグレーションガイド](./MIGRATION_GUIDE.md) を参照してください。
|
||||
|
||||
**主な変更点**:
|
||||
- `coordinates()` の返り値が `Coordinates3` から `io.papermc.paper.math.Position` に変更
|
||||
- `position.toLocation(world)` で `Location` に変換する方式に変更
|
||||
- Player/Entity セレクターの内部処理が改善され、より安定した動作を実現
|
||||
|
||||
## 依存関係
|
||||
|
||||
`build.gradle.kts` では Paper API と Kotlin 標準ライブラリのみを `compileOnly` に追加しています。Paper 1.21.10 対応の API を利用しています。
|
||||
|
||||
```kotlin
|
||||
plugins {
|
||||
kotlin("jvm") version "2.2.21"
|
||||
id("de.eldoria.plugin-yml.paper") version "0.8.0"
|
||||
id("com.gradleup.shadow") version "9.2.2"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT")
|
||||
compileOnly("org.jetbrains.kotlin:kotlin-stdlib")
|
||||
// ../permits-lib を includeBuild しているので module 参照でOK
|
||||
compileOnly("net.hareworks.hcu:permits-lib:1.0")
|
||||
}
|
||||
```
|
||||
|
||||
## 使い方
|
||||
|
||||
1. プラグインの `onEnable` などで `kommand(plugin) { ... }` DSL を呼び出します。
|
||||
2. `command("root", "alias") { ... }` でコマンドを宣言し、`literal` や `string`/`integer` 引数を追加します。
|
||||
3. `executes { ... }` は必ず対象のノード (`literal`, `player`, `integer` など) の中にネストします。これにより、そのノードまでに宣言した引数を `argument<String>("player")` や `argument<Int>("amount")` のように取得できます。
|
||||
|
||||
### サンプル: 経済コマンド
|
||||
|
||||
```kotlin
|
||||
class EconomyPlugin : JavaPlugin() {
|
||||
private lateinit var commands: KommandLib
|
||||
|
||||
override fun onEnable() {
|
||||
commands = kommand(this) {
|
||||
command("eco", "economy") {
|
||||
description = "Economy management"
|
||||
permission = "example.eco"
|
||||
|
||||
literal("give") {
|
||||
player("target") { // プレイヤー名 or セレクター (@p 等)
|
||||
integer("amount", min = 1) {
|
||||
executes {
|
||||
val target: Player = argument("target")
|
||||
val amount = argument<Int>("amount")
|
||||
sender.sendMessage("Giving $amount to ${target.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
literal("speed") {
|
||||
players("targets") { // @a, プレイヤー名, などをまとめて取得
|
||||
float("value", min = 0.1, max = 5.0) {
|
||||
executes {
|
||||
val targets: List<Player> = argument("targets")
|
||||
val speed = argument<Double>("value")
|
||||
targets.forEach { it.walkSpeed = speed.toFloat() / 5.0f }
|
||||
sender.sendMessage("Updated ${targets.size} players")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
literal("setspawn") {
|
||||
coordinates("point") { // "~ ~1 ~-2" のような入力を受け付ける
|
||||
executes {
|
||||
val player = sender as? Player ?: return@executes
|
||||
val position = argument<io.papermc.paper.math.Position>("point")
|
||||
val location = position.toLocation(player.world)
|
||||
player.world.setSpawnLocation(location)
|
||||
sender.sendMessage("Spawn set to ${location.x}, ${location.y}, ${location.z}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
literal("inspect") {
|
||||
selector("entities") {
|
||||
executes {
|
||||
val entities: List<Entity> = argument("entities")
|
||||
sender.sendMessage("Selector resolved ${entities.size} entities")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDisable() {
|
||||
commands.unregister()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### DSL 構文のポイント
|
||||
|
||||
- `literal("sub") { ... }` は固定語句を表すノードです。`requires("permission.node")` でその枝のみにパーミッションを設定できます。
|
||||
- `string("name")` や `integer("value", min = 0)` は値をパースし、成功すると `KommandContext` に記憶されます。取得時は `argument<String>("name")` や `argument<Int>("value")` を呼び出してください。
|
||||
- `float("speed")` や `player("target")`/`players("targets")`/`selector("entities")` は Minecraft の標準セレクター (`@p`, `@a`, `@s` など) やプレイヤー名を型付きで扱えます。実行時は `argument<Double>("speed")`、`argument<Player>("target")`、`argument<List<Player>>("targets")` のように取得できます。
|
||||
- `suggests { prefix -> ... }` を指定すると、タブ補完時に任意の候補リストを返せます。
|
||||
- `coordinates("pos")` は `x y z` をまとめて 1 つの引数として受け取り、`argument<io.papermc.paper.math.Position>("pos")` で取得できます。`position.toLocation(world)` で `Location` に変換できます (`~` を使用した相対座標に対応)。
|
||||
- `command` や各ノードの `condition { sender -> ... }` で実行条件 (例: コンソール禁止) を追加できます。
|
||||
- ルートレベルで `executes { ... }` を指定すると、引数なしで `/eco` を実行した場合に呼び出されます。
|
||||
|
||||
## 自動パーミッション生成 (`permits-lib`)
|
||||
|
||||
`permits-lib` を `compileOnly` で参照している場合、DSL から Bukkit パーミッションツリーを自動生成できます。
|
||||
|
||||
```kotlin
|
||||
commands = kommand(this) {
|
||||
permissions {
|
||||
namespace = "hareworks"
|
||||
rootSegment = "command"
|
||||
defaultDescription { ctx ->
|
||||
"Allows /${ctx.commandName} (${ctx.path.joinToString(" ")})"
|
||||
}
|
||||
}
|
||||
|
||||
command("eco") {
|
||||
permission {
|
||||
description = "Allows /eco"
|
||||
}
|
||||
|
||||
literal("give") {
|
||||
permission {
|
||||
description = "Allows /eco give"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `permissions { ... }` で名前空間やルートセグメント (`rootSegment = "command"`) を定義すると、`hareworks.command.eco`, `hareworks.command.eco.give` のような ID が自動生成され、`permits-lib` の `MutationSession` を使って Bukkit に適用されます。
|
||||
- `literal` や `command` ノードはデフォルトで Bukkit パーミッションとして登録されます。`string` や `integer` などの引数ノードは、`requires("permission.id")` もしくは `permission { id = ... }` を記述した場合のみ登録され、それ以外は構造上のノードに留まります。これにより `/money give <player> <amount>` では `...money.give` だけ付与すれば実行できます。
|
||||
- 各ブロック内の `permission { ... }` で説明文・デフォルト値 (`PermissionDefault.TRUE/FALSE/OP/NOT_OP`)・ワイルドカード・パスを細かく制御できます。`requires(...)` を使うと DSL での検証と permits 側の登録が同じ ID になります。
|
||||
- 引数ノードを明示的に登録したい場合は `permission { id = "example.money.give.amount" }` や `requires("example.money.give.amount")` を設定してください。逆にリテラルでも不要なら `skipPermission()` で除外できます。
|
||||
- `requires("custom.id")` を指定した場合も、同じ ID が DSL の実行と `permits-lib` への登録の両方で利用されます (名前空間外の ID は登録対象外になります)。
|
||||
- `KommandLib` のライフサイクルに合わせて `MutationSession` が適用/解除されるため、プラグインの有効化・無効化に伴い Bukkit のパーミッションリストも最新状態に保たれます。
|
||||
|
||||
## 組み込み引数の一覧
|
||||
|
||||
| DSL | 返り値 | 補足 |
|
||||
| --- | --- | --- |
|
||||
| `string("name")` | `String` | 任意のトークン。`suggests {}` で補完可 |
|
||||
| `integer("value", min, max)` | `Int` | 範囲チェック付き |
|
||||
| `float("speed", min, max)` | `Double` | 小数/指数表記に対応 |
|
||||
| `player("target", allowSelectors = true)` | `Player` | `@p` などのセレクターまたはプレイヤー名を 1 人に解決 |
|
||||
| `players("targets")` | `List<Player>` | `@a`/`@r` など複数指定、プレイヤー名入力も可 |
|
||||
| `selector("entities")` | `List<Entity>` | エンティティセレクター (`@e` など) |
|
||||
| `coordinates("pos")` | `io.papermc.paper.math.Position` | `~` 相対座標を含む 3 軸をまとめて扱う |
|
||||
|
||||
`Position` は `coordinates("pos") { ... }` 直後のコンテキストで `argument<io.papermc.paper.math.Position>("pos")` として取得でき、`position.toLocation(world)` で `Location` に変換できます。
|
||||
|
||||
## クライアント側構文ヒント (Brigadier)
|
||||
|
||||
Paper 1.21 以降の環境では、`LifecycleEventManager` を通じてコマンドが登録されるため、クライアントにコマンドの構造が送信されます。これにより以下のメリットがあります:
|
||||
|
||||
- **構文の可視化**: 入力中に `<speed> <amount>` のような引数名が表示されます。
|
||||
- **クライアント側検証**: `integer("val", min=1, max=10)` などの範囲指定がクライアント側でも判定され、範囲外の値を入力すると赤字になります。
|
||||
- **互換性**: 内部的には `Brigadier` のノードに変換されますが、実際のコマンド実行は `kommand-lib` の既存ロジック(`KommandContext`)を使用するため、古いコードの修正は不要です。
|
||||
|
||||
## ビルドとテスト
|
||||
|
||||
```bash
|
||||
./gradlew build
|
||||
```
|
||||
|
||||
ShadowJar タスクが実行され、`build/libs` に出力されます。Paper サーバーに配置して動作確認してください。
|
||||
|
||||
## ドキュメント
|
||||
|
||||
- **[MIGRATION_GUIDE](./MIGRATION_GUIDE.md)** - 旧バージョンからの移行方法
|
||||
|
||||
## ライセンス
|
||||
|
||||
このプロジェクトは MIT ライセンスの下で公開されています。
|
||||
45
hcu-core/kommand-lib/build.gradle.kts
Normal file
45
hcu-core/kommand-lib/build.gradle.kts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import net.minecrell.pluginyml.paper.PaperPluginDescription
|
||||
|
||||
group = "net.hareworks"
|
||||
version = "1.1"
|
||||
|
||||
plugins {
|
||||
kotlin("jvm") version "2.2.21"
|
||||
id("de.eldoria.plugin-yml.paper") version "0.8.0"
|
||||
id("com.gradleup.shadow") version "9.2.2"
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven("https://repo.papermc.io/repository/maven-public/")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib")
|
||||
implementation("net.hareworks:permits-lib:1.1")
|
||||
}
|
||||
tasks {
|
||||
withType<Jar> {
|
||||
archiveBaseName.set("Kommand-Lib")
|
||||
}
|
||||
shadowJar {
|
||||
minimize()
|
||||
archiveClassifier.set("min")
|
||||
}
|
||||
}
|
||||
|
||||
paper {
|
||||
main = "net.hareworks.kommand_lib.plugin.Plugin"
|
||||
name = "kommand-lib"
|
||||
description = "Command library"
|
||||
version = getVersion().toString()
|
||||
apiVersion = "1.21.10"
|
||||
authors = listOf(
|
||||
"Hare-K02"
|
||||
)
|
||||
serverDependencies {
|
||||
register("permits-lib") {
|
||||
load = PaperPluginDescription.RelativeLoadOrder.BEFORE
|
||||
}
|
||||
}
|
||||
}
|
||||
61
hcu-core/kommand-lib/flake.lock
Normal file
61
hcu-core/kommand-lib/flake.lock
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1764020296,
|
||||
"narHash": "sha256-6zddwDs2n+n01l+1TG6PlyokDdXzu/oBmEejcH5L5+A=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a320ce8e6e2cc6b4397eef214d202a50a4583829",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-25.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
43
hcu-core/kommand-lib/flake.nix
Normal file
43
hcu-core/kommand-lib/flake.nix
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
description = "Minecraft dev environment with JDK 21 and Gradle";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
jdk21
|
||||
gradle
|
||||
kotlin
|
||||
git
|
||||
unzip
|
||||
];
|
||||
|
||||
# 必要に応じて環境変数を設定
|
||||
shellHook = ''
|
||||
export JAVA_HOME=${pkgs.jdk21}/lib/openjdk
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
|
||||
export GRADLE_USER_HOME="$PWD/.gradle"
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
5
hcu-core/kommand-lib/gradle.properties
Normal file
5
hcu-core/kommand-lib/gradle.properties
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
org.gradle.configuration-cache=true
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
|
||||
kotlin.stdlib.default.dependency=false
|
||||
2
hcu-core/kommand-lib/gradle/libs.versions.toml
Normal file
2
hcu-core/kommand-lib/gradle/libs.versions.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# This file was generated by the Gradle 'init' task.
|
||||
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
|
||||
BIN
hcu-core/kommand-lib/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
hcu-core/kommand-lib/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
hcu-core/kommand-lib/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
hcu-core/kommand-lib/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
251
hcu-core/kommand-lib/gradlew
vendored
Executable file
251
hcu-core/kommand-lib/gradlew
vendored
Executable file
|
|
@ -0,0 +1,251 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original 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.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# 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 ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# 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
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
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
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# 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"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
94
hcu-core/kommand-lib/gradlew.bat
vendored
Normal file
94
hcu-core/kommand-lib/gradlew.bat
vendored
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
@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
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@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=.
|
||||
@rem This is normally unused
|
||||
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% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 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!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
1
hcu-core/kommand-lib/permits-lib/.envrc
Normal file
1
hcu-core/kommand-lib/permits-lib/.envrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
use flake
|
||||
12
hcu-core/kommand-lib/permits-lib/.gitattributes
vendored
Normal file
12
hcu-core/kommand-lib/permits-lib/.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
#
|
||||
# Linux start script should use lf
|
||||
/gradlew text eol=lf
|
||||
|
||||
# These are Windows script files and should use crlf
|
||||
*.bat text eol=crlf
|
||||
|
||||
# Binary files should be left untouched
|
||||
*.jar binary
|
||||
|
||||
4
hcu-core/kommand-lib/permits-lib/.gitignore
vendored
Normal file
4
hcu-core/kommand-lib/permits-lib/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
.direnv
|
||||
.gradle
|
||||
bin
|
||||
build
|
||||
136
hcu-core/kommand-lib/permits-lib/AGENT.md
Normal file
136
hcu-core/kommand-lib/permits-lib/AGENT.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# Paper / Bukkit Permission システム解説ドキュメント
|
||||
|
||||
## 1. 基本概念
|
||||
|
||||
### Permission(権限ノード)
|
||||
- プレイヤーが「何を実行できるか」を判定するための文字列。
|
||||
- 例: `myplugin.fly`, `myplugin.home.set`
|
||||
- 権限ノードには以下の情報を持てる。
|
||||
- 説明文(description)
|
||||
- デフォルト値(default: OP / non-OP / true / false)
|
||||
- 子権限(children)
|
||||
|
||||
Paper/Bukkit は `plugin.yml` またはコードによる動的生成で権限を登録できる。
|
||||
|
||||
---
|
||||
|
||||
## 2. 権限の定義方法
|
||||
|
||||
### (1) `plugin.yml` で定義
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
myplugin.fly:
|
||||
description: "Allow /fly"
|
||||
default: op
|
||||
````
|
||||
|
||||
### (2) コードで動的に定義
|
||||
|
||||
```java
|
||||
Permission perm = new Permission("myplugin.fly", "Allow fly", PermissionDefault.OP);
|
||||
getServer().getPluginManager().addPermission(perm);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 動的付与(PermissionAttachment)
|
||||
|
||||
### PermissionAttachment の役割
|
||||
|
||||
* 特定プレイヤーに対して一時的に権限を上書きする仕組み。
|
||||
* **未定義の権限ノードでも自由に付与できる**。
|
||||
|
||||
```java
|
||||
PermissionAttachment attachment = player.addAttachment(plugin);
|
||||
attachment.setPermission("myplugin.fly", true);
|
||||
```
|
||||
|
||||
### 特徴
|
||||
|
||||
* 未定義ノードでも `player.hasPermission("xxx")` が true になる。
|
||||
* default や children などのメタ情報は存在しない。
|
||||
* サーバー再起動やログアウトで消える一時的な扱い。
|
||||
|
||||
---
|
||||
|
||||
## 4. Children(子権限)
|
||||
|
||||
### 概要
|
||||
|
||||
* 「ある権限を true にした時、自動的に他の権限も true にする仕組み」。
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
myplugin.admin:
|
||||
children:
|
||||
myplugin.fly: true
|
||||
myplugin.kick: true
|
||||
```
|
||||
|
||||
### 挙動
|
||||
|
||||
* `myplugin.admin` を持つプレイヤーは
|
||||
→ `myplugin.fly` と `myplugin.kick` も自動的に所持。
|
||||
* 再帰的に適用され、深い階層の権限にも影響する。
|
||||
* false を設定した場合は「明示的に無効」にできる。
|
||||
|
||||
### 動的 Permission に対しても有効
|
||||
|
||||
```java
|
||||
perm.getChildren().put("myplugin.fly", true);
|
||||
perm.recalculatePermissibles();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Permission 判定の仕組み
|
||||
|
||||
`player.hasPermission("xxx")` の評価順序:
|
||||
|
||||
1. **PermissionAttachment のフラグ**
|
||||
2. 登録済 Permission の children
|
||||
3. Permission の default 値(OP / non-OP など)
|
||||
4. 上記に該当しなければ false
|
||||
|
||||
attachment が最優先で反映される。
|
||||
|
||||
---
|
||||
|
||||
## 6. 未定義権限ノードの扱い
|
||||
|
||||
### 特徴
|
||||
|
||||
* 未定義でも `attachment.setPermission("xxx", true)` で有効化可能。
|
||||
* ただし:
|
||||
|
||||
* children なし
|
||||
* default なし
|
||||
* description なし
|
||||
* 権限管理プラグインで見づらい
|
||||
|
||||
### 必要に応じて動的登録を使う
|
||||
|
||||
体系的な管理が必要なら PluginManager で正式な Permission を登録することを推奨。
|
||||
|
||||
---
|
||||
|
||||
## 7. 推奨される運用方法
|
||||
|
||||
### (A) 権限の種類を柔軟に変更したい
|
||||
|
||||
→ **動的 Permission 定義**
|
||||
|
||||
* config.yml → 起動/リロード時に生成
|
||||
|
||||
### (B) プレイヤーごとの一時的な権限管理
|
||||
|
||||
→ **PermissionAttachment**
|
||||
|
||||
* 一時バフ的な権限付与に向く
|
||||
|
||||
### (C) 大規模な権限構造
|
||||
|
||||
→ **children の階層化**
|
||||
|
||||
* `myplugin.admin` → 配下の権限をまとめて管理できる
|
||||
150
hcu-core/kommand-lib/permits-lib/README.md
Normal file
150
hcu-core/kommand-lib/permits-lib/README.md
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
# permits-lib
|
||||
|
||||
Permits Lib provides a declarative way to describe Bukkit/Paper permission hierarchies using a Kotlin
|
||||
DSL. Once a tree is built you can hand it to `PermissionRegistry` (or the higher-level
|
||||
`MutationSession`) and the library will register/unregister the relevant `org.bukkit.permissions.Permission`
|
||||
instances and keep `PermissionAttachment`s in sync.
|
||||
|
||||
## Usage
|
||||
|
||||
```kotlin
|
||||
import net.hareworks.permits_lib.domain.NodeRegistration
|
||||
|
||||
class ExamplePlugin : JavaPlugin() {
|
||||
private val permits = PermitsLib.session(this)
|
||||
|
||||
override fun onEnable() {
|
||||
val tree = permissionTree("example") {
|
||||
node("command", NodeRegistration.STRUCTURAL) {
|
||||
description = "Access to all example commands"
|
||||
defaultValue = PermissionDefault.OP
|
||||
wildcard {
|
||||
exclude("cooldown") // example.command.* will skip cooldown
|
||||
}
|
||||
|
||||
node("reload", NodeRegistration.PERMISSION) {
|
||||
description = "Allows /example reload (permission example.command.reload)"
|
||||
}
|
||||
|
||||
// Link to a helper node defined elsewhere under the command branch:
|
||||
child("helper")
|
||||
|
||||
// Link to a permission outside the current branch (must be fully-qualified):
|
||||
childAbsolute("example.tools.repair")
|
||||
|
||||
node("cooldown", NodeRegistration.PERMISSION) {
|
||||
description = "Allows /example cooldown tweaks"
|
||||
}
|
||||
}
|
||||
|
||||
node("command.helper", NodeRegistration.PERMISSION) {
|
||||
description = "Allows /example helper (referenced via child(\"helper\"))"
|
||||
}
|
||||
|
||||
node("tools.repair", NodeRegistration.PERMISSION) {
|
||||
description = "Allows /example tools repair (linked with childAbsolute(\"example.tools.repair\"))"
|
||||
}
|
||||
}
|
||||
|
||||
permits.applyTree(tree)
|
||||
|
||||
// The tree above materializes as permissions such as:
|
||||
// example.command, example.command.reload, example.command.helper, example.command.cooldown,
|
||||
// example.tools.repair,
|
||||
// plus the auto-generated example.command.* wildcard (command opted in, cooldown was excluded).
|
||||
// export to plugin.yml or inspect Bukkit's /permissions output).
|
||||
|
||||
configureRuntimePermissions()
|
||||
}
|
||||
|
||||
fun grantHelper(player: Player) {
|
||||
permits.attachments.grant(player, PermissionId.of("example.command.reload"))
|
||||
}
|
||||
|
||||
private fun configureRuntimePermissions() {
|
||||
// Later in runtime you can mutate the previously applied structure without rebuilding it:
|
||||
permits.edit("example") {
|
||||
// Update an existing node and link it to new children
|
||||
node("command", NodeRegistration.STRUCTURAL) {
|
||||
description = "Admins for every command path"
|
||||
wildcard = true
|
||||
node("debug", NodeRegistration.PERMISSION) {
|
||||
description = "Allows /example debug"
|
||||
defaultValue = PermissionDefault.OP
|
||||
wildcard = true
|
||||
}
|
||||
}
|
||||
// Remove deprecated permissions entirely
|
||||
removeNode("command.cooldown")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Procedural Edits with `MutablePermissionTree`
|
||||
|
||||
If you prefer an imperative style before handing the structure to Bukkit, you can clone any existing tree,
|
||||
mutate it procedurally, and then apply the result:
|
||||
|
||||
```kotlin
|
||||
val baseTree = permissionTree("example") {
|
||||
node("command", NodeRegistration.STRUCTURAL) {
|
||||
wildcard = true
|
||||
node("reload", NodeRegistration.PERMISSION)
|
||||
}
|
||||
}
|
||||
|
||||
val mutable = MutablePermissionTree.from(baseTree)
|
||||
mutable.node("command", NodeRegistration.STRUCTURAL) {
|
||||
wildcard = true
|
||||
excludeWildcardChild("helper") // keep helper out of command.*
|
||||
node("debug", NodeRegistration.PERMISSION) {
|
||||
description = "Allows /example debug"
|
||||
defaultValue = PermissionDefault.OP
|
||||
wildcard = true
|
||||
}
|
||||
}
|
||||
mutable.removeNode("command.legacy")
|
||||
|
||||
permits.applyTree(mutable.build())
|
||||
```
|
||||
|
||||
The mutable API mirrors the DSL (`node`, `child`, `childAbsolute`, `removeNode`, `renameNode`, etc.) so you can
|
||||
stage edits procedurally before ever touching `MutationSession`.
|
||||
|
||||
### Concepts
|
||||
|
||||
- **Permission tree** – immutable graph of `PermissionNode`s. Nodes specify description, default value,
|
||||
boolean children map, and the `wildcard` flag (disabled by default) that, when enabled per node, keeps
|
||||
`namespace.path.*` aggregate permissions in sync automatically.
|
||||
- **DSL** – `permissionTree("namespace") { ... }` ensures consistent prefixes and validation (no cycles). Every `node("command", NodeRegistration.PERMISSION)` (or `.STRUCTURAL`) is relative to that namespace, so you never include the namespace manually at the root.
|
||||
- **Nested nodes** – `node("command", NodeRegistration.STRUCTURAL) { node("reload", NodeRegistration.PERMISSION) { ... } }` automatically produces
|
||||
`namespace.command` and `namespace.command.reload` plus wires the parent/child relationship so you don't
|
||||
have to repeat the full id.
|
||||
- **Flexible references** – `child("reload")`, `node("command", NodeRegistration.STRUCTURAL) { node("reload", NodeRegistration.PERMISSION) { ... } }`, or
|
||||
even `node("command.reload", NodeRegistration.PERMISSION)` inside `edit` all resolve to the same node; children are auto-created on
|
||||
first reference but you can demand explicit nodes by adding a `node` block later, and you can unlink
|
||||
specific children via `node("command", NodeRegistration.STRUCTURAL) { removeNode("cooldown") }` and the entire subtree disappears.
|
||||
- **Node registration** – `NodeRegistration.PERMISSION` materializes the node as a Bukkit permission, while `NodeRegistration.STRUCTURAL` keeps it purely for grouping (still participates in wildcard aggregation) so you can avoid ambiguous intermediate permissions like `hoge.command`.
|
||||
Nested `child(...)` calls are relative to the current node by default, while `childAbsolute(...)` now
|
||||
expects a fully-qualified permission ID (e.g., `example.tools.repair`) so you can also point at nodes in
|
||||
other namespaces.
|
||||
- **PermissionRegistry** – calculates a diff between snapshots and performs the minimum additions,
|
||||
removals, or updates via Bukkit's `PluginManager`.
|
||||
- **Wildcards** – disabled by default; opt in via `wildcard = true` or the richer `wildcard { ... }` block.
|
||||
The block automatically enables the wildcard and lets you `exclude("sub.path")` so only selected DSL
|
||||
children end up under `namespace.command.*`. Enabled nodes automatically add their wildcard descendants
|
||||
(e.g., `example.command.debug.*`) so granting the wildcard cascades to the remaining children.
|
||||
|
||||
### Selective wildcards
|
||||
|
||||
- **DSL** – call `wildcard { exclude("cooldown") }` to enable the `*. *` permission while skipping specific
|
||||
literal/argument branches. You can chain `exclude` calls and pass multi-segment paths (`exclude("debug.logs")`).
|
||||
- **Mutable tree** – after `wildcard = true`, invoke `excludeWildcardChild("helper")` (relative) or
|
||||
`excludeWildcardChildAbsolute("example.command.helper.extras")` to trim wildcard membership imperatively.
|
||||
- **Mutable edits** – `permits.edit { ... }` clones the currently registered tree, lets you mutate nodes
|
||||
imperatively, re-validates, and only pushes the structural diff to Bukkit.
|
||||
- **AttachmentSynchronizer** – manages identity-based `PermissionAttachment`s and exposes high-level
|
||||
helpers (`grant`, `revoke`, `applyPatch`).
|
||||
- **MutationSession** – ties everything together for plugins that just want to push new trees and manage
|
||||
attachments without worrying about the lower-level services.
|
||||
39
hcu-core/kommand-lib/permits-lib/build.gradle.kts
Normal file
39
hcu-core/kommand-lib/permits-lib/build.gradle.kts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import net.minecrell.pluginyml.paper.PaperPluginDescription
|
||||
|
||||
group = "net.hareworks"
|
||||
version = "1.1"
|
||||
|
||||
plugins {
|
||||
kotlin("jvm") version "2.2.21"
|
||||
id("de.eldoria.plugin-yml.paper") version "0.8.0"
|
||||
id("com.gradleup.shadow") version "9.2.2"
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven("https://repo.papermc.io/repository/maven-public/")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT")
|
||||
compileOnly("org.jetbrains.kotlin:kotlin-stdlib")
|
||||
}
|
||||
tasks {
|
||||
withType<Jar> {
|
||||
archiveBaseName.set("Permits-Lib")
|
||||
}
|
||||
shadowJar {
|
||||
minimize()
|
||||
archiveClassifier.set("min")
|
||||
}
|
||||
}
|
||||
|
||||
paper {
|
||||
main = "net.hareworks.permits_lib.plugin.Plugin"
|
||||
name = "permits-lib"
|
||||
description = "Permission Library"
|
||||
version = getVersion().toString()
|
||||
apiVersion = "1.21.10"
|
||||
authors = listOf(
|
||||
"Hare-K02"
|
||||
)
|
||||
}
|
||||
61
hcu-core/kommand-lib/permits-lib/flake.lock
Normal file
61
hcu-core/kommand-lib/permits-lib/flake.lock
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1764020296,
|
||||
"narHash": "sha256-6zddwDs2n+n01l+1TG6PlyokDdXzu/oBmEejcH5L5+A=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a320ce8e6e2cc6b4397eef214d202a50a4583829",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-25.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
43
hcu-core/kommand-lib/permits-lib/flake.nix
Normal file
43
hcu-core/kommand-lib/permits-lib/flake.nix
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
description = "Minecraft dev environment with JDK 21 and Gradle";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
jdk21
|
||||
gradle
|
||||
kotlin
|
||||
git
|
||||
unzip
|
||||
];
|
||||
|
||||
# 必要に応じて環境変数を設定
|
||||
shellHook = ''
|
||||
export JAVA_HOME=${pkgs.jdk21}/lib/openjdk
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
|
||||
export GRADLE_USER_HOME="$PWD/.gradle"
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
7
hcu-core/kommand-lib/permits-lib/gradle.properties
Normal file
7
hcu-core/kommand-lib/permits-lib/gradle.properties
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# This file was generated by the Gradle 'init' task.
|
||||
# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties
|
||||
|
||||
org.gradle.configuration-cache=true
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# This file was generated by the Gradle 'init' task.
|
||||
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
|
||||
BIN
hcu-core/kommand-lib/permits-lib/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
hcu-core/kommand-lib/permits-lib/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
hcu-core/kommand-lib/permits-lib/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
hcu-core/kommand-lib/permits-lib/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
251
hcu-core/kommand-lib/permits-lib/gradlew
vendored
Executable file
251
hcu-core/kommand-lib/permits-lib/gradlew
vendored
Executable file
|
|
@ -0,0 +1,251 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original 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.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# 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 ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# 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
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
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
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# 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"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
94
hcu-core/kommand-lib/permits-lib/gradlew.bat
vendored
Normal file
94
hcu-core/kommand-lib/permits-lib/gradlew.bat
vendored
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
@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
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@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=.
|
||||
@rem This is normally unused
|
||||
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% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 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!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
1
hcu-core/kommand-lib/permits-lib/settings.gradle.kts
Normal file
1
hcu-core/kommand-lib/permits-lib/settings.gradle.kts
Normal file
|
|
@ -0,0 +1 @@
|
|||
rootProject.name = "permits-lib"
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package net.hareworks.permits_lib
|
||||
|
||||
import net.hareworks.permits_lib.bukkit.MutationSession
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
|
||||
object PermitsLib {
|
||||
fun session(plugin: JavaPlugin): MutationSession = MutationSession.create(plugin)
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package net.hareworks.permits_lib.plugin
|
||||
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
|
||||
@Suppress("unused")
|
||||
class Plugin : JavaPlugin() {}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package net.hareworks.permits_lib.bukkit
|
||||
|
||||
import net.hareworks.permits_lib.domain.PermissionId
|
||||
|
||||
/**
|
||||
* Describes a set of attachment changes to be applied to a [Permissible].
|
||||
* `true`/`false` represent forced grant/deny, while `null` removes the override.
|
||||
*/
|
||||
data class AttachmentPatch(
|
||||
val changes: Map<PermissionId, Boolean?>
|
||||
) {
|
||||
companion object {
|
||||
val EMPTY = AttachmentPatch(emptyMap())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package net.hareworks.permits_lib.bukkit
|
||||
|
||||
import java.util.IdentityHashMap
|
||||
import net.hareworks.permits_lib.domain.PermissionId
|
||||
import net.hareworks.permits_lib.util.ThreadChecks
|
||||
import org.bukkit.permissions.PermissionAttachment
|
||||
import org.bukkit.permissions.Permissible
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
|
||||
/**
|
||||
* Manages [PermissionAttachment] instances per [Permissible], applying patches and cleaning up once no
|
||||
* overrides remain.
|
||||
*/
|
||||
class AttachmentSynchronizer(
|
||||
private val plugin: JavaPlugin
|
||||
) {
|
||||
private data class AttachmentHandle(
|
||||
val attachment: PermissionAttachment,
|
||||
val overrides: MutableMap<PermissionId, Boolean> = linkedMapOf()
|
||||
)
|
||||
|
||||
private val handles = IdentityHashMap<Permissible, AttachmentHandle>()
|
||||
|
||||
fun applyPatch(permissible: Permissible, patch: AttachmentPatch) {
|
||||
ThreadChecks.ensurePrimaryThread("AttachmentSynchronizer.applyPatch")
|
||||
if (patch.changes.isEmpty()) return
|
||||
val handle = ensureHandle(permissible)
|
||||
patch.changes.forEach { (id, value) ->
|
||||
if (value == null) {
|
||||
handle.overrides.remove(id)
|
||||
handle.attachment.unsetPermission(id.value)
|
||||
} else {
|
||||
handle.overrides[id] = value
|
||||
handle.attachment.setPermission(id.value, value)
|
||||
}
|
||||
}
|
||||
if (handle.overrides.isEmpty()) {
|
||||
release(permissible)
|
||||
}
|
||||
}
|
||||
|
||||
fun grant(permissible: Permissible, permission: PermissionId, value: Boolean = true) {
|
||||
applyPatch(permissible, AttachmentPatch(mapOf(permission to value)))
|
||||
}
|
||||
|
||||
fun revoke(permissible: Permissible, permission: PermissionId) {
|
||||
applyPatch(permissible, AttachmentPatch(mapOf(permission to null)))
|
||||
}
|
||||
|
||||
fun clear(permissible: Permissible) {
|
||||
ThreadChecks.ensurePrimaryThread("AttachmentSynchronizer.clear")
|
||||
handles.remove(permissible)?.attachment?.remove()
|
||||
}
|
||||
|
||||
fun clearAll() {
|
||||
ThreadChecks.ensurePrimaryThread("AttachmentSynchronizer.clearAll")
|
||||
handles.values.forEach { it.attachment.remove() }
|
||||
handles.clear()
|
||||
}
|
||||
|
||||
private fun ensureHandle(permissible: Permissible): AttachmentHandle =
|
||||
handles[permissible] ?: AttachmentHandle(permissible.addAttachment(plugin)).also {
|
||||
handles[permissible] = it
|
||||
}
|
||||
|
||||
private fun release(permissible: Permissible) {
|
||||
handles.remove(permissible)?.attachment?.remove()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package net.hareworks.permits_lib.bukkit
|
||||
|
||||
import net.hareworks.permits_lib.domain.MutablePermissionTree
|
||||
import net.hareworks.permits_lib.domain.PermissionTree
|
||||
import net.hareworks.permits_lib.domain.TreeDiff
|
||||
|
||||
/**
|
||||
* High-level façade that ties the registry and attachment synchronizer together.
|
||||
*/
|
||||
class MutationSession(
|
||||
private val registry: PermissionRegistry,
|
||||
val attachments: AttachmentSynchronizer
|
||||
) {
|
||||
private var tree: PermissionTree? = null
|
||||
private var diff: TreeDiff? = null
|
||||
|
||||
fun applyTree(next: PermissionTree): TreeDiff {
|
||||
val computed = registry.applyTree(next)
|
||||
tree = next
|
||||
diff = computed
|
||||
return computed
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutates the currently applied tree (must exist) and immediately applies the resulting diff to the
|
||||
* Bukkit registry.
|
||||
*/
|
||||
fun edit(block: MutablePermissionTree.() -> Unit): TreeDiff {
|
||||
val base = tree ?: error("No permission tree applied yet. Call applyTree or edit(namespace) first.")
|
||||
return editInternal(MutablePermissionTree.from(base), block)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutates the existing tree or creates a fresh one for the provided [namespace] when none was applied
|
||||
* before.
|
||||
*/
|
||||
fun edit(namespace: String, block: MutablePermissionTree.() -> Unit): TreeDiff {
|
||||
val mutable = tree?.let {
|
||||
require(it.namespace == namespace) {
|
||||
"Existing tree namespace '${it.namespace}' differs from requested '$namespace'."
|
||||
}
|
||||
MutablePermissionTree.from(it)
|
||||
} ?: MutablePermissionTree.create(namespace)
|
||||
return editInternal(mutable, block)
|
||||
}
|
||||
|
||||
private fun editInternal(
|
||||
mutable: MutablePermissionTree,
|
||||
block: MutablePermissionTree.() -> Unit
|
||||
): TreeDiff {
|
||||
mutable.block()
|
||||
val next = mutable.build()
|
||||
return applyTree(next)
|
||||
}
|
||||
|
||||
fun clearAll() {
|
||||
registry.clear()
|
||||
attachments.clearAll()
|
||||
tree = null
|
||||
diff = null
|
||||
}
|
||||
|
||||
fun currentTree(): PermissionTree? = tree
|
||||
fun lastDiff(): TreeDiff? = diff
|
||||
|
||||
companion object {
|
||||
fun create(plugin: org.bukkit.plugin.java.JavaPlugin): MutationSession =
|
||||
MutationSession(
|
||||
registry = PermissionRegistry(plugin),
|
||||
attachments = AttachmentSynchronizer(plugin)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package net.hareworks.permits_lib.bukkit
|
||||
|
||||
import net.hareworks.permits_lib.domain.PermissionNode
|
||||
import net.hareworks.permits_lib.domain.PermissionTree
|
||||
import net.hareworks.permits_lib.domain.TreeDiff
|
||||
import net.hareworks.permits_lib.domain.TreeDiffer
|
||||
import net.hareworks.permits_lib.domain.TreeSnapshot
|
||||
import net.hareworks.permits_lib.util.ThreadChecks
|
||||
import org.bukkit.permissions.Permission
|
||||
import org.bukkit.plugin.PluginManager
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
|
||||
/**
|
||||
* Registers permission nodes against Bukkit's [PluginManager], keeping track of previous state so that
|
||||
* only diffs are applied back to the server.
|
||||
*/
|
||||
class PermissionRegistry(
|
||||
private val plugin: JavaPlugin,
|
||||
private val pluginManager: PluginManager = plugin.server.pluginManager
|
||||
) {
|
||||
private var snapshot: TreeSnapshot? = null
|
||||
|
||||
fun applyTree(tree: PermissionTree): TreeDiff {
|
||||
ThreadChecks.ensurePrimaryThread("PermissionRegistry.applyTree")
|
||||
|
||||
val nextSnapshot = tree.toSnapshot()
|
||||
val diff = TreeDiffer.diff(snapshot, nextSnapshot)
|
||||
if (!diff.hasChanges) {
|
||||
return diff
|
||||
}
|
||||
|
||||
diff.removed.forEach { removeNode(it) }
|
||||
diff.added.forEach { registerNode(it) }
|
||||
diff.updated.forEach { updateNode(it) }
|
||||
|
||||
snapshot = nextSnapshot
|
||||
return diff
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
ThreadChecks.ensurePrimaryThread("PermissionRegistry.clear")
|
||||
snapshot?.nodes?.values?.forEach { removeNode(it) }
|
||||
snapshot = null
|
||||
}
|
||||
|
||||
private fun removeNode(node: PermissionNode) {
|
||||
pluginManager.getPermission(node.id.value)?.let { permission ->
|
||||
pluginManager.removePermission(permission)
|
||||
permission.recalculatePermissibles()
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerNode(node: PermissionNode) {
|
||||
val permission = Permission(node.id.value, node.description, node.defaultValue)
|
||||
permission.children.clear()
|
||||
permission.children.putAll(node.children.mapKeys { it.key.value })
|
||||
pluginManager.addPermission(permission)
|
||||
permission.recalculatePermissibles()
|
||||
}
|
||||
|
||||
private fun updateNode(updated: TreeDiff.UpdatedNode) {
|
||||
removeNode(updated.before)
|
||||
registerNode(updated.after)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
package net.hareworks.permits_lib.domain
|
||||
|
||||
import org.bukkit.permissions.PermissionDefault
|
||||
|
||||
/**
|
||||
* Imperative view over a permission tree that lets callers mutate nodes directly. Once the desired
|
||||
* modifications are complete, call [build] to obtain an immutable [PermissionTree].
|
||||
*/
|
||||
class MutablePermissionTree internal constructor(
|
||||
private val namespace: String,
|
||||
private val drafts: MutableMap<PermissionId, PermissionNodeDraft>
|
||||
) {
|
||||
fun node(id: String, registration: NodeRegistration, block: MutableNode.() -> Unit = {}): MutableNode {
|
||||
require(id.isNotBlank()) { "Node id must not be blank." }
|
||||
val permissionId = PermissionId.of("$namespace.${id.lowercase()}")
|
||||
val draft = drafts.getOrPut(permissionId) { PermissionNodeDraft(permissionId) }
|
||||
draft.registration = registration
|
||||
return MutableNode(permissionId, draft).apply(block)
|
||||
}
|
||||
|
||||
fun removeNode(id: String) {
|
||||
require(id.isNotBlank()) { "Node id must not be blank." }
|
||||
val permissionId = PermissionId.of("$namespace.${id.lowercase()}")
|
||||
removeSubtree(permissionId)
|
||||
}
|
||||
|
||||
fun renameNode(oldId: String, newId: String) {
|
||||
require(oldId.isNotBlank()) { "Old node id must not be blank." }
|
||||
require(newId.isNotBlank()) { "New node id must not be blank." }
|
||||
val oldPermissionId = PermissionId.of("$namespace.${oldId.lowercase()}")
|
||||
val newPermissionId = PermissionId.of("$namespace.${newId.lowercase()}")
|
||||
renameSubtree(oldPermissionId, newPermissionId)
|
||||
}
|
||||
|
||||
fun contains(id: String): Boolean {
|
||||
require(id.isNotBlank()) { "Node id must not be blank." }
|
||||
return drafts.containsKey(PermissionId.of("$namespace.${id.lowercase()}"))
|
||||
}
|
||||
|
||||
fun build(): PermissionTree {
|
||||
val nodes = drafts.mapValues { it.value.toNode() }
|
||||
return PermissionTree.from(namespace, nodes)
|
||||
}
|
||||
|
||||
inner class MutableNode internal constructor(
|
||||
val id: PermissionId,
|
||||
private val draft: PermissionNodeDraft
|
||||
) {
|
||||
var description: String?
|
||||
get() = draft.description
|
||||
set(value) {
|
||||
draft.description = value?.trim()
|
||||
}
|
||||
|
||||
var defaultValue: PermissionDefault
|
||||
get() = draft.defaultValue
|
||||
set(value) {
|
||||
draft.defaultValue = value
|
||||
}
|
||||
|
||||
var wildcard: Boolean
|
||||
get() = draft.wildcard
|
||||
set(value) {
|
||||
draft.wildcard = value
|
||||
}
|
||||
|
||||
var registration: NodeRegistration
|
||||
get() = draft.registration
|
||||
set(value) {
|
||||
draft.registration = value
|
||||
}
|
||||
|
||||
fun child(id: String, value: Boolean = true) {
|
||||
require(id.isNotBlank()) { "Child id must not be blank." }
|
||||
val permissionId = PermissionId.of("${this.id.value}.${id.lowercase()}")
|
||||
draft.children[permissionId] = value
|
||||
}
|
||||
|
||||
fun childAbsolute(id: String, value: Boolean = true) {
|
||||
val permissionId = PermissionId.of(id.lowercase())
|
||||
draft.children[permissionId] = value
|
||||
}
|
||||
|
||||
fun node(id: String, registration: NodeRegistration, block: MutableNode.() -> Unit = {}) {
|
||||
require(id.isNotBlank()) { "Node id must not be blank." }
|
||||
val permissionId = PermissionId.of("${this.id.value}.${id.lowercase()}")
|
||||
draft.children[permissionId] = true
|
||||
val childDraft = drafts.getOrPut(permissionId) { PermissionNodeDraft(permissionId) }
|
||||
childDraft.registration = registration
|
||||
MutableNode(permissionId, childDraft).apply(block)
|
||||
}
|
||||
|
||||
fun removeNode(id: String) {
|
||||
require(id.isNotBlank()) { "Node id must not be blank." }
|
||||
val permissionId = PermissionId.of("${this.id.value}.${id.lowercase()}")
|
||||
removeSubtree(permissionId)
|
||||
}
|
||||
|
||||
fun renameNode(oldId: String, newId: String) {
|
||||
require(oldId.isNotBlank()) { "Old node id must not be blank." }
|
||||
require(newId.isNotBlank()) { "New node id must not be blank." }
|
||||
val oldPermissionId = PermissionId.of("${this.id.value}.${oldId.lowercase()}")
|
||||
val newPermissionId = PermissionId.of("${this.id.value}.${newId.lowercase()}")
|
||||
renameSubtree(oldPermissionId, newPermissionId)
|
||||
}
|
||||
|
||||
fun excludeWildcardChild(id: String) {
|
||||
require(id.isNotBlank()) { "Wildcard exclusion id must not be blank." }
|
||||
val permissionId = PermissionId.of("${this.id.value}.${id.lowercase()}")
|
||||
draft.wildcardExclusions.add(permissionId)
|
||||
}
|
||||
|
||||
fun excludeWildcardChildAbsolute(id: String) {
|
||||
require(id.isNotBlank()) { "Wildcard exclusion id must not be blank." }
|
||||
val permissionId = PermissionId.of(id.lowercase())
|
||||
draft.wildcardExclusions.add(permissionId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeSubtree(rootId: PermissionId) {
|
||||
val prefix = "${rootId.value}."
|
||||
val targets = drafts.keys.filter { key ->
|
||||
key.value == rootId.value || key.value.startsWith(prefix)
|
||||
}.toSet()
|
||||
if (targets.isEmpty()) return
|
||||
targets.forEach { drafts.remove(it) }
|
||||
drafts.values.forEach { draft ->
|
||||
val iterator = draft.children.entries.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val entry = iterator.next()
|
||||
if (entry.key in targets) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renameSubtree(oldRoot: PermissionId, newRoot: PermissionId) {
|
||||
if (oldRoot == newRoot) return
|
||||
val prefix = "${oldRoot.value}."
|
||||
val affected = drafts.keys.filter { key ->
|
||||
key.value == oldRoot.value || key.value.startsWith(prefix)
|
||||
}
|
||||
if (affected.isEmpty()) return
|
||||
val affectedSet = affected.toSet()
|
||||
val mapping = linkedMapOf<PermissionId, PermissionId>()
|
||||
affected.forEach { oldId ->
|
||||
val suffix = oldId.value.removePrefix(oldRoot.value)
|
||||
val newValue = newRoot.value + suffix
|
||||
val newId = PermissionId.of(newValue)
|
||||
if (!affectedSet.contains(newId) && drafts.containsKey(newId)) {
|
||||
error("Cannot rename '${oldRoot.value}' to '${newRoot.value}' because '$newValue' already exists.")
|
||||
}
|
||||
mapping[oldId] = newId
|
||||
}
|
||||
|
||||
mapping.forEach { (oldId, newId) ->
|
||||
val draft = drafts.remove(oldId) ?: return@forEach
|
||||
val newDraft = PermissionNodeDraft(
|
||||
id = newId,
|
||||
description = draft.description,
|
||||
defaultValue = draft.defaultValue,
|
||||
children = draft.children.toMutableMap(),
|
||||
wildcard = draft.wildcard,
|
||||
registration = draft.registration,
|
||||
wildcardExclusions = draft.wildcardExclusions.toMutableSet()
|
||||
)
|
||||
drafts[newId] = newDraft
|
||||
}
|
||||
|
||||
drafts.values.forEach { draft ->
|
||||
val pending = mutableListOf<Pair<PermissionId, Boolean>>()
|
||||
val iterator = draft.children.entries.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val entry = iterator.next()
|
||||
val replacement = mapping[entry.key]
|
||||
if (replacement != null) {
|
||||
iterator.remove()
|
||||
pending += replacement to entry.value
|
||||
}
|
||||
}
|
||||
pending.forEach { (id, value) -> draft.children[id] = value }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(namespace: String): MutablePermissionTree =
|
||||
MutablePermissionTree(namespace.trim().lowercase(), linkedMapOf())
|
||||
|
||||
fun from(tree: PermissionTree): MutablePermissionTree =
|
||||
MutablePermissionTree(
|
||||
namespace = tree.namespace,
|
||||
drafts = tree.nodes.mapValues { PermissionNodeDraft.from(it.value) }.toMutableMap()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package net.hareworks.permits_lib.domain
|
||||
|
||||
/**
|
||||
* Declares whether a DSL node should materialize as an actual Bukkit permission or behave as a
|
||||
* purely structural placeholder (still participates in relationships/wildcards).
|
||||
*/
|
||||
enum class NodeRegistration(val registersPermission: Boolean) {
|
||||
PERMISSION(true),
|
||||
STRUCTURAL(false)
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package net.hareworks.permits_lib.domain
|
||||
|
||||
/**
|
||||
* Value object that represents a normalized Bukkit permission node identifier.
|
||||
*
|
||||
* The constructor is private to ensure every instance passes through [of] where we enforce the
|
||||
* naming constraints (lowercase alphanumeric with dots/dashes/underscores) and drop leading/trailing
|
||||
* whitespace.
|
||||
*/
|
||||
@JvmInline
|
||||
value class PermissionId private constructor(val value: String) {
|
||||
override fun toString(): String = value
|
||||
|
||||
companion object {
|
||||
private val VALID_PATTERN = Regex("""^[a-z0-9_.-]+(\.\*)?$""")
|
||||
|
||||
fun of(raw: String): PermissionId {
|
||||
val normalized = raw.trim().lowercase()
|
||||
require(normalized.isNotEmpty()) { "Permission id must not be blank." }
|
||||
require(VALID_PATTERN.matches(normalized)) {
|
||||
"Permission id '$raw' must match ${VALID_PATTERN.pattern}"
|
||||
}
|
||||
return PermissionId(normalized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package net.hareworks.permits_lib.domain
|
||||
|
||||
import org.bukkit.permissions.PermissionDefault
|
||||
|
||||
/**
|
||||
* Immutable description of a permission node in the tree.
|
||||
*
|
||||
* The [children] boolean flag behaves like Bukkit's `Permission.children` where `true` propagates a
|
||||
* grant while `false` explicitly revokes.
|
||||
*/
|
||||
data class PermissionNode(
|
||||
val id: PermissionId,
|
||||
val description: String? = null,
|
||||
val defaultValue: PermissionDefault = PermissionDefault.FALSE,
|
||||
val children: Map<PermissionId, Boolean> = emptyMap(),
|
||||
val wildcard: Boolean = false,
|
||||
val registration: NodeRegistration = NodeRegistration.PERMISSION,
|
||||
val wildcardExclusions: Set<PermissionId> = emptySet()
|
||||
) {
|
||||
init {
|
||||
require(children.keys.none { it == id }) { "Permission node cannot be a child of itself." }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package net.hareworks.permits_lib.domain
|
||||
|
||||
import org.bukkit.permissions.PermissionDefault
|
||||
|
||||
internal data class PermissionNodeDraft(
|
||||
val id: PermissionId,
|
||||
var description: String? = null,
|
||||
var defaultValue: PermissionDefault = PermissionDefault.FALSE,
|
||||
val children: MutableMap<PermissionId, Boolean> = linkedMapOf(),
|
||||
var wildcard: Boolean = false,
|
||||
var registration: NodeRegistration = NodeRegistration.PERMISSION,
|
||||
val wildcardExclusions: MutableSet<PermissionId> = linkedSetOf()
|
||||
) {
|
||||
fun toNode(): PermissionNode =
|
||||
PermissionNode(
|
||||
id = id,
|
||||
description = description,
|
||||
defaultValue = defaultValue,
|
||||
children = children.toMap(),
|
||||
wildcard = wildcard,
|
||||
registration = registration,
|
||||
wildcardExclusions = wildcardExclusions.toSet()
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun from(node: PermissionNode): PermissionNodeDraft =
|
||||
PermissionNodeDraft(
|
||||
id = node.id,
|
||||
description = node.description,
|
||||
defaultValue = node.defaultValue,
|
||||
children = node.children.toMutableMap(),
|
||||
wildcard = node.wildcard,
|
||||
registration = node.registration,
|
||||
wildcardExclusions = node.wildcardExclusions.toMutableSet()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package net.hareworks.permits_lib.domain
|
||||
|
||||
/**
|
||||
* Immutable aggregate of permission nodes.
|
||||
*/
|
||||
class PermissionTree internal constructor(
|
||||
val namespace: String,
|
||||
internal val nodes: Map<PermissionId, PermissionNode>
|
||||
) {
|
||||
init {
|
||||
require(namespace.isNotBlank()) { "Permission namespace must not be blank." }
|
||||
}
|
||||
|
||||
val size: Int get() = nodes.size
|
||||
|
||||
operator fun get(id: PermissionId): PermissionNode? = nodes[id]
|
||||
|
||||
fun toSnapshot(): TreeSnapshot =
|
||||
TreeSnapshot(nodes.filterValues { it.registration.registersPermission })
|
||||
|
||||
companion object {
|
||||
fun empty(namespace: String): PermissionTree = PermissionTree(namespace, emptyMap())
|
||||
|
||||
fun from(namespace: String, rawNodes: Map<PermissionId, PermissionNode>): PermissionTree {
|
||||
val augmented = WildcardAugmentor.apply(rawNodes)
|
||||
PermissionTreeValidator.validate(augmented)
|
||||
return PermissionTree(namespace, augmented)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package net.hareworks.permits_lib.domain
|
||||
|
||||
internal object PermissionTreeValidator {
|
||||
fun validate(nodes: Map<PermissionId, PermissionNode>) {
|
||||
checkForCycles(nodes)
|
||||
}
|
||||
|
||||
private fun checkForCycles(nodes: Map<PermissionId, PermissionNode>) {
|
||||
val visiting = mutableSetOf<PermissionId>()
|
||||
val visited = mutableSetOf<PermissionId>()
|
||||
|
||||
fun dfs(id: PermissionId) {
|
||||
if (!visiting.add(id)) {
|
||||
error("Detected cycle that includes permission '${id.value}'")
|
||||
}
|
||||
val node = nodes[id] ?: return
|
||||
for (child in node.children.keys) {
|
||||
if (child !in visited) {
|
||||
dfs(child)
|
||||
}
|
||||
}
|
||||
visiting.remove(id)
|
||||
visited.add(id)
|
||||
}
|
||||
|
||||
nodes.keys.forEach { id ->
|
||||
if (id !in visited) {
|
||||
dfs(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package net.hareworks.permits_lib.domain
|
||||
|
||||
data class TreeDiff(
|
||||
val added: List<PermissionNode>,
|
||||
val removed: List<PermissionNode>,
|
||||
val updated: List<UpdatedNode>
|
||||
) {
|
||||
val hasChanges: Boolean
|
||||
get() = added.isNotEmpty() || removed.isNotEmpty() || updated.isNotEmpty()
|
||||
|
||||
data class UpdatedNode(val before: PermissionNode, val after: PermissionNode)
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package net.hareworks.permits_lib.domain
|
||||
|
||||
object TreeDiffer {
|
||||
fun diff(previous: TreeSnapshot?, next: TreeSnapshot): TreeDiff {
|
||||
val prevNodes = previous?.nodes.orEmpty()
|
||||
val nextNodes = next.nodes
|
||||
|
||||
val added = mutableListOf<PermissionNode>()
|
||||
val removed = mutableListOf<PermissionNode>()
|
||||
val updated = mutableListOf<TreeDiff.UpdatedNode>()
|
||||
|
||||
val allKeys = (prevNodes.keys + nextNodes.keys).toSet()
|
||||
for (key in allKeys) {
|
||||
val before = prevNodes[key]
|
||||
val after = nextNodes[key]
|
||||
when {
|
||||
before == null && after != null -> added += after
|
||||
before != null && after == null -> removed += before
|
||||
before != null && after != null && before != after -> updated += TreeDiff.UpdatedNode(before, after)
|
||||
}
|
||||
}
|
||||
|
||||
return TreeDiff(
|
||||
added = added.sortedBy { it.id.value },
|
||||
removed = removed.sortedBy { it.id.value },
|
||||
updated = updated.sortedBy { it.after.id.value }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package net.hareworks.permits_lib.domain
|
||||
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* Snapshot of a tree at a specific point in time. Holds a deterministic digest useful for caching.
|
||||
*/
|
||||
class TreeSnapshot internal constructor(
|
||||
internal val nodes: Map<PermissionId, PermissionNode>
|
||||
) {
|
||||
val digest: String = computeDigest(nodes)
|
||||
|
||||
companion object {
|
||||
val EMPTY = TreeSnapshot(emptyMap())
|
||||
|
||||
private fun computeDigest(nodes: Map<PermissionId, PermissionNode>): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
nodes.entries
|
||||
.sortedBy { it.key.value }
|
||||
.forEach { (id, node) ->
|
||||
digest.update(id.value.toByteArray())
|
||||
digest.update(node.description.orEmpty().toByteArray())
|
||||
digest.update(node.defaultValue.name.toByteArray())
|
||||
node.children.toSortedMap(compareBy { it.value }).forEach { (childId, flag) ->
|
||||
digest.update(childId.value.toByteArray())
|
||||
digest.update(if (flag) 1 else 0)
|
||||
}
|
||||
digest.update(if (node.wildcard) 1 else 0)
|
||||
digest.update(node.registration.name.toByteArray())
|
||||
}
|
||||
return digest.digest().joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package net.hareworks.permits_lib.domain
|
||||
|
||||
import org.bukkit.permissions.PermissionDefault
|
||||
|
||||
internal object WildcardAugmentor {
|
||||
fun apply(nodes: Map<PermissionId, PermissionNode>): Map<PermissionId, PermissionNode> {
|
||||
if (nodes.isEmpty()) return nodes
|
||||
val result = nodes.toMutableMap()
|
||||
|
||||
nodes.values.forEach { node ->
|
||||
if (!node.wildcard) return@forEach
|
||||
if (node.id.value.endsWith(".*")) return@forEach
|
||||
|
||||
val wildcardId = PermissionId.of("${node.id.value}.*")
|
||||
val updatedChildren = node.children
|
||||
.filterKeys { childId -> childId !in node.wildcardExclusions }
|
||||
.toMutableMap()
|
||||
|
||||
val existing = result[wildcardId]
|
||||
if (existing == null) {
|
||||
result[wildcardId] = PermissionNode(
|
||||
id = wildcardId,
|
||||
description = "Wildcard for ${node.id.value}",
|
||||
defaultValue = node.defaultValue,
|
||||
children = updatedChildren,
|
||||
wildcard = false
|
||||
)
|
||||
} else {
|
||||
result[wildcardId] = existing.copy(children = updatedChildren)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun parentWildcardId(id: PermissionId): PermissionId? {
|
||||
val value = id.value
|
||||
val lastDot = value.lastIndexOf('.')
|
||||
if (lastDot <= 0) return null
|
||||
val parent = value.substring(0, lastDot)
|
||||
return PermissionId.of("$parent.*")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package net.hareworks.permits_lib.dsl
|
||||
|
||||
@DslMarker
|
||||
annotation class PermissionDsl
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package net.hareworks.permits_lib.dsl
|
||||
|
||||
import net.hareworks.permits_lib.domain.NodeRegistration
|
||||
import net.hareworks.permits_lib.domain.PermissionId
|
||||
import net.hareworks.permits_lib.domain.PermissionNodeDraft
|
||||
import org.bukkit.permissions.PermissionDefault
|
||||
|
||||
@PermissionDsl
|
||||
class PermissionNodeBuilder internal constructor(
|
||||
private val treeBuilder: PermissionTreeBuilder,
|
||||
private val draft: PermissionNodeDraft
|
||||
) {
|
||||
var description: String?
|
||||
get() = draft.description
|
||||
set(value) {
|
||||
draft.description = value?.trim()
|
||||
}
|
||||
|
||||
var defaultValue: PermissionDefault
|
||||
get() = draft.defaultValue
|
||||
set(value) {
|
||||
draft.defaultValue = value
|
||||
}
|
||||
|
||||
var wildcard: Boolean
|
||||
get() = draft.wildcard
|
||||
set(value) {
|
||||
draft.wildcard = value
|
||||
}
|
||||
|
||||
fun wildcard(block: WildcardDsl.() -> Unit) {
|
||||
wildcard = true
|
||||
WildcardDsl(draft).apply(block)
|
||||
}
|
||||
|
||||
var registration: NodeRegistration
|
||||
get() = draft.registration
|
||||
set(value) {
|
||||
draft.registration = value
|
||||
}
|
||||
|
||||
fun child(id: String, value: Boolean = true) {
|
||||
treeBuilder.childRelative(draft, id, value)
|
||||
}
|
||||
|
||||
fun child(id: PermissionId, value: Boolean = true) {
|
||||
treeBuilder.childAbsolute(draft, id.value, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Links to a fully-qualified permission id. The provided [id] must already include its namespace.
|
||||
*/
|
||||
fun childAbsolute(id: String, value: Boolean = true) {
|
||||
treeBuilder.childAbsolute(draft, id, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Declares a nested node whose id is derived from the current node:
|
||||
*
|
||||
* ```
|
||||
* node("command", NodeRegistration.STRUCTURAL) {
|
||||
* node("reload", NodeRegistration.PERMISSION) { ... } // -> namespace.command.reload
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
fun node(
|
||||
id: String,
|
||||
registration: NodeRegistration,
|
||||
block: PermissionNodeBuilder.() -> Unit = {}
|
||||
) {
|
||||
treeBuilder.nestedNode(draft, id, registration, block)
|
||||
}
|
||||
}
|
||||
|
||||
class WildcardDsl internal constructor(
|
||||
private val draft: PermissionNodeDraft
|
||||
) {
|
||||
fun exclude(vararg segments: String) {
|
||||
val normalized = segments
|
||||
.flatMap { it.split('.') }
|
||||
.map { it.trim().lowercase() }
|
||||
.filter { it.isNotEmpty() }
|
||||
if (normalized.isEmpty()) return
|
||||
val suffix = normalized.joinToString(".")
|
||||
val permissionId = PermissionId.of("${draft.id.value}.$suffix")
|
||||
draft.wildcardExclusions.add(permissionId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package net.hareworks.permits_lib.dsl
|
||||
|
||||
import net.hareworks.permits_lib.domain.NodeRegistration
|
||||
import net.hareworks.permits_lib.domain.PermissionId
|
||||
import net.hareworks.permits_lib.domain.PermissionNodeDraft
|
||||
import net.hareworks.permits_lib.domain.PermissionTree
|
||||
|
||||
@PermissionDsl
|
||||
class PermissionTreeBuilder internal constructor(
|
||||
private val namespace: String
|
||||
) {
|
||||
private val drafts = linkedMapOf<PermissionId, PermissionNodeDraft>()
|
||||
|
||||
fun node(
|
||||
id: String,
|
||||
registration: NodeRegistration,
|
||||
block: PermissionNodeBuilder.() -> Unit = {}
|
||||
) {
|
||||
require(id.isNotBlank()) { "Node id must not be blank." }
|
||||
val permissionId = PermissionId.of("$namespace.${id.lowercase()}")
|
||||
val draft = drafts.getOrPut(permissionId) { PermissionNodeDraft(permissionId) }
|
||||
draft.registration = registration
|
||||
PermissionNodeBuilder(this, draft).apply(block)
|
||||
}
|
||||
|
||||
internal fun child(
|
||||
parent: PermissionNodeDraft,
|
||||
id: String,
|
||||
value: Boolean,
|
||||
relative: Boolean
|
||||
) {
|
||||
val target = if (relative) {
|
||||
require(id.isNotBlank()) { "Child id must not be blank." }
|
||||
"${parent.id.value}.${id.lowercase()}"
|
||||
} else {
|
||||
normalizeAbsolute(id)
|
||||
}
|
||||
val permissionId = PermissionId.of(target)
|
||||
parent.children[permissionId] = value
|
||||
}
|
||||
|
||||
internal fun childRelative(
|
||||
parent: PermissionNodeDraft,
|
||||
id: String,
|
||||
value: Boolean
|
||||
) = child(parent, id, value, relative = true)
|
||||
|
||||
internal fun childAbsolute(
|
||||
parent: PermissionNodeDraft,
|
||||
id: String,
|
||||
value: Boolean
|
||||
) = child(parent, id, value, relative = false)
|
||||
|
||||
internal fun nestedNode(
|
||||
parent: PermissionNodeDraft,
|
||||
id: String,
|
||||
registration: NodeRegistration,
|
||||
block: PermissionNodeBuilder.() -> Unit
|
||||
) {
|
||||
require(id.isNotBlank()) { "Nested node id must not be blank." }
|
||||
val composedId = PermissionId.of("${parent.id.value}.${id.lowercase()}")
|
||||
parent.children[composedId] = true
|
||||
val draft = drafts.getOrPut(composedId) { PermissionNodeDraft(composedId) }
|
||||
draft.registration = registration
|
||||
PermissionNodeBuilder(this, draft).apply(block)
|
||||
}
|
||||
|
||||
fun build(): PermissionTree =
|
||||
PermissionTree.from(namespace, drafts.mapValues { it.value.toNode() })
|
||||
|
||||
private fun normalizeAbsolute(id: String): String {
|
||||
require(id.isNotBlank()) { "Absolute permission id must not be blank." }
|
||||
return id.lowercase()
|
||||
}
|
||||
}
|
||||
|
||||
fun permissionTree(namespace: String, block: PermissionTreeBuilder.() -> Unit): PermissionTree =
|
||||
PermissionTreeBuilder(namespace.trim().lowercase()).apply(block).build()
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package net.hareworks.permits_lib.util
|
||||
|
||||
import org.bukkit.Bukkit
|
||||
|
||||
internal object ThreadChecks {
|
||||
fun ensurePrimaryThread(action: String) {
|
||||
check(Bukkit.isPrimaryThread()) {
|
||||
"$action must be invoked from the primary server thread."
|
||||
}
|
||||
}
|
||||
}
|
||||
3
hcu-core/kommand-lib/settings.gradle.kts
Normal file
3
hcu-core/kommand-lib/settings.gradle.kts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
rootProject.name = "kommand-lib"
|
||||
|
||||
includeBuild("permits-lib")
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package net.hareworks.kommand_lib
|
||||
|
||||
import net.hareworks.kommand_lib.context.KommandContext
|
||||
import net.hareworks.kommand_lib.dsl.KommandRegistry
|
||||
import net.hareworks.kommand_lib.permissions.PermissionOptions
|
||||
import net.hareworks.kommand_lib.permissions.PermissionRuntime
|
||||
import org.bukkit.command.CommandSender
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
import org.bukkit.plugin.Plugin
|
||||
|
||||
fun kommand(plugin: JavaPlugin, block: KommandRegistry.() -> Unit): KommandLib {
|
||||
val registry = KommandRegistry(plugin)
|
||||
registry.block()
|
||||
return registry.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages the lifecycle of the commands registered through the DSL.
|
||||
*/
|
||||
class KommandLib internal constructor(
|
||||
private val plugin: JavaPlugin,
|
||||
private val definitions: List<CommandDefinition>,
|
||||
private val permissionRuntime: PermissionRuntime?
|
||||
) {
|
||||
init {
|
||||
registerAll()
|
||||
}
|
||||
|
||||
private fun registerAll() {
|
||||
val manager = plugin.lifecycleManager
|
||||
@Suppress("UnstableApiUsage")
|
||||
manager.registerEventHandler(io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents.COMMANDS) { event ->
|
||||
val registrar = event.registrar()
|
||||
for (definition in definitions) {
|
||||
// Compile the definition to a Brigadier LiteralArgumentBuilder
|
||||
val node = TreeCompiler.compile(plugin, definition)
|
||||
registrar.register(node.build(), definition.description, definition.aliases)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unregister() {
|
||||
// Lifecycle API handles unregistration automatically on disable usually?
|
||||
// Or we might need to verify if manual unregistration is needed.
|
||||
// For now, clearing local state.
|
||||
// Note: Paper Lifecycle API doesn't expose easy unregister for static commands registered in 'COMMANDS' event usually,
|
||||
// it rebuilds the dispatcher on reload.
|
||||
permissionRuntime?.clear()
|
||||
}
|
||||
}
|
||||
|
||||
internal data class CommandDefinition(
|
||||
val name: String,
|
||||
val aliases: List<String>,
|
||||
val description: String?,
|
||||
val usage: String?,
|
||||
var permission: String?,
|
||||
val rootCondition: (CommandSender) -> Boolean,
|
||||
val rootExecutor: (KommandContext.() -> Unit)?,
|
||||
val nodes: List<net.hareworks.kommand_lib.nodes.KommandNode>,
|
||||
val permissionOptions: PermissionOptions
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package net.hareworks.kommand_lib.plugin;
|
||||
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
|
||||
@Suppress("unused")
|
||||
public class Plugin : JavaPlugin() {}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package net.hareworks.kommand_lib
|
||||
|
||||
import com.mojang.brigadier.builder.ArgumentBuilder
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder
|
||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder
|
||||
import com.mojang.brigadier.context.CommandContext
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder
|
||||
import com.mojang.brigadier.tree.CommandNode
|
||||
import io.papermc.paper.command.brigadier.CommandSourceStack
|
||||
import io.papermc.paper.command.brigadier.Commands
|
||||
import net.hareworks.kommand_lib.context.KommandContext
|
||||
import net.hareworks.kommand_lib.nodes.KommandNode
|
||||
import net.hareworks.kommand_lib.nodes.LiteralNode
|
||||
import net.hareworks.kommand_lib.nodes.ValueNode
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
internal object TreeCompiler {
|
||||
|
||||
fun compile(
|
||||
plugin: JavaPlugin,
|
||||
definition: CommandDefinition
|
||||
): LiteralArgumentBuilder<CommandSourceStack> {
|
||||
val root = Commands.literal(definition.name)
|
||||
.requires { source -> definition.rootCondition(source.sender) }
|
||||
|
||||
// Root execution
|
||||
definition.rootExecutor?.let { executor ->
|
||||
root.executes { ctx ->
|
||||
val context = KommandContext(plugin, ctx)
|
||||
executor(context)
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
// Children
|
||||
definition.nodes.forEach { child ->
|
||||
compileNode(plugin, child)?.let { root.then(it) }
|
||||
}
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
private fun compileNode(
|
||||
plugin: JavaPlugin,
|
||||
node: KommandNode
|
||||
): ArgumentBuilder<CommandSourceStack, *>? {
|
||||
val builder = when (node) {
|
||||
is LiteralNode -> {
|
||||
Commands.literal(node.literal)
|
||||
}
|
||||
is ValueNode<*> -> {
|
||||
val argType = node.argument.build()
|
||||
Commands.argument(node.name, argType)
|
||||
}
|
||||
else -> return null
|
||||
}
|
||||
|
||||
builder.requires { source -> node.isVisible(source.sender) }
|
||||
|
||||
// Execution
|
||||
node.executor?.let { executor ->
|
||||
builder.executes { ctx ->
|
||||
val context = KommandContext(plugin, ctx)
|
||||
executor(context)
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Suggestions (if any)
|
||||
if (node is ValueNode<*> && node.suggestionProvider != null && builder is RequiredArgumentBuilder<*, *>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(builder as RequiredArgumentBuilder<CommandSourceStack, Any>).suggests { ctx: CommandContext<CommandSourceStack>, suggestionsBuilder: SuggestionsBuilder ->
|
||||
val context = KommandContext(plugin, ctx)
|
||||
val suggestions = node.suggestionProvider!!.invoke(context, suggestionsBuilder.remaining)
|
||||
suggestions.forEach { suggestionsBuilder.suggest(it) }
|
||||
suggestionsBuilder.buildFuture()
|
||||
}
|
||||
}
|
||||
|
||||
// Recursion
|
||||
node.children.forEach { child ->
|
||||
compileNode(plugin, child)?.let { builder.then(it) }
|
||||
}
|
||||
|
||||
return builder
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package net.hareworks.kommand_lib.arguments
|
||||
|
||||
import com.mojang.brigadier.arguments.ArgumentType
|
||||
import com.mojang.brigadier.arguments.BoolArgumentType
|
||||
import com.mojang.brigadier.arguments.DoubleArgumentType
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType
|
||||
import com.mojang.brigadier.arguments.StringArgumentType
|
||||
import io.papermc.paper.command.brigadier.argument.ArgumentTypes
|
||||
import io.papermc.paper.command.brigadier.argument.resolvers.selector.PlayerSelectorArgumentResolver
|
||||
import io.papermc.paper.command.brigadier.argument.resolvers.selector.EntitySelectorArgumentResolver
|
||||
import org.bukkit.entity.Entity
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.util.Vector
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.command.CommandSender
|
||||
|
||||
/**
|
||||
* A holder for the Brigadier ArgumentType and any metadata needed for the DSL.
|
||||
*
|
||||
* Note: T represents the final type that users will receive in KommandContext.argument<T>(),
|
||||
* not necessarily the raw Brigadier return type. For example, PlayerArgument has T=Player,
|
||||
* but Brigadier returns PlayerSelectorArgumentResolver which is resolved to Player by ArgumentResolver.
|
||||
*/
|
||||
interface KommandArgument<T> {
|
||||
fun build(): ArgumentType<*>
|
||||
}
|
||||
|
||||
class WordArgument : KommandArgument<String> {
|
||||
override fun build(): ArgumentType<String> = StringArgumentType.word()
|
||||
}
|
||||
|
||||
class GreedyStringArgument : KommandArgument<String> {
|
||||
override fun build(): ArgumentType<String> = StringArgumentType.greedyString()
|
||||
}
|
||||
|
||||
|
||||
class IntegerArgument(
|
||||
private val min: Int = Int.MIN_VALUE,
|
||||
private val max: Int = Int.MAX_VALUE
|
||||
) : KommandArgument<Int> {
|
||||
override fun build(): ArgumentType<Int> = IntegerArgumentType.integer(min, max)
|
||||
}
|
||||
|
||||
class FloatArgument(
|
||||
private val min: Double = -Double.MAX_VALUE,
|
||||
private val max: Double = Double.MAX_VALUE
|
||||
) : KommandArgument<Double> {
|
||||
override fun build(): ArgumentType<Double> = DoubleArgumentType.doubleArg(min, max)
|
||||
}
|
||||
|
||||
class BooleanArgument : KommandArgument<Boolean> {
|
||||
override fun build(): ArgumentType<Boolean> = BoolArgumentType.bool()
|
||||
}
|
||||
|
||||
/**
|
||||
* Single player argument. Returns a Player object after resolving the selector.
|
||||
* Supports player names and selectors like @p, @s, @r[limit=1].
|
||||
*/
|
||||
class PlayerArgument : KommandArgument<Player> {
|
||||
override fun build(): ArgumentType<PlayerSelectorArgumentResolver> = ArgumentTypes.player()
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiple players argument. Returns a List<Player> after resolving the selector.
|
||||
* Supports player names and selectors like @a, @r.
|
||||
*/
|
||||
class PlayersArgument : KommandArgument<List<Player>> {
|
||||
override fun build(): ArgumentType<PlayerSelectorArgumentResolver> = ArgumentTypes.players()
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity selector argument. Returns a List<Entity> after resolving the selector.
|
||||
* Supports all entity selectors like @e, @e[type=minecraft:zombie].
|
||||
*/
|
||||
class EntityArgument : KommandArgument<List<Entity>> {
|
||||
override fun build(): ArgumentType<EntitySelectorArgumentResolver> = ArgumentTypes.entities()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fine position argument for coordinates with decimal precision.
|
||||
* Supports relative coordinates like ~ ~1 ~-2.
|
||||
* Returns a Position (io.papermc.paper.math.Position) after resolving.
|
||||
*/
|
||||
class CoordinatesArgument : KommandArgument<io.papermc.paper.math.Position> {
|
||||
override fun build(): ArgumentType<*> = ArgumentTypes.finePosition()
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package net.hareworks.kommand_lib.context
|
||||
|
||||
import com.mojang.brigadier.context.CommandContext
|
||||
import io.papermc.paper.command.brigadier.CommandSourceStack
|
||||
import io.papermc.paper.command.brigadier.argument.resolvers.selector.PlayerSelectorArgumentResolver
|
||||
import io.papermc.paper.command.brigadier.argument.resolvers.selector.EntitySelectorArgumentResolver
|
||||
import io.papermc.paper.command.brigadier.argument.resolvers.FinePositionResolver
|
||||
import io.papermc.paper.command.brigadier.argument.resolvers.BlockPositionResolver
|
||||
import io.papermc.paper.math.Position
|
||||
import org.bukkit.entity.Entity
|
||||
import org.bukkit.entity.Player
|
||||
|
||||
/**
|
||||
* Internal helper to resolve Brigadier argument types to their actual values.
|
||||
* This handles the conversion from Paper's resolver types to concrete Bukkit types.
|
||||
*
|
||||
* Note: This is public because it's called from inline functions in KommandContext,
|
||||
* but it's not intended for direct use by library consumers.
|
||||
*/
|
||||
object ArgumentResolver {
|
||||
|
||||
/**
|
||||
* Resolves an argument from the command context.
|
||||
* Handles special cases for Paper's selector resolvers and position resolvers.
|
||||
*/
|
||||
inline fun <reified T> resolve(context: CommandContext<CommandSourceStack>, name: String): T {
|
||||
val rawValue = context.getArgument(name, Any::class.java)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return when {
|
||||
// Single player selector
|
||||
T::class.java == Player::class.java && rawValue is PlayerSelectorArgumentResolver -> {
|
||||
rawValue.resolve(context.source).firstOrNull() as T
|
||||
?: throw IllegalStateException("Player selector '$name' did not resolve to any player")
|
||||
}
|
||||
|
||||
// Multiple players selector
|
||||
T::class.java == List::class.java && rawValue is PlayerSelectorArgumentResolver -> {
|
||||
rawValue.resolve(context.source) as T
|
||||
}
|
||||
|
||||
// Entity selector
|
||||
T::class.java == List::class.java && rawValue is EntitySelectorArgumentResolver -> {
|
||||
rawValue.resolve(context.source) as T
|
||||
}
|
||||
|
||||
// Fine position (coordinates with decimals)
|
||||
rawValue is FinePositionResolver -> {
|
||||
rawValue.resolve(context.source) as T
|
||||
}
|
||||
|
||||
// Block position (integer coordinates)
|
||||
rawValue is BlockPositionResolver -> {
|
||||
rawValue.resolve(context.source) as T
|
||||
}
|
||||
|
||||
// All other types (primitives, strings, etc.)
|
||||
else -> {
|
||||
context.getArgument(name, T::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves an argument or returns null if not found.
|
||||
*/
|
||||
inline fun <reified T> resolveOrNull(context: CommandContext<CommandSourceStack>, name: String): T? {
|
||||
return try {
|
||||
resolve<T>(context, name)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
null
|
||||
} catch (e: IllegalStateException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package net.hareworks.kommand_lib.context
|
||||
|
||||
import com.mojang.brigadier.context.CommandContext
|
||||
import io.papermc.paper.command.brigadier.CommandSourceStack
|
||||
import org.bukkit.command.CommandSender
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
|
||||
class KommandContext internal constructor(
|
||||
val plugin: JavaPlugin,
|
||||
val internal: CommandContext<CommandSourceStack>
|
||||
) {
|
||||
val sender: CommandSender
|
||||
get() = internal.source.sender
|
||||
|
||||
val commandSource: CommandSourceStack
|
||||
get() = internal.source
|
||||
|
||||
inline fun <reified T> argument(name: String): T {
|
||||
return ArgumentResolver.resolve(internal, name)
|
||||
}
|
||||
|
||||
inline fun <reified T> argumentOrNull(name: String): T? {
|
||||
return ArgumentResolver.resolveOrNull(internal, name)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
package net.hareworks.kommand_lib.dsl
|
||||
|
||||
import net.hareworks.kommand_lib.CommandDefinition
|
||||
import net.hareworks.kommand_lib.arguments.*
|
||||
import net.hareworks.kommand_lib.permissions.PermissionConfigBuilder
|
||||
import net.hareworks.kommand_lib.permissions.PermissionOptions
|
||||
import net.hareworks.kommand_lib.permissions.PermissionPlanner
|
||||
import net.hareworks.kommand_lib.permissions.PermissionRuntime
|
||||
import net.hareworks.kommand_lib.nodes.KommandNode
|
||||
import net.hareworks.kommand_lib.nodes.LiteralNode
|
||||
import net.hareworks.kommand_lib.nodes.ValueNode
|
||||
import org.bukkit.command.CommandSender
|
||||
import org.bukkit.entity.Entity
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
|
||||
@KommandDsl
|
||||
class KommandRegistry internal constructor(private val plugin: JavaPlugin) {
|
||||
private val definitions = mutableListOf<CommandDefinition>()
|
||||
private var permissionConfigBuilder: PermissionConfigBuilder? = null
|
||||
|
||||
fun command(name: String, vararg aliases: String, block: CommandBuilder.() -> Unit) {
|
||||
val builder = CommandBuilder(name, aliases.toList())
|
||||
builder.block()
|
||||
definitions += builder.build()
|
||||
}
|
||||
|
||||
fun command(name: String, aliases: Iterable<String>, block: CommandBuilder.() -> Unit) {
|
||||
val builder = CommandBuilder(name, aliases.toList())
|
||||
builder.block()
|
||||
definitions += builder.build()
|
||||
}
|
||||
|
||||
fun permissions(block: PermissionConfigBuilder.() -> Unit) {
|
||||
val builder = permissionConfigBuilder ?: PermissionConfigBuilder(plugin).also { permissionConfigBuilder = it }
|
||||
builder.block()
|
||||
}
|
||||
|
||||
internal fun build(): net.hareworks.kommand_lib.KommandLib {
|
||||
val snapshot = definitions.toList()
|
||||
val config = permissionConfigBuilder?.build()
|
||||
val runtime = config?.let {
|
||||
val plan = PermissionPlanner(plugin, it, snapshot).plan()
|
||||
if (plan.isEmpty()) null else PermissionRuntime(plugin, plan)
|
||||
}
|
||||
return net.hareworks.kommand_lib.KommandLib(plugin, snapshot, runtime)
|
||||
}
|
||||
}
|
||||
|
||||
@KommandDsl
|
||||
class CommandBuilder internal constructor(
|
||||
val name: String,
|
||||
val aliases: List<String>
|
||||
) : BranchScope(mutableListOf()) {
|
||||
var description: String? = null
|
||||
var usage: String? = null
|
||||
var permission: String? = null
|
||||
set(value) {
|
||||
field = value
|
||||
if (!value.isNullOrBlank()) {
|
||||
permissionOptions.id = value
|
||||
}
|
||||
}
|
||||
val permissionOptions: PermissionOptions = PermissionOptions()
|
||||
|
||||
private var condition: (CommandSender) -> Boolean = { true }
|
||||
private var rootExecutor: (net.hareworks.kommand_lib.context.KommandContext.() -> Unit)? = null
|
||||
|
||||
fun condition(predicate: (CommandSender) -> Boolean) {
|
||||
condition = predicate
|
||||
}
|
||||
|
||||
fun executes(block: net.hareworks.kommand_lib.context.KommandContext.() -> Unit) {
|
||||
rootExecutor = block
|
||||
}
|
||||
|
||||
override val inheritedPermission: String?
|
||||
get() = permission
|
||||
|
||||
override val inheritedCondition: (CommandSender) -> Boolean
|
||||
get() = condition
|
||||
|
||||
fun permission(block: PermissionOptions.() -> Unit) {
|
||||
permissionOptions.block()
|
||||
}
|
||||
|
||||
fun skipPermission() {
|
||||
permissionOptions.skipPermission()
|
||||
}
|
||||
|
||||
internal fun build(): CommandDefinition =
|
||||
CommandDefinition(
|
||||
name = name,
|
||||
aliases = aliases,
|
||||
description = description,
|
||||
usage = usage,
|
||||
permission = permission,
|
||||
rootCondition = condition,
|
||||
rootExecutor = rootExecutor,
|
||||
nodes = children.toList(),
|
||||
permissionOptions = permissionOptions
|
||||
)
|
||||
}
|
||||
|
||||
@KommandDsl
|
||||
abstract class BranchScope internal constructor(
|
||||
protected val children: MutableList<KommandNode>
|
||||
) {
|
||||
protected abstract val inheritedPermission: String?
|
||||
protected abstract val inheritedCondition: (CommandSender) -> Boolean
|
||||
|
||||
fun literal(name: String, block: LiteralBuilder.() -> Unit = {}) {
|
||||
val node = LiteralNode(name)
|
||||
node.permission = inheritedPermission
|
||||
node.condition = inheritedCondition
|
||||
children += node
|
||||
LiteralBuilder(node).apply(block)
|
||||
}
|
||||
|
||||
fun <T> argument(name: String, type: KommandArgument<T>, block: ValueBuilder<T>.() -> Unit = {}) {
|
||||
val node = ValueNode(name, type)
|
||||
node.permission = inheritedPermission
|
||||
node.condition = inheritedCondition
|
||||
node.permissionOptions.preferSkipByDefault = true
|
||||
children += node
|
||||
ValueBuilder(node).apply(block)
|
||||
}
|
||||
|
||||
fun string(name: String, block: ValueBuilder<String>.() -> Unit = {}) = argument(name, WordArgument(), block)
|
||||
|
||||
fun greedyString(name: String, block: ValueBuilder<String>.() -> Unit = {}) = argument(name, GreedyStringArgument(), block)
|
||||
|
||||
|
||||
fun integer(
|
||||
name: String,
|
||||
min: Int = Int.MIN_VALUE,
|
||||
max: Int = Int.MAX_VALUE,
|
||||
block: ValueBuilder<Int>.() -> Unit = {}
|
||||
) = argument(name, IntegerArgument(min, max), block)
|
||||
|
||||
fun float(
|
||||
name: String,
|
||||
min: Double = -Double.MAX_VALUE,
|
||||
max: Double = Double.MAX_VALUE,
|
||||
block: ValueBuilder<Double>.() -> Unit = {}
|
||||
) = argument(name, FloatArgument(min, max), block)
|
||||
|
||||
fun bool(
|
||||
name: String,
|
||||
block: ValueBuilder<Boolean>.() -> Unit = {}
|
||||
) = argument(name, BooleanArgument(), block)
|
||||
|
||||
fun player(
|
||||
name: String,
|
||||
allowSelectors: Boolean = true, // Ignored logic-wise if using native, assuming it handles selectors
|
||||
block: ValueBuilder<Player>.() -> Unit = {}
|
||||
) = argument(name, PlayerArgument(), block)
|
||||
|
||||
fun players(
|
||||
name: String,
|
||||
allowDirectNames: Boolean = true,
|
||||
block: ValueBuilder<List<Player>>.() -> Unit = {}
|
||||
) = argument(name, PlayersArgument(), block)
|
||||
|
||||
fun selector(
|
||||
name: String,
|
||||
requireMatch: Boolean = true,
|
||||
block: ValueBuilder<List<Entity>>.() -> Unit = {}
|
||||
) = argument(name, EntityArgument(), block)
|
||||
|
||||
fun coordinates(
|
||||
name: String,
|
||||
allowRelative: Boolean = true,
|
||||
block: ValueBuilder<io.papermc.paper.math.Position>.() -> Unit = {}
|
||||
) = argument(name, CoordinatesArgument(), block)
|
||||
}
|
||||
|
||||
@KommandDsl
|
||||
abstract class NodeScope internal constructor(
|
||||
protected val node: KommandNode
|
||||
) : BranchScope(node.children) {
|
||||
override val inheritedPermission: String?
|
||||
get() = node.permission
|
||||
|
||||
override val inheritedCondition: (CommandSender) -> Boolean
|
||||
get() = node.condition
|
||||
|
||||
fun requires(permission: String) {
|
||||
node.permission = permission
|
||||
node.permissionOptions.id = permission
|
||||
}
|
||||
|
||||
fun condition(predicate: (CommandSender) -> Boolean) {
|
||||
node.condition = predicate
|
||||
}
|
||||
|
||||
fun executes(block: net.hareworks.kommand_lib.context.KommandContext.() -> Unit) {
|
||||
node.executor = block
|
||||
}
|
||||
|
||||
fun permission(block: PermissionOptions.() -> Unit) {
|
||||
node.permissionOptions.block()
|
||||
}
|
||||
|
||||
fun skipPermission() {
|
||||
node.permissionOptions.skipPermission()
|
||||
}
|
||||
}
|
||||
|
||||
@KommandDsl
|
||||
class LiteralBuilder internal constructor(
|
||||
private val literalNode: LiteralNode
|
||||
) : NodeScope(literalNode)
|
||||
|
||||
@KommandDsl
|
||||
class ValueBuilder<T> internal constructor(
|
||||
private val valueNode: ValueNode<T>
|
||||
) : NodeScope(valueNode) {
|
||||
/**
|
||||
* Overrides the default suggestion provider (wrapper around Brigadier logic)
|
||||
*/
|
||||
fun suggests(block: net.hareworks.kommand_lib.context.KommandContext.(prefix: String) -> List<String>) {
|
||||
valueNode.suggestionProvider = { ctx, prefix -> block(ctx, prefix) }
|
||||
}
|
||||
}
|
||||
|
||||
@DslMarker
|
||||
annotation class KommandDsl
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package net.hareworks.kommand_lib.nodes
|
||||
|
||||
import net.hareworks.kommand_lib.arguments.KommandArgument
|
||||
import net.hareworks.kommand_lib.context.KommandContext
|
||||
import net.hareworks.kommand_lib.permissions.PermissionOptions
|
||||
import org.bukkit.command.CommandSender
|
||||
|
||||
abstract class KommandNode internal constructor() {
|
||||
val children: MutableList<KommandNode> = mutableListOf()
|
||||
var executor: (KommandContext.() -> Unit)? = null
|
||||
var permission: String? = null
|
||||
var condition: (CommandSender) -> Boolean = { true }
|
||||
val permissionOptions: PermissionOptions = PermissionOptions()
|
||||
|
||||
fun isVisible(sender: CommandSender): Boolean {
|
||||
val perm = permission
|
||||
if (!perm.isNullOrBlank() && !sender.hasPermission(perm)) return false
|
||||
return condition(sender)
|
||||
}
|
||||
|
||||
open fun segment(): String? = null
|
||||
}
|
||||
|
||||
class LiteralNode internal constructor(val literal: String) : KommandNode() {
|
||||
override fun segment(): String = literal
|
||||
}
|
||||
|
||||
class ValueNode<T> internal constructor(
|
||||
val name: String,
|
||||
val argument: KommandArgument<T>
|
||||
) : KommandNode() {
|
||||
var suggestionProvider: ((KommandContext, String) -> List<String>)? = null
|
||||
|
||||
override fun segment(): String = name
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package net.hareworks.kommand_lib.permissions
|
||||
|
||||
import net.hareworks.permits_lib.PermitsLib
|
||||
import net.hareworks.permits_lib.bukkit.MutationSession
|
||||
import org.bukkit.permissions.PermissionDefault
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
|
||||
class PermissionConfig internal constructor(
|
||||
val namespace: String,
|
||||
val rootSegment: String,
|
||||
val autoApply: Boolean,
|
||||
val removeOnDisable: Boolean,
|
||||
val includeRootNode: Boolean,
|
||||
val argumentPrefix: String,
|
||||
val defaultDescription: (PermissionContext) -> String?,
|
||||
val defaultValue: PermissionDefault,
|
||||
val defaultWildcard: Boolean,
|
||||
private val sessionProvider: (JavaPlugin) -> MutationSession
|
||||
) {
|
||||
fun session(plugin: JavaPlugin): MutationSession = sessionProvider(plugin)
|
||||
}
|
||||
|
||||
class PermissionConfigBuilder internal constructor(private val plugin: JavaPlugin) {
|
||||
var namespace: String = plugin.name.lowercase()
|
||||
var rootSegment: String = "command"
|
||||
var autoApply: Boolean = true
|
||||
var removeOnDisable: Boolean = true
|
||||
var includeRootNode: Boolean = true
|
||||
var argumentPrefix: String = "arg"
|
||||
var defaultValue: PermissionDefault = PermissionDefault.FALSE
|
||||
var wildcard: Boolean = false
|
||||
private var descriptionTemplate: (PermissionContext) -> String? = { ctx ->
|
||||
when (ctx.kind) {
|
||||
PermissionNodeKind.COMMAND -> "Allows /${ctx.commandName}"
|
||||
PermissionNodeKind.LITERAL -> "Allows '${ctx.path.lastOrNull() ?: ctx.commandName}' sub-command"
|
||||
PermissionNodeKind.ARGUMENT -> "Allows argument '${ctx.path.lastOrNull()}'"
|
||||
}
|
||||
}
|
||||
private var sessionFactory: ((JavaPlugin) -> MutationSession)? = null
|
||||
|
||||
fun defaultDescription(block: (PermissionContext) -> String?) {
|
||||
descriptionTemplate = block
|
||||
}
|
||||
|
||||
fun session(factory: (JavaPlugin) -> MutationSession) {
|
||||
sessionFactory = factory
|
||||
}
|
||||
|
||||
fun session(instance: MutationSession) {
|
||||
sessionFactory = { instance }
|
||||
}
|
||||
|
||||
fun build(): PermissionConfig =
|
||||
PermissionConfig(
|
||||
namespace = namespace.trim().lowercase(),
|
||||
rootSegment = rootSegment.trim().lowercase(),
|
||||
autoApply = autoApply,
|
||||
removeOnDisable = removeOnDisable,
|
||||
includeRootNode = includeRootNode,
|
||||
argumentPrefix = argumentPrefix.trim().lowercase(),
|
||||
defaultDescription = descriptionTemplate,
|
||||
defaultValue = defaultValue,
|
||||
defaultWildcard = wildcard,
|
||||
sessionProvider = sessionFactory ?: { PermitsLib.session(it) }
|
||||
)
|
||||
}
|
||||
|
||||
data class PermissionContext(
|
||||
val commandName: String,
|
||||
val path: List<String>,
|
||||
val kind: PermissionNodeKind
|
||||
)
|
||||
|
||||
enum class PermissionNodeKind {
|
||||
COMMAND,
|
||||
LITERAL,
|
||||
ARGUMENT
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package net.hareworks.kommand_lib.permissions
|
||||
|
||||
import org.bukkit.permissions.PermissionDefault
|
||||
|
||||
class PermissionOptions {
|
||||
var id: String? = null
|
||||
var description: String? = null
|
||||
var defaultValue: PermissionDefault? = null
|
||||
var wildcard: Boolean? = null
|
||||
var skip: Boolean = false
|
||||
private var customPath: MutableList<String>? = null
|
||||
private val wildcardExclusionSpecs: MutableList<List<String>> = mutableListOf()
|
||||
internal var preferSkipByDefault: Boolean = false
|
||||
|
||||
internal var resolvedId: String? = null
|
||||
private set
|
||||
|
||||
fun rename(vararg segments: String) {
|
||||
customPath = segments
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.toMutableList()
|
||||
}
|
||||
|
||||
internal fun renameOverride(): List<String>? = customPath?.toList()
|
||||
|
||||
internal fun resolve(id: String) {
|
||||
resolvedId = id
|
||||
}
|
||||
|
||||
fun skipPermission() {
|
||||
skip = true
|
||||
}
|
||||
|
||||
val wildcardExclusions: List<List<String>>
|
||||
get() = wildcardExclusionSpecs.map { it.toList() }
|
||||
|
||||
fun wildcard(block: WildcardOptions.() -> Unit) {
|
||||
wildcard = true
|
||||
WildcardOptions(wildcardExclusionSpecs).apply(block)
|
||||
}
|
||||
}
|
||||
|
||||
class WildcardOptions internal constructor(
|
||||
private val sink: MutableList<List<String>>
|
||||
) {
|
||||
fun exclude(vararg segments: String) {
|
||||
val normalized = segments
|
||||
.flatMap { it.split('.') }
|
||||
.map { it.trim().lowercase() }
|
||||
.filter { it.isNotEmpty() }
|
||||
if (normalized.isNotEmpty()) {
|
||||
sink += normalized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package net.hareworks.kommand_lib.permissions
|
||||
|
||||
import net.hareworks.permits_lib.domain.NodeRegistration
|
||||
import org.bukkit.permissions.PermissionDefault
|
||||
|
||||
data class PermissionPlan(
|
||||
val config: PermissionConfig,
|
||||
val entries: List<PlannedPermission>
|
||||
) {
|
||||
val namespace: String get() = config.namespace
|
||||
fun isEmpty(): Boolean = entries.isEmpty()
|
||||
}
|
||||
|
||||
data class PlannedPermission(
|
||||
val id: String,
|
||||
val relativePath: List<String>,
|
||||
val parentPath: List<String>?,
|
||||
val description: String?,
|
||||
val defaultValue: PermissionDefault,
|
||||
val wildcardExclusions: List<List<String>>,
|
||||
val inheritsParentDefault: Boolean,
|
||||
val wildcard: Boolean,
|
||||
val registration: NodeRegistration
|
||||
)
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
package net.hareworks.kommand_lib.permissions
|
||||
|
||||
import net.hareworks.kommand_lib.CommandDefinition
|
||||
import net.hareworks.kommand_lib.nodes.KommandNode
|
||||
import net.hareworks.kommand_lib.nodes.LiteralNode
|
||||
import net.hareworks.kommand_lib.nodes.ValueNode
|
||||
import net.hareworks.permits_lib.domain.NodeRegistration
|
||||
import org.bukkit.permissions.PermissionDefault
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
|
||||
internal class PermissionPlanner(
|
||||
private val plugin: JavaPlugin,
|
||||
private val config: PermissionConfig,
|
||||
private val definitions: List<CommandDefinition>
|
||||
) {
|
||||
fun plan(): PermissionPlan {
|
||||
val entries = linkedMapOf<String, PlannedPermission>()
|
||||
val (rootPath, rootDefault) = if (config.includeRootNode && config.rootSegment.isNotBlank()) {
|
||||
val path = listOf(config.rootSegment)
|
||||
val entry = createEntry(
|
||||
options = PermissionOptions().apply { id = buildId(path) },
|
||||
pathSegments = path,
|
||||
context = PermissionContext(commandName = "", path = path, kind = PermissionNodeKind.LITERAL),
|
||||
parentDefault = config.defaultValue,
|
||||
registration = NodeRegistration.STRUCTURAL
|
||||
)
|
||||
if (entry != null) entries[entry.id] = entry
|
||||
path to (entry?.defaultValue ?: config.defaultValue)
|
||||
} else {
|
||||
emptyList<String>() to config.defaultValue
|
||||
}
|
||||
|
||||
definitions.forEach { definition ->
|
||||
val overridePath = definition.permissionOptions.renameOverride()
|
||||
val commandPath = if (overridePath != null) {
|
||||
normalizeSegments(overridePath)
|
||||
} else {
|
||||
val sanitized = sanitize(definition.name)
|
||||
val base = if (rootPath.isNotEmpty()) rootPath else emptyList()
|
||||
base + sanitized
|
||||
}
|
||||
val commandEntry = createEntry(
|
||||
options = definition.permissionOptions,
|
||||
pathSegments = commandPath,
|
||||
context = PermissionContext(definition.name, commandPath, PermissionNodeKind.COMMAND),
|
||||
parentDefault = rootDefault
|
||||
)
|
||||
if (commandEntry != null) {
|
||||
entries[commandEntry.id] = commandEntry
|
||||
if (definition.permission.isNullOrBlank()) {
|
||||
definition.permission = commandEntry.id
|
||||
}
|
||||
}
|
||||
val childDefault = commandEntry?.defaultValue ?: rootDefault
|
||||
definition.nodes.forEach { node ->
|
||||
planNode(node, commandPath, entries, definition.name, childDefault)
|
||||
}
|
||||
}
|
||||
return PermissionPlan(config, entries.values.toList())
|
||||
}
|
||||
|
||||
private fun planNode(
|
||||
node: KommandNode,
|
||||
basePath: List<String>,
|
||||
entries: MutableMap<String, PlannedPermission>,
|
||||
commandName: String,
|
||||
parentDefault: PermissionDefault
|
||||
) {
|
||||
val rawOverride = node.permissionOptions.renameOverride()
|
||||
val shouldSkip =
|
||||
node.permissionOptions.skip ||
|
||||
(node.permissionOptions.preferSkipByDefault &&
|
||||
node.permissionOptions.id.isNullOrBlank() &&
|
||||
rawOverride == null)
|
||||
if (shouldSkip) {
|
||||
node.children.forEach { child ->
|
||||
planNode(child, basePath, entries, commandName, parentDefault)
|
||||
}
|
||||
return
|
||||
}
|
||||
val segment = node.segment()?.let { sanitize(it) }
|
||||
val pathAddition = rawOverride?.let { normalizeSegments(it) }
|
||||
val path = when {
|
||||
pathAddition != null -> basePath + pathAddition
|
||||
segment != null -> basePath + segment
|
||||
else -> basePath
|
||||
}
|
||||
val entry = createEntry(
|
||||
options = node.permissionOptions,
|
||||
pathSegments = path,
|
||||
context = PermissionContext(commandName, path, node.toKind()),
|
||||
parentDefault = parentDefault
|
||||
)
|
||||
val currentBase = if (entry != null) {
|
||||
entries[entry.id] = entry
|
||||
if (node.permission.isNullOrBlank()) {
|
||||
node.permission = entry.id
|
||||
}
|
||||
path
|
||||
} else {
|
||||
basePath
|
||||
}
|
||||
val nextDefault = entry?.defaultValue ?: parentDefault
|
||||
node.children.forEach { child ->
|
||||
planNode(child, currentBase, entries, commandName, nextDefault)
|
||||
}
|
||||
}
|
||||
|
||||
private fun KommandNode.toKind(): PermissionNodeKind = when (this) {
|
||||
is LiteralNode -> PermissionNodeKind.LITERAL
|
||||
is ValueNode<*> -> PermissionNodeKind.ARGUMENT
|
||||
else -> PermissionNodeKind.LITERAL
|
||||
}
|
||||
|
||||
private fun createEntry(
|
||||
options: PermissionOptions,
|
||||
pathSegments: List<String>,
|
||||
context: PermissionContext,
|
||||
parentDefault: PermissionDefault,
|
||||
registration: NodeRegistration = NodeRegistration.PERMISSION
|
||||
): PlannedPermission? {
|
||||
val finalId = (options.id?.takeIf { it.isNotBlank() } ?: buildId(pathSegments)).trim()
|
||||
if (finalId.isEmpty()) return null
|
||||
if (!finalId.startsWith(config.namespace)) {
|
||||
plugin.logger.warning("Permission '$finalId' is outside namespace '${config.namespace}', skipping auto-registration.")
|
||||
options.resolve(finalId)
|
||||
return null
|
||||
}
|
||||
val relative = finalId.removePrefix(config.namespace).trimStart('.')
|
||||
val relativePath = if (relative.isEmpty()) emptyList() else relative.split('.')
|
||||
val description = options.description ?: config.defaultDescription(context)
|
||||
val explicitDefault = options.defaultValue
|
||||
val defaultValue = explicitDefault ?: parentDefault
|
||||
val wildcard = options.wildcard ?: config.defaultWildcard
|
||||
val wildcardExclusions = options.wildcardExclusions
|
||||
.map { normalizeSegments(it) }
|
||||
.filter { it.isNotEmpty() }
|
||||
options.resolve(finalId)
|
||||
val parentPath = if (relativePath.isNotEmpty()) relativePath.dropLast(1).takeIf { it.isNotEmpty() } else null
|
||||
return PlannedPermission(
|
||||
id = finalId,
|
||||
relativePath = relativePath,
|
||||
parentPath = parentPath,
|
||||
description = description,
|
||||
defaultValue = defaultValue,
|
||||
wildcardExclusions = wildcardExclusions,
|
||||
inheritsParentDefault = explicitDefault == null,
|
||||
wildcard = wildcard,
|
||||
registration = registration
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildId(pathSegments: List<String>): String =
|
||||
(listOf(config.namespace) + pathSegments).filter { it.isNotBlank() }.joinToString(".")
|
||||
|
||||
private fun sanitize(segment: String): String =
|
||||
segment.trim().lowercase().replace(Regex("[^a-z0-9._-]"), "-").trim('-')
|
||||
|
||||
private fun normalizeSegments(segments: List<String>): List<String> =
|
||||
segments.map { sanitize(it) }.filter { it.isNotBlank() }
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package net.hareworks.kommand_lib.permissions
|
||||
|
||||
import net.hareworks.permits_lib.bukkit.MutationSession
|
||||
import net.hareworks.permits_lib.domain.MutablePermissionTree
|
||||
import net.hareworks.permits_lib.domain.NodeRegistration
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
|
||||
internal class PermissionRuntime(
|
||||
private val plugin: JavaPlugin,
|
||||
private val plan: PermissionPlan
|
||||
) {
|
||||
private val session: MutationSession by lazy { plan.config.session(plugin) }
|
||||
val config: PermissionConfig get() = plan.config
|
||||
|
||||
fun apply() {
|
||||
if (plan.isEmpty()) return
|
||||
val mutable = MutablePermissionTree.create(plan.config.namespace)
|
||||
val sorted = plan.entries.sortedBy { it.relativePath.size }
|
||||
val registrations = sorted
|
||||
.mapNotNull { entry ->
|
||||
entry.relativePath.takeIf { it.isNotEmpty() }?.joinToString(".")?.let { it to entry.registration }
|
||||
}
|
||||
.toMap()
|
||||
val entriesByPath = sorted
|
||||
.filter { it.relativePath.isNotEmpty() }
|
||||
.associateBy { it.relativePath.joinToString(".") }
|
||||
sorted.forEach { entry ->
|
||||
if (entry.relativePath.isEmpty()) {
|
||||
plugin.logger.warning("Skipping permission '${entry.id}' because it resolved to the namespace root.")
|
||||
return@forEach
|
||||
}
|
||||
val nodeId = entry.relativePath.joinToString(".")
|
||||
val currentNode = mutable.node(nodeId, entry.registration) {
|
||||
entry.description?.let { description = it }
|
||||
defaultValue = entry.defaultValue
|
||||
wildcard = entry.wildcard
|
||||
}
|
||||
if (entry.wildcard && entry.wildcardExclusions.isNotEmpty()) {
|
||||
entry.wildcardExclusions.forEach { exclusion ->
|
||||
val absolutePath = entry.relativePath + exclusion
|
||||
if (absolutePath.isNotEmpty()) {
|
||||
currentNode.excludeWildcardChildAbsolute(buildId(absolutePath))
|
||||
}
|
||||
}
|
||||
}
|
||||
val parent = entry.parentPath
|
||||
if (parent != null && parent.isNotEmpty()) {
|
||||
val parentId = parent.joinToString(".")
|
||||
val parentRegistration = registrations[parentId] ?: NodeRegistration.STRUCTURAL
|
||||
val parentEntry = entriesByPath[parentId]
|
||||
val shouldLinkChildren = parentEntry?.registration == NodeRegistration.STRUCTURAL || parentEntry?.wildcard == true
|
||||
mutable.node(parentId, parentRegistration) {
|
||||
if (shouldLinkChildren) {
|
||||
child(entry.relativePath.last())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
session.applyTree(mutable.build())
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
if (!plan.config.removeOnDisable) return
|
||||
session.clearAll()
|
||||
}
|
||||
|
||||
fun attachments() = session.attachments
|
||||
|
||||
private fun buildId(pathSegments: List<String>): String =
|
||||
(listOf(plan.config.namespace) + pathSegments).filter { it.isNotBlank() }.joinToString(".")
|
||||
}
|
||||
3
hcu-core/settings.gradle.kts
Normal file
3
hcu-core/settings.gradle.kts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
rootProject.name = "hcu-core"
|
||||
|
||||
includeBuild("kommand-lib")
|
||||
198
hcu-core/src/main/kotlin/net/hareworks/hcu/core/Main.kt
Normal file
198
hcu-core/src/main/kotlin/net/hareworks/hcu/core/Main.kt
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
package net.hareworks.hcu.core
|
||||
|
||||
import java.util.logging.Level
|
||||
import net.hareworks.hcu.core.actor.ActorIdentityService
|
||||
import net.hareworks.hcu.core.actor.ActorIdentityServiceImpl
|
||||
import net.hareworks.hcu.core.command.CommandRegistrar
|
||||
import net.hareworks.hcu.core.config.ConfigManager
|
||||
import net.hareworks.hcu.core.database.DatabaseSessionManager
|
||||
import net.hareworks.hcu.core.database.DatabaseSettings
|
||||
import net.hareworks.hcu.core.listeners.AdminAlertListener
|
||||
import net.hareworks.hcu.core.listeners.PlayerRegistrationListener
|
||||
import net.hareworks.hcu.core.player.PlayerIdService
|
||||
import net.hareworks.hcu.core.player.PlayerIdServiceImpl
|
||||
import net.hareworks.kommand_lib.KommandLib
|
||||
import net.hareworks.permits_lib.PermitsLib
|
||||
import net.hareworks.permits_lib.bukkit.MutationSession
|
||||
import net.hareworks.permits_lib.domain.NodeRegistration
|
||||
import org.bukkit.permissions.PermissionDefault
|
||||
import org.bukkit.plugin.ServicePriority
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
import org.bukkit.scheduler.BukkitTask
|
||||
import org.jetbrains.exposed.v1.jdbc.Database
|
||||
|
||||
class Main : JavaPlugin() {
|
||||
private var commands: KommandLib? = null
|
||||
private var activeSettings: DatabaseSettings? = null
|
||||
private var initialConnectTask: BukkitTask? = null
|
||||
private var permissionSession: MutationSession? = null
|
||||
|
||||
override fun onEnable() {
|
||||
instance = this
|
||||
val settings = loadConfiguration(disableOnFailure = true) ?: return
|
||||
setupPermissionSession()
|
||||
commands = CommandRegistrar.register(this, permissionSession)
|
||||
ensureAdminPermissionBridge()
|
||||
server.pluginManager.registerEvents(AdminAlertListener(this), this)
|
||||
server.pluginManager.registerEvents(PlayerRegistrationListener(this), this)
|
||||
scheduleInitialConnectionAttempt()
|
||||
}
|
||||
|
||||
override fun onDisable() {
|
||||
cancelInitialConnectionAttempt()
|
||||
commands?.unregister()
|
||||
commands = null
|
||||
server.servicesManager.unregisterAll(this)
|
||||
DatabaseSessionManager.disconnect(logger)
|
||||
permissionSession?.clearAll()
|
||||
permissionSession = null
|
||||
}
|
||||
|
||||
fun reloadConfiguration(): Boolean {
|
||||
val settings = loadConfiguration(disableOnFailure = false) ?: return false
|
||||
logger.info("hcu-core configuration reloaded (${describeTarget()})")
|
||||
return true
|
||||
}
|
||||
|
||||
fun reconnectDatabase(): Boolean {
|
||||
val settings = activeSettings ?: loadConfiguration(disableOnFailure = false) ?: return false
|
||||
val success = DatabaseSessionManager.reload(settings, logger)
|
||||
if (success) {
|
||||
val schemaReady = ensureDatabaseSchema()
|
||||
if (!schemaReady) {
|
||||
logger.severe("Database schema initialization failed during reconnect; actor tracking remains offline.")
|
||||
}
|
||||
registerDatabaseService()
|
||||
logger.info("hcu-core database session re-established (${describeTarget()})")
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
fun connectionStatus(): ConnectionStatus =
|
||||
ConnectionStatus(
|
||||
connected = DatabaseSessionManager.isConnected(),
|
||||
pingSucceeded = DatabaseSessionManager.ping(),
|
||||
target = describeTarget()
|
||||
)
|
||||
|
||||
fun describeTarget(): String? =
|
||||
activeSettings?.let { "${it.dialect.name.lowercase()}:${it.host}:${it.port}/${it.database}" }
|
||||
|
||||
fun exposedDatabase(): Database? = DatabaseSessionManager.database
|
||||
|
||||
fun requireDatabase(): Database = DatabaseSessionManager.requireDatabase()
|
||||
|
||||
fun currentSettings(): DatabaseSettings? = activeSettings
|
||||
|
||||
private fun loadConfiguration(disableOnFailure: Boolean): DatabaseSettings? {
|
||||
val settings = runCatching { ConfigManager.load(this) }
|
||||
.onFailure {
|
||||
logger.log(Level.SEVERE, "Failed to load hcu-core configuration", it)
|
||||
if (disableOnFailure) disableSelf()
|
||||
}
|
||||
.getOrNull()
|
||||
settings?.let { activeSettings = it }
|
||||
return settings
|
||||
}
|
||||
|
||||
private fun setupPermissionSession() {
|
||||
permissionSession = runCatching { PermitsLib.session(this) }
|
||||
.onFailure { logger.log(Level.WARNING, "Failed to acquire permits session", it) }
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
private fun ensureAdminPermissionBridge() {
|
||||
val session = permissionSession ?: return
|
||||
runCatching {
|
||||
session.edit("hcu-core") {
|
||||
node("admin", NodeRegistration.PERMISSION) {
|
||||
description = "Allows receiving hcu-core administrative warnings"
|
||||
defaultValue = PermissionDefault.OP
|
||||
wildcard = false
|
||||
childAbsolute("hcu-core.command")
|
||||
childAbsolute("hcu-core.command.*")
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
logger.log(Level.WARNING, "Failed to register admin permission hierarchy", it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleInitialConnectionAttempt() {
|
||||
cancelInitialConnectionAttempt()
|
||||
initialConnectTask =
|
||||
server.scheduler.runTaskLaterAsynchronously(
|
||||
this,
|
||||
Runnable {
|
||||
initialConnectTask = null
|
||||
attemptInitialConnection()
|
||||
},
|
||||
40L
|
||||
)
|
||||
}
|
||||
|
||||
private fun cancelInitialConnectionAttempt() {
|
||||
initialConnectTask?.cancel()
|
||||
initialConnectTask = null
|
||||
}
|
||||
|
||||
private fun attemptInitialConnection() {
|
||||
val target = describeTarget() ?: "unknown target"
|
||||
val settings = activeSettings
|
||||
if (settings == null) {
|
||||
logger.warning("Initial database connection skipped because no configuration is loaded")
|
||||
return
|
||||
}
|
||||
if (DatabaseSessionManager.connect(settings, logger)) {
|
||||
val schemaReady = ensureDatabaseSchema()
|
||||
if (!schemaReady) {
|
||||
logger.severe("Connected to $target but failed to initialize schema; player registration will be unavailable.")
|
||||
}
|
||||
server.scheduler.runTask(
|
||||
this,
|
||||
Runnable {
|
||||
registerDatabaseService()
|
||||
logger.info("hcu-core ready; database session online ($target)")
|
||||
}
|
||||
)
|
||||
} else {
|
||||
logger.warning(
|
||||
"hcu-core failed to establish database session at startup ($target). Update config and run /hcu db reconnect."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerDatabaseService() {
|
||||
server.servicesManager.unregisterAll(this)
|
||||
DatabaseSessionManager.database?.let { database ->
|
||||
server.servicesManager.register(Database::class.java, database, this, ServicePriority.Normal)
|
||||
val actorService = ActorIdentityServiceImpl(this, logger)
|
||||
val playerService = PlayerIdServiceImpl(logger)
|
||||
server.servicesManager.register(
|
||||
ActorIdentityService::class.java,
|
||||
actorService,
|
||||
this,
|
||||
ServicePriority.Normal
|
||||
)
|
||||
server.servicesManager.register(PlayerIdService::class.java, playerService, this, ServicePriority.Normal)
|
||||
}
|
||||
}
|
||||
|
||||
private fun disableSelf(reason: String? = null) {
|
||||
reason?.let { logger.severe(it) }
|
||||
server.pluginManager.disablePlugin(this)
|
||||
}
|
||||
|
||||
private fun ensureDatabaseSchema(): Boolean = DatabaseSessionManager.ensureSchema(logger)
|
||||
|
||||
data class ConnectionStatus(
|
||||
val connected: Boolean,
|
||||
val pingSucceeded: Boolean,
|
||||
val target: String?
|
||||
)
|
||||
|
||||
companion object {
|
||||
lateinit var instance: Main
|
||||
private set
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package net.hareworks.hcu.core.actor
|
||||
|
||||
/**
|
||||
* Bukkit service for issuing sequential actor IDs backed by the actors table.
|
||||
*/
|
||||
interface ActorIdentityService {
|
||||
/**
|
||||
* Issues a brand new actor_id by inserting a row into the actors table.
|
||||
* Throws if the database layer is unavailable or the insert fails.
|
||||
*/
|
||||
fun issueActorId(type: String): Int
|
||||
|
||||
/**
|
||||
* Deletes the actor row for the provided id.
|
||||
* @return true if a row was deleted, false if the actor was missing.
|
||||
*/
|
||||
fun deleteActor(actorId: Int): Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
package net.hareworks.hcu.core.actor
|
||||
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.todayIn
|
||||
import net.hareworks.hcu.core.actor.events.ActorCreatedEvent
|
||||
import net.hareworks.hcu.core.actor.events.ActorDeletedEvent
|
||||
import net.hareworks.hcu.core.database.DatabaseSessionManager
|
||||
import net.hareworks.hcu.core.database.schema.ActorsTable
|
||||
import net.hareworks.hcu.core.database.schema.PlayersTable
|
||||
import net.hareworks.hcu.core.database.schema.createActorRecord
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.event.Event
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.andWhere
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
class ActorIdentityServiceImpl(
|
||||
private val plugin: JavaPlugin,
|
||||
private val logger: Logger
|
||||
) : ActorIdentityService {
|
||||
override fun issueActorId(type: String): Int =
|
||||
runOperation {
|
||||
ensureConnected()
|
||||
val normalizedType = type.trim()
|
||||
require(normalizedType.isNotEmpty()) { "Actor type must not be blank" }
|
||||
val registeredAt = Clock.System.todayIn(TimeZone.UTC)
|
||||
val actorId = DatabaseSessionManager.transaction {
|
||||
createActorRecord(registeredAt, normalizedType)
|
||||
}
|
||||
dispatchActorCreated(actorId, registeredAt, normalizedType)
|
||||
actorId
|
||||
}
|
||||
|
||||
override fun deleteActor(actorId: Int): Boolean =
|
||||
runOperation {
|
||||
ensureConnected()
|
||||
when (val result = DatabaseSessionManager.transaction { markActorRemoved(actorId) }) {
|
||||
is RemovalResult.Removed -> {
|
||||
dispatchActorDeleted(actorId, result.type)
|
||||
true
|
||||
}
|
||||
is RemovalResult.Blocked -> {
|
||||
logger.fine("Skipping removal for actor_id=$actorId (${result.reason})")
|
||||
false
|
||||
}
|
||||
is RemovalResult.AlreadyRemoved -> false
|
||||
RemovalResult.Missing -> false
|
||||
is RemovalResult.Failed -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun markActorRemoved(actorId: Int): RemovalResult {
|
||||
val row = ActorsTable
|
||||
.selectAll()
|
||||
.andWhere { ActorsTable.actorId eq actorId }
|
||||
.firstOrNull()
|
||||
?: return RemovalResult.Missing
|
||||
val type = row[ActorsTable.type]
|
||||
if (row[ActorsTable.removed]) {
|
||||
return RemovalResult.AlreadyRemoved(type)
|
||||
}
|
||||
if (type == ActorTypes.PLAYER) {
|
||||
return RemovalResult.Blocked(type, "player actors cannot be removed")
|
||||
}
|
||||
val linkedToPlayer = PlayersTable
|
||||
.selectAll()
|
||||
.andWhere { PlayersTable.actorId eq actorId }
|
||||
.firstOrNull() != null
|
||||
if (linkedToPlayer) {
|
||||
return RemovalResult.Blocked(type, "actor is linked to a player entry")
|
||||
}
|
||||
val updated = ActorsTable.update({ ActorsTable.actorId eq actorId }) {
|
||||
it[ActorsTable.removed] = true
|
||||
}
|
||||
return if (updated > 0) {
|
||||
RemovalResult.Removed(type)
|
||||
} else {
|
||||
RemovalResult.Failed(type)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dispatchActorCreated(actorId: Int, registeredAt: LocalDate, type: String) {
|
||||
callEvent(ActorCreatedEvent(actorId, registeredAt, type))
|
||||
}
|
||||
|
||||
private fun dispatchActorDeleted(actorId: Int, type: String) {
|
||||
callEvent(ActorDeletedEvent(actorId, type))
|
||||
}
|
||||
|
||||
private fun callEvent(event: Event) {
|
||||
val server = plugin.server
|
||||
if (Bukkit.isPrimaryThread()) {
|
||||
server.pluginManager.callEvent(event)
|
||||
} else {
|
||||
server.scheduler.runTask(plugin, Runnable { server.pluginManager.callEvent(event) })
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureConnected() {
|
||||
if (!DatabaseSessionManager.isConnected()) {
|
||||
throw IllegalStateException("Database session is not connected")
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> runOperation(block: () -> T): T =
|
||||
runCatching(block)
|
||||
.onFailure { logger.log(Level.SEVERE, "Actor identity operation failed", it) }
|
||||
.getOrThrow()
|
||||
|
||||
private sealed interface RemovalResult {
|
||||
data object Missing : RemovalResult
|
||||
data class AlreadyRemoved(val type: String) : RemovalResult
|
||||
data class Blocked(val type: String, val reason: String) : RemovalResult
|
||||
data class Removed(val type: String) : RemovalResult
|
||||
data class Failed(val type: String) : RemovalResult
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package net.hareworks.hcu.core.actor
|
||||
|
||||
/** Well-known actor classifications stored in actors.type. */
|
||||
object ActorTypes {
|
||||
const val PLAYER: String = "player"
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package net.hareworks.hcu.core.actor.events
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import org.bukkit.event.Event
|
||||
import org.bukkit.event.HandlerList
|
||||
|
||||
/** Fired whenever a new actor row is inserted. */
|
||||
class ActorCreatedEvent(
|
||||
val actorId: Int,
|
||||
val registeredAt: LocalDate,
|
||||
val type: String
|
||||
) : Event() {
|
||||
override fun getHandlers(): HandlerList = handlerList
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
private val handlerList = HandlerList()
|
||||
|
||||
@JvmStatic
|
||||
fun getHandlerList(): HandlerList = handlerList
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package net.hareworks.hcu.core.actor.events
|
||||
|
||||
import org.bukkit.event.Event
|
||||
import org.bukkit.event.HandlerList
|
||||
|
||||
/** Fired whenever an actor row is marked as removed. */
|
||||
class ActorDeletedEvent(
|
||||
val actorId: Int,
|
||||
val type: String
|
||||
) : Event() {
|
||||
override fun getHandlers(): HandlerList = handlerList
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
private val handlerList = HandlerList()
|
||||
|
||||
@JvmStatic
|
||||
fun getHandlerList(): HandlerList = handlerList
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
package net.hareworks.hcu.core.command
|
||||
|
||||
import net.hareworks.hcu.core.Main
|
||||
import net.hareworks.kommand_lib.KommandLib
|
||||
import net.hareworks.kommand_lib.kommand
|
||||
import net.hareworks.permits_lib.bukkit.MutationSession
|
||||
import net.kyori.adventure.text.Component
|
||||
import net.kyori.adventure.text.format.NamedTextColor
|
||||
import net.kyori.adventure.text.format.TextDecoration
|
||||
import org.bukkit.command.CommandSender
|
||||
import org.bukkit.permissions.PermissionDefault
|
||||
|
||||
object CommandRegistrar {
|
||||
fun register(plugin: Main, permissionSession: MutationSession?): KommandLib =
|
||||
kommand(plugin) {
|
||||
permissions {
|
||||
namespace = "hcu-core"
|
||||
defaultValue = PermissionDefault.OP
|
||||
permissionSession?.let { session(it) }
|
||||
}
|
||||
command("hcu") {
|
||||
description = "Manage hcu-core configuration and database sessions"
|
||||
|
||||
executes { sender.showUsage() }
|
||||
|
||||
literal("help") {
|
||||
executes { sender.showUsage() }
|
||||
}
|
||||
|
||||
literal("config") {
|
||||
executes {
|
||||
sender.error("Usage: /hcu config reload")
|
||||
}
|
||||
literal("reload") {
|
||||
executes {
|
||||
if (plugin.reloadConfiguration()) {
|
||||
sender.success("Configuration reloaded: ${plugin.describeTarget() ?: "unknown target"}")
|
||||
} else {
|
||||
sender.error("Failed to reload configuration. See console for details.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
literal("db") {
|
||||
executes {
|
||||
sender.error("Usage: /hcu db <status|reconnect>")
|
||||
}
|
||||
|
||||
literal("status") {
|
||||
executes {
|
||||
val status = plugin.connectionStatus()
|
||||
val target = status.target ?: "unknown"
|
||||
|
||||
sender.sendMessage(
|
||||
Component.text()
|
||||
.append(Component.text("[hcu-core] ", NamedTextColor.GRAY))
|
||||
.append(Component.text("Database Status", NamedTextColor.GOLD, TextDecoration.BOLD))
|
||||
.build()
|
||||
)
|
||||
|
||||
sender.sendMessage(
|
||||
Component.text()
|
||||
.append(Component.text(" Connection: ", NamedTextColor.GRAY))
|
||||
.append(
|
||||
Component.text(
|
||||
if (status.connected) "Connected" else "Disconnected",
|
||||
if (status.connected) NamedTextColor.GREEN else NamedTextColor.RED
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
|
||||
sender.sendMessage(
|
||||
Component.text()
|
||||
.append(Component.text(" Ping: ", NamedTextColor.GRAY))
|
||||
.append(
|
||||
Component.text(
|
||||
if (status.pingSucceeded) "Reachable" else "Unreachable",
|
||||
if (status.pingSucceeded) NamedTextColor.GREEN else NamedTextColor.RED
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
|
||||
sender.sendMessage(
|
||||
Component.text()
|
||||
.append(Component.text(" Target: ", NamedTextColor.GRAY))
|
||||
.append(Component.text(target, NamedTextColor.AQUA))
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
literal("reconnect") {
|
||||
executes {
|
||||
sender.info("Attempting to reconnect to database...")
|
||||
if (plugin.reconnectDatabase()) {
|
||||
sender.success("Database reconnection succeeded.")
|
||||
} else {
|
||||
sender.error("Database reconnection failed. Check server logs.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CommandSender.showUsage() {
|
||||
sendMessage(
|
||||
Component.text()
|
||||
.append(Component.text("[hcu-core] ", NamedTextColor.GRAY))
|
||||
.append(Component.text("Available Commands", NamedTextColor.GOLD, TextDecoration.BOLD))
|
||||
.build()
|
||||
)
|
||||
sendMessage(
|
||||
Component.text()
|
||||
.append(Component.text(" /hcu config reload", NamedTextColor.YELLOW))
|
||||
.append(Component.text(" - ", NamedTextColor.DARK_GRAY))
|
||||
.append(Component.text("Reload hcu-core configuration", NamedTextColor.GRAY))
|
||||
.build()
|
||||
)
|
||||
sendMessage(
|
||||
Component.text()
|
||||
.append(Component.text(" /hcu db status", NamedTextColor.YELLOW))
|
||||
.append(Component.text(" - ", NamedTextColor.DARK_GRAY))
|
||||
.append(Component.text("Show current database status", NamedTextColor.GRAY))
|
||||
.build()
|
||||
)
|
||||
sendMessage(
|
||||
Component.text()
|
||||
.append(Component.text(" /hcu db reconnect", NamedTextColor.YELLOW))
|
||||
.append(Component.text(" - ", NamedTextColor.DARK_GRAY))
|
||||
.append(Component.text("Reconnect using current configuration", NamedTextColor.GRAY))
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
private fun CommandSender.success(message: String) {
|
||||
sendMessage(
|
||||
Component.text()
|
||||
.append(Component.text("[hcu-core] ", NamedTextColor.GRAY))
|
||||
.append(Component.text("✓ ", NamedTextColor.GREEN))
|
||||
.append(Component.text(message, NamedTextColor.WHITE))
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
private fun CommandSender.error(message: String) {
|
||||
sendMessage(
|
||||
Component.text()
|
||||
.append(Component.text("[hcu-core] ", NamedTextColor.GRAY))
|
||||
.append(Component.text("✗ ", NamedTextColor.RED))
|
||||
.append(Component.text(message, NamedTextColor.WHITE))
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
private fun CommandSender.info(message: String) {
|
||||
sendMessage(
|
||||
Component.text()
|
||||
.append(Component.text("[hcu-core] ", NamedTextColor.GRAY))
|
||||
.append(Component.text("ℹ ", NamedTextColor.AQUA))
|
||||
.append(Component.text(message, NamedTextColor.WHITE))
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package net.hareworks.hcu.core.config
|
||||
|
||||
import net.hareworks.hcu.core.database.DatabaseSettings
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
|
||||
object ConfigManager {
|
||||
@Volatile
|
||||
private var cachedSettings: DatabaseSettings? = null
|
||||
|
||||
fun load(plugin: JavaPlugin): DatabaseSettings {
|
||||
plugin.saveDefaultConfig()
|
||||
plugin.reloadConfig()
|
||||
return DatabaseSettings.fromConfig(plugin.config).also { cachedSettings = it }
|
||||
}
|
||||
|
||||
fun current(): DatabaseSettings? = cachedSettings
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
package net.hareworks.hcu.core.database
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
import net.hareworks.hcu.core.database.schema.ActorIdSequence
|
||||
import net.hareworks.hcu.core.database.schema.ActorsTable
|
||||
import net.hareworks.hcu.core.database.schema.PlayersTable
|
||||
import org.bukkit.configuration.file.FileConfiguration
|
||||
import org.jetbrains.exposed.v1.jdbc.Database
|
||||
import org.jetbrains.exposed.v1.jdbc.JdbcTransaction
|
||||
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
|
||||
enum class DatabaseDialect(
|
||||
val driverClass: String,
|
||||
private val jdbcPrefix: String,
|
||||
val defaultPort: Int
|
||||
) {
|
||||
POSTGRESQL(
|
||||
driverClass = "org.postgresql.Driver",
|
||||
jdbcPrefix = "jdbc:postgresql://",
|
||||
defaultPort = 5432
|
||||
),
|
||||
MYSQL(
|
||||
driverClass = "com.mysql.cj.jdbc.Driver",
|
||||
jdbcPrefix = "jdbc:mysql://",
|
||||
defaultPort = 3306
|
||||
);
|
||||
|
||||
fun jdbcUrl(host: String, port: Int, database: String): String = "$jdbcPrefix$host:$port/$database"
|
||||
|
||||
companion object {
|
||||
fun from(raw: String?): DatabaseDialect = when (raw?.lowercase()) {
|
||||
"postgresql", "postgres", "pg" -> POSTGRESQL
|
||||
"mysql", "mariadb" -> MYSQL
|
||||
else -> throw IllegalArgumentException("Unsupported database dialect: $raw")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class DatabaseSettings(
|
||||
val dialect: DatabaseDialect,
|
||||
val host: String,
|
||||
val port: Int,
|
||||
val database: String,
|
||||
val username: String,
|
||||
val password: String,
|
||||
val pool: PoolSettings
|
||||
) {
|
||||
val jdbcUrl: String = dialect.jdbcUrl(host, port, database)
|
||||
|
||||
data class PoolSettings(
|
||||
val maxPoolSize: Int,
|
||||
val minIdle: Int,
|
||||
val maxLifetimeMillis: Long,
|
||||
val connectionTimeoutMillis: Long
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun fromConfig(config: FileConfiguration): DatabaseSettings {
|
||||
val section = config.getConfigurationSection("database")
|
||||
?: error("Missing 'database' section in config.yml")
|
||||
val dialect = DatabaseDialect.from(section.getString("dialect"))
|
||||
val host = section.getString("host")?.takeUnless { it.isBlank() }
|
||||
?: error("database.host must be provided")
|
||||
val port = section.getInt("port", dialect.defaultPort)
|
||||
val database = section.getString("name")?.takeUnless { it.isBlank() }
|
||||
?: error("database.name must be provided")
|
||||
val username = section.getString("user")?.takeUnless { it.isBlank() }
|
||||
?: error("database.user must be provided")
|
||||
val password = section.getString("password")?.takeUnless { it.isBlank() }
|
||||
?: error("database.password must be provided")
|
||||
val poolSection = section.getConfigurationSection("pool")
|
||||
val maxPoolSize = poolSection?.getInt("maxPoolSize") ?: 8
|
||||
val minIdle = poolSection?.getInt("minIdle") ?: 2
|
||||
val maxLifetimeSeconds = poolSection?.getLong("maxLifetimeSeconds") ?: 1800L
|
||||
val connectionTimeoutSeconds = poolSection?.getLong("connectionTimeoutSeconds") ?: 30L
|
||||
|
||||
return DatabaseSettings(
|
||||
dialect = dialect,
|
||||
host = host,
|
||||
port = port,
|
||||
database = database,
|
||||
username = username,
|
||||
password = password,
|
||||
pool = PoolSettings(
|
||||
maxPoolSize = maxPoolSize,
|
||||
minIdle = minIdle,
|
||||
maxLifetimeMillis = maxLifetimeSeconds * 1000,
|
||||
connectionTimeoutMillis = connectionTimeoutSeconds * 1000
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object DatabaseSessionManager {
|
||||
@Volatile
|
||||
private var dataSource: HikariDataSource? = null
|
||||
@Volatile
|
||||
private var _database: Database? = null
|
||||
|
||||
val database: Database?
|
||||
get() = _database
|
||||
|
||||
fun requireDatabase(): Database =
|
||||
_database ?: throw IllegalStateException("Database session is not available")
|
||||
|
||||
fun isConnected(): Boolean = _database != null
|
||||
|
||||
fun connect(settings: DatabaseSettings, logger: Logger): Boolean {
|
||||
synchronized(this) {
|
||||
disconnectInternal(logger)
|
||||
return try {
|
||||
val hikariConfig = HikariConfig().apply {
|
||||
jdbcUrl = settings.jdbcUrl
|
||||
driverClassName = settings.dialect.driverClass
|
||||
username = settings.username
|
||||
password = settings.password
|
||||
maximumPoolSize = settings.pool.maxPoolSize
|
||||
minimumIdle = settings.pool.minIdle
|
||||
maxLifetime = settings.pool.maxLifetimeMillis
|
||||
connectionTimeout = settings.pool.connectionTimeoutMillis
|
||||
validate()
|
||||
}
|
||||
val dataSource = HikariDataSource(hikariConfig)
|
||||
val database = Database.connect(dataSource)
|
||||
dataSource.also { this.dataSource = it }
|
||||
this._database = database
|
||||
TransactionManager.defaultDatabase = database
|
||||
logger.info("Connected to database ${settings.jdbcUrl}")
|
||||
true
|
||||
} catch (ex: Exception) {
|
||||
logger.log(Level.SEVERE, "Failed to initialize database session", ex)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reload(settings: DatabaseSettings, logger: Logger): Boolean = connect(settings, logger)
|
||||
|
||||
fun disconnect(logger: Logger) {
|
||||
synchronized(this) {
|
||||
disconnectInternal(logger)
|
||||
}
|
||||
}
|
||||
|
||||
private fun disconnectInternal(logger: Logger) {
|
||||
_database = null
|
||||
dataSource?.close()
|
||||
if (dataSource != null) {
|
||||
logger.info("Database connection pool shut down")
|
||||
}
|
||||
dataSource = null
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun <T> transaction(block: JdbcTransaction.() -> T): T {
|
||||
val db = _database ?: throw IllegalStateException("Database is not connected")
|
||||
return transaction(db) { block() }
|
||||
}
|
||||
|
||||
fun ping(): Boolean {
|
||||
val db = _database ?: return false
|
||||
return runCatching {
|
||||
transaction(db) {
|
||||
exec("SELECT 1") { }
|
||||
}
|
||||
}.isSuccess
|
||||
}
|
||||
|
||||
fun ensureSchema(logger: Logger): Boolean {
|
||||
if (!isConnected()) {
|
||||
logger.severe("Cannot ensure schema because database is not connected")
|
||||
return false
|
||||
}
|
||||
return runCatching {
|
||||
transaction {
|
||||
SchemaUtils.createSequence(ActorIdSequence)
|
||||
SchemaUtils.createMissingTablesAndColumns(ActorsTable, PlayersTable)
|
||||
}
|
||||
logger.fine("Ensured actors/players schema")
|
||||
true
|
||||
}.onFailure {
|
||||
logger.log(Level.SEVERE, "Failed to ensure actors/players schema", it)
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user