diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml
new file mode 100644
index 0000000..b4693a1
--- /dev/null
+++ b/.forgejo/workflows/build.yml
@@ -0,0 +1,19 @@
+on: [push]
+jobs:
+ build:
+ runs-on: docker
+ container: eclipse-temurin:21-alpine
+ steps:
+ - name: Install build dependencies
+ run: apk add nodejs # GitHub/actions require Node.js can you believe it
+
+ - name: Checkout
+ uses: https://github.com/actions/checkout@v4
+
+ - name: Build
+ run: ./mvnw package
+
+ - name: Upload artifacts
+ uses: https://github.com/actions/upload-artifact@v3
+ with:
+ path: target/tweaks-*.jar
\ No newline at end of file
diff --git a/.idea/giants.iml b/.idea/giants.iml
deleted file mode 100644
index a589521..0000000
--- a/.idea/giants.iml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
- SPIGOT
-
- 1
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
index fc57e88..b183bdb 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -2,7 +2,7 @@
-
+
\ No newline at end of file
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..d58dfb7
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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
+#
+# http://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.
+wrapperVersion=3.3.2
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
diff --git a/mvnw b/mvnw
new file mode 100755
index 0000000..19529dd
--- /dev/null
+++ b/mvnw
@@ -0,0 +1,259 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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
+#
+# http://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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.3.2
+#
+# Optional ENV vars
+# -----------------
+# JAVA_HOME - location of a JDK home dir, required when download maven via java source
+# MVNW_REPOURL - repo url base for downloading maven distribution
+# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
+# ----------------------------------------------------------------------------
+
+set -euf
+[ "${MVNW_VERBOSE-}" != debug ] || set -x
+
+# OS specific support.
+native_path() { printf %s\\n "$1"; }
+case "$(uname)" in
+CYGWIN* | MINGW*)
+ [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
+ native_path() { cygpath --path --windows "$1"; }
+ ;;
+esac
+
+# set JAVACMD and JAVACCMD
+set_java_home() {
+ # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
+ 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"
+ JAVACCMD="$JAVA_HOME/jre/sh/javac"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ JAVACCMD="$JAVA_HOME/bin/javac"
+
+ if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
+ echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
+ echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
+ return 1
+ fi
+ fi
+ else
+ JAVACMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v java
+ )" || :
+ JAVACCMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v javac
+ )" || :
+
+ if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
+ echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
+ return 1
+ fi
+ fi
+}
+
+# hash string like Java String::hashCode
+hash_string() {
+ str="${1:-}" h=0
+ while [ -n "$str" ]; do
+ char="${str%"${str#?}"}"
+ h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
+ str="${str#?}"
+ done
+ printf %x\\n $h
+}
+
+verbose() { :; }
+[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
+
+die() {
+ printf %s\\n "$1" >&2
+ exit 1
+}
+
+trim() {
+ # MWRAPPER-139:
+ # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
+ # Needed for removing poorly interpreted newline sequences when running in more
+ # exotic environments such as mingw bash on Windows.
+ printf "%s" "${1}" | tr -d '[:space:]'
+}
+
+# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
+while IFS="=" read -r key value; do
+ case "${key-}" in
+ distributionUrl) distributionUrl=$(trim "${value-}") ;;
+ distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
+ esac
+done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+
+case "${distributionUrl##*/}" in
+maven-mvnd-*bin.*)
+ MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
+ case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
+ *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
+ :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
+ :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
+ :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
+ *)
+ echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
+ distributionPlatform=linux-amd64
+ ;;
+ esac
+ distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
+ ;;
+maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
+*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
+esac
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
+[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
+distributionUrlName="${distributionUrl##*/}"
+distributionUrlNameMain="${distributionUrlName%.*}"
+distributionUrlNameMain="${distributionUrlNameMain%-bin}"
+MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
+MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
+
+exec_maven() {
+ unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
+ exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
+}
+
+if [ -d "$MAVEN_HOME" ]; then
+ verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ exec_maven "$@"
+fi
+
+case "${distributionUrl-}" in
+*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
+*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
+esac
+
+# prepare tmp dir
+if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
+ clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
+ trap clean HUP INT TERM EXIT
+else
+ die "cannot create temp dir"
+fi
+
+mkdir -p -- "${MAVEN_HOME%/*}"
+
+# Download and Install Apache Maven
+verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+verbose "Downloading from: $distributionUrl"
+verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+# select .zip or .tar.gz
+if ! command -v unzip >/dev/null; then
+ distributionUrl="${distributionUrl%.zip}.tar.gz"
+ distributionUrlName="${distributionUrl##*/}"
+fi
+
+# verbose opt
+__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
+[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
+
+# normalize http auth
+case "${MVNW_PASSWORD:+has-password}" in
+'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+esac
+
+if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
+ verbose "Found wget ... using wget"
+ wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
+elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
+ verbose "Found curl ... using curl"
+ curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
+elif set_java_home; then
+ verbose "Falling back to use Java to download"
+ javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
+ targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
+ cat >"$javaSource" <<-END
+ public class Downloader extends java.net.Authenticator
+ {
+ protected java.net.PasswordAuthentication getPasswordAuthentication()
+ {
+ return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
+ }
+ public static void main( String[] args ) throws Exception
+ {
+ setDefault( new Downloader() );
+ java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
+ }
+ }
+ END
+ # For Cygwin/MinGW, switch paths to Windows format before running javac and java
+ verbose " - Compiling Downloader.java ..."
+ "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
+ verbose " - Running Downloader.java ..."
+ "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
+fi
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+if [ -n "${distributionSha256Sum-}" ]; then
+ distributionSha256Result=false
+ if [ "$MVN_CMD" = mvnd.sh ]; then
+ echo "Checksum validation is not supported for maven-mvnd." >&2
+ echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ elif command -v sha256sum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ elif command -v shasum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
+ echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ fi
+ if [ $distributionSha256Result = false ]; then
+ echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
+ echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+# unzip and move
+if command -v unzip >/dev/null; then
+ unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
+else
+ tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
+fi
+printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
+mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
+
+clean || :
+exec_maven "$@"
diff --git a/mvnw.cmd b/mvnw.cmd
new file mode 100644
index 0000000..249bdf3
--- /dev/null
+++ b/mvnw.cmd
@@ -0,0 +1,149 @@
+<# : batch portion
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.3.2
+@REM
+@REM Optional ENV vars
+@REM MVNW_REPOURL - repo url base for downloading maven distribution
+@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
+@REM ----------------------------------------------------------------------------
+
+@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
+@SET __MVNW_CMD__=
+@SET __MVNW_ERROR__=
+@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
+@SET PSModulePath=
+@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
+ IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
+)
+@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
+@SET __MVNW_PSMODULEP_SAVE=
+@SET __MVNW_ARG0_NAME__=
+@SET MVNW_USERNAME=
+@SET MVNW_PASSWORD=
+@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
+@echo Cannot start maven from wrapper >&2 && exit /b 1
+@GOTO :EOF
+: end batch / begin powershell #>
+
+$ErrorActionPreference = "Stop"
+if ($env:MVNW_VERBOSE -eq "true") {
+ $VerbosePreference = "Continue"
+}
+
+# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
+$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
+if (!$distributionUrl) {
+ Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
+}
+
+switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
+ "maven-mvnd-*" {
+ $USE_MVND = $true
+ $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
+ $MVN_CMD = "mvnd.cmd"
+ break
+ }
+ default {
+ $USE_MVND = $false
+ $MVN_CMD = $script -replace '^mvnw','mvn'
+ break
+ }
+}
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
+if ($env:MVNW_REPOURL) {
+ $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
+ $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
+}
+$distributionUrlName = $distributionUrl -replace '^.*/',''
+$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
+$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
+if ($env:MAVEN_USER_HOME) {
+ $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
+}
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
+$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
+
+if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
+ Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+ exit $?
+}
+
+if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
+ Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
+}
+
+# prepare tmp dir
+$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
+$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
+$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
+trap {
+ if ($TMP_DOWNLOAD_DIR.Exists) {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+ }
+}
+
+New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
+
+# Download and Install Apache Maven
+Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+Write-Verbose "Downloading from: $distributionUrl"
+Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+$webclient = New-Object System.Net.WebClient
+if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
+ $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
+}
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
+if ($distributionSha256Sum) {
+ if ($USE_MVND) {
+ Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
+ }
+ Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
+ if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
+ Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
+ }
+}
+
+# unzip and move
+Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
+try {
+ Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
+} catch {
+ if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
+ Write-Error "fail to move MAVEN_HOME"
+ }
+} finally {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+}
+
+Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
diff --git a/pom.xml b/pom.xml
index 5bcf6bc..e9db78d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
4.0.0
eu.m724
giants
- 2.0.12-SNAPSHOT
+ 2.1.0-SNAPSHOT
21
@@ -30,21 +30,6 @@
spigot-api
1.16.5-R0.1-SNAPSHOT
provided
-
-
-
- org.yaml
- snakeyaml
-
-
- com.google.guava
- guava
-
-
- com.google.code.gson
- gson
-
-
@@ -53,7 +38,6 @@
0.1.2
-
eu.m724
jarupdater
diff --git a/src/main/java/eu/m724/giants/DebugLogger.java b/src/main/java/eu/m724/giants/DebugLogger.java
new file mode 100644
index 0000000..d51abd3
--- /dev/null
+++ b/src/main/java/eu/m724/giants/DebugLogger.java
@@ -0,0 +1,71 @@
+package eu.m724.giants;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+public class DebugLogger {
+ private DebugLogger() {}
+ static Logger logger;
+
+ public static void info(String message, Object... format) {
+ log(Level.INFO, message, format);
+ }
+
+ public static void warning(String message, Object... format) {
+ log(Level.WARNING, message, format);
+ }
+
+ public static void severe(String message, Object... format) {
+ log(Level.SEVERE, message, format);
+ }
+
+ public static void fine(String message, Object... format) {
+ log(Level.FINE, message, format);
+ }
+
+ public static void finer(String message, Object... format) {
+ log(Level.FINER, message, format);
+ }
+
+ private static void log(Level level, String message, Object... format) {
+ if (!logger.isLoggable(level)) {
+ return;
+ }
+
+ message = message.formatted(format);
+
+ if (logger.getLevel().intValue() <= Level.FINE.intValue()) {
+ message = "[" + getCaller() + "] " + message;
+ }
+
+ if (level.intValue() < Level.INFO.intValue()) { // levels below info are never logged even if set for some reason
+ // colors text gray (cyan is close to gray)
+ if (level == Level.FINE) {
+ message = "\033[38;5;250m" + message + "\033[39m";
+ } else {
+ message = "\033[38;5;245m" + message + "\033[39m";
+ }
+ level = Level.INFO;
+ }
+
+ logger.log(level, message);
+ }
+
+ private static String getCaller() {
+ String caller = Thread.currentThread().getStackTrace()[4].getClassName();
+
+ if (caller.startsWith("eu.m724.giants.")) {
+ caller = caller.substring(15);
+
+ String[] packages = caller.split("\\.");
+
+ caller = IntStream.range(0, packages.length - 1)
+ .mapToObj(i -> packages[i].substring(0, 2))
+ .collect(Collectors.joining(".")) + "." + packages[packages.length - 1];
+ }
+
+ return caller;
+ }
+}
diff --git a/src/main/java/eu/m724/giants/Drop.java b/src/main/java/eu/m724/giants/Drop.java
index ad51803..fbb3c74 100644
--- a/src/main/java/eu/m724/giants/Drop.java
+++ b/src/main/java/eu/m724/giants/Drop.java
@@ -5,14 +5,27 @@ import org.bukkit.inventory.ItemStack;
import java.util.concurrent.ThreadLocalRandom;
-public record Drop(ItemStack itemStack, int min, int max, double chance) {
+/**
+ * Represents an item drop.
+ *
+ * @param itemStack the item to drop
+ * @param min Minimum quantity to drop, inclusive
+ * @param max Maximum quantity to drop, inclusive
+ * @param chance The chance that the item will be dropped (doesn't affect quantity)
+ */
+public record Drop(
+ ItemStack itemStack,
+ int min,
+ int max,
+ double chance
+) {
/**
* Randomizes quantity and returns {@link ItemStack}.
* This should be called every drop.
*
* @return A {@link ItemStack} with randomized quantity
*/
- private ItemStack generate() {
+ private ItemStack generateItemStack() {
int amount = ThreadLocalRandom.current().nextInt(min, max + 1);
ItemStack itemStack = this.itemStack.clone();
@@ -21,13 +34,13 @@ public record Drop(ItemStack itemStack, int min, int max, double chance) {
}
/**
- * Drops the item at {@code location} taking into account quantity and chance.
+ * Drops the item at the specified location. Quantity and chance are used.
*
* @param location The location to drop the drop at
*/
public void dropAt(Location location) {
- if (chance > ThreadLocalRandom.current().nextDouble()) {
- ItemStack itemStack = generate();
+ if (chance > ThreadLocalRandom.current().nextDouble()) { // TODO faster random here as well?
+ ItemStack itemStack = generateItemStack();
location.getWorld().dropItemNaturally(location, itemStack);
}
}
diff --git a/src/main/java/eu/m724/giants/GiantsCommand.java b/src/main/java/eu/m724/giants/GiantsCommand.java
index d7bfa35..2785e9b 100644
--- a/src/main/java/eu/m724/giants/GiantsCommand.java
+++ b/src/main/java/eu/m724/giants/GiantsCommand.java
@@ -50,6 +50,8 @@ public class GiantsCommand implements CommandExecutor {
updateCommand.updateCommand(sender, args);
else
sender.sendMessage(ChatColor.GRAY + "Updater is disabled");
+ } else {
+ sender.sendMessage(ChatColor.RED + "No such command: " + action);
}
return true;
diff --git a/src/main/java/eu/m724/giants/GiantsPlugin.java b/src/main/java/eu/m724/giants/GiantsPlugin.java
index a215922..c69c65d 100644
--- a/src/main/java/eu/m724/giants/GiantsPlugin.java
+++ b/src/main/java/eu/m724/giants/GiantsPlugin.java
@@ -1,6 +1,6 @@
package eu.m724.giants;
-import eu.m724.giants.ai.GiantProcessor;
+import eu.m724.giants.ai.*;
import eu.m724.giants.configuration.Configuration;
import eu.m724.giants.updater.PluginUpdater;
import eu.m724.giants.updater.UpdateCommand;
@@ -12,27 +12,43 @@ import org.bukkit.command.CommandExecutor;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
+import java.util.logging.Level;
+// TODO unmess this
public class GiantsPlugin extends MStatsPlugin implements CommandExecutor {
private static GiantsPlugin INSTANCE;
private final File configFile = new File(getDataFolder(), "config.yml");
private Configuration configuration;
- private GiantProcessor giantProcessor;
+ private GiantSpawnTools giantSpawnTools;
@Override
public void onEnable() {
+ long start = System.nanoTime();
INSTANCE = this;
if (!configFile.exists()) {
saveResource("config.yml", false);
}
- this.configuration = Configuration.load(this, configFile);
- this.giantProcessor = new GiantProcessor(this, configuration);
+ this.configuration = Configuration.load(configFile);
- giantProcessor.start();
+ getLogger().setLevel(configuration.debug() ? Level.FINEST : Level.INFO);
+ DebugLogger.logger = getLogger();
+
+ GiantAndZombieTracker tracker = new GiantAndZombieTracker();
+
+ this.giantSpawnTools = new GiantSpawnTools(this, configuration.potionEffects(), tracker);
+
+ if (configuration.aiEnabled()) {
+ new GiantTicker(
+ new GiantJumper(configuration.jumpMode(), configuration.jumpDelay(), configuration.jumpCondition(), configuration.jumpHeight()),
+ tracker, configuration.attackReach(), configuration.attackDamage()
+ ).runTaskTimer(this, 0, configuration.attackDelay());
+ }
+
+ getServer().getPluginManager().registerEvents(new GiantEventListener(giantSpawnTools, tracker, configuration.drops(), configuration.spawnChance(), configuration.burningHead()), this);
PluginUpdater updater = null;
UpdateCommand updateCommand = null;
@@ -64,6 +80,8 @@ public class GiantsPlugin extends MStatsPlugin implements CommandExecutor {
getCommand("giants").setExecutor(new GiantsCommand(this, updateCommand));
mStats(3);
+
+ DebugLogger.fine("Took %.3f milliseconds", (System.nanoTime() - start) / 1000000.0);
}
public static GiantsPlugin getInstance() {
@@ -74,6 +92,10 @@ public class GiantsPlugin extends MStatsPlugin implements CommandExecutor {
return configuration;
}
+ public GiantSpawnTools getGiantSpawnTools() {
+ return giantSpawnTools;
+ }
+
/**
* Checks if a giant can be spawned at a location
* The check is very approximate, but works for most scenarios
@@ -82,6 +104,6 @@ public class GiantsPlugin extends MStatsPlugin implements CommandExecutor {
* @return Whether a giant can be spawned
*/
public boolean isSpawnableAt(Location location) {
- return giantProcessor.isSpawnableAt(location);
+ return giantSpawnTools.isSpawnableAt(location);
}
}
diff --git a/src/main/java/eu/m724/giants/ai/GiantAndZombieTracker.java b/src/main/java/eu/m724/giants/ai/GiantAndZombieTracker.java
new file mode 100644
index 0000000..e05c5e6
--- /dev/null
+++ b/src/main/java/eu/m724/giants/ai/GiantAndZombieTracker.java
@@ -0,0 +1,47 @@
+package eu.m724.giants.ai;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import org.bukkit.entity.Giant;
+import org.bukkit.entity.Zombie;
+import org.bukkit.event.Listener;
+
+import java.util.*;
+
+public class GiantAndZombieTracker implements Listener {
+ private final BiMap giantZombieMap = HashBiMap.create();
+
+ public Set getZombies() {
+ // Shallow copy; copies references, efficient.
+ return new HashSet<>(giantZombieMap.values());
+ }
+
+ public Set getGiants() {
+ // Shallow copy; copies references, efficient.
+ return new HashSet<>(giantZombieMap.keySet());
+ }
+
+ void add(Giant giant, Zombie zombie) {
+ giantZombieMap.put(giant, zombie);
+ }
+
+ void remove(Giant giant) {
+ giantZombieMap.remove(giant);
+ }
+
+ void remove(Zombie zombie) {
+ giantZombieMap.inverse().remove(zombie);
+ }
+
+ Zombie getZombieOf(Giant giant) {
+ return giantZombieMap.get(giant);
+ }
+
+ Giant getGiantOf(Zombie zombie) {
+ return giantZombieMap.inverse().get(zombie);
+ }
+
+ Zombie removeAndGetZombieOf(Giant giant) {
+ return giantZombieMap.remove(giant);
+ }
+}
diff --git a/src/main/java/eu/m724/giants/ai/GiantEventListener.java b/src/main/java/eu/m724/giants/ai/GiantEventListener.java
new file mode 100644
index 0000000..d72309e
--- /dev/null
+++ b/src/main/java/eu/m724/giants/ai/GiantEventListener.java
@@ -0,0 +1,151 @@
+package eu.m724.giants.ai;
+
+import eu.m724.giants.DebugLogger;
+import eu.m724.giants.Drop;
+import org.bukkit.entity.*;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.*;
+import org.bukkit.event.world.ChunkLoadEvent;
+import org.bukkit.event.world.ChunkUnloadEvent;
+
+import java.util.Arrays;
+import java.util.concurrent.ThreadLocalRandom;
+
+public class GiantEventListener implements Listener {
+ private final GiantSpawnTools spawnTools;
+ private final GiantAndZombieTracker tracker;
+
+ private final Drop[] drops;
+ private final double spawnChance;
+ private final boolean burningHead;
+
+ public GiantEventListener(GiantSpawnTools spawnTools, GiantAndZombieTracker tracker, Drop[] drops, double spawnChance, boolean burningHead) {
+ this.spawnTools = spawnTools;
+ this.tracker = tracker;
+
+ this.drops = drops;
+ this.spawnChance = spawnChance;
+ this.burningHead = burningHead;
+ }
+
+ @EventHandler
+ public void onChunkLoad(ChunkLoadEvent event) {
+ DebugLogger.finer("Chunk %d, %d loaded, checking for Giants", event.getChunk().getX(), event.getChunk().getZ());
+
+ Entity[] entities = event.getChunk().getEntities();
+
+ Arrays.stream(entities)
+ .filter(spawnTools::isValidLegacyPassengerHusk)
+ .forEach(Entity::remove); // Legacy husks removed
+
+ Arrays.stream(entities)
+ .filter(spawnTools::isValidPassengerZombie)
+ .forEach(Entity::remove); // Will be respawned just below, if needed
+
+ Arrays.stream(entities)
+ .filter(entity -> entity.getType() == EntityType.GIANT)
+ .map(Giant.class::cast)
+ .forEach(spawnTools::applyGiantsLogic); // Respawn Zombie. Effects can be reapplied
+ }
+
+ @EventHandler
+ public void onChunkUnload(ChunkUnloadEvent event) {
+ Entity[] entities = event.getChunk().getEntities();
+
+ // Zombies are supposed to be ephemeral
+ Arrays.stream(entities)
+ .filter(spawnTools::isValidPassengerZombie)
+ .map(Zombie.class::cast)
+ .forEach(Entity::remove);
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void entitySpawn(EntitySpawnEvent e) {
+ if (e.isCancelled()) return;
+
+ if (e.getEntity() instanceof Giant giant) {
+ DebugLogger.fine("Giant #%d just spawned at %s", giant.getEntityId(), giant.getLocation().toString());
+
+ spawnTools.applyGiantsLogic(giant);
+ }
+
+ if (e.getEntityType() == EntityType.ZOMBIE) {
+ DebugLogger.finer("Zombie spawned at %s", e.getLocation().toString());
+ if (spawnChance > ThreadLocalRandom.current().nextDouble()) { // TODO faster random
+ if (spawnTools.isSpawnableAt(e.getLocation())) {
+ DebugLogger.fine("Spawning a Giant at %s due to chance", e.getLocation().toString());
+ e.getLocation().getWorld().spawnEntity(e.getLocation(), EntityType.GIANT);
+ e.setCancelled(true);
+ }
+ }
+ }
+ }
+
+ @EventHandler
+ public void entityDeath(EntityDeathEvent e) {
+ LivingEntity entity = e.getEntity();
+
+ if (entity instanceof Giant giant) {
+ DebugLogger.fine("Giant #%d just died at %s", giant.getEntityId(), giant.getLocation().toString());
+
+ Zombie zombie = tracker.removeAndGetZombieOf(giant);
+
+ for (Drop drop : drops) {
+ DebugLogger.finer("Dropping: %s", drop.itemStack().toString());
+
+ drop.dropAt(giant.getLocation());
+ }
+
+ if (zombie != null) {
+ DebugLogger.finer("Removing Zombie #%d", zombie.getEntityId());
+ zombie.remove();
+ } else {
+ DebugLogger.finer("Giant #%d had no passenger Zombie!", giant.getEntityId());
+ }
+ }
+
+ // TODO can Zombie die?
+ }
+
+ @EventHandler
+ public void onEntityDamageByEntity(EntityDamageByEntityEvent event) {
+ if (!(event.getEntity() instanceof Giant giant)) return;
+
+ DebugLogger.finer("Giant #%d damaged", giant.getEntityId());
+
+ Zombie zombie = tracker.getZombieOf(giant);
+ if (zombie != null) {
+ DebugLogger.fine("Giant #%d damaged, damaging Zombie #%d", giant.getEntityId(), zombie.getEntityId());
+
+ // Make the Zombie target the attacker.
+ // TODO this seems to not work.
+ zombie.damage(0, event.getDamager());
+ } else {
+ DebugLogger.finer("Giant #%d has no passenger Zombie!", giant.getEntityId());
+ }
+ }
+
+ // TODO remove this debug event
+ @EventHandler
+ public void onEntityTarget(EntityTargetEvent event) {
+ DebugLogger.fine("%s targeted %s", event.getEntity().toString(), event.getTarget());
+ }
+
+ @EventHandler
+ public void onEntityBurn(EntityCombustEvent event) {
+ if (burningHead) return;
+
+ if (spawnTools.isValidPassengerZombie(event.getEntity())) {
+ event.setCancelled(true);
+ }
+ }
+
+ @EventHandler
+ public void onEntityDamage(EntityDamageEvent event) {
+ if (spawnTools.isValidPassengerZombie(event.getEntity())) {
+ event.setCancelled(true);
+ }
+ }
+}
diff --git a/src/main/java/eu/m724/giants/ai/GiantJumper.java b/src/main/java/eu/m724/giants/ai/GiantJumper.java
index cb40b89..bed6f0e 100644
--- a/src/main/java/eu/m724/giants/ai/GiantJumper.java
+++ b/src/main/java/eu/m724/giants/ai/GiantJumper.java
@@ -1,7 +1,6 @@
package eu.m724.giants.ai;
-import eu.m724.giants.GiantsPlugin;
-import eu.m724.giants.configuration.Configuration;
+import eu.m724.giants.DebugLogger;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.util.Vector;
@@ -10,42 +9,73 @@ import java.util.HashMap;
import java.util.Map;
public class GiantJumper {
- private final Configuration configuration = GiantsPlugin.getInstance().getConfiguration();
-
private final Map giantLastJump = new HashMap<>();
+ private final Map giantLocations = new HashMap<>();
+
+ private final int jumpMode; // this too
+ private final int jumpDelayMillis;
+ private final int jumpCondition; // TODO make enum?
+ private final double jumpHeight;
+
+ public GiantJumper(int jumpMode, int jumpDelayMillis, int jumpCondition, double jumpHeight) {
+ this.jumpMode = jumpMode;
+ this.jumpDelayMillis = jumpDelayMillis;
+ this.jumpCondition = jumpCondition;
+ this.jumpHeight = jumpHeight;
+ }
+
+ void tick(Entity giant, Entity target) {
+ if (jumpMode == 0) return;
+ if (target == null) return;
+
+ DebugLogger.finer("Attempting to make Giant #%d jump", giant.getEntityId());
+
+ // tracking location is only required for jumping
+ Location currentLocation = giant.getLocation();
+ Location previousLocation = giantLocations.put(giant, currentLocation);
+ if (previousLocation == null) {
+ previousLocation = currentLocation;
+ }
- void processJump(Entity giant, Location prevLocation, Location location, Location targetLocation) {
long now = System.currentTimeMillis();
- if (now - giantLastJump.getOrDefault(giant, 0L) < configuration.jumpDelay()) {
+ if (now - giantLastJump.getOrDefault(giant, 0L) < jumpDelayMillis) {
return;
}
if (giant.isOnGround()) {
giantLastJump.put(giant, now);
- if (configuration.jumpCondition() == 0) {
- if (targetLocation.subtract(location).getY() > 0) {
- jump(giant);
- }
- } else if (configuration.jumpCondition() == 1) {
- Location delta = prevLocation.subtract(location);
- if (targetLocation.subtract(location).getY() > 0 && (delta.getX() == 0 || delta.getZ() == 0)) {
- jump(giant);
- }
- } else if (configuration.jumpCondition() == 2) {
- Location delta = prevLocation.subtract(location);
- if (delta.getX() == 0 || delta.getZ() == 0) {
- jump(giant);
- }
- } // I could probably simplify that code
+ switch (jumpCondition) {
+ case 0: {
+ if (target.getLocation().subtract(currentLocation).getY() > 0) {
+ makeJump(giant);
+ }
+ }
+ case 1: {
+ Location delta = previousLocation.subtract(currentLocation);
+ if (target.getLocation().subtract(currentLocation).getY() > 0 && (delta.getX() == 0 || delta.getZ() == 0)) {
+ makeJump(giant);
+ }
+ }
+ case 2: {
+ Location delta = previousLocation.subtract(currentLocation);
+ if (delta.getX() == 0 || delta.getZ() == 0) {
+ makeJump(giant);
+ }
+ }
+ }
}
}
- private void jump(Entity giant) {
- if (configuration.jumpMode() == 1) {
- giant.setVelocity(new Vector(0, configuration.jumpHeight(), 0));
- } else if (configuration.jumpMode() == 2) {
- giant.teleport(giant.getLocation().add(0, configuration.jumpHeight(), 0));
+ private void makeJump(Entity giant) {
+ DebugLogger.finer("Yes, Giant #%d should jump", giant.getEntityId());
+ switch (jumpMode) {
+ case 1: {
+ giant.setVelocity(new Vector(0, jumpHeight, 0));
+ }
+ case 2: {
+ giant.teleport(giant.getLocation().add(0, jumpHeight, 0));
+ }
}
}
}
diff --git a/src/main/java/eu/m724/giants/ai/GiantProcessor.java b/src/main/java/eu/m724/giants/ai/GiantProcessor.java
deleted file mode 100644
index 400a898..0000000
--- a/src/main/java/eu/m724/giants/ai/GiantProcessor.java
+++ /dev/null
@@ -1,163 +0,0 @@
-package eu.m724.giants.ai;
-
-import eu.m724.giants.Drop;
-import eu.m724.giants.configuration.Configuration;
-import org.bukkit.Location;
-import org.bukkit.NamespacedKey;
-import org.bukkit.entity.*;
-import org.bukkit.event.EventHandler;
-import org.bukkit.event.Listener;
-import org.bukkit.event.entity.EntityDeathEvent;
-import org.bukkit.event.entity.EntitySpawnEvent;
-import org.bukkit.event.world.ChunkLoadEvent;
-import org.bukkit.persistence.PersistentDataType;
-import org.bukkit.plugin.java.JavaPlugin;
-import org.bukkit.potion.PotionEffect;
-import org.bukkit.potion.PotionEffectType;
-
-import java.util.*;
-import java.util.concurrent.ThreadLocalRandom;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-// TODO move ai stuff to another class
-/**
- * A processor class that processes giants
- */
-public class GiantProcessor implements Listener {
- private static final int VERSION = 1;
-
- private final JavaPlugin plugin;
- final Configuration configuration;
- private final Logger logger;
-
- final Set trackedHusks = new HashSet<>();
- final Map giantLocationMap = new HashMap<>();
-
- private final ThreadLocalRandom random = ThreadLocalRandom.current();
- private final NamespacedKey huskKey;
-
- public GiantProcessor(JavaPlugin plugin, Configuration configuration) {
- this.plugin = plugin;
- this.configuration = configuration;
- this.logger = Logger.getLogger(plugin.getLogger().getName() + ".GiantProcessor");
- logger.setLevel(Level.ALL);
- this.huskKey = new NamespacedKey(plugin, "husk");
- }
-
- public void start() {
- if (configuration.aiEnabled()) {
- new GiantTicker(this).schedule(plugin);
- }
-
- plugin.getServer().getPluginManager().registerEvents(this, plugin);
- }
-
- /**
- * The check is very approximate
- *
- * @param location the location
- * @return whether a giant can be spawned here
- */
- public boolean isSpawnableAt(Location location) {
- for (int y=0; y<=12; y++) {
- if (!location.clone().add(0, y, 0).getBlock().isEmpty()) // isPassable also seems good
- return false;
- }
-
- return true;
- }
-
- @EventHandler
- public void onChunkLoad(ChunkLoadEvent event) {
- Entity[] entities = event.getChunk().getEntities();
- logger.fine("Chunk loaded: " + event.getChunk().getX() + " " + event.getChunk().getZ());
-
- Husk[] husks = Arrays.stream(entities)
- .filter(entity -> entity instanceof Husk && entity.getPersistentDataContainer().has(huskKey, PersistentDataType.INTEGER))
- .map(Husk.class::cast)
- .toArray(Husk[]::new);
-
- for (Husk husk : husks) {
- logger.fine("Husk found at " + husk.getLocation());
-
- Entity giant = husk.getVehicle();
-
- if (giant instanceof Giant) {
- trackedHusks.add(husk);
-
- logger.fine("Tracking a loaded Giant at " + giant.getLocation());
- } else {
- // kill stray husks, that is those without a giant
- husk.setHealth(0);
-
- logger.fine("Stray Husk killed at " + husk.getLocation());
- }
- }
- }
-
- public void applyGiantsLogic(Giant giant) {
- if (configuration.aiEnabled()) {
- // The husk moves the giant. That's the magic.
- LivingEntity passenger = (LivingEntity) giant.getWorld().spawnEntity(giant.getLocation(), EntityType.HUSK);
- new PotionEffect(PotionEffectType.INVISIBILITY, Integer.MAX_VALUE, 1).apply(passenger);
- passenger.setInvulnerable(true);
- passenger.setPersistent(true);
- passenger.getPersistentDataContainer().set(huskKey, PersistentDataType.INTEGER, VERSION);
-
- giant.addPassenger(passenger);
-
- trackedHusks.add((Husk) passenger);
- }
-
- configuration.potionEffects().forEach(giant::addPotionEffect);
- }
-
- @EventHandler
- public void entitySpawn(EntitySpawnEvent e) {
- if (e.getEntityType() == EntityType.GIANT) {
- logger.fine("Handling spawned Giant at " + e.getLocation());
-
- var giant = (Giant) e.getEntity();
- if (giant.hasAI()) // NoAI flag
- applyGiantsLogic(giant);
- }
-
- if (configuration.worldBlacklist().contains(e.getLocation().getWorld().getName()))
- return;
-
- if (e.getEntityType() == EntityType.ZOMBIE) {
- if (configuration.spawnChance() > random.nextDouble()) {
- logger.fine("Trying to spawn a Giant by chance at " + e.getLocation());
- if (isSpawnableAt(e.getLocation())) {
- logger.fine("Spawned a Giant by chance at " + e.getLocation());
- e.getLocation().getWorld().spawnEntity(e.getLocation(), EntityType.GIANT);
- e.setCancelled(true);
- }
- }
- }
- }
-
- @EventHandler
- public void entityDeath(EntityDeathEvent e) {
- LivingEntity entity = e.getEntity();
-
- if (entity.getType() == EntityType.GIANT) {
- Location location = entity.getLocation();
- logger.fine("A Giant died at " + location);
-
- for (Drop drop : configuration.drops()) {
- logger.fine("Rolling a drop: " + drop.itemStack().toString());
-
- drop.dropAt(location);
- }
-
- for (Entity passenger : entity.getPassengers()) {
- if (passenger.getPersistentDataContainer().has(huskKey, PersistentDataType.INTEGER)) {
- ((LivingEntity) passenger).setHealth(0);
- logger.fine("Killed a Husk");
- }
- }
- }
- }
-}
diff --git a/src/main/java/eu/m724/giants/ai/GiantSpawnTools.java b/src/main/java/eu/m724/giants/ai/GiantSpawnTools.java
new file mode 100644
index 0000000..5d0cbf3
--- /dev/null
+++ b/src/main/java/eu/m724/giants/ai/GiantSpawnTools.java
@@ -0,0 +1,90 @@
+package eu.m724.giants.ai;
+
+import eu.m724.giants.DebugLogger;
+import org.bukkit.Location;
+import org.bukkit.NamespacedKey;
+import org.bukkit.entity.*;
+import org.bukkit.event.Listener;
+import org.bukkit.persistence.PersistentDataType;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.potion.PotionEffect;
+
+/**
+ * A processor class that processes giants
+ */
+public class GiantSpawnTools implements Listener {
+ /**
+ * The mechanics version. If it's significantly changed how the plugin works, it will be incremented.
+ */
+ public static final int VERSION = 1;
+
+ private final PotionEffect[] potionEffects;
+ private final GiantAndZombieTracker tracker;
+
+ private final NamespacedKey passengerZombieKey;
+ private final NamespacedKey legacyPassengerHuskKey;
+
+
+ public GiantSpawnTools(Plugin plugin, PotionEffect[] potionEffects, GiantAndZombieTracker tracker) {
+ this.potionEffects = potionEffects;
+ this.tracker = tracker;
+
+ this.passengerZombieKey = new NamespacedKey(plugin, "zombie");
+ this.legacyPassengerHuskKey = new NamespacedKey(plugin, "husk");
+ }
+
+ /**
+ * The check is very approximate
+ *
+ * @param location the location
+ * @return whether a giant can be spawned here
+ */
+ public boolean isSpawnableAt(Location location) {
+ for (int y=0; y<=12; y++) {
+ if (!location.clone().add(0, y, 0).getBlock().isEmpty()) // isPassable also seems good
+ return false;
+ }
+
+ return true;
+ }
+
+ public void applyGiantsLogic(Giant giant) {
+ DebugLogger.fine("Applying Giants logic to Giant #%d", giant.getEntityId());
+
+ Zombie zombie = spawnZombie(giant.getLocation());
+ giant.addPassenger(zombie);
+
+ // TODO probably not to everyone's liking
+ giant.getActivePotionEffects()
+ .forEach(potionEffect -> giant.removePotionEffect(potionEffect.getType()));
+
+ for (PotionEffect potionEffect : potionEffects) {
+ potionEffect.apply(giant);
+ }
+
+ tracker.add(giant, zombie);
+ }
+
+ private Zombie spawnZombie(Location location) {
+ // The zombie controls the giant. That's the entire magic.
+ Zombie zombie = (Zombie) location.getWorld().spawnEntity(location, EntityType.ZOMBIE);
+
+ zombie.setInvisible(true);
+ zombie.getPersistentDataContainer().set(passengerZombieKey, PersistentDataType.INTEGER, VERSION);
+
+ DebugLogger.finer("Spawned Zombie #%d at %d, %d, %d", zombie.getEntityId(), location.getBlockX(), location.getBlockY(), location.getBlockZ());
+
+ return zombie;
+ }
+
+ public boolean isValidPassengerZombie(Entity entity) {
+ return entity.getType() == EntityType.ZOMBIE
+ && entity.getPersistentDataContainer().has(passengerZombieKey, PersistentDataType.INTEGER);
+ }
+
+ @Deprecated
+ public boolean isValidLegacyPassengerHusk(Entity entity) {
+ return entity.getType() == EntityType.HUSK
+ && entity.getPersistentDataContainer().has(legacyPassengerHuskKey, PersistentDataType.INTEGER);
+ }
+}
diff --git a/src/main/java/eu/m724/giants/ai/GiantTicker.java b/src/main/java/eu/m724/giants/ai/GiantTicker.java
index 4f23567..c917385 100644
--- a/src/main/java/eu/m724/giants/ai/GiantTicker.java
+++ b/src/main/java/eu/m724/giants/ai/GiantTicker.java
@@ -1,75 +1,62 @@
package eu.m724.giants.ai;
-import eu.m724.giants.GiantsPlugin;
-import eu.m724.giants.configuration.Configuration;
-import org.bukkit.Location;
-import org.bukkit.entity.*;
-import org.bukkit.plugin.Plugin;
+import eu.m724.giants.DebugLogger;
+import org.bukkit.entity.Giant;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Zombie;
import org.bukkit.scheduler.BukkitRunnable;
-
-import java.util.Set;
+import org.bukkit.util.Vector;
/**
* Ticks giants
*/
public class GiantTicker extends BukkitRunnable {
- private final Configuration configuration = GiantsPlugin.getInstance().getConfiguration();
- private final GiantJumper jumper = new GiantJumper();
+ private final GiantJumper giantJumper;
+ private final GiantAndZombieTracker tracker;
- private final GiantProcessor giantProcessor;
+ private final Vector attackReach;
+ private final double attackDamage;
- public GiantTicker(GiantProcessor giantProcessor) {
- this.giantProcessor = giantProcessor;
- }
+ public GiantTicker(GiantJumper giantJumper, GiantAndZombieTracker tracker, Vector attackReach, double attackDamage) {
+ this.giantJumper = giantJumper;
+ this.tracker = tracker;
- void schedule(Plugin plugin) {
- this.runTaskTimer(plugin, 0, configuration.attackDelay());
+ this.attackReach = attackReach;
+ this.attackDamage = attackDamage;
}
@Override
public void run() {
- for (Husk husk : Set.copyOf(giantProcessor.trackedHusks)) {
- if (husk.isValid()) {
- Location huskLocation = husk.getLocation();
- Entity giant = husk.getVehicle();
+ DebugLogger.finer("Ticking Giants");
- if (giant instanceof Giant) {
- if (giant.isValid()) { // TODO reconsider
- giant.getWorld().getNearbyEntities(
- giant.getBoundingBox().expand(configuration.attackReach()),
- e -> (e instanceof Player && !e.isInvulnerable())
- ).forEach(p -> ((Player) p).damage(configuration.attackDamage(), giant));
- giant.setRotation(huskLocation.getYaw(), huskLocation.getPitch());
+ for (Giant giant : tracker.getGiants()) {
+ DebugLogger.finer("Ticking Giant #%d at %s", giant.getEntityId(), giant.getLocation().toString());
+ Zombie zombie = tracker.getZombieOf(giant);
- // TODO move whole into that class?
- if (configuration.jumpMode() != 0) {
- // tracking location is only required for jumping
- Location prevLocation = giantProcessor.giantLocationMap.get(giant);
- Location location = giant.getLocation();
- if (prevLocation == null) {
- prevLocation = location;
- }
- giantProcessor.giantLocationMap.put(giant, location);
+ if (!giant.isValid()) {
+ DebugLogger.fine("Removing Giant #%d, it's invalid", giant.getEntityId());
+ tracker.remove(giant);
- LivingEntity target = husk.getTarget();
- if (target != null) {
- jumper.processJump(giant, prevLocation, location, target.getLocation());
- }
- }
- }
- } else {
- // no vehicle means the giant doesn't exist anymore and the husk should also not exist
- husk.setHealth(0);
+ giant.remove();
+ if (zombie != null) zombie.remove();
- giantProcessor.trackedHusks.remove(husk);
- //logger.fine("Husk killed because Giant died at " + husk.getLocation());
- }
- } else {
- giantProcessor.trackedHusks.remove(husk);
- //logger.fine("Husk unloaded at " + husk.getLocation());
+ continue;
}
+
+ // TODO https://git.m724.eu/Minecon724/Giants/issues/5
+ giant.getWorld().getNearbyEntities(
+ giant.getBoundingBox().expand(attackReach),
+ e -> (e instanceof Player && !e.isInvulnerable())
+ ).forEach(p -> ((Player) p).damage(attackDamage, giant));
+
+ giant.setRotation(
+ zombie.getLocation().getYaw(),
+ zombie.getLocation().getPitch()
+ );
+
+ giantJumper.tick(giant, zombie.getTarget());
}
+
+ // TODO remove zombies?
}
-
-
}
diff --git a/src/main/java/eu/m724/giants/configuration/Configuration.java b/src/main/java/eu/m724/giants/configuration/Configuration.java
index bf2b216..6264032 100644
--- a/src/main/java/eu/m724/giants/configuration/Configuration.java
+++ b/src/main/java/eu/m724/giants/configuration/Configuration.java
@@ -1,18 +1,17 @@
package eu.m724.giants.configuration;
+import eu.m724.giants.DebugLogger;
import eu.m724.giants.Drop;
import org.bukkit.configuration.file.YamlConfiguration;
-import org.bukkit.plugin.Plugin;
import org.bukkit.potion.PotionEffect;
import org.bukkit.util.Vector;
import java.io.File;
-import java.util.List;
import java.util.Map;
import java.util.Objects;
-import java.util.logging.Logger;
public record Configuration(
+ boolean debug,
boolean updaterEnabled,
String updaterChannel,
@@ -28,19 +27,16 @@ public record Configuration(
double jumpHeight,
double spawnChance,
- List worldBlacklist,
- List potionEffects,
- List drops
+ PotionEffect[] potionEffects,
+ Drop[] drops,
+ boolean burningHead
) {
- // TODO not use logger here
- private static Logger LOGGER;
-
- public static Configuration load(Plugin plugin, File file) {
- LOGGER = Logger.getLogger(plugin.getLogger().getName() + ".Configuration");
-
+ public static Configuration load(File file) {
YamlConfiguration config = YamlConfiguration.loadConfiguration(file);
+ boolean debug = config.getBoolean("debug");
+
boolean updaterEnabled = true;
String updaterChannel = config.getString("updater", "release");
@@ -48,7 +44,6 @@ public record Configuration(
updaterEnabled = false;
}
-
boolean aiEnabled = config.getBoolean("ai");
double attackDamage = config.getDouble("attackDamage");
@@ -66,20 +61,20 @@ public record Configuration(
double spawnChance = config.getDouble("chance");
- List worldBlacklist = config.getStringList("blacklist");
-
- List potionEffects = config.getStringList("effects").stream()
+ PotionEffect[] potionEffects = config.getStringList("effects").stream()
.map(Configuration::makePotionEffect)
.filter(Objects::nonNull)
- .toList();
+ .toArray(PotionEffect[]::new);
- List drops = config.getMapList("drops").stream()
+ Drop[] drops = config.getMapList("drops").stream()
.map(Configuration::makeDrop)
.filter(Objects::nonNull)
- .toList();
+ .toArray(Drop[]::new);
+
+ boolean burningHead = config.getBoolean("burningHead"); // easter egg, hidden
return new Configuration(
- updaterEnabled, updaterChannel, aiEnabled, attackDamage, attackDelay, attackReach, jumpMode, jumpCondition, jumpDelay, jumpHeight, spawnChance, worldBlacklist, potionEffects, drops
+ debug, updaterEnabled, updaterChannel, aiEnabled, attackDamage, attackDelay, attackReach, jumpMode, jumpCondition, jumpDelay, jumpHeight, spawnChance, potionEffects, drops, burningHead
);
}
@@ -95,9 +90,9 @@ public record Configuration(
try {
return ListParsers.makeDrop(dropMap);
} catch (ParseException e) {
- LOGGER.warning("Failed to parse drop:");
- LOGGER.warning(" At: " + dropMap);
- LOGGER.warning(" " + e.getMessage());
+ DebugLogger.warning("Failed to parse drop:");
+ DebugLogger.warning(" At: " + dropMap);
+ DebugLogger.warning(" " + e.getMessage());
}
return null;
@@ -107,19 +102,14 @@ public record Configuration(
try {
return ListParsers.makePotionEffect(line);
} catch (ParseException e) {
- LOGGER.warning("Failed to parse potion effect:");
- LOGGER.warning(" At line: " + line);
- LOGGER.warning(" " + e.getMessage());
+ DebugLogger.warning("Failed to parse potion effect:");
+ DebugLogger.warning(" At line: " + line);
+ DebugLogger.warning(" " + e.getMessage());
}
return null;
}
-
- static void assertParse(boolean assertion, String message) throws ParseException {
- if (!assertion) throw new ParseException(message);
- }
-
public static class ParseException extends Exception {
public ParseException(String message) {
super(message);
diff --git a/src/main/java/eu/m724/giants/configuration/ListParsers.java b/src/main/java/eu/m724/giants/configuration/ListParsers.java
index 2a12be1..8c486cd 100644
--- a/src/main/java/eu/m724/giants/configuration/ListParsers.java
+++ b/src/main/java/eu/m724/giants/configuration/ListParsers.java
@@ -9,8 +9,6 @@ import org.bukkit.potion.PotionEffectType;
import java.util.Map;
-import static eu.m724.giants.configuration.Configuration.assertParse;
-
public class ListParsers {
public static PotionEffect makePotionEffect(String line) throws ParseException {
if (line == null || line.trim().isEmpty()) {
@@ -33,7 +31,7 @@ public class ListParsers {
assertParse(amplifier > 0, "Amplifier must be bigger than 0, is: " + amplifier);
assertParse(amplifier < 256, "Amplifier must be at most 255, is: " + amplifier);
- return new PotionEffect(effectType, Integer.MAX_VALUE, amplifier);
+ return new PotionEffect(effectType, Integer.MAX_VALUE, amplifier, true, false);
}
// TODO refactor this
@@ -66,4 +64,8 @@ public class ListParsers {
return new Drop(itemStack, min, max, chance);
}
+
+ private static void assertParse(boolean assertion, String message) throws ParseException {
+ if (!assertion) throw new ParseException(message);
+ }
}
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index 47ae44a..a2421e6 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -47,12 +47,6 @@ jumpHeight: -1
# 0 is 0% (no zombie becomes a giant), 1 is 100% (every zombie becomes a giant), so the default 0.005 is 0.5%
chance: 0.005
-# Worlds that Giants will not spawn in naturally.
-# You can still use the command to spawn
-blacklist:
- - "world_nether"
- - "world_the_end"
-
# Potion effects applied to a giant
# type:amplifier
# types: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/potion/PotionEffectType.html
@@ -72,5 +66,4 @@ drops:
- material: WOODEN_SWORD
chance: 0.05 # 5%
quantityMin: 1
- quantityMax: 1
-
+ quantityMax: 1
\ No newline at end of file