feat: implement initial hcu Items plugin with command and event handling for special items

This commit is contained in:
Kariya 2025-12-07 17:21:56 +00:00
commit b19d9cf17a
115 changed files with 5929 additions and 0 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake

9
.gitattributes vendored Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,5 @@
Kotlin + Gradle kts
# How to Build
```./gradlew shadowJar```:

66
build.gradle.kts Normal file
View 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
View 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
View 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

Binary file not shown.

View 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
View 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
View 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
View File

@ -0,0 +1 @@
use flake

12
hcu-core/.gitattributes vendored Normal file
View 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
View File

@ -0,0 +1,5 @@
.direnv
.gradle
.kotlin
build
bin

4
hcu-core/.gitmodules vendored Normal file
View 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
View 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
View 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
View 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
'';
};
}
);
}

View 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

View 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

Binary file not shown.

View 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
View 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
View 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

View File

@ -0,0 +1 @@
use flake

12
hcu-core/kommand-lib/.gitattributes vendored Normal file
View 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
View File

@ -0,0 +1,5 @@
.direnv
.kotlin
.gradle
build
bin

4
hcu-core/kommand-lib/.gitmodules vendored Normal file
View File

@ -0,0 +1,4 @@
[submodule "permits-lib"]
path = permits-lib
url = git@gitea.hareworks.net:Hare/permits-lib.git
branch = master

View 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()` を使用している場合は一度にすべて移行する必要があります。

View 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 ライセンスの下で公開されています。

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

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

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

View File

@ -0,0 +1,5 @@
org.gradle.configuration-cache=true
org.gradle.parallel=true
org.gradle.caching=true
kotlin.stdlib.default.dependency=false

View 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

Binary file not shown.

View 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
View 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
View 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

View File

@ -0,0 +1 @@
use flake

View 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

View File

@ -0,0 +1,4 @@
.direnv
.gradle
bin
build

View 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` → 配下の権限をまとめて管理できる

View 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.

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

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

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

View 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

View 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

Binary file not shown.

View 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
View 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" "$@"

View 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

View File

@ -0,0 +1 @@
rootProject.name = "permits-lib"

View File

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

View File

@ -0,0 +1,6 @@
package net.hareworks.permits_lib.plugin
import org.bukkit.plugin.java.JavaPlugin
@Suppress("unused")
class Plugin : JavaPlugin() {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
package net.hareworks.permits_lib.dsl
@DslMarker
annotation class PermissionDsl

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
rootProject.name = "kommand-lib"
includeBuild("permits-lib")

View File

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

View File

@ -0,0 +1,6 @@
package net.hareworks.kommand_lib.plugin;
import org.bukkit.plugin.java.JavaPlugin
@Suppress("unused")
public class Plugin : JavaPlugin() {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
rootProject.name = "hcu-core"
includeBuild("kommand-lib")

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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