diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml index 64b848b..e49175c 100644 --- a/.github/workflows/maven-publish.yml +++ b/.github/workflows/maven-publish.yml @@ -1,34 +1,59 @@ -# This workflow will build a package using Maven and then publish it to GitHub packages when a release is created -# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path - -name: Maven Package +name: Build on: + push: + branches: ['**'] + pull_request: release: types: [created] jobs: build: - runs-on: ubuntu-latest permissions: contents: read - packages: write steps: - - uses: actions/checkout@v4 - - name: Set up JDK 11 - uses: actions/setup-java@v4 - with: - java-version: '11' - distribution: 'temurin' - server-id: github # Value of the distributionManagement/repository/id field of the pom.xml - settings-path: ${{ github.workspace }} # location for the settings.xml file - - - name: Build with Maven - run: mvn -B package --file pom.xml - - - name: Publish to GitHub Packages Apache Maven - run: mvn deploy -s $GITHUB_WORKSPACE/settings.xml - env: - GITHUB_TOKEN: ${{ github.token }} + - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: maven + + - name: Build and test + run: mvn -B clean package --file pom.xml + + - name: Upload plugin jar + uses: actions/upload-artifact@v4 + with: + name: story-plugin + path: target/story-plugin-*.jar + if-no-files-found: error + + publish: + if: github.event_name == 'release' + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: maven + + - name: Build release jar + run: mvn -B clean package --file pom.xml + + - name: Attach jar to release + uses: softprops/action-gh-release@v2 + with: + files: target/story-plugin-*.jar diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..12d1ead --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Maven +target/ +dependency-reduced-pom.xml +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +# Gradle +.gradle/ +build/ + +# IDE - IntelliJ +.idea/ +*.iml +*.ipr +*.iws +out/ + +# IDE - Eclipse +.classpath +.project +.settings/ +bin/ + +# IDE - VS Code +.vscode/ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + +# Local Minecraft test server artifacts +run/ +server/ +plugins/ +world/ +world_nether/ +world_the_end/ diff --git a/README.md b/README.md index d64f9df..ec690f2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Mmmm Story Plugin -[![Build Status](https://img.shields.io/badge/build-passing-brightgreen.svg)](https://github.com/Rethinger/plugin) +[![Build](https://github.com/Rethinger/plugin/actions/workflows/maven-publish.yml/badge.svg)](https://github.com/Rethinger/plugin/actions/workflows/maven-publish.yml) [![Version](https://img.shields.io/badge/version-1.0-blue.svg)](https://github.com/Rethinger/plugin/releases) [![Java](https://img.shields.io/badge/java-21+-orange.svg)](https://openjdk.java.net/) [![PaperMC](https://img.shields.io/badge/PaperMC-1.21.1-green.svg)](https://papermc.io/) @@ -23,9 +23,9 @@ A sophisticated Minecraft story campaign plugin built for PaperMC 1.21.x, featur - **Manager-Based Architecture** - Clean separation of concerns across 13 specialized managers - **Event-Driven Design** - Complex event listeners for each story act with precise timing - **Configuration-Driven** - All story content externalized in YAML files -- **Performance Optimized** - Particle effects with radius optimization, mob throttling, and async data saving -- **Persistent Data Storage** - JSON-based player progress with 5-minute auto-save intervals -- **Memory Management** - Proper task cleanup and NPC memory management +- **Performance Optimized** - Particle effects with radius optimization, mob throttling, and off-thread autosave writes +- **Persistent Data Storage** - YAML player progress with 5-minute auto-save intervals +- **Memory Management** - Boss bars and NPCs released on shutdown, player profiles evicted on quit - **Debug Tools** - Comprehensive debugging commands and detailed logging ### User Experience @@ -43,7 +43,7 @@ A sophisticated Minecraft story campaign plugin built for PaperMC 1.21.x, featur - **Maven 3.6+** (for building from source) ### Included Dependencies -- **NpcApi-Paper 1.21.x-4** - Advanced NPC functionality (bundled) +- **NpcApi-Paper 1.21.x-4** - Advanced NPC functionality (resolved from JitPack, shaded and relocated into the plugin jar) ## Installation @@ -121,7 +121,7 @@ The plugin uses a sophisticated manager-based architecture with clear separation #### Core Managers 1. **ConfigManager** - Configuration hub with multi-language support -2. **DataManager** - JSON-based player progress persistence +2. **DataManager** - YAML player progress persistence 3. **NPCManager** - Advanced NPC animations and behavioral AI 4. **DialogManager** - Interactive story system with sound synchronization 5. **ActManager** - Story progression and world state control diff --git a/pom.xml b/pom.xml index 2cc252c..6578c3d 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,10 @@ 21 UTF-8 + 1.21.1-R0.1-SNAPSHOT + 1.21.x-4 + 5.10.2 + 5.11.0 @@ -23,68 +27,49 @@ papermc https://repo.papermc.io/repository/maven-public/ - + - spigot-repo - https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + jitpack.io + https://jitpack.io - + io.papermc.paper paper-api - 1.21.1-R0.1-SNAPSHOT + ${paper.version} provided - - - - dev.folia - folia-api - 1.20.1-R0.1-SNAPSHOT - provided - - - + + de.eisi05 NpcApi-Paper - 1.21.x-4 + ${npcapi.version} compile - + - org.junit.jupiter junit-jupiter - 5.10.0 + ${junit.version} test - - + org.mockito mockito-core - 5.5.0 + ${mockito.version} test - - + org.mockito mockito-junit-jupiter - 5.5.0 - test - - - - - org.spigotmc - spigot-api - 1.21.3-R0.1-SNAPSHOT + ${mockito.version} test @@ -94,30 +79,75 @@ org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.13.0 - 21 - 21 + ${java.version} + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + org.apache.maven.plugins maven-shade-plugin - 3.5.0 + 3.5.3 package shade + + false + + + + de.eisi05.npc + com.mmmm.story.libs.eisi05.npc + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + module-info.class + + + + + src/main/resources true + + plugin.yml + + + + src/main/resources + false + + plugin.yml + diff --git a/src/main/NpcApi-Paper-master/.github/ISSUE_TEMPLATE/bug_report.md b/src/main/NpcApi-Paper-master/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index dd84ea7..0000000 --- a/src/main/NpcApi-Paper-master/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. diff --git a/src/main/NpcApi-Paper-master/.github/ISSUE_TEMPLATE/feature_request.md b/src/main/NpcApi-Paper-master/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index bbcbbe7..0000000 --- a/src/main/NpcApi-Paper-master/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/src/main/NpcApi-Paper-master/.gitignore b/src/main/NpcApi-Paper-master/.gitignore deleted file mode 100644 index 2b06feb..0000000 --- a/src/main/NpcApi-Paper-master/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Project exclude paths -/.idea/ -/.gradle/ -/build/ -/build/classes/java/main/ \ No newline at end of file diff --git a/src/main/NpcApi-Paper-master/LICENSE b/src/main/NpcApi-Paper-master/LICENSE deleted file mode 100644 index c09ad0d..0000000 --- a/src/main/NpcApi-Paper-master/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 Eisi05 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/src/main/NpcApi-Paper-master/README.md b/src/main/NpcApi-Paper-master/README.md deleted file mode 100644 index 292cc50..0000000 --- a/src/main/NpcApi-Paper-master/README.md +++ /dev/null @@ -1,233 +0,0 @@ -[![](https://jitpack.io/v/Eisi05/NpcApi-Paper.svg)](https://jitpack.io/#Eisi05/NpcApi-Paper) - -[NPC Plugin for PaperMC](https://modrinth.com/plugin/npc-plugin) - -[NpcApi for SpigotMC](https://github.com/Eisi05/NpcApi-Spigot) - -# NpcAPI - -A powerful and easy-to-use NPC (Non-Player Character) API for Minecraft Spigot plugins that allows you to create, manage, and customize NPCs with -advanced features. - -## Features - -- 🎭 Create custom NPCs with ease -- 🎨 Customize NPC appearance (skins, glowing effects, etc.) -- 👆 Handle click events and interactions -- 🎬 Play animations and control NPC behavior -- 💾 Save and load NPCs persistently -- 👥 Show/hide NPCs for specific players -- 🔍 Comprehensive NPC management system - -## Installation -Choose your preferred installation method based on your project needs: - -### Method 1: Plugin Dependency (Recommended) - -This method requires [NpcPlugin-Paper](https://modrinth.com/plugin/npc-plugin?loader=paper#download) to be installed as a separate plugin on the server. - -#### Maven -```xml - - - jitpack.io - https://jitpack.io - - - - - com.github.Eisi05 - NpcApi-Paper - 1.21.x-4 - provided - -``` - -#### Gradle -```gradle -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - mavenCentral() - maven { url 'https://jitpack.io' } - } -} - -dependencies { - compileOnly 'com.github.Eisi05:NpcApi-Paper:1.21.x-4' -} -``` - -#### Plugin Configuration -Add NpcPlugin-Paper as a dependency in your `plugin.yml`: -```yaml -# Required dependency (hard dependency) -dependencies: - server: - - name: NpcPlugin-Paper - required: true - -# Or optional dependency (soft dependency) -dependencies: - server: - - name: NpcPlugin-Paper - required: false -``` ---- - -### Method 2: Shaded Dependency - -This method bundles NpcApi directly into your plugin JAR file. - -#### Maven -Add the repository and dependency to your `pom.xml`: -```xml - - - jitpack.io - https://jitpack.io - - - - - - com.github.Eisi05 - NpcApi-Paper - 1.21.x-4 - - -``` - -#### Gradle -Add the following to your `build.gradle`: -```gradle -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - mavenCentral() - maven { url 'https://jitpack.io' } - } -} - -dependencies { - implementation 'com.github.Eisi05:NpcApi-Paper:1.21.x-4' -} -``` - -#### Plugin Configuration -To enabled/disable the NpcApi add this to your Plugin Main class: -```java - -@Override -public void onEnable() -{ - // Initialize NpcAPI with default configuration - NpcApi.createInstance(this, new NpcConfig()); -} - -@Override -public void onDisable() -{ - // Properly disable NpcAPI - NpcApi.disable(); -} -``` ---- - -## Usage Examples - -### Creating a Basic NPC - -```java -// Create a location where the NPC should spawn -Location location = new Location(world, x, y, z); - -// Create a new NPC with a name -NPC npc = new NPC(location, Component.text("Test")); - -// Enable the NPC to make it visible to all players -npc.setEnabled(true); -``` - -### Customizing NPC Appearance - -```java -// Make the NPC glow with a red color -npc.setOption(NpcOption.GLOWING, ChatFormat.RED); - -// Set a custom skin from a player -npc.setOption(NpcOption.SKIN, Skin.fromPlayer(player)); -``` - -### Handling Click Events - -```java -// Set up a click event handler -npc.setClickEvent(event -> { - Player player = event.getPlayer(); - NPC clickedNpc = event.getNpc(); - player.sendMessage(Component.text("You clicked ").append(clickedNpc.getName())); -}); -``` - -### Managing NPC State - -```java -npc.save(); -npc.setName(Component.text("New Name")); -npc.setLocation(newLocation); -npc.reload(); -``` - -### Advanced NPC Control - -```java -npc.playAnimation(/* animation parameters */); -npc.showNPCToPlayer(player); -npc.hideNpcFromPlayer(player); -npc.lookAtPlayer(player); -npc.delete(); -``` - -## NPC Management - -### Getting All NPCs - -```java -// Get a list of all available NPCs -List allNpcs = NpcManager.getList(); -``` - -### Finding NPCs by UUID - -```java -// Get a specific NPC by its UUID -UUID npcUuid = /* your NPC's UUID */; -NPC npc = NpcManager.fromUUID(npcUuid); -``` - -## API Reference - -### NPC Class - -| Method | Description | -|---------------------------------------|------------------------------------------| -| `setOption(NpcOption, Object)` | Set NPC options like glowing, skin, etc. | -| `setClickEvent(Consumer)` | Set the click event handler | -| `setEnabled(boolean)` | Enable/disable NPC visibility | -| `save()` | Save NPC to persistent storage | -| `reload()` | Reload NPC data | -| `setName(Component)` | Update NPC display name | -| `setLocation(Location)` | Move NPC to new location | -| `playAnimation(...)` | Play NPC animation | -| `showNPCToPlayer(Player)` | Show NPC to specific player | -| `hideNpcFromPlayer(Player)` | Hide NPC from specific player | -| `lookAtPlayer(Player)` | Make NPC look at player | -| `delete()` | Remove NPC permanently | -| `walkTo(Path, player, double, boolean, Consumer)` | Let the NPC walk along a path (can be created with PathfindingUtils class) | - -## Requirements - -- Java 21+ -- Paper 1.21 - 1.21.10 -- Minecraft server with NPC support diff --git a/src/main/NpcApi-Paper-master/build.gradle b/src/main/NpcApi-Paper-master/build.gradle deleted file mode 100644 index b608dea..0000000 --- a/src/main/NpcApi-Paper-master/build.gradle +++ /dev/null @@ -1,67 +0,0 @@ -plugins { - id 'java' - id 'maven-publish' - id("xyz.jpenilla.run-paper") version "2.3.1" - id("io.papermc.paperweight.userdev") version "2.0.0-beta.18" - id 'com.gradleup.shadow' version '8.3.3' -} - -group = "de.eisi05" -version = "1.21.x-4" - -base { - archivesName = "NpcApi-Paper" -} - -repositories { - maven { - name = "papermc-repo" - url = "https://repo.papermc.io/repository/maven-public/" - } - mavenCentral() - mavenLocal() -} - -dependencies { - paperweightDevelopmentBundle("io.papermc.paper:dev-bundle:1.21.10-R0.1-SNAPSHOT") - compileOnly("io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT") -} - -tasks { - runServer { - // Configure the Minecraft version for our task. - // This is the only required configuration besides applying the plugin. - // Your plugin's jar (or shadowJar if present) will be used automatically. - minecraftVersion("1.21") - } -} - -def targetJavaVersion = 21 -java { - toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) - withSourcesJar() -} - -tasks.withType(JavaCompile).configureEach { - options.encoding = 'UTF-8' - options.release.set(targetJavaVersion) -} - -publishing { - publications { - mavenJava(MavenPublication) { - artifact(tasks.jar) // plain jar, no reobf - groupId = project.group - artifactId = "npcapi-paper" // lowercase! - version = project.version - } - } - repositories { - maven { - url = uri("file://${buildDir}/repo") // or your actual repo - } - } -} - -tasks.assemble { dependsOn tasks.reobfJar } - diff --git a/src/main/NpcApi-Paper-master/gradle.properties b/src/main/NpcApi-Paper-master/gradle.properties deleted file mode 100644 index b5e6725..0000000 --- a/src/main/NpcApi-Paper-master/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip \ No newline at end of file diff --git a/src/main/NpcApi-Paper-master/gradle/wrapper/gradle-wrapper.jar b/src/main/NpcApi-Paper-master/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e644113..0000000 Binary files a/src/main/NpcApi-Paper-master/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/src/main/NpcApi-Paper-master/gradle/wrapper/gradle-wrapper.properties b/src/main/NpcApi-Paper-master/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index ca025c8..0000000 --- a/src/main/NpcApi-Paper-master/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/src/main/NpcApi-Paper-master/gradlew b/src/main/NpcApi-Paper-master/gradlew deleted file mode 100644 index b740cf1..0000000 --- a/src/main/NpcApi-Paper-master/gradlew +++ /dev/null @@ -1,249 +0,0 @@ -#!/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/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 "${APP_HOME:-./}" > /dev/null && pwd -P ) || 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=$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 - 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, 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" \ - 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" "$@" diff --git a/src/main/NpcApi-Paper-master/gradlew.bat b/src/main/NpcApi-Paper-master/gradlew.bat deleted file mode 100644 index 25da30d..0000000 --- a/src/main/NpcApi-Paper-master/gradlew.bat +++ /dev/null @@ -1,92 +0,0 @@ -@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. 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=%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 diff --git a/src/main/NpcApi-Paper-master/jitpack.yml b/src/main/NpcApi-Paper-master/jitpack.yml deleted file mode 100644 index 4fb582c..0000000 --- a/src/main/NpcApi-Paper-master/jitpack.yml +++ /dev/null @@ -1,3 +0,0 @@ -jdk: - - openjdk21 - diff --git a/src/main/NpcApi-Paper-master/settings.gradle b/src/main/NpcApi-Paper-master/settings.gradle deleted file mode 100644 index 7dc74fb..0000000 --- a/src/main/NpcApi-Paper-master/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'NpcApi-Paper' diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/NpcApi.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/NpcApi.java deleted file mode 100644 index 0bc7477..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/NpcApi.java +++ /dev/null @@ -1,131 +0,0 @@ -package de.eisi05.npc.api; - -import de.eisi05.npc.api.listeners.ChangeWorldListener; -import de.eisi05.npc.api.listeners.ConnectionListener; -import de.eisi05.npc.api.listeners.NpcInteractListener; -import de.eisi05.npc.api.manager.NpcManager; -import de.eisi05.npc.api.manager.TeamManager; -import de.eisi05.npc.api.objects.NPC; -import de.eisi05.npc.api.objects.NpcConfig; -import de.eisi05.npc.api.pathfinding.Path; -import de.eisi05.npc.api.scheduler.Tasks; -import de.eisi05.npc.api.utils.PacketReader; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.Bukkit; -import org.bukkit.configuration.serialization.ConfigurationSerialization; -import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.java.JavaPlugin; -import org.jetbrains.annotations.NotNull; - -import java.util.function.Function; - -/** - * The main entry point and singleton class for the NPC API. - * This class handles the initialization, configuration, and shutdown - * of the NPC functionality within a Bukkit plugin. - */ -public final class NpcApi -{ - /** - * A static reference to the Bukkit plugin instance that is using this API. - * This is set during the API's initialization. - */ - public static Plugin plugin; - - public static Function DISABLED_MESSAGE_PROVIDER = player -> - Component.text("DISABLED").color(NamedTextColor.RED); - - /** - * The configuration object for the NPC API, containing various settings - * like the look-at timer. - */ - public static NpcConfig config; - - private static NpcApi npcApi; - - /** - * Private constructor to enforce the singleton pattern. - * Initializes the API by registering listeners, loading existing NPCs, - * injecting packet readers, and starting recurring tasks. - * - * @param plugin The {@link JavaPlugin} instance using this API. Must not be {@code null}. - * @param config The {@link NpcConfig} object for the API. Must not be {@code null}. - */ - private NpcApi(@NotNull JavaPlugin plugin, @NotNull NpcConfig config) - { - NpcApi.plugin = plugin; - NpcApi.config = config; - - Bukkit.getPluginManager().registerEvents(new ChangeWorldListener(), plugin); - Bukkit.getPluginManager().registerEvents(new ConnectionListener(), plugin); - Bukkit.getPluginManager().registerEvents(new NpcInteractListener(), plugin); - - ConfigurationSerialization.registerClass(Path.class); - - NpcManager.loadNPCs(); - PacketReader.injectAll(); - - Tasks.start(); - } - - /** - * Creates or retrieves the singleton instance of the {@code NpcApi} with a default configuration. - * If the API instance does not exist or the provided plugin is null, a new instance is created. - * - * @param plugin The {@link JavaPlugin} instance using this API. Must not be {@code null}. - * @return The singleton {@link NpcApi} instance. Must not be {@code null}. - */ - public static @NotNull NpcApi createInstance(@NotNull JavaPlugin plugin) - { - return createInstance(plugin, new NpcConfig()); - } - - /** - * Creates or retrieves the singleton instance of the {@code NpcApi} with a custom configuration. - * If the API instance does not exist or the provided plugin is null, a new instance is created. - * - * @param plugin The {@link JavaPlugin} instance using this API. Must not be {@code null}. - * @param config The {@link NpcConfig} object to use for the API. Must not be {@code null}. - * @return The singleton {@link NpcApi} instance. Must not be {@code null}. - */ - public static @NotNull NpcApi createInstance(@NotNull JavaPlugin plugin, @NotNull NpcConfig config) - { - if(npcApi == null || plugin == null) - npcApi = new NpcApi(plugin, config); - - return npcApi; - } - - /** - * Sets a function that provides the message shown when an NPC is disabled. - * - * @param function a {@link Function} that takes a {@link Player} and returns the disabled message - * @return this {@link NpcApi} instance for method chaining - */ - public @NotNull NpcApi setDisabledMessageProvider(Function function) - { - DISABLED_MESSAGE_PROVIDER = function; - return this; - } - - /** - * Disables the NPC API, performing the necessary cleanup. - * This includes hiding all active NPCs from players, clearing the NPC manager, - * and uninjecting packet readers. It also nullifies the static references. - */ - public static void disable() - { - NpcManager.getList().forEach(NPC::hideNpcFromAllPlayers); - NpcManager.clear(); - PacketReader.uninjectAll(); - Tasks.stop(); - TeamManager.clear(); - ConfigurationSerialization.unregisterClass(Path.class); - - NpcManager.loadExceptions.clear(); - npcApi = null; - plugin = null; - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/enums/ClickActionType.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/enums/ClickActionType.java deleted file mode 100644 index 82783c3..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/enums/ClickActionType.java +++ /dev/null @@ -1,39 +0,0 @@ -package de.eisi05.npc.api.enums; - -import org.jetbrains.annotations.NotNull; - -import java.io.Serializable; - -/** - * Represents the type of click action that can be performed on an NPC. - * This includes left click, right click, or both. - */ -public enum ClickActionType implements Serializable -{ - /** - * Represents a left-click action. - */ - LEFT("Left"), - - /** - * Represents a right-click action. - */ - RIGHT("Right"), - /** - * Represents both left and right-click actions. - */ - - BOTH("Left & Right"); - - public final @NotNull String title; - - /** - * Constructs a ClickActionType with the given title. - * - * @param title The display name for this click action type. - */ - ClickActionType(@NotNull String title) - { - this.title = title; - } -} \ No newline at end of file diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/enums/Result.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/enums/Result.java deleted file mode 100644 index d1dee8d..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/enums/Result.java +++ /dev/null @@ -1,7 +0,0 @@ -package de.eisi05.npc.api.enums; - -public enum Result -{ - SUCCESS, - CANCELLED -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/enums/SkinParts.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/enums/SkinParts.java deleted file mode 100644 index 2b4a6d8..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/enums/SkinParts.java +++ /dev/null @@ -1,50 +0,0 @@ -package de.eisi05.npc.api.enums; - -import org.bukkit.Material; -import org.jetbrains.annotations.NotNull; - -import java.io.Serializable; - -/** - * Represents the different customizable skin parts of an NPC, - * each associated with a unique byte value and an icon material. - */ -public enum SkinParts implements Serializable -{ - CAPE((byte) 0x01, Material.ELYTRA), - JACKET((byte) 0x02, Material.LEATHER_CHESTPLATE), - LEFT_SLEEVE((byte) 0x04, Material.SHIELD), - RIGHT_SLEEVE((byte) 0x08, Material.DIAMOND_SWORD), - LEFT_PANTS_LEG((byte) 0x10, Material.LEATHER_LEGGINGS), - RIGHT_PANTS_LEG((byte) 0x20, Material.LEATHER_LEGGINGS), - HAT((byte) 0x40, Material.TURTLE_HELMET); - - private final byte value; - private final Material icon; - - SkinParts(byte value, @NotNull Material icon) - { - this.value = value; - this.icon = icon; - } - - /** - * Returns the byte value representing this skin part. - * - * @return the bitmask value - */ - public byte getValue() - { - return value; - } - - /** - * Returns the icon material associated with this skin part. - * - * @return the icon Material, never null - */ - public @NotNull Material getIcon() - { - return icon; - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/events/NpcInteractEvent.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/events/NpcInteractEvent.java deleted file mode 100644 index 7088615..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/events/NpcInteractEvent.java +++ /dev/null @@ -1,96 +0,0 @@ -package de.eisi05.npc.api.events; - -import de.eisi05.npc.api.enums.ClickActionType; -import de.eisi05.npc.api.objects.NPC; -import org.bukkit.entity.Player; -import org.bukkit.event.Cancellable; -import org.bukkit.event.Event; -import org.bukkit.event.HandlerList; -import org.jetbrains.annotations.NotNull; - -import java.io.Serializable; - -/** - * Event triggered when a player interacts with an NPC. - * Contains information about the player, the NPC, and the type of click action. - */ -public class NpcInteractEvent extends Event implements Serializable, Cancellable -{ - private static final HandlerList HANDLERS = new HandlerList(); - private final Player player; - private final NPC npc; - private final ClickActionType action; - private boolean cancelled; - - /** - * Creates a new NpcInteractEvent. - * - * @param player the player who interacted with the NPC - * @param npc the NPC that was interacted with - * @param action the type of click action performed - */ - public NpcInteractEvent(@NotNull Player player, @NotNull NPC npc, @NotNull ClickActionType action) - { - this.player = player; - this.npc = npc; - this.action = action; - } - - /** - * Returns the HandlerList for this event. - * - * @return the static HandlerList instance - */ - public static HandlerList getHandlerList() - { - return HANDLERS; - } - - /** - * Returns the player who triggered this event. - * - * @return the interacting player, never null - */ - public @NotNull Player getPlayer() - { - return player; - } - - /** - * Returns the NPC involved in this event. - * - * @return the interacted NPC, never null - */ - public @NotNull NPC getNpc() - { - return npc; - } - - /** - * Returns the click action type of this interaction. - * - * @return the ClickActionType, never null - */ - public @NotNull ClickActionType getAction() - { - return action; - } - - @Override - public @NotNull HandlerList getHandlers() - { - return getHandlerList(); - } - - @Override - public boolean isCancelled() - { - return cancelled; - } - - @Override - public void setCancelled(boolean cancelled) - { - this.cancelled = cancelled; - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/interfaces/NpcClickAction.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/interfaces/NpcClickAction.java deleted file mode 100644 index a3c586f..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/interfaces/NpcClickAction.java +++ /dev/null @@ -1,44 +0,0 @@ -package de.eisi05.npc.api.interfaces; - -import de.eisi05.npc.api.events.NpcInteractEvent; -import org.jetbrains.annotations.NotNull; - -import java.io.Serial; -import java.io.Serializable; - -/** - * Functional interface representing an action to be performed - * when an NPC is clicked. - */ -@FunctionalInterface -public interface NpcClickAction extends Serializable -{ - @Serial - long serialVersionUID = 1L; - - /** - * Called when an NPC click event occurs. - * - * @param event the NpcInteractEvent containing interaction details - */ - void call(@NotNull NpcInteractEvent event); - - /** - * Returns a copy of this NpcClickAction. - * The default implementation returns the same instance. - * - * @return a copy of this action - */ - default NpcClickAction copy() {return this;} - - /** - * Initializes this {@link NpcClickAction}. - * This method can be used for any setup or configuration that needs to occur - * after the action is created or loaded. - * The default implementation simply returns the current instance, - * indicating no specific initialization is required by default. - * - * @return The initialized {@link NpcClickAction} instance. By default, it returns {@code this}. - */ - default NpcClickAction initialize() {return this;} -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/listeners/ChangeWorldListener.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/listeners/ChangeWorldListener.java deleted file mode 100644 index f35401e..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/listeners/ChangeWorldListener.java +++ /dev/null @@ -1,17 +0,0 @@ -package de.eisi05.npc.api.listeners; - -import de.eisi05.npc.api.manager.NpcManager; -import de.eisi05.npc.api.objects.NPC; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerChangedWorldEvent; - -public class ChangeWorldListener implements Listener -{ - @EventHandler - public void onChange(PlayerChangedWorldEvent event) - { - for(NPC npc : NpcManager.getList()) - npc.showNPCToPlayer(event.getPlayer()); - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/listeners/ConnectionListener.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/listeners/ConnectionListener.java deleted file mode 100644 index 093ffc7..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/listeners/ConnectionListener.java +++ /dev/null @@ -1,38 +0,0 @@ -package de.eisi05.npc.api.listeners; - -import de.eisi05.npc.api.NpcApi; -import de.eisi05.npc.api.manager.NpcManager; -import de.eisi05.npc.api.objects.NPC; -import de.eisi05.npc.api.utils.PacketReader; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.scheduler.BukkitRunnable; - -public class ConnectionListener implements Listener -{ - @EventHandler - public void onJoin(PlayerJoinEvent event) - { - PacketReader.inject(event.getPlayer()); - - new BukkitRunnable() - { - @Override - public void run() - { - NpcManager.getList().forEach(npc -> npc.showNPCToPlayer(event.getPlayer())); - } - }.runTaskLater(NpcApi.plugin, 10L); - } - - @EventHandler - public void onLeave(PlayerQuitEvent event) - { - PacketReader.uninject(event.getPlayer()); - - for(NPC npc : NpcManager.getList()) - npc.hideNpcFromPlayer(event.getPlayer()); - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/listeners/NpcInteractListener.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/listeners/NpcInteractListener.java deleted file mode 100644 index e6b52e6..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/listeners/NpcInteractListener.java +++ /dev/null @@ -1,16 +0,0 @@ -package de.eisi05.npc.api.listeners; - -import de.eisi05.npc.api.events.NpcInteractEvent; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; - -public class NpcInteractListener implements Listener -{ - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onClick(NpcInteractEvent event) - { - if(event.getNpc().getClickEvent() != null) - event.getNpc().getClickEvent().call(event); - } -} \ No newline at end of file diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/manager/NpcManager.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/manager/NpcManager.java deleted file mode 100644 index 4d125cf..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/manager/NpcManager.java +++ /dev/null @@ -1,119 +0,0 @@ -package de.eisi05.npc.api.manager; - -import de.eisi05.npc.api.NpcApi; -import de.eisi05.npc.api.objects.NPC; -import de.eisi05.npc.api.utils.ObjectSaver; -import org.jetbrains.annotations.NotNull; - -import java.io.File; -import java.util.*; - -/** - * Manages the collection and lifecycle of NPC instances. - */ -public class NpcManager -{ - /** - * Map storing the file name and the exception that occurred during loading. - */ - public static Map loadExceptions = new HashMap<>(); - - private static final List listNPC = new ArrayList<>(); - - /** - * Adds an NPC to the manager's list. - * - * @param npc the NPC to add - */ - public static void addNPC(@NotNull NPC npc) - { - listNPC.add(npc); - } - - /** - * Returns the list of all managed NPCs. - * - * @return the list of NPCs - */ - public static @NotNull List getList() - { - return listNPC; - } - - /** - * Removes an NPC from the manager's list. - * - * @param npc the NPC to remove - */ - public static void removeNPC(@NotNull NPC npc) - { - listNPC.remove(npc); - } - - /** - * Clears all NPCs from the manager. - */ - public static void clear() - { - listNPC.clear(); - } - - /** - * Finds an NPC by its UUID. - * - * @param uuid the UUID to search for - * @return an Optional containing the NPC if found, empty otherwise - */ - public static @NotNull Optional fromUUID(@NotNull UUID uuid) - { - return listNPC.stream().filter(npc -> npc.getUUID().equals(uuid)).findFirst(); - } - - /** - * Loads NPCs from disk files in the plugin data folder. - * Logs the count of successfully and unsuccessfully loaded NPCs. - */ - public static void loadNPCs() - { - File file = new File(NpcApi.plugin.getDataFolder(), "NPC"); - - File[] files = file.listFiles(); - if(files == null) - return; - - long failCounter = 0; - long successCounter = 0; - - Exception exception = null; - for(File file1 : files) - { - if(!file1.getName().endsWith(".npc")) - continue; - - try - { - NPC.SerializedNPC serializedNPC = new ObjectSaver(file1).read(); - serializedNPC.deserializedNPC().showNpcToAllPlayers(); - successCounter++; - } catch(Exception e) - { - failCounter++; - exception = e; - loadExceptions.put(file1.getName(), e); - } - } - - if(successCounter == 1) - NpcApi.plugin.getLogger().info("Successfully loaded " + successCounter + " NPC"); - else if(successCounter > 1) - NpcApi.plugin.getLogger().info("Successfully loaded " + successCounter + " NPC's"); - - if(failCounter == 1) - NpcApi.plugin.getLogger().warning("Failed to load " + failCounter + " NPC"); - else if(failCounter > 1) - NpcApi.plugin.getLogger().warning("Failed to load " + failCounter + " NPC's"); - - if(exception != null && NpcApi.config.debug()) - exception.printStackTrace(); - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/manager/TeamManager.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/manager/TeamManager.java deleted file mode 100644 index 260c962..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/manager/TeamManager.java +++ /dev/null @@ -1,48 +0,0 @@ -package de.eisi05.npc.api.manager; - -import net.minecraft.world.scores.PlayerTeam; -import net.minecraft.world.scores.Scoreboard; -import org.bukkit.craftbukkit.scoreboard.CraftScoreboard; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -public class TeamManager -{ - private static final Map> teams = new HashMap<>(); - - public static @NotNull Object create(@NotNull Player player, @NotNull String name) - { - if(exists(player, name)) - return teams.get(player.getUniqueId()).get(name); - - Scoreboard scoreboard = ((CraftScoreboard) player.getScoreboard()).getHandle(); - - PlayerTeam team = new PlayerTeam(scoreboard, name); - - var map = teams.getOrDefault(player.getUniqueId(), new HashMap<>()); - map.put(name, team); - teams.put(player.getUniqueId(), map); - - return team; - } - - public static boolean exists(@NotNull Player player, @NotNull String name) - { - return teams.getOrDefault(player.getUniqueId(), new HashMap<>()).containsKey(name); - } - - public static void clear() - { - teams.clear(); - } - - public static void clear(String name) - { - for(var entry : teams.entrySet()) - entry.getValue().keySet().removeIf(s -> s.equals(name)); - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/CustomNameTag.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/CustomNameTag.java deleted file mode 100644 index c6dbfe7..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/CustomNameTag.java +++ /dev/null @@ -1,290 +0,0 @@ -package de.eisi05.npc.api.objects; - -import de.eisi05.npc.api.utils.Var; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import net.minecraft.network.chat.Component; -import net.minecraft.network.syncher.EntityDataAccessor; -import net.minecraft.network.syncher.EntityDataSerializers; -import net.minecraft.network.syncher.SynchedEntityData; -import net.minecraft.world.entity.Display; -import org.bukkit.craftbukkit.util.CraftChatMessage; -import org.bukkit.util.Vector; -import org.joml.Vector3f; - -import javax.annotation.Nullable; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Optional; - -/** - * Utility class to configure and apply custom nametag settings for Minecraft TextDisplay entities. - */ -public class CustomNameTag -{ - private final Display.TextDisplay display; - private final Map, Object> dataMap = new LinkedHashMap<>(); - - /** - * Creates a new CustomNameTag wrapper for the given TextDisplay entity. - * - * @param display The TextDisplay entity to customize. - */ - public CustomNameTag(Object display) - { - this.display = (Display.TextDisplay) display; - } - - /** - * Returns the wrapped TextDisplay entity. - * - * @return The TextDisplay entity. - */ - public Object getDisplay() - { - return display; - } - - private CustomNameTag set(EntityDataAccessor accessor, T value) - { - dataMap.put(accessor, value); - return this; - } - - /** - * Sets the translation offset of the nametag. - * Default: (0.0, 0.25, 0.0) - * - * @param vector Translation vector. - * @return This instance for chaining. - */ - public CustomNameTag translation(Vector vector) - { - return set(EntityDataSerializers.VECTOR3.createAccessor(11), vector.toVector3f()); - } - - /** - * Sets the scale of the nametag. - * Default: (1.0, 1.0, 1.0) - * - * @param vector Scale vector. - * @return This instance for chaining. - */ - public CustomNameTag scale(Vector vector) - { - return set(EntityDataSerializers.VECTOR3.createAccessor(12), vector.toVector3f()); - } - - /** - * Sets the billboard alignment constraints. - * Default: CENTER - * - * @param constraints BillboardConstraints enum. - * @return This instance for chaining. - */ - public CustomNameTag billboardConstraints(BillboardConstraints constraints) - { - return set(EntityDataSerializers.BYTE.createAccessor(15), (byte) constraints.ordinal()); - } - - /** - * Sets brightness override. - * Default: -1 - * - * @param brightness Brightness value. - * @return This instance for chaining. - */ - public CustomNameTag brightnessOverride(int brightness) - { - return set(EntityDataSerializers.INT.createAccessor(16), brightness); - } - - /** - * Sets the viewing range of the nametag. - * Default: 1.0 - * - * @param range View range. - * @return This instance for chaining. - */ - public CustomNameTag viewRange(float range) - { - return set(EntityDataSerializers.FLOAT.createAccessor(17), range); - } - - /** - * Sets the shadow radius. - * Default: 0.0 - * - * @param radius Shadow radius. - * @return This instance for chaining. - */ - public CustomNameTag shadowRadius(float radius) - { - return set(EntityDataSerializers.FLOAT.createAccessor(18), radius); - } - - /** - * Sets the shadow strength. - * Default: 1.0 - * - * @param strength Shadow strength. - * @return This instance for chaining. - */ - public CustomNameTag shadowStrength(float strength) - { - return set(EntityDataSerializers.FLOAT.createAccessor(19), strength); - } - - /** - * Sets the width of the nametag. - * Default: 0.0 - * - * @param width Width value. - * @return This instance for chaining. - */ - public CustomNameTag width(float width) - { - return set(EntityDataSerializers.FLOAT.createAccessor(20), width); - } - - /** - * Sets the height of the nametag. - * Default: 1.0 - * - * @param height Height value. - * @return This instance for chaining. - */ - public CustomNameTag height(float height) - { - return set(EntityDataSerializers.FLOAT.createAccessor(21), height); - } - - /** - * Sets a glow color override. - * Default: -1 - * - * @param color Glow color as integer. - * @return This instance for chaining. - */ - public CustomNameTag glowColorOverride(int color) - { - return set(EntityDataSerializers.INT.createAccessor(22), color); - } - - /** - * Sets the line width of the text. - * Default: 200 - * - * @param width Line width. - * @return This instance for chaining. - */ - public CustomNameTag lineWidth(int width) - { - return set(EntityDataSerializers.INT.createAccessor(24), width); - } - - /** - * Sets the background color. - * Default: 1073741824 (0x40000000) - * - * @param color Background color as integer. - * @return This instance for chaining. - */ - public CustomNameTag backgroundColor(int color) - { - return set(EntityDataSerializers.INT.createAccessor(25), color); - } - - /** - * Sets the text opacity. - * Default: -1 (fully opaque) - * - * @param opacity Text opacity. - * @return This instance for chaining. - */ - public CustomNameTag textOpacity(byte opacity) - { - return set(EntityDataSerializers.BYTE.createAccessor(26), opacity); - } - - /** - * Sets flags including shadow, see-through, background color, and alignment. - * Default: NONE - * - * @param flags Varargs of TextDisplayFlags. - * @return This instance for chaining. - */ - public CustomNameTag flags(TextDisplayFlags... flags) - { - return set(EntityDataSerializers.BYTE.createAccessor(27), TextDisplayFlags.combineFlags(flags)); - } - - /** - * Applies all configured data to the given TextDisplay and component. - * - * @param component The text component to display. - * @return The SynchedEntityData after applying values. - */ - Object applyData(@Nullable net.kyori.adventure.text.Component component) - { - SynchedEntityData data = display.getEntityData(); - - if(component == null) - component = net.kyori.adventure.text.Component.empty(); - - String legacy = LegacyComponentSerializer.legacySection().serialize(component).replace("\\n", "\n"); - Component nmsComponent = CraftChatMessage.fromStringOrNull(legacy, true); - - if(nmsComponent == null) - nmsComponent = Component.empty(); - - // Default values - data.set(EntityDataSerializers.OPTIONAL_COMPONENT.createAccessor(2), Optional.of(nmsComponent)); - data.set(EntityDataSerializers.BOOLEAN.createAccessor(4), true); - data.set(EntityDataSerializers.VECTOR3.createAccessor(11), new Vector3f(0, 0.25f, 0)); - data.set(EntityDataSerializers.BYTE.createAccessor(15), (byte) 3); - data.set(EntityDataSerializers.COMPONENT.createAccessor(23), nmsComponent); - - // Apply custom data - dataMap.forEach((accessor, value) -> data.set(accessor, Var.unsafeCast(value))); - - return data; - } - - - /** - * Alignment constraints for TextDisplay nametags. - */ - public enum BillboardConstraints - { - FIXED, - VERTICAL, - HORIZONTAL, - CENTER - } - - /** - * Flags for text display, including shadow, see-through, background, and alignment. - */ - public enum TextDisplayFlags - { - NONE((byte) 0x00), - HAS_SHADOW((byte) 0x01), - IS_SEE_THROUGH((byte) 0x02), - USE_DEFAULT_BACKGROUND_COLOR((byte) 0x04), - CENTER_ALIGNMENT((byte) 0x00), - LEFT_ALIGNMENT((byte) 0x01), - RIGHT_ALIGNMENT((byte) 0x02); - - private final byte flag; - - TextDisplayFlags(byte flag) {this.flag = flag;} - - public static byte combineFlags(TextDisplayFlags... flags) - { - byte result = 0; - for(TextDisplayFlags flag : flags) - result |= flag.flag; - return result; - } - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NPC.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NPC.java deleted file mode 100644 index bbfd694..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NPC.java +++ /dev/null @@ -1,897 +0,0 @@ -package de.eisi05.npc.api.objects; - -import com.google.common.collect.ImmutableList; -import com.mojang.authlib.GameProfile; -import de.eisi05.npc.api.NpcApi; -import de.eisi05.npc.api.enums.Result; -import de.eisi05.npc.api.interfaces.NpcClickAction; -import de.eisi05.npc.api.manager.NpcManager; -import de.eisi05.npc.api.manager.TeamManager; -import de.eisi05.npc.api.utils.ObjectSaver; -import de.eisi05.npc.api.utils.Reflections; -import de.eisi05.npc.api.utils.Var; -import de.eisi05.npc.api.utils.Versions; -import de.eisi05.npc.api.wrapper.packets.AnimatePacket; -import de.eisi05.npc.api.wrapper.packets.SetEntityDataPacket; -import de.eisi05.npc.api.wrapper.packets.SetPlayerTeamPacket; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.json.JSONComponentSerializer; -import net.minecraft.network.Connection; -import net.minecraft.network.protocol.Packet; -import net.minecraft.network.protocol.PacketFlow; -import net.minecraft.network.protocol.game.*; -import net.minecraft.network.syncher.SynchedEntityData; -import net.minecraft.server.MinecraftServer; -import net.minecraft.server.level.ClientInformation; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.server.network.CommonListenerCookie; -import net.minecraft.server.network.ServerGamePacketListenerImpl; -import net.minecraft.world.entity.Display; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.EntityType; -import net.minecraft.world.entity.PositionMoveRotation; -import net.minecraft.world.phys.Vec3; -import net.minecraft.world.scores.PlayerTeam; -import net.minecraft.world.scores.Team; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.OfflinePlayer; -import org.bukkit.World; -import org.bukkit.block.Block; -import org.bukkit.craftbukkit.CraftServer; -import org.bukkit.craftbukkit.CraftWorld; -import org.bukkit.craftbukkit.entity.CraftPlayer; -import org.bukkit.craftbukkit.util.CraftChatMessage; -import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitRunnable; -import org.bukkit.scheduler.BukkitTask; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.io.Serial; -import java.io.Serializable; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Instant; -import java.util.*; -import java.util.function.Consumer; - -/** - * Represents a Non-Player Character (NPC) with location, appearance, options, and interaction logic. - */ -public class NPC extends NpcHolder -{ - ServerPlayer serverPlayer; - private final List viewers = new ArrayList<>(); - private final Map, Object> options; - private final CustomNameTag nameTag; - private Component name; - private Location location; - private NpcClickAction clickEvent; - private Instant createdAt = Instant.now(); - private Path npcPath; - - /** - * Creates an NPC at the specified location with a random UUID and default name. - * The default name is an empty component. - * - * @param location the location to spawn the NPC. Must not be null. - */ - public NPC(@NotNull Location location) - { - this(location, UUID.randomUUID()); - } - - /** - * Creates an NPC at the specified location with a random UUID and given name. - * - * @param location the location to spawn the NPC. Must not be null. - * @param name the display name of the NPC. Must not be null. - */ - public NPC(@NotNull Location location, @NotNull Component name) - { - this(location, UUID.randomUUID(), name); - } - - /** - * Creates an NPC at the specified location with the given UUID and default name. - * The default name is an empty component. - * - * @param location the location to spawn the NPC. Must not be null. - * @param uuid the UUID of the NPC. Must not be null. - */ - public NPC(@NotNull Location location, @NotNull UUID uuid) - { - this(location, uuid, Component.empty()); - } - - /** - * Creates an NPC at the specified location with the given UUID and name. - * This is the primary constructor that initializes the NPC's core properties. - * - * @param location the location to spawn the NPC. Must not be null. - * @param uuid the UUID of the NPC. Must not be null. - * @param name the display name of the NPC. Must not be null. - */ - public NPC(@NotNull Location location, @NotNull UUID uuid, @NotNull Component name) - { - this.name = name; - this.location = location; - - MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer(); - ServerLevel level = ((CraftWorld) location.getWorld()).getHandle(); - GameProfile profile = new GameProfile(uuid, "NPC" + uuid.toString().substring(0, 13)); - - this.serverPlayer = new ServerPlayer(server, level, profile, ClientInformation.createDefault()); - Var.moveEntity(serverPlayer, location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch()); - - npcPath = NpcApi.plugin.getDataFolder().toPath().resolve("NPC").resolve(uuid + ".npc"); - - serverPlayer.connection = new ServerGamePacketListenerImpl(server, new Connection(PacketFlow.SERVERBOUND), serverPlayer, - CommonListenerCookie.createInitial(profile, true)); - - this.options = new HashMap<>(); - for(NpcOption value : NpcOption.values()) - setOption(value, Var.unsafeCast(value.getDefaultValue())); - - Display.TextDisplay display = new Display.TextDisplay(EntityType.TEXT_DISPLAY, ((CraftWorld) location.getWorld()).getHandle()); - Var.moveEntity(display, location.getX(), location.getY() + 2, location.getZ(), 0f, 0f); - - nameTag = new CustomNameTag(display); - serverPlayer.listName = CraftChatMessage.fromJSON(JSONComponentSerializer.json().serialize(name)); - serverPlayer.passengers = ImmutableList.of((Display.TextDisplay) nameTag.getDisplay()); - - NpcManager.addNPC(this); - } - - /** - * Private constructor used for creating a copy of an NPC. - * - * @param location The new location for the NPC. Must not be null. - * @param name The name for the NPC. Must not be null. - * @param options The options map for the NPC. Must not be null. - * @param clickEvent The click event for the NPC. Can be null. - */ - private NPC(@NotNull Location location, @NotNull Component name, @NotNull Map, Object> options, - @Nullable NpcClickAction clickEvent) - { - this(location, UUID.randomUUID(), name); - this.options.putAll(options); - this.clickEvent = clickEvent; - } - - /** - * Creates a copy of this NPC at a new location. - * The copied NPC will have a new UUID but will retain the original NPC's name, options, and click event. - * - * @param newLocation the location for the copied NPC. Must not be null. - * @return the new NPC instance. Will not be null. - */ - public @NotNull NPC copy(@NotNull Location newLocation) - { - return new NPC(newLocation, name, new HashMap<>(options), clickEvent == null ? null : clickEvent.copy()); - } - - /** - * Checks if this NPC has been saved to a file. - * - * @return {@code true} if the NPC's data file exists, {@code false} otherwise. - */ - public boolean isSaved() - { - return Files.exists(npcPath); - } - - /** - * Saves the NPC's data to a file. - * This method serializes the NPC's current state and writes it to a .npc file. - * - * @throws IOException if an I/O error occurs during saving. - */ - @Override - public void save() throws IOException - { - npcPath.toFile().getParentFile().mkdirs(); - new ObjectSaver(npcPath.toFile()).write(SerializedNPC.serializedNPC(this), false); - super.save(); - } - - /** - * Gets the underlying server player representation for this NPC. - * - * @return the {@link ServerPlayer} instance for this NPC. Will not be null. - */ - public @NotNull Object getServerPlayer() - { - return serverPlayer; - } - - /** - * Gets the click action associated with this NPC. - * - * @return the {@link NpcClickAction} for this NPC, or {@code null} if no action is set. - */ - public @Nullable NpcClickAction getClickEvent() - { - return clickEvent; - } - - /** - * Sets the click action for this NPC. - * - * @param event the {@link NpcClickAction} to set, or {@code null} to remove the current action. - * @return this NPC instance for method chaining. Will not be null. - */ - public @NotNull NPC setClickEvent(@Nullable NpcClickAction event) - { - this.clickEvent = event; - return this; - } - - /** - * Checks if the NPC is currently enabled. - * An enabled NPC is visible and interactable (unless overridden by player permissions). - * - * @return {@code true} if the NPC is enabled, {@code false} otherwise. - */ - public boolean isEnabled() - { - return getOption(NpcOption.ENABLED); - } - - /** - * Sets the enabled state of the NPC. - * Changing this state will trigger a reload of the NPC for all viewers. - * - * @param enabled {@code true} to enable the NPC, {@code false} to disable it. - */ - public void setEnabled(boolean enabled) - { - setOption(NpcOption.ENABLED, enabled); - reload(); - } - - /** - * Checks if this NPC is marked as editable through the {@code NpcPlugin}. - *

- * The default state is {@code false}. - * - * @return {@code true} if the NPC is editable, {@code false} otherwise - */ - public boolean isEditable() - { - return getOption(NpcOption.EDITABLE); - } - - /** - * Sets whether this NPC can be edited through the {@code NpcPlugin}. - *

- * By default, an NPC is not editable ({@code false}). - * - * @param editable {@code true} if the NPC should be editable, {@code false} otherwise - */ - public void setEditable(boolean editable) - { - setOption(NpcOption.EDITABLE, editable); - } - - /** - * Sets a specific option for this NPC. - * - * @param option the {@link NpcOption} to set. Must not be null. - * @param value the value for the option. If {@code null}, the option will be removed (reverting to default). - * @param the type of the option's value. - */ - public void setOption(@NotNull NpcOption option, @Nullable T value) - { - if(value == null) - options.remove(option); - else - options.put(option, value); - - if(NpcApi.config.autoUpdate()) - { - viewers.forEach(uuid -> - { - Player player = Bukkit.getPlayer(uuid); - if(player == null) - return; - - option.getPacket(value, this, player).ifPresent(packetWrapper -> - ((CraftPlayer) player).getHandle().connection.send((Packet) packetWrapper)); - }); - } - } - - /** - * Gets the value of a specific option for this NPC. - * If the option has not been explicitly set, its default value will be returned. - * - * @param option the {@link NpcOption} to get. Must not be null. - * @param the type of the option's value. - * @return the value of the option. Will not be null (guaranteed by NpcOption default values). - */ - @SuppressWarnings("unchecked") - public @NotNull T getOption(@NotNull NpcOption option) - { - return (T) options.getOrDefault(option, option.getDefaultValue()); - } - - /** - * Plays an animation for this NPC, visible to the specified player. - * - * @param player the player who will see the animation. Must not be null. - * @param animation the {@link AnimatePacket.Animation} to play. Must not be null. - */ - public void playAnimation(@NotNull Player player, @NotNull AnimatePacket.Animation animation) - { - ((CraftPlayer) player).getHandle().connection.send((Packet) AnimatePacket.create(serverPlayer, animation)); - } - - /** - * Reloads the NPC for all current viewers. - * This typically involves hiding and then re-showing the NPC to apply any changes. - */ - public void reload() - { - final List viewers = new ArrayList<>(this.viewers); - hideNpcFromAllPlayers(); - TeamManager.clear(getGameProfileName()); - viewers.stream().filter(uuid -> Bukkit.getPlayer(uuid) != null).forEach(uuid -> showNPCToPlayer(Bukkit.getPlayer(uuid))); - } - - /** - * Gets the current location of the NPC. - * - * @return the {@link Location} of the NPC. Will not be null. - */ - public @NotNull Location getLocation() - { - return location; - } - - /** - * Sets the location of the NPC. - * This will also update the underlying server player's position. - * - * @param location the new {@link Location} for the NPC. Must not be null. - */ - public void setLocation(@NotNull Location location) - { - this.location = location; - Var.moveEntity(serverPlayer, location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch()); - } - - /** - * Gets the unique identifier (UUID) of this NPC. - * - * @return the {@link UUID} of the NPC. Will not be null. - */ - public @NotNull UUID getUUID() - { - return serverPlayer.getUUID(); - } - - /** - * Gets the display name of this NPC. - * - * @return the {@link Component} representing the NPC's name. Will not be null. - */ - public @NotNull Component getName() - { - return name; - } - - public @NotNull String getGameProfileName() - { - if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_9)) - return (String) Reflections.invokeMethod(serverPlayer.getGameProfile(), "getName").get(); - return serverPlayer.getGameProfile().name(); - } - - /** - * Sets the display name of this NPC. - * This also updates the name for the underlying server player and its list name. - * - * @param name the new {@link Component} name for the NPC. Must not be null. - */ - public void setName(@NotNull Component name) - { - this.name = name; - serverPlayer.listName = CraftChatMessage.fromJSON(JSONComponentSerializer.json().serialize(name)); - - viewers.stream().filter(uuid -> Bukkit.getPlayer(uuid) != null).forEach( - uuid -> ((CraftPlayer) Bukkit.getPlayer(uuid)).getHandle().connection.send( - ((Packet) SetEntityDataPacket.create(((Display.TextDisplay) nameTag.getDisplay()).getId(), - (SynchedEntityData) nameTag.applyData( - isEnabled() ? name : NpcApi.DISABLED_MESSAGE_PROVIDER.apply(Bukkit.getPlayer(uuid)) - .appendNewline().append(name)))))); - } - - /** - * Gets the timestamp when this NPC was created. - * - * @return the {@link Instant} of creation. Will not be null. - */ - public Instant getCreatedAt() - { - return createdAt; - } - - /** - * Makes the NPC visible to all currently online players. - * This respects the NPC's enabled state and player permissions. - */ - public void showNpcToAllPlayers() - { - Bukkit.getOnlinePlayers().forEach(this::showNPCToPlayer); - } - - /** - * Makes the NPC visible to a specific player. - * If the NPC is disabled and the player is not an operator, the NPC will not be shown. - * This method handles sending all necessary packets to display the NPC correctly. - * - * @param player the player to show the NPC to. Must not be null. - */ - public void showNPCToPlayer(@NotNull Player player) - { - if(!getOption(NpcOption.ENABLED) && !player.isOp()) - return; - - if(!player.getWorld().getName().equals(serverPlayer.getBukkitEntity().getWorld().getName())) - { - hideNpcFromPlayer(player); - return; - } - - if(!viewers.contains(player.getUniqueId())) - viewers.add(player.getUniqueId()); - - List> packets = new ArrayList<>(); - - Arrays.stream(NpcOption.values()).filter(NpcOption::loadBefore) - .forEach(npcOption -> npcOption.getPacket(getOption(npcOption), this, player).ifPresent(o -> packets.add((Packet) o))); - - packets.add(ClientboundPlayerInfoUpdatePacket.createSinglePlayerInitializing(serverPlayer, true)); - packets.add(serverPlayer.getAddEntityPacket(Var.getServerEntity(serverPlayer, Var.getServerLevel(serverPlayer)))); - - boolean modified = TeamManager.exists(player, getGameProfileName()); - PlayerTeam wrappedPlayerTeam = (PlayerTeam) TeamManager.create(player, getGameProfileName()); - wrappedPlayerTeam.setNameTagVisibility(Team.Visibility.NEVER); - - packets.add((Packet) SetPlayerTeamPacket.createAddOrModifyPacket(wrappedPlayerTeam, !modified)); - packets.add((Packet) SetPlayerTeamPacket.createPlayerPacket(wrappedPlayerTeam, getGameProfileName(), - ClientboundSetPlayerTeamPacket.Action.ADD)); - - packets.add(new ClientboundRotateHeadPacket(serverPlayer, (byte) ((location.getYaw() % 360) * 256 / 360))); - packets.add(new ClientboundMoveEntityPacket.Rot(serverPlayer.getId(), (byte) location.getYaw(), (byte) location.getPitch(), - serverPlayer.onGround)); - - if(!getOption(NpcOption.HIDE_NAMETAG)) - { - packets.add(((Display.TextDisplay) nameTag.getDisplay()).getAddEntityPacket( - Var.getServerEntity((Display.TextDisplay) nameTag.getDisplay(), Var.getServerLevel(serverPlayer)))); - - packets.add((Packet) SetEntityDataPacket.create(((Display.TextDisplay) nameTag.getDisplay()).getId(), - (SynchedEntityData) nameTag.applyData(isEnabled() ? name : NpcApi.DISABLED_MESSAGE_PROVIDER.apply(player) - .appendNewline().append(name)))); - - packets.add(new ClientboundSetPassengersPacket(serverPlayer)); - } - - Arrays.stream(NpcOption.values()).filter(npcOption -> !npcOption.equals(NpcOption.ENABLED)) - .forEach(npcOption -> npcOption.getPacket(getOption(npcOption), this, player).map(o -> (Packet) o) - .ifPresent(packets::add)); - - NpcOption.ENABLED.getPacket(isEnabled(), this, player).map(o -> (Packet) o).ifPresent(packets::add); - - ServerGamePacketListenerImpl connection = ((CraftPlayer) player).getHandle().connection; - packets.forEach(connection::send); - } - - /** - * Hides the NPC from all currently online players. - */ - public void hideNpcFromAllPlayers() - { - Bukkit.getOnlinePlayers().forEach(this::hideNpcFromPlayer); - } - - /** - * Hides the NPC from a specific player. - * This method sends packets to remove the NPC and its associated entities from the player's view. - * - * @param player the player to hide the NPC from. Must not be null. - */ - public void hideNpcFromPlayer(@NotNull Player player) - { - ServerGamePacketListenerImpl connection = ((CraftPlayer) player).getHandle().connection; - connection.send(new ClientboundRemoveEntitiesPacket(serverPlayer.getId(), ((Display.TextDisplay) nameTag.getDisplay()).getId())); - - if(TeamManager.exists(player, getGameProfileName())) - { - PlayerTeam team = (PlayerTeam) TeamManager.create(player, getGameProfileName()); - connection.send((Packet) SetPlayerTeamPacket.createPlayerPacket(team, getGameProfileName(), - ClientboundSetPlayerTeamPacket.Action.REMOVE)); - connection.send((Packet) SetPlayerTeamPacket.createRemovePacket(team)); - } - - connection.send(new ClientboundPlayerInfoRemovePacket(List.of(getUUID()))); - - viewers.remove(player.getUniqueId()); - } - - /** - * Deletes the NPC. - * This hides the NPC from all players, removes it from the NPC manager, and deletes its saved data file. - */ - public void delete() throws IOException - { - if(serverPlayer == null) - return; - - hideNpcFromAllPlayers(); - NpcManager.removeNPC(this); - - serverPlayer.remove(Entity.RemovalReason.DISCARDED); - serverPlayer = null; - - npcPath.toFile().getParentFile().mkdirs(); - Files.deleteIfExists(npcPath); - } - - /** - * Makes the NPC look at a specific player. - * This calculates the required yaw and pitch and sends update packets to the viewing player. - * - * @param viewer the player the NPC should look at. Must not be null. - */ - public void lookAtPlayer(@NotNull Player viewer) - { - Location npcLoc = serverPlayer.getBukkitEntity().getLocation(); - Location playerLoc = viewer.getLocation(); - - if(npcLoc.getWorld() != playerLoc.getWorld()) - return; - - double dx = playerLoc.getX() - npcLoc.getX(); - double dy = ((playerLoc.getY() + viewer.getEyeHeight())) - - ((npcLoc.getY() + serverPlayer.getBukkitEntity().getEyeHeight() * getOption(NpcOption.SCALE))); - double dz = playerLoc.getZ() - npcLoc.getZ(); - - double distanceXZ = Math.sqrt(dx * dx + dz * dz); - float yaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); - float pitch = (float) Math.toDegrees(-Math.atan2(dy, distanceXZ)); - - byte yawByte = (byte) (yaw * 256 / 360); - byte pitchByte = (byte) (pitch * 256 / 360); - - ServerGamePacketListenerImpl connection = ((CraftPlayer) viewer).getHandle().connection; - - connection.send(new ClientboundRotateHeadPacket(serverPlayer, yawByte)); - connection.send(new ClientboundMoveEntityPacket.Rot(serverPlayer.getId(), yawByte, pitchByte, serverPlayer.onGround())); - } - - /** - * Moves the NPC along a precomputed {@link de.eisi05.npc.api.pathfinding.Path}, simulating walking, jumping, and gravity. - * The NPC's position and rotation are updated each tick and sent to the specified player(s). - * - * @param path The {@link de.eisi05.npc.api.pathfinding.Path} containing the ordered waypoints the NPC should follow. - * @param player The player who should see the NPC move. If null, updates all viewers in the `viewers` set. - * @param walkSpeed The walking speed of the NPC (clamped between 0.1 and 1). - * @param changeRealLocation If true, the NPC's actual server-side location will be updated; otherwise only packets are sent. - * @param onEnd A {@link Runnable} to be executed when the NPC reaches the end of the path. - * @return The {@link BukkitTask} representing the movement task. - */ - public @NotNull BukkitTask walkTo(@NotNull de.eisi05.npc.api.pathfinding.Path path, @Nullable Player player, double walkSpeed, - boolean changeRealLocation, @Nullable Consumer onEnd) - { - final double speed = Math.max(Math.min(walkSpeed, 1), 0.1); - - final double gravity = -0.08; - final double jumpVelocity = 0.5; - final double terminal = -0.5; - final double stepHeight = 0.6; - - return new BukkitRunnable() - { - final List pathPoints = path.asLocations(); - int index = 0; - org.bukkit.util.Vector current = location.toVector(); - double yVel = 0.0; - float previousYaw = location.getYaw(); - org.bukkit.util.Vector previousMovement = location.getDirection(); - - @Override - public void run() - { - if(index >= pathPoints.size()) - { - if(!path.getWaypoints().isEmpty()) - { - Location last = path.getWaypoints().getLast(); - - org.bukkit.util.Vector lastVector = last.toVector(); - org.bukkit.util.Vector lastMovement = lastVector.clone().subtract(current); - - ClientboundRotateHeadPacket rotateHeadPacket = new ClientboundRotateHeadPacket(serverPlayer, - (byte) (last.getYaw() * 256 / 360)); - ClientboundTeleportEntityPacket teleportEntityPacket = new ClientboundTeleportEntityPacket(serverPlayer.getId(), - new PositionMoveRotation(new Vec3(lastVector.toVector3f()), new Vec3(lastMovement.toVector3f()), last.getYaw(), - last.getPitch()), Set.of(), true); - - sendNpcMovePackets(player, teleportEntityPacket, rotateHeadPacket); - } - - if(changeRealLocation) - { - setLocation(path.getWaypoints().isEmpty() ? pathPoints.getLast() : path.getWaypoints().getLast()); - if(player != null) - { - for(UUID uuid : viewers) - { - OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid); - if(!offlinePlayer.isOnline() || uuid.equals(player.getUniqueId())) - continue; - - hideNpcFromPlayer(offlinePlayer.getPlayer()); - showNPCToPlayer(offlinePlayer.getPlayer()); - } - } - } - - if(onEnd != null) - onEnd.accept(Result.SUCCESS); - - cancel(); - return; - } - - org.bukkit.util.Vector target = pathPoints.get(index).toVector(); - org.bukkit.util.Vector toTarget = target.clone().subtract(current); - - if(toTarget.lengthSquared() < 0.04 && Math.abs(toTarget.getY()) < 0.2) - { - index++; - return; - } - - org.bukkit.util.Vector horizontal = new org.bukkit.util.Vector(toTarget.getX(), 0, toTarget.getZ()); - org.bukkit.util.Vector horizontalMove = - (horizontal.lengthSquared() > 1e-6) ? horizontal.clone().normalize().multiply(speed) : new org.bukkit.util.Vector(0, 0, 0); - - double nextDist = target.clone().subtract(current.clone().add(horizontalMove)).lengthSquared(); - if(nextDist > toTarget.lengthSquared()) - { - current = target; - index++; - return; - } - - World world = location.getWorld(); - int bx = (int) Math.floor(current.getX()); - int bz = (int) Math.floor(current.getZ()); - int searchStart = (int) Math.floor(current.getY()); - int groundBlockY = Integer.MIN_VALUE; - - for(int y = searchStart; y >= searchStart - 3; y--) - { - Block block = world.getBlockAt(bx, y - 1, bz); - if(block.getType().isSolid() && !block.getType().isAir() && !block.isPassable()) - { - groundBlockY = y - 1; - break; - } - } - if(groundBlockY == Integer.MIN_VALUE) - groundBlockY = world.getHighestBlockYAt(bx, bz) - 1; - double groundY = groundBlockY + 1.0; - boolean onGround = current.getY() <= groundY + 1e-5; - - if(onGround) - { - if(toTarget.getY() > 0 && toTarget.getY() <= stepHeight && horizontal.lengthSquared() > 1e-6) - { - current = current.clone().add(new org.bukkit.util.Vector(0, Math.min(toTarget.getY(), stepHeight), 0)); - yVel = 0; - onGround = true; - } - else if(toTarget.getY() > 0.5) - { - yVel = jumpVelocity; - onGround = false; - } - else - { - yVel = 0; - current = new org.bukkit.util.Vector(current.getX(), groundY, current.getZ()); - } - } - - double yDelta = 0; - if(!onGround) - { - yVel += gravity; - if(yVel < terminal) - yVel = terminal; - yDelta = yVel; - - if(current.getY() + yDelta <= groundY) - { - yDelta = groundY - current.getY(); - yVel = 0; - onGround = true; - } - } - - org.bukkit.util.Vector movement = new org.bukkit.util.Vector(horizontalMove.getX(), yDelta, horizontalMove.getZ()); - current = current.clone().add(movement); - - org.bukkit.util.Vector lookDir; - if(index + 1 < pathPoints.size()) - { - org.bukkit.util.Vector currentTarget = pathPoints.get(index).toVector().clone(); - org.bukkit.util.Vector nextTarget = pathPoints.get(index + 1).toVector().clone(); - - lookDir = currentTarget.clone().add(nextTarget).multiply(0.5).subtract(current); - } - else - lookDir = pathPoints.get(index).toVector().clone().subtract(current); - - org.bukkit.util.Vector horizontalVec = new org.bukkit.util.Vector(lookDir.getX(), 0, lookDir.getZ()); - if(horizontalVec.lengthSquared() < 1e-6) - horizontalVec = previousMovement.clone(); - - float targetYaw = (float) (Math.atan2(horizontalVec.getZ(), horizontalVec.getX()) * 180 / Math.PI - 90); - - while(targetYaw > 180) - targetYaw -= 360; - while(targetYaw < -180) - targetYaw += 360; - - float diff = targetYaw - previousYaw; - if(diff > 180) - diff -= 360; - if(diff < -180) - diff += 360; - - float maxTurn = 15f; - diff = Math.max(-maxTurn, Math.min(maxTurn, diff)); - - float yaw = previousYaw + diff; - previousYaw = yaw; - - previousMovement = horizontalVec.clone(); - - org.bukkit.util.Vector targetVec = pathPoints.get(Math.min(index + 1, pathPoints.size() - 1)).toVector().clone().subtract(current); - double horizontalLen = Math.sqrt(targetVec.getX() * targetVec.getX() + targetVec.getZ() * targetVec.getZ()); - float pitch = (float) (-Math.atan2(targetVec.getY(), horizontalLen) * 180 / Math.PI) / 1.5f; - - ClientboundRotateHeadPacket rotateHeadPacket = new ClientboundRotateHeadPacket(serverPlayer, (byte) (yaw * 256 / 360)); - ClientboundTeleportEntityPacket teleportEntityPacket = new ClientboundTeleportEntityPacket(serverPlayer.getId(), - new PositionMoveRotation(new Vec3(current.toVector3f()), new Vec3(movement.toVector3f()), yaw, pitch), Set.of(), onGround); - - sendNpcMovePackets(player, teleportEntityPacket, rotateHeadPacket); - } - - @Override - public synchronized void cancel() throws IllegalStateException - { - super.cancel(); - if(onEnd != null) - onEnd.accept(Result.CANCELLED); - } - }.runTaskTimer(NpcApi.plugin, 1L, 1L); - } - - /** - * Sends NPC movement and rotation packets to a specific player or all viewers. - * - * @param player The player to send packets to. If null, packets are sent to all viewers. - * @param teleportEntityPacket The packet containing the NPC's teleport/move data. Must not be null. - * @param rotateHeadPacket The packet containing the NPC's head rotation data. Must not be null. - */ - private void sendNpcMovePackets(@Nullable Player player, @NotNull ClientboundTeleportEntityPacket teleportEntityPacket, - @NotNull ClientboundRotateHeadPacket rotateHeadPacket) - { - if(player != null) - { - ServerPlayer serverPlayer1 = ((CraftPlayer) player).getHandle(); - serverPlayer1.connection.send(teleportEntityPacket); - serverPlayer1.connection.send(rotateHeadPacket); - } - else - { - for(UUID uuid : viewers) - { - OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid); - if(!offlinePlayer.isOnline()) - continue; - - ServerPlayer serverPlayer1 = ((CraftPlayer) offlinePlayer.getPlayer()).getHandle(); - serverPlayer1.connection.send(teleportEntityPacket); - serverPlayer1.connection.send(rotateHeadPacket); - } - } - } - - void changeUUID(@NotNull UUID newUUID) - { - try - { - Files.deleteIfExists(npcPath); - npcPath = NpcApi.plugin.getDataFolder().toPath().resolve("NPC").resolve(newUUID + ".npc"); - save(); - } catch(Exception e) - { - throw new RuntimeException(e); - } - } - - public CustomNameTag getNameTag() - { - return nameTag; - } - - /** - * A serializable representation of an NPC, used for saving and loading NPC data. - * This record stores all essential properties of an NPC that need to be persisted. - * - * @param world The UUID of the world where the NPC is located. - * @param x The x-coordinate of the NPC's location. - * @param y The y-coordinate of the NPC's location. - * @param z The z-coordinate of the NPC's location. - * @param yaw The yaw (horizontal rotation) of the NPC. - * @param pitch The pitch (vertical rotation) of the NPC. - * @param id The unique identifier (UUID) of the NPC. - * @param name The serialized representation of the NPC's display name. - * @param options A map of NPC options, where keys are option paths (strings) and values are serializable option values. - * @param clickEvent The click action associated with the NPC. Can be null. - * @param createdAt The timestamp when the NPC was originally created. - */ - public record SerializedNPC(@NotNull UUID world, double x, double y, double z, float yaw, float pitch, @NotNull UUID id, - @NotNull String name, @NotNull Map options, - @Nullable NpcClickAction clickEvent, @NotNull Instant createdAt) implements Serializable - { - @Serial - private static final long serialVersionUID = 1L; - - /** - * Creates a {@link SerializedNPC} instance from an existing {@link NPC} object. - * - * @param npc The NPC to serialize. Must not be null. - * @return A new {@link SerializedNPC} instance representing the given NPC. Will not be null. - */ - public static @NotNull SerializedNPC serializedNPC(@NotNull NPC npc) - { - Map options = new HashMap<>(); - npc.options.forEach((key, value) -> options.put(key.getPath(), Var.unsafeCast(key.serialize(npc.getOption(key))))); - - return new SerializedNPC(npc.getLocation().getWorld().getUID(), npc.getLocation().getX(), npc.getLocation().getY(), - npc.getLocation().getZ(), npc.getLocation().getYaw(), npc.getLocation().getPitch(), npc.getUUID(), - JSONComponentSerializer.json().serialize(npc.getName()), options, npc.clickEvent, npc.createdAt); - } - - /** - * Deserializes this {@link SerializedNPC} object back into a fully functional {@link NPC} instance. - * - * @param The type of the NpcOption value. - * @param The serializable type of the NpcOption value. - * @return A new {@link NPC} instance reconstructed from the serialized data. Will not be null. - */ - @SuppressWarnings("unchecked") - public @NotNull NPC deserializedNPC() - { - NPC npc = new NPC(new Location(Bukkit.getWorld(world), x, y, z, yaw, pitch), id, - JSONComponentSerializer.json().deserialize(name)).setClickEvent( - clickEvent == null ? clickEvent : clickEvent.initialize()); - options.forEach((string, serializable) -> NpcOption.getOption(string) - .ifPresent(npcOption -> npc.setOption((NpcOption) npcOption, (T) npcOption.deserialize(Var.unsafeCast(serializable))))); - npc.createdAt = createdAt == null ? Instant.now() : createdAt; - return npc; - } - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NpcConfig.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NpcConfig.java deleted file mode 100644 index 2444fca..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NpcConfig.java +++ /dev/null @@ -1,150 +0,0 @@ -package de.eisi05.npc.api.objects; - -import org.jetbrains.annotations.NotNull; - -/** - * Configuration settings for NPC behavior. - * This class allows for customizing various aspects of an NPC, such as interaction timers. - */ -public class NpcConfig -{ - /** - * The time in ticks an NPC will look at a player after interaction. - * The default value is 5 ticks. - */ - private long lookAtTimer = 5; - - /** - * If true, command validation will be skipped. - * Useful for allowing proxy commands like BungeeCord. - */ - private boolean avoidCommandCheck = false; - - /** - * If true, debug mode is enabled. - * Can be used for logging or diagnostic purposes. - */ - private boolean debug = false; - - /** - * Time allowed for input, measured in seconds. - * Default is 60 seconds. - */ - private int inputTime = 60; - - /** - * If true, NPCs are automatically updated when changed. - */ - private boolean autoUpdate = false; - - /** - * Sets the duration an NPC will look at a player after an interaction. - * - * @param time The time in ticks. For example, 20 ticks = 1 second. - * @return This {@link NpcConfig} instance for method chaining. Will not be null. - */ - public @NotNull NpcConfig lookAtTimer(long time) - { - lookAtTimer = time; - return this; - } - - /** - * Sets whether to skip command validation. - * Useful for allowing BungeeCord or proxy commands. - * - * @param avoidCommandCheck True to skip command checks, false to validate. - * @return This {@link NpcConfig} instance for method chaining. Never null. - */ - public @NotNull NpcConfig avoidCommandCheck(boolean avoidCommandCheck) - { - this.avoidCommandCheck = avoidCommandCheck; - return this; - } - - /** - * Enables or disables debug mode. - * - * @param debug True to enable debug mode, false to disable it. - * @return This {@link NpcConfig} instance for method chaining. Never null. - */ - public @NotNull NpcConfig debug(boolean debug) - { - this.debug = debug; - return this; - } - - /** - * Sets the input time limit. - * - * @param inputTime The time in seconds. - * @return This {@link NpcConfig} instance for method chaining. Never null. - */ - public @NotNull NpcConfig inputTime(int inputTime) - { - this.inputTime = inputTime; - return this; - } - - /** - * Sets whether automatic updates should be enabled. - * - * @param autoUpdate True to enable auto updates, false otherwise. - * @return This {@link NpcConfig} instance for method chaining. Never null. - */ - public @NotNull NpcConfig autoUpdate(boolean autoUpdate) - { - this.autoUpdate = autoUpdate; - return this; - } - - /** - * Gets the configured duration an NPC will look at a player. - * - * @return The time in ticks. - */ - public long lookAtTimer() - { - return lookAtTimer; - } - - /** - * Checks whether command validation is disabled. - * - * @return True if validation is skipped; false otherwise. - */ - public boolean avoidCommandCheck() - { - return avoidCommandCheck; - } - - /** - * Checks whether debug mode is enabled. - * - * @return True if debug is enabled; false otherwise. - */ - public boolean debug() - { - return debug; - } - - /** - * Gets the configured input time limit. - * - * @return The time in seconds. - */ - public int inputTime() - { - return inputTime; - } - - /** - * Checks whether automatic updates are enabled. - * - * @return True if auto updates are enabled; false otherwise. - */ - public boolean autoUpdate() - { - return autoUpdate; - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NpcHolder.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NpcHolder.java deleted file mode 100644 index dc25fcd..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NpcHolder.java +++ /dev/null @@ -1,62 +0,0 @@ -package de.eisi05.npc.api.objects; - -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.InventoryHolder; -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; - -/** - * Abstract class representing an entity that can hold NPC-related data - * and has a concept of unsaved changes. - * It implements {@link InventoryHolder} but onlay as placeholder. - */ -public abstract class NpcHolder implements InventoryHolder -{ - /** - * Flag indicating whether there are unsaved changes to this holder. - * Defaults to {@code false}. - */ - private boolean unsavedChanges = false; - - /** - * Checks if there are any unsaved changes for this NPC holder. - * - * @return {@code true} if there are unsaved changes, {@code false} otherwise. - */ - - public boolean hasUnsavedChanges() - { - return unsavedChanges; - } - - /** - * Marks that there are unsaved changes to this NPC holder. - * This should be called whenever a modifiable property of the holder is changed. - */ - public void markChange() - { - unsavedChanges = true; - } - - /** - * Saves the current state of the NPC holder. - * This method is intended to persist any changes. After successful execution, - * the {@code unsavedChanges} flag is reset to {@code false}. - * - * @throws IOException if an error occurs during the saving process. - */ - public void save() throws IOException - { - unsavedChanges = false; - } - - /** - * @throws UnsupportedOperationException always, as this inventory is not used. - */ - @Override - public @NotNull Inventory getInventory() - { - throw new UnsupportedOperationException("This inventory is not used!"); - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NpcOption.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NpcOption.java deleted file mode 100644 index bb1f312..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NpcOption.java +++ /dev/null @@ -1,591 +0,0 @@ -package de.eisi05.npc.api.objects; - -import com.google.common.collect.Multimaps; -import com.mojang.authlib.GameProfile; -import com.mojang.authlib.properties.Property; -import com.mojang.authlib.properties.PropertyMap; -import com.mojang.datafixers.util.Pair; -import de.eisi05.npc.api.NpcApi; -import de.eisi05.npc.api.enums.SkinParts; -import de.eisi05.npc.api.manager.TeamManager; -import de.eisi05.npc.api.scheduler.Tasks; -import de.eisi05.npc.api.utils.*; -import de.eisi05.npc.api.wrapper.enums.ChatFormat; -import de.eisi05.npc.api.wrapper.packets.SetEntityDataPacket; -import de.eisi05.npc.api.wrapper.packets.SetPlayerTeamPacket; -import net.minecraft.ChatFormatting; -import net.minecraft.network.Connection; -import net.minecraft.network.protocol.Packet; -import net.minecraft.network.protocol.PacketFlow; -import net.minecraft.network.protocol.game.*; -import net.minecraft.network.syncher.EntityDataSerializers; -import net.minecraft.network.syncher.SynchedEntityData; -import net.minecraft.server.MinecraftServer; -import net.minecraft.server.level.ClientInformation; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.server.network.CommonListenerCookie; -import net.minecraft.server.network.ServerGamePacketListenerImpl; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.ai.attributes.AttributeInstance; -import net.minecraft.world.entity.ai.attributes.Attributes; -import net.minecraft.world.scores.PlayerTeam; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.craftbukkit.CraftServer; -import org.bukkit.craftbukkit.CraftWorld; -import org.bukkit.craftbukkit.entity.CraftPlayer; -import org.bukkit.craftbukkit.inventory.CraftItemStack; -import org.bukkit.entity.Player; -import org.bukkit.entity.Pose; -import org.bukkit.inventory.EquipmentSlot; -import org.bukkit.inventory.ItemStack; -import org.bukkit.scheduler.BukkitRunnable; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.Serializable; -import java.lang.reflect.Field; -import java.util.*; -import java.util.function.Function; - -/** - * Represents a configurable option for an NPC. - * Each option has a path, a default value, serialization/deserialization logic, - * and a function to generate a network packet for applying the option. - * - * @param The type of the option's value in its usable form. - * @param The type of the option's value in its serialized form. - */ -public class NpcOption -{ - /** - * NPC option to determine if the NPC should use the skin of the viewing player. - * If true, the NPC's skin will be dynamically set to the skin of the player looking at it. - */ - public static final NpcOption USE_PLAYER_SKIN = new NpcOption<>("use-player-skin", false, - aBoolean -> aBoolean, aBoolean -> aBoolean, - (skin, npc, player) -> - { - if(!skin) - return null; - - ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle(); - ServerPlayer npcServerPlayer = (ServerPlayer) npc.getServerPlayer(); - - if(!Versions.isCurrentVersionSmallerThan(Versions.V1_21_9)) - { - var textureProperties = ((PropertyMap) Reflections.getField(serverPlayer.getGameProfile(), "properties") - .get()).get("textures").iterator(); - - var npcTextureProperties = ((PropertyMap) Reflections.getField(serverPlayer.getGameProfile(), "properties") - .get()).get("textures").iterator(); - - Property property = textureProperties.hasNext() ? textureProperties.next() : null; - Property npcProperty = npcTextureProperties.hasNext() ? npcTextureProperties.next() : null; - - if((property == null && npcProperty == null) || (property != null && npcProperty != null && - Reflections.getField(property, "value").get().equals(Reflections.getField(npcProperty, "value").get()))) - return null; - - UUID newUUID = UUID.randomUUID(); - GameProfile profile = Reflections.getInstance(GameProfile.class, newUUID, "NPC" + newUUID.toString().substring(0, 13), - Reflections.getInstance(PropertyMap.class, Multimaps.forMap(property == null ? Map.of() : Map.of("textures", property))) - .orElseThrow()).orElseThrow(); - - Location location = npc.getLocation(); - MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer(); - ServerLevel level = ((CraftWorld) location.getWorld()).getHandle(); - npc.serverPlayer = new ServerPlayer(server, level, profile, ClientInformation.createDefault()); - Var.moveEntity(npc.serverPlayer, location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch()); - npc.serverPlayer.connection = new ServerGamePacketListenerImpl(server, new Connection(PacketFlow.SERVERBOUND), npc.serverPlayer, - CommonListenerCookie.createInitial(profile, true)); - npc.changeUUID(newUUID); - return null; - } - - PropertyMap playerProperty = (PropertyMap) Reflections.invokeMethod(serverPlayer.getGameProfile(), "getProperties").get(); - PropertyMap npcProperty = (PropertyMap) Reflections.invokeMethod(serverPlayer.getGameProfile(), "getProperties").get(); - - var textureProperties = playerProperty.get("textures").iterator(); - npcProperty.removeAll("textures"); - - if(!textureProperties.hasNext()) - return null; - - var textureProperty = textureProperties.next(); - npcProperty.put("textures", textureProperty); - return null; - }).loadBefore(!Versions.isCurrentVersionSmallerThan(Versions.V1_21_9)); - - /** - * NPC option to set a specific skin using a value and signature. - * This is ignored if {@link #USE_PLAYER_SKIN} is true. - */ - public static final NpcOption SKIN = new NpcOption("skin", null, - skin -> skin, skin -> skin, - (skin, npc, player) -> - { - if(npc.getOption(USE_PLAYER_SKIN)) - return null; - - ServerPlayer npcServerPlayer = (ServerPlayer) npc.getServerPlayer(); - - - if(!Versions.isCurrentVersionSmallerThan(Versions.V1_21_9)) - { - var npcTextureProperties = ((PropertyMap) Reflections.getField(npcServerPlayer.getGameProfile(), "properties") - .get()).get("textures").iterator(); - - Property npcProperty = npcTextureProperties.hasNext() ? npcTextureProperties.next() : null; - - if((skin == null && npcProperty == null) || - (npcProperty != null && skin.value().equals(Reflections.getField(npcProperty, "value").get()))) - return null; - - UUID newUUID = UUID.randomUUID(); - var textures = new Property("textures", skin.value(), skin.signature()); - - PropertyMap propertyMap = Reflections.getInstance(PropertyMap.class, - Multimaps.forMap(skin == null ? Map.of() : Map.of("textures", textures))).orElseThrow(); - - GameProfile profile = Reflections.getInstance(GameProfile.class, newUUID, "NPC" + newUUID.toString().substring(0, 13), - propertyMap).orElseThrow(); - - Location location = npc.getLocation(); - MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer(); - ServerLevel level = ((CraftWorld) location.getWorld()).getHandle(); - npc.serverPlayer = new ServerPlayer(server, level, profile, ClientInformation.createDefault()); - Var.moveEntity(npc.serverPlayer, location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch()); - npc.serverPlayer.connection = new ServerGamePacketListenerImpl(server, new Connection(PacketFlow.SERVERBOUND), npc.serverPlayer, - CommonListenerCookie.createInitial(profile, true)); - npc.changeUUID(newUUID); - return null; - } - - PropertyMap properties = (PropertyMap) Reflections.invokeMethod(npcServerPlayer.getGameProfile(), "getProperties").get(); - - properties.removeAll("textures"); - - if(skin == null) - return null; - - var textures = new Property("textures", skin.value(), skin.signature()); - - properties.put("textures", textures); - return null; - }).loadBefore(!Versions.isCurrentVersionSmallerThan(Versions.V1_21_9)); - - /** - * NPC option to control whether the NPC is shown in the player tab list. - * If false, the NPC will be removed from the tab list for the viewing player after a short delay. - */ - public static final NpcOption SHOW_TAB_LIST = new NpcOption<>("show-tab-list", true, - aBoolean -> aBoolean, aBoolean -> aBoolean, - (show, npc, player) -> - { - if(show) - return null; - - new BukkitRunnable() - { - @Override - public void run() - { - ((CraftPlayer) player).getHandle().connection.send(new ClientboundPlayerInfoRemovePacket(List.of(npc.getUUID()))); - } - }.runTaskLater(NpcApi.plugin, 50); - return null; - }); - - /** - * NPC option to set the simulated latency (ping) of the NPC in the tab list. - */ - public static final NpcOption LATENCY = new NpcOption<>("latency", 0, - aInteger -> aInteger, aInteger -> aInteger, - (latency, npc, player) -> - { - ServerPlayer npcServerPlayer = (ServerPlayer) npc.getServerPlayer(); - - CommonListenerCookie commonListenerCookie; - if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_7)) - commonListenerCookie = Reflections.getInstanceFirstConstructor(CommonListenerCookie.class, - npcServerPlayer.getGameProfile(), latency, ClientInformation.createDefault(), true).orElseThrow(); - else - commonListenerCookie = Reflections.getInstanceFirstConstructor(CommonListenerCookie.class, - npcServerPlayer.getGameProfile(), latency, ClientInformation.createDefault(), true, null, - new HashSet<>(), Reflections.getInstance("io.papermc.paper.util.KeepAlive").orElseThrow()).orElseThrow(); - - if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_9)) - npcServerPlayer.connection = new ServerGamePacketListenerImpl( - (MinecraftServer) Reflections.invokeMethod(npcServerPlayer, "getServer").get(), - new Connection(PacketFlow.SERVERBOUND), npcServerPlayer, commonListenerCookie); - else - npcServerPlayer.connection = new ServerGamePacketListenerImpl(npcServerPlayer.level().getServer(), - new Connection(PacketFlow.SERVERBOUND), npcServerPlayer, commonListenerCookie); - - return new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY, npcServerPlayer); - }); - - /** - * NPC option to control the visibility of the NPC's nametag. - */ - public static final NpcOption HIDE_NAMETAG = new NpcOption<>("hide-nametag", false, - aBoolean -> aBoolean, aBoolean -> aBoolean, - (hide, npc, player) -> - { - if(!hide) - return null; - - return new ClientboundRemoveEntitiesPacket(((Entity) npc.getNameTag().getDisplay()).getId()); - }); - - /** - * NPC option to set the pose of the NPC (e.g., standing, sleeping, swimming). - * For a full list look at {@link Pose}. - */ - public static final NpcOption POSE = new NpcOption<>("pose", Pose.STANDING, - pose -> pose, pose -> pose, - (pose, npc, player) -> - { - net.minecraft.world.entity.Pose nmsPose = net.minecraft.world.entity.Pose.values()[pose.ordinal()]; - - if(nmsPose == null) - throw new RuntimeException("Pose (" + pose.name() + ") not found"); - - ServerPlayer npcServerPlayer = (ServerPlayer) npc.getServerPlayer(); - - npcServerPlayer.setPose(nmsPose); - - SynchedEntityData data = npcServerPlayer.getEntityData(); - data.set(EntityDataSerializers.POSE.createAccessor(6), nmsPose); - - if(pose == Pose.SPIN_ATTACK) - data.set(EntityDataSerializers.BYTE.createAccessor(8), (byte) 0x04); - else - data.set(EntityDataSerializers.BYTE.createAccessor(8), (byte) 0x01); - - return (Packet) SetEntityDataPacket.create(npcServerPlayer.getId(), data); - }); - - /** - * NPC option to set the equipment worn by the NPC (armor, items in hand). - * The map uses {@link EquipmentSlot} as keys and {@link ItemStack} as values. - * Serialized form uses item base64 strings. - */ - public static final NpcOption, HashMap> EQUIPMENT = new NpcOption<>("equipment", Map.of(), - map -> - { - HashMap serializedMap = new HashMap<>(); - map.forEach((slot, item) -> serializedMap.put(slot, ItemSerializer.itemStackToBase64(item))); - return serializedMap; - }, - serializedMap -> - { - HashMap map = new HashMap<>(); - serializedMap.forEach((slot, string) -> map.put(slot, ItemSerializer.itemStackFromBase64(string))); - return map; - }, - (map, npc, player) -> - { - if(map.isEmpty()) - return null; - - List> list = new ArrayList<>(); - - map.forEach((slot, item) -> list.add( - new Pair<>(net.minecraft.world.entity.EquipmentSlot.values()[slot.ordinal()], CraftItemStack.asNMSCopy(item)))); - - return new ClientboundSetEquipmentPacket(((ServerPlayer) npc.getServerPlayer()).getId(), list); - }); - - /** - * NPC option to control which parts of the NPC's skin are visible (e.g., hat, jacket). - * For a full list look at {@link SkinParts}. - */ - public static final NpcOption SKIN_PARTS = new NpcOption<>("skin-parts", SkinParts.values(), - skinParts -> skinParts, skinParts -> skinParts, - (skinParts, npc, player) -> - { - ServerPlayer npcServerPlayer = (ServerPlayer) npc.getServerPlayer(); - SynchedEntityData data = npcServerPlayer.getEntityData(); - data.set(EntityDataSerializers.BYTE.createAccessor(Versions.isCurrentVersionSmallerThan(Versions.V1_21_9) ? 17 : 16), - (byte) Arrays.stream(skinParts).mapToInt(SkinParts::getValue).sum()); - return (Packet) SetEntityDataPacket.create(npcServerPlayer.getId(), data); - }); - - /** - * NPC option to make the NPC look at the player if they are within a certain distance. - * The value is the maximum distance in blocks. A value of 0 or less disables this. - * The actual looking logic is handled by {@link Tasks}. - */ - public static final NpcOption LOOK_AT_PLAYER = new NpcOption<>("look-at-player", 0.0, - distance -> distance, distance -> distance, - (distance, npc, player) -> null); - - /** - * NPC option to make the NPC glow with a specific color. - * If null, the glowing effect is removed. - */ - @SuppressWarnings("unchecked") - public static final NpcOption GLOWING = new NpcOption<>("glowing", null, - color -> color, color -> color, - (color, npc, player) -> - { - ServerPlayer npcServerPlayer = (ServerPlayer) npc.getServerPlayer(); - - if(color == null) - { - SynchedEntityData entityData = npcServerPlayer.getEntityData(); - entityData.set(EntityDataSerializers.BYTE.createAccessor(0), (byte) 0); - return (Packet) SetEntityDataPacket.create(npcServerPlayer.getId(), entityData); - } - - String teamName = npc.getGameProfileName(); - boolean modified = TeamManager.exists(player, teamName); - PlayerTeam team = (PlayerTeam) TeamManager.create(player, teamName); - - team.setColor(ChatFormatting.getByCode(color.getColorCode())); - - var teamPacket = SetPlayerTeamPacket.createAddOrModifyPacket(team, !modified); - - SynchedEntityData entityData = npcServerPlayer.getEntityData(); - entityData.set(EntityDataSerializers.BYTE.createAccessor(0), (byte) 0x40); - - return new ClientboundBundlePacket(List.of((Packet) teamPacket, - (Packet) SetEntityDataPacket.create( - npcServerPlayer.getId(), entityData))); - }); - - /** - * NPC option to set the scale (size) of the NPC. - * A value of 1.0 is normal size. Requires Minecraft 1.20.6 or newer. - */ - public static final NpcOption SCALE = new NpcOption<>("scale", 1.0, - scale -> scale, scale -> scale, - (scale, npc, player) -> - { - ServerPlayer npcServerPlayer = (ServerPlayer) npc.getServerPlayer(); - - AttributeInstance instance = npcServerPlayer.getAttribute(Attributes.SCALE); - instance.setBaseValue(scale); - - return new ClientboundUpdateAttributesPacket(npcServerPlayer.getId(), List.of(instance)); - }); - - /** - * NPC option to control the position of the NPC in the TAB list. - *

- * Only works on versions older than 1.21.2. - * On 1.21.2 and newer, this option has no effect. - *

- */ - public static final NpcOption LIST_ORDER = new NpcOption<>("list-order", 0, - aInt -> aInt, aInt -> aInt, - (order, npc, player) -> - { - if(!Versions.isCurrentVersionSmallerThan(Versions.V1_21_2)) - return null; - - ((ServerPlayer) npc.getServerPlayer()).listOrder = order; - - return new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LIST_ORDER, - (ServerPlayer) npc.getServerPlayer()); - }).since(Versions.V1_21_2); - - /** - * NPC option to control if the NPC is enabled (visible and interactable). - * If false, a "DISABLED" marker may be shown. - * This is an internal option, typically not directly set by users but controlled by {@link NPC#setEnabled(boolean)}. - */ - static final NpcOption ENABLED = new NpcOption<>("enabled", false, - aBoolean -> aBoolean, aBoolean -> aBoolean, - (enabled, npc, player) -> null); - - /** - * NPC option to control if the NPC is enabled (visible and interactable). - * If false, a "DISABLED" marker may be shown. - * This is an internal option, typically not directly set by users but controlled by {@link NPC#setEnabled(boolean)}. - */ - static final NpcOption EDITABLE = new NpcOption<>("editable", false, - aBoolean -> aBoolean, aBoolean -> aBoolean, - (enabled, npc, player) -> null); - - private final String path; - private final T defaultValue; - private final Function serializer; - private final Function deserializer; - private final TriFunction> packet; - private Versions since = Versions.V1_17; - private boolean loadBefore = false; - - /** - * Private constructor to create a new NpcOption. - * - * @param path The configuration path string. Must not be null. - * @param defaultValue The default value for the option. Can be null. - * @param serializer The serialization function. Must not be null. - * @param deserializer The deserialization function. Must not be null. - * @param packet The packet generation function. Must not be null. - */ - private NpcOption(@NotNull String path, @Nullable T defaultValue, @NotNull Function serializer, @NotNull Function deserializer, - @NotNull TriFunction> packet) - { - this.path = path; - this.defaultValue = defaultValue; - this.serializer = serializer; - this.deserializer = deserializer; - this.packet = packet; - } - - /** - * Retrieves all declared {@link NpcOption} constants within this class using reflection. - * - * @return An array of {@link NpcOption} instances. Will not be null. - */ - public static @NotNull NpcOption[] values() - { - List fields = Arrays.stream(NpcOption.class.getDeclaredFields()).filter(field -> field.getType().equals(NpcOption.class)).toList(); - - NpcOption[] values = new NpcOption[fields.size()]; - - for(int i = 0; i < fields.size(); i++) - { - try - { - values[i] = (NpcOption) fields.get(i).get(null); - } catch(IllegalAccessException e) - { - } - } - - return values; - } - - /** - * Retrieves an {@link NpcOption} instance by its configuration path. - * - * @param path The configuration path string to search for. Must not be null. - * @return An {@link Optional} containing the found {@link NpcOption}, or an empty Optional if no option matches the path. - */ - public static @NotNull Optional> getOption(@NotNull String path) - { - return Arrays.stream(values()).filter(npcOption -> npcOption.getPath().equals(path)).findFirst(); - } - - /** - * Sets the minimum Minecraft version required for this option. - * Used for options that are only available in newer versions of the game. - * - * @param since The minimum {@link Versions} required. Must not be null. - * @return This {@link NpcOption} instance for method chaining. - */ - public @NotNull NpcOption since(@NotNull Versions since) - { - this.since = since; - return this; - } - - public @NotNull NpcOption loadBefore(boolean loadBefore) - { - this.loadBefore = loadBefore; - return this; - } - - public boolean loadBefore() - { - return loadBefore; - } - - /** - * Checks if this NPC option is compatible with the current server version. - * An option is compatible if the current server version is greater than or equal to - * the version specified by {@link #since()}. - * - * @return {@code true} if the option is compatible, {@code false} otherwise. - */ - public boolean isCompatible() - { - return !Versions.isCurrentVersionSmallerThan(since); - } - - /** - * Gets the minimum Minecraft version required for this option. - * - * @return The {@link Versions} instance. - */ - public Versions since() - { - return since; - } - - /** - * Gets the default value for this option. - * - * @return The default value, which can be null if defined as such. - */ - public @Nullable T getDefaultValue() - { - return defaultValue; - } - - /** - * Gets the configuration path string for this option. - * - * @return The path string. Will not be null. - */ - public @NotNull String getPath() - { - return path; - } - - /** - * Serializes the given value (of type T) into its serializable form (type S). - * - * @param var1 The value to serialize. Can be null. - * @return The serialized value. Can be null if the input or serializer result is null. - * @throws RuntimeException if a {@link ClassCastException} occurs during serialization, - * indicating an incorrect type was passed. - */ - @SuppressWarnings("unchecked") - public @Nullable S serialize(@Nullable Object var1) - { - try - { - return serializer.apply((T) var1); - } catch(ClassCastException e) - { - throw new RuntimeException(path + " -> " + var1); - } - } - - /** - * Deserializes the given value (of type S) back into its usable form (type T). - * - * @param var1 The serialized value to deserialize. Can be null. - * @return The deserialized value. Can be null if the input or deserializer result is null. - */ - public @Nullable T deserialize(@Nullable S var1) - { - return deserializer.apply(var1); - } - - /** - * Generates the network packet(s) needed to apply this option's value to an NPC for a specific player. - * The method checks for version compatibility before generating the packet. - * - * @param object The value of the option to apply. Can be null. - * @param npc The {@link NPC} to apply the option to. Must not be null. - * @param player The {@link Player} who will receive the update. Must not be null. - * @return An {@link Optional} containing the {@link Packet} if one is generated and the option is compatible, - * otherwise an empty Optional. - */ - @SuppressWarnings("unchecked") - public @NotNull Optional getPacket(@Nullable Object object, @NotNull NPC npc, Player player) - { - if(packet == null || !isCompatible()) - return Optional.empty(); - - return Optional.ofNullable(packet.apply((T) object, npc, player)); - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/Skin.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/Skin.java deleted file mode 100644 index e91de26..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/Skin.java +++ /dev/null @@ -1,260 +0,0 @@ -package de.eisi05.npc.api.objects; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.mojang.authlib.properties.Property; -import com.mojang.authlib.properties.PropertyMap; -import de.eisi05.npc.api.utils.Reflections; -import de.eisi05.npc.api.utils.Versions; -import net.minecraft.server.level.ServerPlayer; -import org.bukkit.craftbukkit.entity.CraftPlayer; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.*; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URL; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.file.Files; -import java.time.Duration; -import java.util.*; -import java.util.concurrent.CompletableFuture; - -/** - * Represents a player's skin, containing its name, value, and signature. - * This record is immutable and implements {@link Serializable} for easy persistence. - * The skin value and signature are typically obtained from Mojang's session servers - * and are used to display the correct player texture. - * - * @param name The name associated with the skin (usually the player's username). Can be {@code null}. - * @param value The base64 encoded string representing the skin data (texture URL, model, etc.). - * @param signature The signature used to verify the authenticity of the skin data. - */ -public record Skin(@Nullable String name, @NotNull String value, @NotNull String signature) implements Serializable -{ - @Serial - private static final long serialVersionUID = 1L; - - /** - * A static cache to store fetched skins, mapping UUIDs to Skin objects. - * This helps reduce redundant API calls to Mojang's servers. - */ - private static final Map skinCache = new HashMap<>(); - - /** - * Retrieves the skin data directly from a currently online Bukkit player. - * This method uses reflection to access the player's game profile properties. - * - * @param player The Bukkit player from whom to retrieve the skin. Must not be {@code null}. - * @return A {@link Skin} object representing the player's current skin, or {@code null} if no skin properties are found. - */ - public static @Nullable Skin fromPlayer(@NotNull Player player) - { - if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_9)) - { - ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle(); - PropertyMap properties = (PropertyMap) Reflections.invokeMethod(serverPlayer.getGameProfile(), "getProperties").get(); - Iterator it = properties.get("textures").iterator(); - - if(!it.hasNext()) - return null; - - var property = it.next(); - - return new Skin(player.getName(), property.value(), property.signature()); - } - - ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle(); - var properties = serverPlayer.getGameProfile().properties().get("textures").iterator(); - - if(!properties.hasNext()) - return null; - - var property = properties.next(); - - return new Skin(player.getName(), property.value(), property.signature()); - } - - /** - * Fetches a player's skin from Mojang's session server using their UUID. - * This method first checks the local cache (`skinCache`) before making an HTTP request. - * The fetched skin is added to the cache for future use. - * - * @param uuid The UUID of the player whose skin is to be fetched. Must not be {@code null}. - * @return A {@link Skin} object if the skin is successfully fetched or found in cache, otherwise {@code null}. - */ - public static @Nullable Skin fetchSkin(@NotNull UUID uuid) - { - - if(skinCache.containsKey(uuid)) - return skinCache.get(uuid); - - try - { - URL url = URI.create("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid + "?unsigned=false").toURL(); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setReadTimeout(10000); - - try(InputStream is = connection.getInputStream(); Scanner scanner = new Scanner(is)) - { - String response = scanner.useDelimiter("\\A").next(); - - JsonObject json = JsonParser.parseString(response).getAsJsonObject(); - String name = json.get("name").getAsString(); - - JsonArray properties = json.getAsJsonArray("properties"); - for(JsonElement prop : properties) - { - JsonObject obj = prop.getAsJsonObject(); - String value = obj.get("value").getAsString(); - String signature = obj.has("signature") ? obj.get("signature").getAsString() : null; - Skin skin = new Skin(name, value, signature); - skinCache.put(uuid, skin); - return skin; - } - - return null; - } - } catch(Exception e) - { - return null; - } - } - - /** - * Fetches a player's skin by their username. - * This method first calls Mojang's API to get the player's UUID from their name - * and then uses the UUID to fetch the skin data. - * - * @param name The username of the player whose skin is to be fetched. Must not be {@code null}. - * @return A {@link Skin} object if the skin is successfully fetched, otherwise {@code null}. - */ - public static Skin fetchSkin(@NotNull String name) - { - try - { - URL url = URI.create("https://api.mojang.com/users/profiles/minecraft/" + name).toURL(); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setReadTimeout(10000); - - try(InputStream is = conn.getInputStream(); Scanner scanner = new Scanner(is)) - { - String response = scanner.useDelimiter("\\A").next(); - JsonObject json = JsonParser.parseString(response).getAsJsonObject(); - String id = json.get("id").getAsString(); - return fetchSkin(UUID.fromString(id.replaceFirst( - "(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", - "$1-$2-$3-$4-$5"))); - } - } catch(Exception e) - { - return null; - } - } - - /** - * Uploads a skin file to MineSkin and retrieves the resulting {@link Skin}. - * - * @param skinFile the PNG file of the skin to upload. - * @return an {@link Optional} containing the skin if successful, otherwise empty. - * @throws IllegalArgumentException if the file does not exist. - */ - public static Optional fetchSkin(@NotNull File skinFile) - { - if(!skinFile.exists()) - throw new IllegalArgumentException("File does not exist"); - - try(HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build()) - { - String boundary = "----MineSkinBoundary" + System.currentTimeMillis(); - String CRLF = "\r\n"; - - byte[] fileBytes = Files.readAllBytes(skinFile.toPath()); - String bodyBuilder = "--" + boundary + CRLF + - "Content-Disposition: form-data; name=\"file\"; filename=\"" + - skinFile.getName() + "\"" + CRLF + - "Content-Type: image/png" + CRLF + CRLF; - - String endPart = CRLF + "--" + boundary + "--" + CRLF; - - byte[] requestBody = combine(bodyBuilder.getBytes(), fileBytes, endPart.getBytes()); - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("https://api.mineskin.org/generate/upload")) - .timeout(Duration.ofSeconds(10)) - .header("Content-Type", "multipart/form-data; boundary=" + boundary) - .POST(HttpRequest.BodyPublishers.ofByteArray(requestBody)) - .build(); - - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - JsonObject obj = JsonParser.parseString(response.body()).getAsJsonObject(); - JsonObject texture = obj.getAsJsonObject("data").getAsJsonObject("texture"); - - String value = texture.get("value").getAsString(); - String signature = texture.get("signature").getAsString(); - - return Optional.of(new Skin(null, value, signature)); - } catch(IOException | InterruptedException e) - { - return Optional.empty(); - } - } - - /** - * Asynchronously fetches a player's skin using their UUID. - * This method wraps the synchronous {@link #fetchSkin(UUID)} call in a {@link CompletableFuture} - * to prevent blocking the main thread. - * - * @param uuid The UUID of the player whose skin is to be fetched. Must not be {@code null}. - * @return A {@link CompletableFuture} that will complete with the {@link Skin} object, or {@code null} if fetching fails. - */ - public static CompletableFuture fetchSkinAsync(@NotNull UUID uuid) - { - return CompletableFuture.supplyAsync(() -> fetchSkin(uuid)); - } - - /** - * Asynchronously fetches a player's skin using their username. - * This method wraps the synchronous {@link #fetchSkin(String)} call in a {@link CompletableFuture} - * to prevent blocking the main thread. - * - * @param name The username of the player whose skin is to be fetched. Must not be {@code null}. - * @return A {@link CompletableFuture} that will complete with the {@link Skin} object, or {@code null} if fetching fails. - */ - public static CompletableFuture fetchSkinAsync(@NotNull String name) - { - return CompletableFuture.supplyAsync(() -> fetchSkin(name)); - } - - /** - * Asynchronously fetches a skin from a local file. - * - * @param skinFile the PNG file containing the skin - * @return a CompletableFuture containing an Optional of the Skin - */ - public static CompletableFuture> fetchSkinAsync(@NotNull File skinFile) - { - return CompletableFuture.supplyAsync(() -> fetchSkin(skinFile)); - } - - private static byte[] combine(byte[]... arrays) throws IOException - { - int length = 0; - for(byte[] arr : arrays) - length += arr.length; - byte[] result = new byte[length]; - int pos = 0; - for(byte[] arr : arrays) - { - System.arraycopy(arr, 0, result, pos, arr.length); - pos += arr.length; - } - return result; - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/AStar.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/AStar.java deleted file mode 100644 index 4920fa6..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/AStar.java +++ /dev/null @@ -1,385 +0,0 @@ -package de.eisi05.npc.api.pathfinding; - -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.block.Block; -import org.jetbrains.annotations.NotNull; - -import java.util.*; - -/** - * Implementation of the A* pathfinding algorithm for Bukkit worlds. - * Calculates paths between two locations with optional diagonal movement. - */ -public class AStar -{ - private final int sx; - private final int sy; - private final int sz; - private final int ex; - private final int ey; - private final int ez; - - private final World w; - private final int maxIterations; - private final String endUID; - private final HashMap open = new HashMap<>(); - private final HashMap closed = new HashMap<>(); - private final Location start; - private final boolean allowDiagonalMovement; - private PathingResult result; - - /** - * Constructs an A* pathfinder between two locations. - * - * @param start the starting location - * @param end the target location - * @param maxIterations maximum number of iterations before aborting - * @param allowDiagonalMovement whether diagonal movement is allowed - * @throws InvalidPathException if start or end location is not walkable - */ - public AStar(Location start, Location end, int maxIterations, boolean allowDiagonalMovement) throws InvalidPathException - { - this.start = start; - this.allowDiagonalMovement = allowDiagonalMovement; - - boolean s, e = true; - if(!(s = isLocationWalkable(start)) || !(e = isLocationWalkable(end))) - throw new InvalidPathException(s, e); - this.w = start.getWorld(); - this.sx = start.getBlockX(); - this.sy = start.getBlockY(); - this.sz = start.getBlockZ(); - this.ex = end.getBlockX(); - this.ey = end.getBlockY(); - this.ez = end.getBlockZ(); - this.maxIterations = maxIterations; - short sh = 0; - Tile t = new Tile(sh, sh, sh, null); - t.calculateBoth(this.sx, this.sy, this.sz, this.ex, this.ey, this.ez, true); - this.open.put(t.getUID(), t); - processAdjacentTiles(t); - this.endUID = String.valueOf(this.ex - this.sx) + (this.ey - this.sy) + (this.ez - this.sz); - } - - /** - * Adds a tile to the open list if not already present. - * - * @param t the tile to add - */ - private void addToOpenList(Tile t) - { - if(!this.open.containsKey(t.getUID())) - this.open.put(t.getUID(), t); - } - - /** - * Adds a tile to the closed list if not already present. - * - * @param t the tile to add - */ - private void addToClosedList(Tile t) - { - if(!this.closed.containsKey(t.getUID())) - this.closed.put(t.getUID(), t); - } - - /** - * Returns the starting location of this pathfinding instance. - * - * @return the start location - */ - public Location getStart() - { - return start; - } - - /** - * Returns the end location of this pathfinding instance. - * - * @return the end location - */ - public Location getEndLocation() - { - return new Location(this.w, this.ex, this.ey, this.ez); - } - - /** - * Returns the result of the last pathfinding attempt. - * - * @return pathing result status - */ - public PathingResult getPathingResult() - { - return this.result; - } - - /** - * Runs the A* iteration until a path is found or terminated. - * - * @return list of tiles representing the path, or null if no path exists - */ - public ArrayList iterate() - { - Tile current = null; - int iterations = 0; - while(canContinue()) - { - iterations++; - if(iterations > this.maxIterations) - { - this.result = PathingResult.ITERATIONS_EXCEEDED; - break; - } - current = getLowestFTile(); - processAdjacentTiles(current); - } - - if(this.result != PathingResult.SUCCESS) - return null; - LinkedList routeTrace = new LinkedList<>(); - routeTrace.add(current); - Tile parent; - - while((parent = current.getParent()) != null) - { - routeTrace.add(parent); - current = parent; - } - - Collections.reverse(routeTrace); - return new ArrayList<>(routeTrace); - } - - /** - * Determines if the algorithm can continue. - * Stops if the open list is empty or the target has been reached. - * - * @return true if the search can continue, false otherwise - */ - private boolean canContinue() - { - if(this.open.isEmpty()) - { - this.result = PathingResult.NO_PATH; - return false; - } - if(this.closed.containsKey(this.endUID)) - { - this.result = PathingResult.SUCCESS; - return false; - } - return true; - } - - /** - * Finds and removes the tile with the lowest f-cost from the open list. - * - * @return tile with the lowest f-cost - */ - private Tile getLowestFTile() - { - double f = 0.0D; - Tile drop = null; - for(Tile t : this.open.values()) - { - if(f == 0.0D) - { - t.calculateBoth(this.sx, this.sy, this.sz, this.ex, this.ey, this.ez, true); - f = t.getF(); - drop = t; - continue; - } - t.calculateBoth(this.sx, this.sy, this.sz, this.ex, this.ey, this.ez, true); - double posF = t.getF(); - if(posF < f) - { - f = posF; - drop = t; - } - } - this.open.remove(drop.getUID()); - addToClosedList(drop); - return drop; - } - - /** - * Checks whether the given tile is already on the closed list. - * - * @param t the tile to check - * @return true if the tile is on the closed list - */ - private boolean isOnClosedList(Tile t) - { - return this.closed.containsKey(t.getUID()); - } - - /** - * Processes the adjacent tiles around a given tile, adding valid ones to the open list. - * - * @param current the current tile to expand from - */ - private void processAdjacentTiles(Tile current) - { - HashSet possible = new HashSet<>(allowDiagonalMovement ? 26 : 14); - - if(allowDiagonalMovement) - for(byte x = -1; x <= 1; x = (byte) (x + 1)) - { - for(byte y = -1; y <= 1; y = (byte) (y + 1)) - { - for(byte z = -1; z <= 1; z = (byte) (z + 1)) - { - if(x != 0 || y != 0 || z != 0) - { - Tile t = new Tile((short) (current.getX() + x), (short) (current.getY() + y), (short) (current.getZ() + z), current); - if(!isOnClosedList(t) && isTileWalkable(t)) - { - t.calculateBoth(this.sx, this.sy, this.sz, this.ex, this.ey, this.ez, true); - possible.add(t); - } - } - } - } - } - else - { - int[][] directions = { - {1, 0, 0}, {-1, 0, 0}, - {0, 1, 0}, {0, -1, 0}, - {0, 0, 1}, {0, 0, -1}, - {0, -1, 1}, {0, -1, -1}, - {0, 1, 1}, {0, 1, -1}, - {1, -1, 0}, {-1, -1, 0}, - {1, 1, 0}, {-1, 1, 0}, - }; - - for(int[] d : directions) - { - Tile t = new Tile( - (short) (current.getX() + d[0]), - (short) (current.getY() + d[1]), - (short) (current.getZ() + d[2]), - current - ); - if(!isOnClosedList(t) && isTileWalkable(t)) - { - t.calculateBoth(this.sx, this.sy, this.sz, this.ex, this.ey, this.ez, true); - possible.add(t); - } - } - } - - for(Tile t : possible) - { - Tile openRef; - if((openRef = isOnOpenList(t)) == null) - { - addToOpenList(t); - continue; - } - if(t.getG() < openRef.getG()) - { - openRef.setParent(current); - openRef.calculateBoth(this.sx, this.sy, this.sz, this.ex, this.ey, this.ez, true); - } - } - } - - /** - * Returns a tile if it is already on the open list. - * - * @param t the tile to check - * @return the reference from the open list, or null if not present - */ - private Tile isOnOpenList(Tile t) - { - return this.open.getOrDefault(t.getUID(), null); - } - - /** - * Determines if the given tile is walkable (i.e., solid ground with space above). - * - * @param t the tile to check - * @return true if walkable, false otherwise - */ - private boolean isTileWalkable(Tile t) - { - Location l = new Location(this.w, (this.sx + t.getX()), (this.sy + t.getY()), (this.sz + t.getZ())); - Block b = l.getBlock(); - return !b.isLiquid() && b.getType().isSolid() && (b.getRelative(0, 1, 0).isPassable() && b.getRelative(0, 2, 0).isPassable()); - } - - /** - * Checks if the given location is a valid starting or ending point. - * - * @param l the location to check - * @return true if the location is walkable, false otherwise - */ - private boolean isLocationWalkable(Location l) - { - Block b = l.getBlock(); - - if(!b.isLiquid() && b.getType().isSolid()) - return ((b.getRelative(0, 1, 0).getType().isAir() || b.getRelative(0, 1, 0).isPassable()) && - (b.getRelative(0, 2, 0).getType().isAir() || b.getRelative(0, 2, 0).isPassable())); - return false; - } - - /** - * Exception thrown when path initialization fails due to invalid start or end. - */ - public static class InvalidPathException extends Exception - { - private final boolean s; - private final boolean e; - - /** - * Creates an exception describing invalid start or end tiles. - * - * @param s whether the start is valid - * @param e whether the end is valid - */ - public InvalidPathException(boolean s, boolean e) - { - super(getErrorReason(s, e)); - this.s = s; - this.e = e; - } - - /** - * Returns a textual explanation of why the path is invalid. - * - * @return error reason string - */ - private static @NotNull String getErrorReason(boolean s, boolean e) - { - StringBuilder sb = new StringBuilder(); - if(!s) - sb.append("Start Location was air."); - if(!e) - sb.append("End Location was air."); - return sb.toString(); - } - - /** - * Checks if the start location was invalid. - * - * @return true if the start location is not solid - */ - public boolean isStartNotSolid() - { - return !this.s; - } - - /** - * Checks if the end location was invalid. - * - * @return true if the end location is not solid - */ - public boolean isEndNotSolid() - { - return !this.e; - } - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/Path.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/Path.java deleted file mode 100644 index 3c878b4..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/Path.java +++ /dev/null @@ -1,196 +0,0 @@ -package de.eisi05.npc.api.pathfinding; - -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.configuration.serialization.ConfigurationSerializable; -import org.bukkit.util.Vector; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.Serial; -import java.io.Serializable; -import java.util.*; - - -/** - * Represents a path in the world, providing both {@link Location} and {@link Vector} representations - * of the path's waypoints. The lists returned are unmodifiable to ensure immutability. - */ -public class Path implements ConfigurationSerializable -{ - private final List vectors; - private final List locations; - private final List waypoints; - - private String name; - - /** - * Constructs a Path from a list of Bukkit {@link Location} objects. - * Converts each location to a {@link Vector} for easy mathematical manipulation. - * - * @param nodes the ordered list of {@link Location} waypoints - */ - public Path(@NotNull List nodes, @Nullable List waypoints) - { - this.locations = Collections.unmodifiableList(nodes); - this.vectors = nodes.stream().map(Location::toVector).toList(); - this.waypoints = waypoints == null ? null : Collections.unmodifiableList(waypoints); - } - - /** - * Constructs a Path from a list of {@link Vector} waypoints in the given world. - * Converts each vector to a {@link Location} for compatibility with Bukkit APIs. - * - * @param nodes the ordered list of {@link Vector} waypoints - * @param world the Bukkit {@link World} where the locations reside - */ - public Path(@NotNull List nodes, @NotNull World world, @Nullable List waypoints) - { - this.vectors = Collections.unmodifiableList(nodes); - this.locations = nodes.stream().map(vector -> new Location(world, vector.getX(), vector.getY(), vector.getZ())).toList(); - this.waypoints = waypoints == null ? null : Collections.unmodifiableList(waypoints); - } - - public @NotNull Path setName(@Nullable String name) - { - this.name = name; - return this; - } - - public @Nullable String getName() - { - return name; - } - - @SuppressWarnings("unchecked") - public static Path deserialize(Map map) - { - return new Path((List) map.get("locations"), (List) map.get("waypoints")); - } - - /** - * Returns the path as an unmodifiable list of {@link Location} objects. - * - * @return an unmodifiable list of Bukkit locations representing the path - */ - public List asLocations() - { - return locations; - } - - /** - * Returns the path as an unmodifiable list of {@link Vector} objects. - * - * @return an unmodifiable list of vectors representing the path - */ - public List asVectors() - { - return vectors; - } - - @Override - public String toString() - { - if(locations.isEmpty()) - return "Empty path"; - - Vector start = vectors.getFirst(); - Vector end = vectors.getLast(); - - return String.format("Start: [%.2f, %.2f, %.2f] -> End: [%.2f, %.2f, %.2f]", - start.getX(), start.getY(), start.getZ(), - end.getX(), end.getY(), end.getZ()); - } - - /** - * Returns th waypoints with which the path was calculated. - * - * @return an unmodifiable list of {@link Location} objects - */ - public @NotNull List getWaypoints() - { - return waypoints == null ? new ArrayList<>() : waypoints; - } - - @Override - public boolean equals(Object obj) - { - if(this == obj) - return true; - - if(!(obj instanceof Path other)) - return false; - - return locations.equals(other.locations); - } - - @Override - public int hashCode() - { - return locations.hashCode(); - } - - @Override - public @NotNull Map serialize() - { - return Map.of("locations", new ArrayList<>(locations), "waypoints", new ArrayList<>(waypoints)); - } - - public SerializablePath toSerializablePath() - { - return new SerializablePath(this); - } - - public static class SerializablePath implements Serializable - { - @Serial - private static final long serialVersionUID = 1L; - - private final List locations; - private final List waypoints; - - private final String name; - - private SerializablePath(@NotNull Path path) - { - locations = new ArrayList<>(path.locations.stream().map(SerializableLocation::new).toList()); - waypoints = path.waypoints == null ? null : new ArrayList<>(path.waypoints.stream().map(SerializableLocation::new).toList()); - name = path.getName(); - } - - public @NotNull Path toPath() - { - return new Path(locations.stream().map(SerializableLocation::toLocation).toList(), - waypoints == null ? null : waypoints.stream().map(SerializableLocation::toLocation).toList()).setName(name); - } - - private static class SerializableLocation implements Serializable - { - @Serial - private static final long serialVersionUID = 1L; - - private final double x; - private final double y; - private final double z; - private final float pitch; - private final float yaw; - private final UUID world; - - public SerializableLocation(@NotNull Location location) - { - this.x = location.getX(); - this.y = location.getY(); - this.z = location.getZ(); - this.pitch = location.getPitch(); - this.yaw = location.getYaw(); - this.world = location.getWorld().getUID(); - } - - public @NotNull Location toLocation() - { - return new Location(Bukkit.getWorld(world), x, y, z, yaw, pitch); - } - } - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/PathfindingUtils.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/PathfindingUtils.java deleted file mode 100644 index 0d54976..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/PathfindingUtils.java +++ /dev/null @@ -1,97 +0,0 @@ -package de.eisi05.npc.api.pathfinding; - -import org.bukkit.Location; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.function.BiConsumer; - -/** - * Utility class for calculating paths between locations using A* pathfinding. - * Provides synchronous and asynchronous methods. - */ -public class PathfindingUtils -{ - /** - * Asynchronously calculates a path through a list of waypoints. - *

- * Each segment between consecutive waypoints is calculated in parallel using {@link CompletableFuture}. - * The returned future completes with a {@link Path} containing the full path, or completes exceptionally - * if an {@link AStar.InvalidPathException} occurs. - * - * @param waypoints the ordered list of locations to traverse - * @param maxIterations the maximum number of iterations the A* algorithm will attempt per segment - * @param allowDiagonalMovement whether diagonal movement is allowed - * @param progressListener a progress listener with the signature (segmentIndex, totalSegments) - * @return a {@link CompletableFuture} that completes with the calculated {@link Path} - */ - public static @NotNull CompletableFuture findPathAsync(@NotNull List waypoints, int maxIterations, boolean allowDiagonalMovement, - @Nullable BiConsumer progressListener) - { - return CompletableFuture.supplyAsync(() -> - { - try - { - return findPath(waypoints, maxIterations, allowDiagonalMovement, progressListener); - } catch(AStar.InvalidPathException e) - { - throw new RuntimeException(e); - } - }); - } - - /** - * Synchronously calculates a path through a list of waypoints. - *

- * Each segment between consecutive waypoints is calculated in parallel internally using {@link CompletableFuture}, - * but this method blocks until all segments are calculated and combined into a single {@link Path}. - * - * @param waypoints the ordered list of locations to traverse - * @param maxIterations the maximum number of iterations the A* algorithm will attempt per segment - * @param allowDiagonalMovement whether diagonal movement is allowed - * @param progressListener a progress listener with the signature (segmentIndex, totalSegments) - * @return the calculated {@link Path} containing all intermediate locations - * @throws AStar.InvalidPathException if any segment's start or end location is invalid/unwalkable - */ - public static @NotNull Path findPath(@NotNull List waypoints, int maxIterations, boolean allowDiagonalMovement, - @Nullable BiConsumer progressListener) throws AStar.InvalidPathException - { - List>> futures = new ArrayList<>(); - - for(int i = 0; i < waypoints.size() - 1; i++) - { - final int idx = i; - futures.add(CompletableFuture.supplyAsync(() -> - { - try - { - AStar aStar = new AStar(waypoints.get(idx), waypoints.get(idx + 1), maxIterations, allowDiagonalMovement); - var segment = aStar.iterate().stream() - .map(tile -> tile.getLocation(aStar.getStart()).add(0.5, 1, 0.5)) - .toList(); - - if(progressListener != null) - progressListener.accept(idx + 1, waypoints.size()); - - return segment; - } catch(AStar.InvalidPathException e) - { - throw new RuntimeException(e); - } - })); - } - - Path path = new Path(futures.stream() - .map(CompletableFuture::join) - .flatMap(List::stream) - .toList(), waypoints); - - if(progressListener != null) - progressListener.accept(waypoints.size(), waypoints.size()); - - return path; - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/PathingResult.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/PathingResult.java deleted file mode 100644 index c1c0815..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/PathingResult.java +++ /dev/null @@ -1,44 +0,0 @@ -package de.eisi05.npc.api.pathfinding; - -/** - * Represents the result of an A* pathfinding attempt. - */ -public enum PathingResult -{ - /** - * Pathfinding succeeded and a path was found. - */ - SUCCESS(0), - - /** - * No valid path could be found. - */ - NO_PATH(-1), - - /** - * Pathfinding stopped because the maximum iteration limit was exceeded. - */ - ITERATIONS_EXCEEDED(-2); - - private final int ec; - - /** - * Creates a new pathing result with the given end code. - * - * @param ec numerical code representing the result - */ - PathingResult(int ec) - { - this.ec = ec; - } - - /** - * Returns the numerical code associated with this pathing result. - * - * @return the end code - */ - public int getEndCode() - { - return this.ec; - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/Tile.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/Tile.java deleted file mode 100644 index c8f8d3a..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/Tile.java +++ /dev/null @@ -1,284 +0,0 @@ -package de.eisi05.npc.api.pathfinding; - -import org.bukkit.Location; - -/** - * Represents a tile (node) used in the A* pathfinding algorithm. - * Each tile stores its relative coordinates, costs, and parent reference. - */ -public class Tile -{ - private final short x; - private final short y; - private final short z; - - private final String uid; - private double g = -1.0D; - private double h = -1.0D; - private Tile parent; - - /** - * Creates a new tile with given relative coordinates and parent. - * - * @param x relative x-coordinate - * @param y relative y-coordinate - * @param z relative z-coordinate - * @param parent the parent tile leading to this tile - */ - public Tile(short x, short y, short z, Tile parent) - { - this.x = x; - this.y = y; - this.z = z; - this.parent = parent; - this.uid = String.valueOf(x) + y + z; - } - - /** - * Converts this tile to an absolute {@link Location} using a start reference. - * - * @param start the starting location - * @return absolute location of this tile - */ - public Location getLocation(Location start) - { - return new Location(start.getWorld(), (start.getBlockX() + this.x), (start.getBlockY() + this.y), (start.getBlockZ() + this.z)); - } - - /** - * Returns the parent tile of this tile. - * - * @return the parent tile, or null if none - */ - public Tile getParent() - { - return this.parent; - } - - /** - * Updates the parent tile reference. - * - * @param parent the new parent tile - */ - public void setParent(Tile parent) - { - this.parent = parent; - } - - /** - * Returns the relative x-coordinate. - * - * @return relative x - */ - public short getX() - { - return this.x; - } - - /** - * Returns the absolute x-coordinate relative to a base location. - * - * @param i base location - * @return absolute x - */ - public int getX(Location i) - { - return i.getBlockX() + this.x; - } - - /** - * Returns the relative y-coordinate. - * - * @return relative y - */ - public short getY() - { - return this.y; - } - - /** - * Returns the absolute y-coordinate relative to a base location. - * - * @param i base location - * @return absolute y - */ - public int getY(Location i) - { - return i.getBlockY() + this.y; - } - - /** - * Returns the relative z-coordinate. - * - * @return relative z - */ - public short getZ() - { - return this.z; - } - - /** - * Returns the absolute z-coordinate relative to a base location. - * - * @param i base location - * @return absolute z - */ - public int getZ(Location i) - { - return i.getBlockZ() + this.z; - } - - /** - * Returns the unique identifier string for this tile. - * - * @return tile UID - */ - public String getUID() - { - return this.uid; - } - - /** - * Checks whether this tile is equal to another based on coordinates. - * - * @param t the other tile - * @return true if coordinates match - */ - public boolean equals(Tile t) - { - return (t.getX() == this.x && t.getY() == this.y && t.getZ() == this.z); - } - - /** - * Calculates both G and H values for this tile. - * - * @param sx start x - * @param sy start y - * @param sz start z - * @param ex end x - * @param ey end y - * @param ez end z - * @param update whether to force recalculation - */ - public void calculateBoth(int sx, int sy, int sz, int ex, int ey, int ez, boolean update) - { - calculateG(sx, sy, sz, update); - calculateH(sx, sy, sz, ex, ey, ez, update); - } - - /** - * Calculates the heuristic (H) cost using Euclidean distance. - * - * @param sx start x - * @param sy start y - * @param sz start z - * @param ex end x - * @param ey end y - * @param ez end z - * @param update whether to force recalculation - */ - public void calculateH(int sx, int sy, int sz, int ex, int ey, int ez, boolean update) - { - if(update || this.h == -1.0D) - { - int hx = sx + this.x, hy = sy + this.y, hz = sz + this.z; - this.h = getEuclideanDistance(hx, hy, hz, ex, ey, ez); - } - } - - /** - * Calculates the movement cost (G) from the start to this tile. - * - * @param sx start x - * @param sy start y - * @param sz start z - * @param update whether to force recalculation - */ - public void calculateG(int sx, int sy, int sz, boolean update) - { - if(update || this.g == -1.0D) - { - Tile currentParent, currentTile = this; - int gCost = 0; - while((currentParent = currentTile.getParent()) != null) - { - int dx = currentTile.getX() - currentParent.getX(), - dy = currentTile.getY() - currentParent.getY(), - dz = currentTile.getZ() - currentParent.getZ(); - - dx = abs(dx); - dy = abs(dy); - dz = abs(dz); - - if(dx == 1 && dy == 1 && dz == 1) - gCost = (int) (gCost + 1.7D); - - else if(((dx == 1 || dz == 1) && dy == 1) || ((dx == 1 || dz == 1) && dy == 0)) - gCost = (int) (gCost + 1.4D); - else - gCost = (int) (gCost + 1.0D); - - currentTile = currentParent; - } - this.g = gCost; - } - } - - /** - * Returns the G cost (movement cost from start). - * - * @return g cost - */ - public double getG() - { - return this.g; - } - - /** - * Returns the H cost (heuristic estimate to end). - * - * @return h cost - */ - public double getH() - { - return this.h; - } - - /** - * Returns the total F cost (G + H). - * - * @return f cost - */ - public double getF() - { - return this.h + this.g; - } - - /** - * Computes Euclidean distance between two points. - * - * @param sx start x - * @param sy start y - * @param sz start z - * @param ex end x - * @param ey end y - * @param ez end z - * @return Euclidean distance - */ - private double getEuclideanDistance(int sx, int sy, int sz, int ex, int ey, int ez) - { - double dx = (sx - ex), dy = (sy - ey), dz = (sz - ez); - return Math.sqrt(dx * dx + dy * dy + dz * dz); - } - - /** - * Returns the absolute value of the given integer. - * - * @param i input value - * @return absolute value - */ - private int abs(int i) - { - return (i < 0) ? -i : i; - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/scheduler/Tasks.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/scheduler/Tasks.java deleted file mode 100644 index 64ae065..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/scheduler/Tasks.java +++ /dev/null @@ -1,66 +0,0 @@ -package de.eisi05.npc.api.scheduler; - -import de.eisi05.npc.api.NpcApi; -import de.eisi05.npc.api.manager.NpcManager; -import de.eisi05.npc.api.objects.NpcOption; -import net.minecraft.server.level.ServerPlayer; -import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitRunnable; -import org.bukkit.scheduler.BukkitTask; - -/** - * The {@link Tasks} class manages and starts various recurring tasks - * related to Non-Player Characters (NPCs) within the Bukkit environment. - * These tasks often involve NPC behavior such as looking at nearby players. - */ -public class Tasks -{ - private static BukkitTask task; - - /** - * Starts all defined NPC-related tasks. - * This method should be called when the plugin is enabled to ensure - * that NPC behaviors are active. - */ - public static void start() - { - lookAtTask(); - } - - /** - * Stops all defined NPC-related tasks. - */ - public static void stop() - { - if(task != null && !task.isCancelled()) - task.cancel(); - } - - /** - * Implements a recurring task that makes NPCs look at nearby players. - * The task runs on a timer defined by {@code NpcApi.config.getLookAtTimer()}. - * NPCs will only look at players within a specified range, which is - * configured via {@link NpcOption#LOOK_AT_PLAYER}. - */ - private static void lookAtTask() - { - task = new BukkitRunnable() - { - @Override - public void run() - { - NpcManager.getList().forEach(npc -> - { - double range = npc.getOption(NpcOption.LOOK_AT_PLAYER); - - if(range <= 0) - return; - - ((ServerPlayer) npc.getServerPlayer()).getBukkitEntity().getNearbyEntities(range, range, range) - .stream().filter(entity -> entity instanceof Player) - .forEach(entity -> npc.lookAtPlayer((Player) entity)); - }); - } - }.runTaskTimer(NpcApi.plugin, 0, NpcApi.config.lookAtTimer()); - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/ItemSerializer.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/ItemSerializer.java deleted file mode 100644 index 614a8a9..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/ItemSerializer.java +++ /dev/null @@ -1,206 +0,0 @@ -package de.eisi05.npc.api.utils; - -import org.bukkit.Bukkit; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.PlayerInventory; -import org.bukkit.util.io.BukkitObjectInputStream; -import org.bukkit.util.io.BukkitObjectOutputStream; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; - -/** - * The {@link ItemSerializer} class provides utility methods for serializing and deserializing - * Bukkit {@link ItemStack} arrays, {@link PlayerInventory} contents, and generic {@link Inventory} - * contents to and from Base64 encoded strings. This is useful for storing inventory data - * in configurations or databases. - */ -public class ItemSerializer -{ - /** - * Serializes the contents of a {@link PlayerInventory} into a Base64 encoded string array. - * The array will contain two elements: the first for the main inventory storage contents, - * and the second for the armor contents plus the off-hand item. - * - * @param playerInventory The {@link PlayerInventory} to serialize. Must not be {@code null}. - * @return A {@code String[]} array containing two Base64 encoded strings: - * index 0 is the main inventory, index 1 is armor and off-hand. - * @throws IllegalStateException If an error occurs during serialization (e.g., I/O error). - */ - public static @NotNull String[] playerInventoryToBase64(@NotNull PlayerInventory playerInventory) throws IllegalStateException - { - ItemStack[] storage = playerInventory.getStorageContents(); - if(storage == null) - throw new IllegalStateException("Storage contents of player inventory is null"); - - String content = itemStackArrayToBase64(storage); - ItemStack[] items = new ItemStack[playerInventory.getArmorContents().length + 1]; - - for(int i = 0; i < playerInventory.getArmorContents().length; ++i) - items[i] = playerInventory.getArmorContents()[i]; - - items[items.length - 1] = playerInventory.getItemInOffHand(); - String armor = itemStackArrayToBase64(items); - return new String[]{content, armor}; - } - - /** - * Serializes a single {@link ItemStack} into a Base64 encoded string. - * This is a convenience method that wraps the item in an array and calls {@link #itemStackArrayToBase64(ItemStack[])}. - * - * @param item The {@link ItemStack} to serialize. Must not be {@code null}. - * @return A Base64 encoded {@link String} representing the item. - * Returns {@code null} if an error occurs during serialization. - */ - public static @NotNull String itemStackToBase64(@NotNull ItemStack item) - { - return itemStackArrayToBase64(new ItemStack[]{item}); - } - - /** - * Deserializes a single {@link ItemStack} from a Base64 encoded string. - * This method expects the Base64 string to represent a single item. - * - * @param data The Base64 encoded {@link String} representing the item. Must not be {@code null}. - * @return The deserialized {@link ItemStack}, or {@code null} if an error occurs during deserialization - * or if the input data does not represent a valid item. - */ - public static @Nullable ItemStack itemStackFromBase64(@NotNull String data) - { - try - { - return itemStackArrayFromBase64(data)[0]; - } catch(Exception e) - { - return null; - } - } - - /** - * Serializes an array of {@link ItemStack} objects into a Base64 encoded string. - * This method uses Bukkit's object serialization for {@link ItemStack}s. - * - * @param items An array of {@link ItemStack} objects to serialize. Must not be {@code null}. - * @return A Base64 encoded {@link String} representing the array of items. - * Returns {@code null} if an error occurs during serialization. - */ - public static @Nullable String itemStackArrayToBase64(@NotNull ItemStack[] items) - { - try - { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - ObjectOutputStream dataOutput = new ObjectOutputStream(outputStream); - dataOutput.writeInt(items.length); - - for(ItemStack item : items) - dataOutput.writeObject(item); - - dataOutput.close(); - return Base64Coder.encodeLines(outputStream.toByteArray()); - } catch(Exception e) - { - return null; - } - } - - /** - * Serializes the contents of a generic {@link Inventory} into a Base64 encoded string. - * This method is suitable for any type of inventory (e.g., chests, custom inventories). - * - * @param inventory The {@link Inventory} to serialize. Must not be {@code null}. - * @return A Base64 encoded {@link String} representing the inventory contents. - * Returns an empty string if an error occurs during serialization. - */ - public static @NotNull String toBase64(@NotNull Inventory inventory) - { - try - { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - ObjectOutputStream dataOutput = new ObjectOutputStream(outputStream); - dataOutput.writeInt(inventory.getSize()); - - for(ItemStack item : inventory.getContents()) - dataOutput.writeObject(item); - - dataOutput.close(); - return Base64Coder.encodeLines(outputStream.toByteArray()); - } catch(Exception e) - { - return ""; - } - } - - /** - * Deserializes two Base64 encoded strings back into two {@link ItemStack} arrays. - * This is typically used for deserializing player inventory content and armor/off-hand. - * - * @param s A {@code String[]} array containing two Base64 encoded strings, - * where {@code s[0]} is the main inventory and {@code s[1]} is armor/off-hand. Must not be {@code null}. - * @return A two-dimensional {@code ItemStack[][]} array, where the first array - * contains the deserialized main inventory items and the second array - * contains the deserialized armor and off-hand items. - */ - public static @NotNull ItemStack[][] doubleInventoryFromBase64(@NotNull String[] s) - { - return new ItemStack[][]{itemStackArrayFromBase64(s[0]), itemStackArrayFromBase64(s[1])}; - } - - /** - * Deserializes a Base64 encoded string back into a Bukkit {@link Inventory} object. - * The inventory's size and contents are restored from the provided data. - * - * @param data The Base64 encoded {@link String} representing the inventory. Must not be {@code null}. - * @return The deserialized {@link Inventory} object, or {@code null} if an error occurs - * during deserialization or if the input data is invalid. - */ - public static @Nullable Inventory fromBase64(@NotNull String data) - { - try - { - ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64Coder.decodeLines(data)); - ObjectInputStream dataInput = new ObjectInputStream(inputStream); - Inventory inventory = Bukkit.getServer().createInventory(null, dataInput.readInt()); - - for(int i = 0; i < inventory.getSize(); ++i) - inventory.setItem(i, (ItemStack) dataInput.readObject()); - - dataInput.close(); - return inventory; - } catch(Exception e) - { - return null; - } - } - - /** - * Deserializes a Base64 encoded string back into an array of {@link ItemStack} objects. - * - * @param data The Base64 encoded {@link String} representing the array of items. Must not be {@code null}. - * @return An array of deserialized {@link ItemStack} objects. - * Returns an empty {@code ItemStack[]} array if an error occurs during deserialization. - */ - public static @NotNull ItemStack[] itemStackArrayFromBase64(@NotNull String data) - { - try - { - ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64Coder.decodeLines(data)); - ObjectInputStream dataInput = new ObjectInputStream(inputStream); - ItemStack[] items = new ItemStack[dataInput.readInt()]; - - for(int i = 0; i < items.length; ++i) - items[i] = (ItemStack) dataInput.readObject(); - - dataInput.close(); - return items; - } catch(Exception e) - { - return new ItemStack[0]; - } - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/ObjectSaver.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/ObjectSaver.java deleted file mode 100644 index 4f5d94e..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/ObjectSaver.java +++ /dev/null @@ -1,164 +0,0 @@ -package de.eisi05.npc.api.utils; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.*; -import java.util.ArrayList; -import java.util.List; - -/** - * The {@link ObjectSaver} class provides utility methods for serializing and deserializing - * Java objects and lists of objects to and from a file. It uses standard Java - * object serialization. - */ -public class ObjectSaver -{ - private final File file; - - /** - * Constructs a new {@code ObjectSaver} instance, creating a {@link File} object - * from the given file path string. It ensures the parent directories exist - * and creates the file if it doesn't already exist. - * - * @param file The path to the file where objects will be saved/loaded. Must not be {@code null}. - * @throws RuntimeException If an {@link IOException} occurs while creating the file. - */ - public ObjectSaver(@NotNull String file) - { - this(new File(file)); - } - - /** - * Constructs a new {@code ObjectSaver} instance with the specified {@link File} object. - * It ensures the parent directories of the file exist and creates the file itself - * if it does not already exist. - * - * @param file The {@link File} object where objects will be saved/loaded. Must not be {@code null}. - * @throws RuntimeException If an {@link IOException} occurs while creating the file. - */ - public ObjectSaver(@NotNull File file) - { - file.getParentFile().mkdirs(); - this.file = file; - if(!file.exists()) - { - try - { - file.createNewFile(); - } catch(IOException e) - { - throw new RuntimeException(e); - } - } - } - - /** - * Writes a single serializable object to the file, appending to the file if it already exists. - * This is a convenience method that calls {@link #write(Serializable, boolean)} with {@code append} set to {@code true}. - * - * @param object The object to write to the file. Must implement {@link Serializable}. - * @param The type of the object, which must extend {@link Serializable}. - * @throws IOException If an I/O error occurs during writing. - */ - public void write(T object) throws IOException - { - this.write(object, true); - } - - /** - * Writes a single serializable object to the file. - * - * @param object The object to write to the file. Must implement {@link Serializable}. - * @param append If {@code true}, the object will be appended to the end of the file. - * If {@code false}, the file will be truncated (its contents deleted) - * before writing the new object. - * @param The type of the object, which must extend {@link Serializable}. - * @throws IOException If an I/O error occurs during writing. - */ - public void write(T object, boolean append) throws IOException - { - FileOutputStream fileOut = new FileOutputStream(this.file, append); - ObjectOutputStream objectOut = new ObjectOutputStream(fileOut); - objectOut.writeObject(object); - objectOut.close(); - } - - /** - * Writes a list of serializable objects to the file, appending to the file if it already exists. - * This is a convenience method that calls {@link #writeList(List, boolean)} with {@code append} set to {@code true}. - * - * @param object The list of objects to write to the file. Each object in the list must implement {@link Serializable}. Must not be {@code null}. - * @param The type of the objects in the list, which must extend {@link Serializable}. - * @throws IOException If an I/O error occurs during writing. - */ - public void writeList(@NotNull List object) throws IOException - { - this.writeList(object, true); - } - - /** - * Writes a list of serializable objects to the file. - * - * @param object The list of objects to write to the file. Each object in the list must implement {@link Serializable}. Must not be {@code null}. - * @param append If {@code true}, the list will be appended to the end of the file. - * If {@code false}, the file will be truncated (its contents deleted) - * before writing the new list. - * @param The type of the objects in the list, which must extend {@link Serializable}. - * @throws IOException If an I/O error occurs during writing. - */ - public void writeList(@NotNull List object, boolean append) throws IOException - { - FileOutputStream fileOut = new FileOutputStream(this.file, append); - ObjectOutputStream objectOut = new ObjectOutputStream(fileOut); - objectOut.writeObject(object); - objectOut.close(); - } - - /** - * Reads a single serializable object from the file. - * - * @param The expected type of the object, which must extend {@link Serializable}. - * @return The deserialized object, or {@code null} if an error occurs during reading - * (e.g., file not found, EOF, class not found, or I/O error). - */ - @SuppressWarnings("unchecked") - public @Nullable T read() - { - try - { - FileInputStream fileIn = new FileInputStream(this.file); - ObjectInputStream objectOut = new ObjectInputStream(fileIn); - Object object = objectOut.readObject(); - objectOut.close(); - return (T) object; - } catch(IOException | ClassNotFoundException e) - { - throw new RuntimeException(e); - } - } - - /** - * Reads a list of serializable objects from the file. - * - * @param The expected type of the objects in the list, which must extend {@link Serializable}. - * @return The deserialized list of objects. Returns an empty {@link ArrayList} if an error occurs - * during reading (e.g., file not found, EOF, class not found, or I/O error), or if the file is empty. - */ - @SuppressWarnings("unchecked") - public @NotNull List readList() - { - try - { - FileInputStream fileIn = new FileInputStream(this.file); - ObjectInputStream objectOut = new ObjectInputStream(fileIn); - Object object = objectOut.readObject(); - objectOut.close(); - return (List) object; - } catch(Exception var4) - { - return new ArrayList(); - } - } -} - diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/PacketReader.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/PacketReader.java deleted file mode 100644 index 12163b3..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/PacketReader.java +++ /dev/null @@ -1,160 +0,0 @@ -package de.eisi05.npc.api.utils; - -import de.eisi05.npc.api.NpcApi; -import de.eisi05.npc.api.enums.ClickActionType; -import de.eisi05.npc.api.events.NpcInteractEvent; -import de.eisi05.npc.api.manager.NpcManager; -import de.eisi05.npc.api.objects.NPC; -import io.netty.channel.Channel; -import io.netty.channel.ChannelDuplexHandler; -import io.netty.channel.ChannelHandlerContext; -import net.minecraft.network.protocol.Packet; -import net.minecraft.network.protocol.game.ServerboundInteractPacket; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.InteractionHand; -import org.bukkit.Bukkit; -import org.bukkit.craftbukkit.entity.CraftPlayer; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -import java.util.*; -import java.util.function.BiConsumer; - -/** - * The {@link PacketReader} class is responsible for injecting a custom Netty - * channel handler into a player's network pipeline to intercept incoming packets. - * It specifically listens for packets related to entity interaction (e.g., attacking or interacting with NPCs) - * and dispatches custom events based on these interactions. It also allows for - * custom packet readers to be added. - */ -public class PacketReader -{ - private static final Map channels = new HashMap<>(); - private static final List> readers = new ArrayList<>(); - - /** - * Adds a custom packet reader to the list of readers. - * This reader will be called for every incoming packet processed by the injected handler. - * - * @param reader The {@link BiConsumer} to add. It accepts the {@link Player} - * and the raw packet {@link Object}. Must not be {@code null}. - */ - public static void addReader(@NotNull BiConsumer reader) - { - readers.add(reader); - } - - /** - * Injects a custom {@link ChannelDuplexHandler} into the specified player's Netty pipeline. - * This handler intercepts incoming packets to check for NPC interactions. - * The handler is named after the plugin's name to avoid conflicts and ensure proper removal. - * - * @param player The {@link Player} whose pipeline is to be injected. Must not be {@code null}. - */ - public static void inject(@NotNull Player player) - { - Channel channel = ((CraftPlayer) player).getHandle().connection.connection.channel; - channels.put(player.getUniqueId(), channel); - - if(channel.pipeline().get(NpcApi.plugin.getName()) != null) - return; - - ChannelDuplexHandler duplexHandler = new ChannelDuplexHandler() - { - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception - { - checkForPacket(msg, player); - - readers.forEach(consumer -> consumer.accept(player, msg)); - - super.channelRead(ctx, msg); - } - }; - - if(channel.pipeline().get("packet_handler") == null) - channel.pipeline().addLast(NpcApi.plugin.getName(), duplexHandler); - else - channel.pipeline().addBefore("packet_handler", NpcApi.plugin.getName(), duplexHandler); - } - - /** - * Checks if the given packet is a {@link ServerboundInteractPacket} and, if so, - * processes the interaction to dispatch a {@link NpcInteractEvent}. - * This method is responsible for determining if a player has clicked or attacked an NPC. - * - * @param packet The raw packet object received from the Netty pipeline. Must not be {@code null}. - * @param player The {@link Player} who sent the packet. Must not be {@code null}. - */ - private static void checkForPacket(@NotNull Object packet, @NotNull Player player) - { - if(!(packet instanceof Packet)) - return; - - if(!(packet instanceof ServerboundInteractPacket interactPacket)) - return; - - int id = interactPacket.getEntityId(); - - NPC npc = NpcManager.getList().stream().filter(npc1 -> ((ServerPlayer) npc1.getServerPlayer()).getId() == id).findFirst().orElse(null); - - if(npc == null) - return; - - if(interactPacket.isAttack()) - { - Bukkit.getScheduler().scheduleSyncDelayedTask(NpcApi.plugin, - () -> Bukkit.getPluginManager().callEvent(new NpcInteractEvent(player, npc, ClickActionType.LEFT)), 0); - - return; - } - - var action = Reflections.getField(interactPacket, "action"); - - if(action.get().getClass().getDeclaredFields().length == 2) - return; - - InteractionHand hand = (InteractionHand) action.thanGetField("hand").get(); - - if(hand == InteractionHand.MAIN_HAND) - Bukkit.getScheduler().scheduleSyncDelayedTask(NpcApi.plugin, - () -> Bukkit.getPluginManager().callEvent(new NpcInteractEvent(player, npc, ClickActionType.RIGHT)), 0); - } - - /** - * Uninjects the custom {@link ChannelDuplexHandler} from the specified player's Netty pipeline. - * This stops the interception of packets for that player. - * - * @param player The {@link Player} whose pipeline is to be uninject. Must not be {@code null}. - */ - public static void uninject(@NotNull Player player) - { - Channel channel = channels.get(player.getUniqueId()); - - if(channel == null) - return; - - if(channel.pipeline().get(NpcApi.plugin.getName()) != null) - channel.pipeline().remove(NpcApi.plugin.getName()); - } - - /** - * Uninjects the custom packet handler from all currently online players. - * This is typically called during plugin shutdown or reload. - */ - public static void uninjectAll() - { - for(Player player : Bukkit.getOnlinePlayers()) - uninject(player); - } - - /** - * Injects the custom packet handler into all currently online players. - * This is typically called during plugin startup or after a reload. - */ - public static void injectAll() - { - for(Player player : Bukkit.getOnlinePlayers()) - inject(player); - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/Reflections.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/Reflections.java deleted file mode 100644 index e586d3c..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/Reflections.java +++ /dev/null @@ -1,406 +0,0 @@ -package de.eisi05.npc.api.utils; - -import com.google.common.primitives.Primitives; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.Optional; - -/** - * Utility class providing methods for reflective operations such as loading classes, - * invoking methods, and accessing fields dynamically at runtime. - */ -@SuppressWarnings("unchecked") -public class Reflections -{ - /** - * Loads a class by its fully qualified name. - * - * @param path fully qualified class name - * @param type of the class - * @return Optional containing the Class if found, empty otherwise - */ - public static @NotNull Optional> getClass(@NotNull String path) - { - try - { - return Optional.of((Class) Class.forName(path)); - } catch(ClassNotFoundException e) - { - e.printStackTrace(); - return Optional.empty(); - } - } - - /** - * Instantiates an object of the given class using the constructor that matches the argument types. - * - * @param path fully qualified class name - * @param args constructor arguments - * @param type of the instance - * @return Optional containing the instance if created successfully, empty otherwise - */ - public static @NotNull Optional getInstance(@NotNull String path, @Nullable Object... args) - { - return getClass(path).flatMap(objectClass -> getInstance((Class) objectClass, args)); - } - - public static @NotNull Optional getInstanceFirstConstructor(@NotNull Class clazz, @Nullable Object... args) - { - try - { - Constructor ctor = clazz.getDeclaredConstructors()[0]; - ctor.setAccessible(true); - return Optional.of((T) ctor.newInstance(args)); - } catch(Exception e) - { - e.printStackTrace(); - return Optional.empty(); - } - } - - public static @NotNull Optional getInstance(@NotNull Class clazz, @Nullable Object... args) - { - try - { - Class[] argTypes = Arrays.stream(args) - .map(Object::getClass) - .toArray(Class[]::new); - Constructor ctor = clazz.getDeclaredConstructor(argTypes); - ctor.setAccessible(true); - return Optional.of((T) ctor.newInstance(args)); - } catch(Exception e) - { - e.printStackTrace(); - return Optional.empty(); - } - } - - /** - * Finds a declared method in the specified class that matches the given name and parameter types. - * - * @param clazz the class to search in - * @param name the name of the method - * @param args the arguments whose types will be used to find the method - * @return the matching Method object - * @throws NoSuchMethodException if no matching method is found - */ - private static @NotNull Method findMethod(@NotNull Class clazz, @NotNull String name, @Nullable Object[] args) throws NoSuchMethodException - { - Class current = clazz; - Class[] argTypes = Arrays.stream(args) - .map(Object::getClass) - .toArray(Class[]::new); - - while(current != null) - { - for(Method method : current.getDeclaredMethods()) - { - if(!method.getName().equals(name)) - continue; - - Class[] paramTypes = method.getParameterTypes(); - boolean isVarArgs = method.isVarArgs(); - - if(isCompatible(argTypes, paramTypes, isVarArgs)) - { - method.setAccessible(true); - return method; - } - } - current = current.getSuperclass(); - } - - throw new NoSuchMethodException("No compatible method " + name + " found in class " + clazz.getName() + "(" + Arrays.toString(args) + ")"); - } - - /** - * Checks if a given set of argument types is compatible with the parameter types of a method. - *

- * This method supports both regular and varargs methods. It also handles primitive-to-wrapper - * type conversions (e.g., int to Integer). - * - * @param args The types of the provided arguments. - * @param params The types of the method's parameters. - * @param isVarArgs Whether the method accepts a variable number of arguments (varargs). - * @return {@code true} if the argument types are compatible with the parameter types; {@code false} otherwise. - */ - private static boolean isCompatible(@NotNull Class[] args, @NotNull Class[] params, boolean isVarArgs) - { - if(!isVarArgs) - { - if(args.length != params.length) - return false; - for(int i = 0; i < args.length; i++) - { - if(!Primitives.wrap(params[i]).isAssignableFrom(Primitives.wrap(args[i]))) - return false; - } - return true; - } - - if(args.length < params.length - 1) - return false; - for(int i = 0; i < params.length - 1; i++) - { - if(!Primitives.wrap(params[i]).isAssignableFrom(Primitives.wrap(args[i]))) - return false; - } - - Class varArgType = Primitives.wrap(params[params.length - 1].getComponentType()); - for(int i = params.length - 1; i < args.length; i++) - { - if(!varArgType.isAssignableFrom(Primitives.wrap(args[i]))) - return false; - } - return true; - } - - /** - * Invokes an instance method on the given object with specified arguments. - * - * @param object the target object - * @param methodName name of the method to invoke - * @param args method arguments - * @param return type of the method - * @return a ReflectionChain wrapping the method's return value - */ - public static @NotNull ReflectionChain invokeMethod(@NotNull Object object, @NotNull String methodName, @Nullable Object... args) - { - try - { - Method method = findMethod(object.getClass(), methodName, args); - method.setAccessible(true); - return new ReflectionChain<>((V) method.invoke(object, args)); - } catch(Exception e) - { - throw new RuntimeException(e); - } - } - - /** - * Invokes a static method of a class given its fully qualified name. - * - * @param classPath fully qualified class name - * @param methodName name of the static method - * @param args method arguments - * @param return type of the method - * @return a ReflectionChain wrapping the method's return value - */ - public static @NotNull ReflectionChain invokeStaticMethod(@NotNull String classPath, @NotNull String methodName, @Nullable Object... args) - { - try - { - Class clazz = Class.forName(classPath); - Method method = findMethod(clazz, methodName, args); - method.setAccessible(true); - return new ReflectionChain<>((V) method.invoke(null, args)); - } catch(Exception e) - { - throw new RuntimeException(e); - } - } - - /** - * Finds a declared field in the specified class by its name and makes it accessible. - * - * @param clazz the class to search in - * @param fieldName the name of the field - * @return the Field object with accessibility set to true - * @throws NoSuchFieldException if the field is not found - */ - private static @NotNull Field findField(@NotNull Class clazz, @NotNull String fieldName) throws NoSuchFieldException - { - while(clazz != null) - { - try - { - Field field = clazz.getDeclaredField(fieldName); - field.setAccessible(true); - return field; - } catch(NoSuchFieldException ignored) - { - clazz = clazz.getSuperclass(); - } - } - throw new NoSuchFieldException(); - } - - /** - * Retrieves the value of a field from an object. - * - * @param object the target object - * @param fieldName name of the field - * @param type of the field value - * @return a ReflectionChain wrapping the field's value - */ - public static @NotNull ReflectionChain getField(@NotNull Object object, @NotNull String fieldName) - { - try - { - Field field = findField(object.getClass(), fieldName); - return new ReflectionChain<>((T) field.get(object)); - } catch(Exception e) - { - throw new RuntimeException(e); - } - } - - /** - * Retrieves the value of a static field from a class. - * - * @param clazz the target class - * @param fieldName name of the static field - * @param class type - * @param type of the field value - * @return the value of the static field, or null if inaccessible - */ - public static @Nullable V getStaticField(@NotNull Class clazz, @Nullable String fieldName) - { - try - { - Field field = findField(clazz, fieldName); - return (V) field.get(null); - } catch(Exception e) - { - throw new RuntimeException(e); - } - } - - /** - * Retrieves the value of a static field by class name. - * - * @param classPath fully qualified class name - * @param fieldName name of the static field - * @param type of the field value - * @return the value of the static field, or null if inaccessible - */ - public static @Nullable T getStaticField(@NotNull String classPath, @NotNull String fieldName) - { - try - { - Class clazz = Class.forName(classPath); - Field field = findField(clazz, fieldName); - return (T) field.get(null); - } catch(Exception e) - { - throw new RuntimeException(e); - } - } - - /** - * Sets the value of a field on an object. - * - * @param object target object - * @param fieldName name of the field - * @param value new value to set - */ - public static void setField(@NotNull Object object, @NotNull String fieldName, @Nullable Object value) - { - try - { - Field field = findField(object.getClass(), fieldName); - field.set(object, value); - } catch(Exception e) - { - throw new RuntimeException(e); - } - } - - /** - * Sets the value of a static field in a class by class name. - * - * @param classPath fully qualified class name - * @param fieldName name of the static field - * @param value new value to set - */ - public static void setStaticField(@NotNull String classPath, @NotNull String fieldName, @Nullable Object value) - { - try - { - Class clazz = Class.forName(classPath); - Field field = findField(clazz, fieldName); - field.set(null, value); - } catch(Exception e) - { - throw new RuntimeException(e); - } - } - - /** - * Helper class to chain reflection calls on the result of previous reflective operations. - * - * @param the wrapped value type - */ - public static class ReflectionChain - { - private final @Nullable V value; - - /** - * Creates a new ReflectionChain wrapping the given value. - * - * @param value the wrapped value may be null - */ - public ReflectionChain(@Nullable V value) - { - this.value = value; - } - - /** - * Invokes a method on the wrapped object. - * - * @param methodName name of the method - * @param args method arguments - * @return new ReflectionChain wrapping the method's result, or null if error - */ - public @NotNull ReflectionChain thanInvoke(@NotNull String methodName, @Nullable Object... args) - { - if(value == null) - return new ReflectionChain<>(null); - - try - { - Method method = findMethod(value.getClass(), methodName, args); - method.setAccessible(true); - return new ReflectionChain<>((V) method.invoke(value, args)); - } catch(Exception e) - { - throw new RuntimeException(e); - } - } - - /** - * Retrieves a field value from the wrapped object. - * - * @param fieldName name of the field - * @return new ReflectionChain wrapping the field's value, or null if error - */ - public @NotNull ReflectionChain thanGetField(@NotNull String fieldName) - { - if(value == null) - return new ReflectionChain<>(null); - try - { - Field field = findField(value.getClass(), fieldName); - return new ReflectionChain<>((V) field.get(value)); - } catch(Exception e) - { - throw new RuntimeException(e); - } - } - - /** - * Returns the wrapped value. - * - * @return wrapped value may be null - */ - public @Nullable V get() - { - return value; - } - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/TriFunction.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/TriFunction.java deleted file mode 100644 index 7b2ea34..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/TriFunction.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.eisi05.npc.api.utils; - -/** - * Represents a function that accepts three arguments and produces a result. - * This is a functional interface whose functional method is {@link #apply(Object, Object, Object)}. - * - * @param the type of the first argument to the function - * @param the type of the second argument to the function - * @param the type of the third argument to the function - * @param the type of the result of the function - */ -@FunctionalInterface -public interface TriFunction -{ - /** - * Applies this function to the given arguments. - * - * @param t the first function argument - * @param u the second function argument - * @param v the third function argument - * @return the function result - */ - R apply(T t, U u, V v); -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/Var.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/Var.java deleted file mode 100644 index 2a17750..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/Var.java +++ /dev/null @@ -1,103 +0,0 @@ -package de.eisi05.npc.api.utils; - -import net.minecraft.network.protocol.Packet; -import net.minecraft.server.level.ServerEntity; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.entity.Entity; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Set; -import java.util.UUID; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - -public class Var -{ - /** - * Performs an unchecked cast of an object to a specified type. - * This method can be used to bypass Java's type checking at compile time, - * but it comes with the risk of {@link ClassCastException} at runtime if the - * object is not an instance of the target type. - * - * @param o The object to cast. Can be {@code null}. - * @param The target type to which the object will be cast. - * @return The object cast to the specified type, or {@code null} if the input object was {@code null}. - */ - @SuppressWarnings("unchecked") - public static @Nullable T unsafeCast(@Nullable Object o) - { - return (T) o; - } - - public static void moveEntity(Entity entity, double x, double y, double z, float yaw, float pitch) - { - if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_5)) - Reflections.invokeMethod(entity, "absMoveTo", x, y, z, yaw, pitch); - else - Reflections.invokeMethod(entity, "snapTo", x, y, z, yaw, pitch); - } - - public static ServerLevel getServerLevel(ServerPlayer player) - { - return (ServerLevel) Reflections.invokeMethod(player, "level").get(); - } - - public static ServerEntity getServerEntity(Entity entity, ServerLevel level) - { - ServerEntity serverEntity; - if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_5)) - serverEntity = Reflections.getInstanceFirstConstructor(ServerEntity.class, level, entity, 0, false, - new Consumer>() - { - @Override - public void accept(Packet packet) - { - - } - - @Override - public @NotNull Consumer> andThen(@NotNull Consumer> after) - { - return Consumer.super.andThen(after); - } - }, - Set.of()).orElseThrow(); - else if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_9)) - serverEntity = Reflections.getInstanceFirstConstructor(ServerEntity.class, level, entity, 0, false, - new Consumer>() - { - @Override - public void accept(Packet packet) - { - - } - - @Override - public @NotNull Consumer> andThen(@NotNull Consumer> after) - { - return Consumer.super.andThen(after); - } - }, - new BiConsumer, UUID>() - { - @Override - public void accept(Packet packet, UUID uuid) - { - - } - - @Override - public @NotNull BiConsumer, UUID> andThen(@NotNull BiConsumer, ? super UUID> after) - { - return BiConsumer.super.andThen(after); - } - }, Set.of()).orElseThrow(); - else - serverEntity = Reflections.getInstanceFirstConstructor(ServerEntity.class, level, entity, 0, false, - null, Set.of()).orElseThrow(); - - return serverEntity; - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/Versions.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/Versions.java deleted file mode 100644 index 07490d4..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/Versions.java +++ /dev/null @@ -1,199 +0,0 @@ -package de.eisi05.npc.api.utils; - -import org.bukkit.Bukkit; -import org.jetbrains.annotations.NotNull; - -import java.util.Arrays; - -/** - * The {@link Versions} enum represents the supported Minecraft server versions - * and provides utility methods for working with version-specific paths and comparisons. - * It helps in adapting the plugin's functionality to different server environments. - */ -public enum Versions -{ - /** - * Represents an unknown or unsupported version. - */ - NONE(""), - /** - * Minecraft 1.17 version. - */ - V1_17("v1_17_R1"), - /** - * Minecraft 1.18 version. - */ - V1_18("v1_18_R1"), - /** - * Minecraft 1.18.2 version. - */ - V1_18_2("v1_18_R2"), - /** - * Minecraft 1.19 version. - */ - V1_19("v1_19_R1"), - /** - * Minecraft 1.19.1 version. - */ - V1_19_1("v1_19_R1"), - /** - * Minecraft 1.19.3 version. - */ - V1_19_3("v1_19_R2"), - /** - * Minecraft 1.19.4 version. - */ - V1_19_4("v1_19_R3"), - /** - * Minecraft 1.20 version. - */ - V1_20("v1_20_R1"), - /** - * Minecraft 1.20.2 version. - */ - V1_20_2("v1_20_R2"), - /** - * Minecraft 1.20.4 version. - */ - V1_20_4("v1_20_R3"), - /** - * Minecraft 1.20.6 version. - */ - V1_20_6("v1_20_R4"), - /** - * Minecraft 1.21 version. - */ - V1_21("v1_21_R1"), - /** - * Minecraft 1.21.2 version. - */ - V1_21_2("v1_21_R2"), - /** - * Minecraft 1.21.4 version. - */ - V1_21_4("v1_21_R3"), - /** - * Minecraft 1.21.5 version. - */ - V1_21_5("v1_21_R4"), - /** - * Minecraft 1.21.5 version. - */ - V1_21_6("v1_21_R5"), - - /** - * Minecraft 1.21.5 version. - */ - V1_21_7("v1_21_R5"), - - /** - * Minecraft 1.21.9 version. - */ - V1_21_9("v1_21_R6"); - - /** - * Caches the determined current server version to avoid repeated lookups. - */ - private static Versions VERSION; - - /** - * The NMS (Net Minecraft Server) path component corresponding to this version. - * For example, "v1_17_R1" for Minecraft 1.17. - */ - private final String path; - - /** - * Constructs a {@code Versions} enum entry with the specified NMS path. - * - * @param path The NMS path string for this version. Must not be {@code null}. - */ - Versions(@NotNull String path) - { - this.path = path; - } - - /** - * Determines and returns the current Minecraft server version based on the Bukkit server's package name. - * The determined version is cached for later calls. - * - * @return The {@link Versions} enum entry corresponding to the current server version. Must not be {@code null}. - */ - public static @NotNull Versions getVersion() - { - if(VERSION != null) - return VERSION; - - return VERSION = switch(Bukkit.getMinecraftVersion()) - { - case "1.17.1", "1.17.2" -> Versions.V1_17; - case "1.18", "1.18.1" -> Versions.V1_18; - case "1.18.2" -> Versions.V1_18_2; - case "1.19" -> Versions.V1_19; - case "1.19.1", "1.19.2" -> Versions.V1_19_1; - case "1.19.3" -> Versions.V1_19_3; - case "1.19.4", "1.19.5" -> Versions.V1_19_4; - case "1.20", "1.20.1" -> Versions.V1_20; - case "1.20.2", "1.20.3" -> Versions.V1_20_2; - case "1.20.4", "1.20.5" -> Versions.V1_20_4; - case "1.20.6" -> Versions.V1_20_6; - case "1.21", "1.21.1" -> Versions.V1_21; - case "1.21.2", "1.21.3" -> Versions.V1_21_2; - case "1.21.4" -> Versions.V1_21_4; - case "1.21.5" -> Versions.V1_21_5; - case "1.21.6" -> Versions.V1_21_6; - case "1.21.7", "1.21.8" -> V1_21_7; - case "1.21.9", "1.21.10" -> V1_21_9; - default -> Versions.NONE; - }; - } - - /** - * Returns an array of {@link Versions} enum entries that fall inclusively between - * two specified versions (based on their ordinal values). - * The {@code NONE} version is excluded from the result. - * - * @param versions1 The starting version (inclusive). Must not be {@code null}. - * @param versions2 The ending version (inclusive). Must not be {@code null}. - * @return An array of {@link Versions} enum entries within the specified range. Must not be {@code null}. - */ - private static @NotNull Versions[] getVersionBetween(@NotNull Versions versions1, @NotNull Versions versions2) - { - return Arrays.stream(values()) - .filter(v -> v != Versions.NONE) - .filter(v -> v.ordinal() >= versions1.ordinal() && v.ordinal() <= versions2.ordinal()) - .toArray(Versions[]::new); - } - - /** - * Checks if the current server version is numerically smaller than a specified version. - * This comparison is based on the ordinal value of the enum entries. - * - * @param versions The version to compare against. Must not be {@code null}. - * @return {@code true} if the current version is smaller, {@code false} otherwise. - */ - public static boolean isCurrentVersionSmallerThan(@NotNull Versions versions) - { - return getVersion().ordinal() < versions.ordinal(); - } - - /** - * Returns the NMS (Net Minecraft Server) path component associated with this version. - * - * @return The NMS path as a {@link String}. Must not be {@code null}. - */ - public @NotNull String getPath() - { - return path; - } - - /** - * Returns the name of the version, with underscores replaced by dots for better readability. - * For example, {@code V1_17} becomes "V1.17". - * - * @return The formatted name of the version as a {@link String}. Must not be {@code null}. - */ - public @NotNull String getName() - { - return name().replace("_", "."); - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/enums/ChatFormat.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/enums/ChatFormat.java deleted file mode 100644 index 8147817..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/enums/ChatFormat.java +++ /dev/null @@ -1,58 +0,0 @@ -package de.eisi05.npc.api.wrapper.enums; - -import net.kyori.adventure.text.format.NamedTextColor; -import net.kyori.adventure.text.format.TextDecoration; -import net.kyori.adventure.text.format.TextFormat; -import net.kyori.adventure.text.serializer.legacy.Reset; - -import java.io.Serializable; - -public enum ChatFormat implements Serializable -{ - BLACK('0', NamedTextColor.BLACK), - DARK_BLUE('1', NamedTextColor.DARK_BLUE), - DARK_GREEN('2', NamedTextColor.DARK_GREEN), - DARK_AQUA('3', NamedTextColor.DARK_AQUA), - DARK_RED('4', NamedTextColor.DARK_RED), - DARK_PURPLE('5', NamedTextColor.DARK_PURPLE), - GOLD('6', NamedTextColor.GOLD), - GRAY('7', NamedTextColor.GRAY), - DARK_GRAY('8', NamedTextColor.DARK_GRAY), - BLUE('9', NamedTextColor.BLUE), - GREEN('a', NamedTextColor.GREEN), - AQUA('b', NamedTextColor.AQUA), - RED('c', NamedTextColor.RED), - LIGHT_PURPLE('d', NamedTextColor.LIGHT_PURPLE), - YELLOW('e', NamedTextColor.YELLOW), - WHITE('f', NamedTextColor.WHITE), - OBFUSCATED('k', TextDecoration.OBFUSCATED), - BOLD('l', TextDecoration.BOLD), - STRIKETHROUGH('m', TextDecoration.STRIKETHROUGH), - UNDERLINE('n', TextDecoration.UNDERLINED), - ITALIC('o', TextDecoration.ITALIC), - RESET('p', Reset.INSTANCE); - - private final char color; - private final TextFormat textFormat; - - ChatFormat(char color, TextFormat textFormat) - { - this.color = color; - this.textFormat = textFormat; - } - - public char getColorCode() - { - return color; - } - - public TextFormat getTextFormat() - { - return textFormat; - } - - public boolean isColor() - { - return textFormat instanceof NamedTextColor; - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/packets/AnimatePacket.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/packets/AnimatePacket.java deleted file mode 100644 index 11f107d..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/packets/AnimatePacket.java +++ /dev/null @@ -1,29 +0,0 @@ -package de.eisi05.npc.api.wrapper.packets; - -import net.minecraft.network.protocol.game.ClientboundAnimatePacket; -import net.minecraft.network.protocol.game.ClientboundHurtAnimationPacket; -import net.minecraft.server.level.ServerPlayer; -import org.jetbrains.annotations.NotNull; - -import java.io.Serializable; - -public class AnimatePacket -{ - public static Object create(@NotNull ServerPlayer player, @NotNull Animation animation) - { - if(animation != Animation.HURT) - return new ClientboundAnimatePacket(player, animation.ordinal()); - - return new ClientboundHurtAnimationPacket(player); - } - - public enum Animation implements Serializable - { - SWING_MAIN_HAND, - HURT, - WAKE_UP, - SWING_OFF_HAND, - CRITICAL_HIT, - MAGIC_CRITICAL_HIT - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/packets/SetEntityDataPacket.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/packets/SetEntityDataPacket.java deleted file mode 100644 index c8a4f74..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/packets/SetEntityDataPacket.java +++ /dev/null @@ -1,13 +0,0 @@ -package de.eisi05.npc.api.wrapper.packets; - -import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket; -import net.minecraft.network.syncher.SynchedEntityData; -import org.jetbrains.annotations.NotNull; - -public class SetEntityDataPacket -{ - public static Object create(int id, @NotNull SynchedEntityData data) - { - return new ClientboundSetEntityDataPacket(id, data.packAll()); - } -} diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/packets/SetPlayerTeamPacket.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/packets/SetPlayerTeamPacket.java deleted file mode 100644 index a1093a6..0000000 --- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/packets/SetPlayerTeamPacket.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.eisi05.npc.api.wrapper.packets; - -import net.minecraft.network.protocol.game.ClientboundSetPlayerTeamPacket; -import net.minecraft.world.scores.PlayerTeam; -import org.jetbrains.annotations.NotNull; - -public class SetPlayerTeamPacket -{ - public static Object createAddOrModifyPacket(@NotNull PlayerTeam team, boolean create) - { - return ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(team, create); - } - - public static Object createRemovePacket(@NotNull PlayerTeam team) - { - return ClientboundSetPlayerTeamPacket.createRemovePacket(team); - } - - public static Object createPlayerPacket(@NotNull PlayerTeam team, @NotNull String playerName, - @NotNull ClientboundSetPlayerTeamPacket.Action action) - { - return ClientboundSetPlayerTeamPacket.createPlayerPacket(team, playerName, action); - } -} diff --git a/src/main/java/com/mmmm/story/Cleanable.java b/src/main/java/com/mmmm/story/Cleanable.java new file mode 100644 index 0000000..6718250 --- /dev/null +++ b/src/main/java/com/mmmm/story/Cleanable.java @@ -0,0 +1,27 @@ +package com.mmmm.story; + +/** + * Implemented by listeners and managers that own boss bars, spawned entities or other + * runtime state which must be released when the plugin shuts down. + * + *

Bukkit cancels a plugin's scheduler tasks on disable by itself, but nothing else: + * boss bars stay pinned to the screens of players who survive a {@code /reload}, and + * summoned bosses, warrior skeletons and VFX armour stands stay in the world with no + * listener tracking them. After the reload a fresh listener is constructed that knows + * nothing about them. + * + *

Everything registered through {@link MmmmStoryPlugin#registerListeners()} is + * collected into a single list, which {@link MmmmStoryPlugin#onDisable()} walks in + * reverse registration order. + */ +public interface Cleanable { + + /** + * Cancel tasks, hide boss bars and drop any other runtime state. + * + *

Must be safe to call more than once - {@code cleanup()} also runs when a boss + * dies normally. The shutdown loop logs and continues on failure so that one broken + * component cannot block the rest. + */ + void cleanup(); +} diff --git a/src/main/java/com/mmmm/story/MmmmStoryPlugin.java b/src/main/java/com/mmmm/story/MmmmStoryPlugin.java index 4ae08e8..c2c751a 100644 --- a/src/main/java/com/mmmm/story/MmmmStoryPlugin.java +++ b/src/main/java/com/mmmm/story/MmmmStoryPlugin.java @@ -1,18 +1,28 @@ package com.mmmm.story; -import com.mmmm.story.commands.StoryCommand; import com.mmmm.story.commands.ServerCommand; +import com.mmmm.story.commands.StoryCommand; +import com.mmmm.story.commands.TextureTestCommand; import com.mmmm.story.listeners.*; import com.mmmm.story.managers.*; import de.eisi05.npc.api.NpcApi; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.PluginCommand; +import org.bukkit.event.Listener; import org.bukkit.plugin.java.JavaPlugin; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.logging.Level; public class MmmmStoryPlugin extends JavaPlugin { - + private static MmmmStoryPlugin instance; - + + /** Registered components that must release runtime state on shutdown. */ + private final List cleanables = new ArrayList<>(); + private ConfigManager configManager; private DataManager dataManager; private ItemManager itemManager; @@ -52,58 +62,108 @@ public void onEnable() { menuManager = new MenuManager(this, messageManager, actManager, dialogManager); // Register commands - getCommand("story").setExecutor(new StoryCommand(this)); - getCommand("server").setExecutor(new ServerCommand(this)); - + registerCommand("story", new StoryCommand(this)); + registerCommand("server", new ServerCommand(this)); + registerCommand("testtexture", new TextureTestCommand(this)); + // Register event listeners registerListeners(); - - // Start auto-save task (every 5 minutes) - getServer().getScheduler().runTaskTimer(this, () -> { - dataManager.save(); - }, 6000L, 6000L); - + + startAutoSaveTask(); + getLogger().info(getMessageManager().getMessage("log.plugin_enabled")); - + } catch (Exception e) { getLogger().log(Level.SEVERE, "Failed to initialize plugin!", e); getServer().getPluginManager().disablePlugin(this); } } - + @Override public void onDisable() { getLogger().info("Saving data..."); - + if (dataManager != null) { dataManager.save(); } - + + // Reverse order so components tear down opposite to how they were built. + // One failing component must not stop the rest from cleaning up. + List reversed = new ArrayList<>(cleanables); + Collections.reverse(reversed); + for (Cleanable cleanable : reversed) { + try { + cleanable.cleanup(); + } catch (Exception e) { + getLogger().log(Level.WARNING, + "Cleanup failed for " + cleanable.getClass().getSimpleName(), e); + } + } + cleanables.clear(); + if (npcManager != null) { npcManager.cleanup(); } - - getLogger().info(getMessageManager().getMessage("log.plugin_disabled")); + + // Deliberately not localised: messageManager is null when onEnable() failed + // early, and an NPE here would mask the original startup error. + getLogger().info("Mmmm Story Plugin disabled"); } - + + /** + * Persist story data every five minutes. + * + *

Serialisation happens on the main thread (Bukkit configuration objects are + * not thread safe); only the file writes are pushed off it. + */ + private void startAutoSaveTask() { + long fiveMinutes = 20L * 60L * 5L; + getServer().getScheduler().runTaskTimer(this, () -> dataManager.saveAsync(), + fiveMinutes, fiveMinutes); + } + + /** + * Bind an executor to a command declared in plugin.yml, failing loudly when the + * two drift apart instead of throwing a bare NPE. + */ + private void registerCommand(String name, CommandExecutor executor) { + PluginCommand command = getCommand(name); + if (command == null) { + getLogger().severe("Command '" + name + "' is missing from plugin.yml - not registered"); + return; + } + command.setExecutor(executor); + } + private void registerListeners() { act1Listener = new Act1Listener(this); - getServer().getPluginManager().registerEvents(act1Listener, this); - getServer().getPluginManager().registerEvents(new Act2Listener(this), this); - getServer().getPluginManager().registerEvents(new Act3Listener(this), this); - // Act4Listener disabled - artifacts only through chest search, not auto-spawn - // getServer().getPluginManager().registerEvents(new Act4Listener(this), this); - getServer().getPluginManager().registerEvents(new Act5Listener(this), this); - getServer().getPluginManager().registerEvents(new PortalListener(this), this); - getServer().getPluginManager().registerEvents(new PlayerListener(this), this); - getServer().getPluginManager().registerEvents(new MobListener(this), this); - getServer().getPluginManager().registerEvents(new ChestSpawnManager(this), this); - getServer().getPluginManager().registerEvents(new StoryItemProtectionListener(this), this); - getServer().getPluginManager().registerEvents(new BlockTrackingListener(this), this); - getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this); - getServer().getPluginManager().registerEvents(new MenuClickListener(this), this); + register(act1Listener); + register(new Act2Listener(this)); + register(new Act3Listener(this)); + if (getConfig().getBoolean("act4.autoSpawnArtifacts", false)) { + register(new Act4Listener(this)); + } + register(new Act5Listener(this)); + register(new PortalListener(this)); + register(new PlayerListener(this)); + register(new MobListener(this)); + register(new ChestSpawnManager(this)); + register(new StoryItemProtectionListener(this)); + register(new BlockTrackingListener(this)); + register(new PlayerJoinListener(this)); + register(new MenuClickListener(this)); } - + + /** + * Register a listener and, when it owns runtime state, remember it for shutdown. + */ + private void register(Listener listener) { + getServer().getPluginManager().registerEvents(listener, this); + if (listener instanceof Cleanable cleanable) { + cleanables.add(cleanable); + } + } + // Getters public static MmmmStoryPlugin getInstance() { @@ -126,10 +186,6 @@ public NPCManager getNPCManager() { return npcManager; } - public NPCManager getNpcManager() { - return npcManager; - } - public ActManager getActManager() { return actManager; } diff --git a/src/main/java/com/mmmm/story/bosses/backup/OriginalBoss2Backup.java b/src/main/java/com/mmmm/story/bosses/backup/OriginalBoss2Backup.java deleted file mode 100644 index e7e0980..0000000 --- a/src/main/java/com/mmmm/story/bosses/backup/OriginalBoss2Backup.java +++ /dev/null @@ -1,393 +0,0 @@ -// BACKUP FILE: Original Boss #2 (Изверг Адских Глубин) Mechanics -// This file contains the backed-up Wither-based boss implementation from Act2Listener -// Created on: 2025-11-09 -// For rollback purposes if Enderman replacement needs to be reverted - -package com.mmmm.story.bosses.backup; - -import com.mmmm.story.MmmmStoryPlugin; -import org.bukkit.*; -import org.bukkit.entity.*; -import org.bukkit.scheduler.BukkitRunnable; -import org.bukkit.scheduler.BukkitTask; -import org.bukkit.attribute.Attribute; -import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; -import org.bukkit.inventory.ItemStack; - -import net.kyori.adventure.bossbar.BossBar; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; - -import java.util.*; - -/** - * Backup of original Wither-based boss mechanics - * This contains the complete boss implementation that was replaced by the Enderman boss - */ -public class OriginalBoss2Backup { - - private final MmmmStoryPlugin plugin; - private Wither bossEntity; - private int bossPhase = 1; - private BossBar bossBar; - - // Combat tracking (simplified for backup) - private Map playersAboveBoss = new HashMap<>(); - private Map playersNearBoss = new HashMap<>(); - private Map teleportCooldown = new HashMap<>(); - private Map playerArrowsShot = new HashMap<>(); - - // Task references - private BukkitTask bossBarTask; - private BukkitTask bossAITask; - private BukkitTask heightCheckTask; - private BukkitTask antiWallTask; - private BukkitTask teleportTask; - - public OriginalBoss2Backup(MmmmStoryPlugin plugin) { - this.plugin = plugin; - } - - /** - * Original boss summoning method - */ - public void spawnOriginalBoss(Location location) { - World world = location.getWorld(); - - // Spawn Wither-based boss entity - Location spawnLoc = location.clone().add(0, 3, 0); - bossEntity = (Wither) world.spawnEntity(spawnLoc, EntityType.WITHER); - bossEntity.setCustomName("Изверг Адских Глубин"); - bossEntity.setCustomNameVisible(true); - - // Set original attributes - bossEntity.getAttribute(Attribute.GENERIC_MAX_HEALTH).setBaseValue(500.0); - bossEntity.setHealth(500.0); - bossEntity.getAttribute(Attribute.GENERIC_KNOCKBACK_RESISTANCE).setBaseValue(0.6); - - // Mark as boss - bossEntity.setPersistent(true); - bossEntity.setRemoveWhenFarAway(false); - - // Create boss bar - bossBar = BossBar.bossBar( - Component.text("Изверг Адских Глубин"), - 1.0f, - BossBar.Color.PURPLE, - BossBar.Overlay.PROGRESS - ); - - // Add nearby players to boss bar - for (Player player : world.getPlayers()) { - if (player.getLocation().distance(bossEntity.getLocation()) < 100) { - // Note: Adventure API may have different method names - // bossBar.addPlayer(player); // Commented out for compatibility - } - } - - // Start combat tasks - startBossTasks(); - } - - /** - * Start original boss combat tasks - */ - private void startBossTasks() { - // Boss bar update task - bossBarTask = new BukkitRunnable() { - @Override - public void run() { - if (bossEntity == null || !bossEntity.isValid()) { - this.cancel(); - return; - } - - double healthPercentage = bossEntity.getHealth() / bossEntity.getMaxHealth(); - bossBar.progress((float) healthPercentage); - - // Update phase based on health - int newPhase = healthPercentage > 0.5 ? 1 : 2; - if (newPhase != bossPhase) { - bossPhase = newPhase; - onPhaseTransition(bossPhase); - } - } - }.runTaskTimer(plugin, 0L, 5L); - - // Boss AI task - bossAITask = new BukkitRunnable() { - @Override - public void run() { - if (bossEntity == null || !bossEntity.isValid()) { - this.cancel(); - return; - } - - // Original boss AI logic - executeOriginalBossAI(); - } - }.runTaskTimer(plugin, 0L, 10L); - - // Height check task (anti-exploit) - heightCheckTask = new BukkitRunnable() { - @Override - public void run() { - checkPlayerHeightExploit(); - } - }.runTaskTimer(plugin, 0L, 5L); - - // Anti-fortification task - antiWallTask = new BukkitRunnable() { - @Override - public void run() { - preventPlayerFortification(); - } - }.runTaskTimer(plugin, 0L, 20L); - } - - /** - * Execute original boss AI - */ - private void executeOriginalBossAI() { - // Check for nearby players - List nearbyPlayers = getNearbyPlayers(75); - if (nearbyPlayers.isEmpty()) { - return; // No players nearby, wait - } - - // Target nearest player - Player target = findNearestPlayer(nearbyPlayers); - if (target != null) { - bossEntity.setTarget(target); - - // Phase-specific behavior - if (bossPhase == 1) { - executePhase1Behavior(target); - } else { - executePhase2Behavior(target); - } - } - } - - /** - * Execute Phase 1 behavior (original) - */ - private void executePhase1Behavior(Player target) { - // Basic attacks - if (bossEntity.getLocation().distance(target.getLocation()) < 3.0) { - // Teleport behind player if too close - executeProximityTeleport(target); - } - } - - /** - * Execute Phase 2 behavior (original) - */ - private void executePhase2Behavior(Player target) { - // Enhanced aggression in Phase 2 - bossEntity.addPotionEffect(new PotionEffect(PotionEffectType.SPEED, 100, 1)); - bossEntity.addPotionEffect(new PotionEffect(PotionEffectType.STRENGTH, 100, 0)); - } - - /** - * Execute proximity teleport (original mechanic) - */ - private void executeProximityTeleport(Player player) { - UUID playerId = player.getUniqueId(); - long currentTime = System.currentTimeMillis(); - - // Check cooldown - if (teleportCooldown.containsKey(playerId)) { - long lastTeleport = teleportCooldown.get(playerId); - if (currentTime - lastTeleport < 10000) { // 10 second cooldown - return; - } - } - - // Teleport behind player - Location behindPlayer = player.getLocation().clone() - .add(player.getLocation().getDirection().multiply(-4)); - behindPlayer.setY(bossEntity.getLocation().getY()); - - bossEntity.teleport(behindPlayer); - teleportCooldown.put(playerId, currentTime); - - // Effects - World world = bossEntity.getWorld(); - world.spawnParticle(Particle.PORTAL, behindPlayer, 50, 1, 1, 1, 0.2); - world.playSound(behindPlayer, Sound.ENTITY_ENDERMAN_TELEPORT, 1.0f, 1.0f); - } - - /** - * Find nearest player to location - */ - private Player findNearestPlayer(List players) { - Player nearest = null; - double minDistance = Double.MAX_VALUE; - - for (Player player : players) { - double distance = bossEntity.getLocation().distance(player.getLocation()); - if (distance < minDistance) { - minDistance = distance; - nearest = player; - } - } - - return nearest; - } - - /** - * Get nearby players - */ - private List getNearbyPlayers(double radius) { - List players = new ArrayList<>(); - for (Entity entity : bossEntity.getNearbyEntities(radius, radius, radius)) { - if (entity instanceof Player) { - players.add((Player) entity); - } - } - return players; - } - - /** - * Handle phase transition - */ - private void onPhaseTransition(int newPhase) { - // Handle phase transition effects - World world = bossEntity.getWorld(); - world.spawnParticle(Particle.EXPLOSION, bossEntity.getLocation(), 50, 2, 2, 2, 0.1); - world.playSound(bossEntity.getLocation(), Sound.ENTITY_WITHER_AMBIENT, 2.0f, 0.5f); - - if (newPhase == 2) { - // Phase 2 enhancements - bossEntity.addPotionEffect(new PotionEffect(PotionEffectType.SPEED, 999999, 1)); - bossEntity.addPotionEffect(new PotionEffect(PotionEffectType.STRENGTH, 999999, 0)); - } - } - - /** - * Check player height exploit - */ - private void checkPlayerHeightExploit() { - if (bossEntity == null) return; - - for (Player player : getNearbyPlayers(20)) { - Location playerLoc = player.getLocation(); - Location bossLoc = bossEntity.getLocation(); - - if (playerLoc.getY() > bossLoc.getY() + 5) { - // Player is too high above boss - UUID playerId = player.getUniqueId(); - long currentTime = System.currentTimeMillis(); - - if (!playersAboveBoss.containsKey(player.getUniqueId())) { - playersAboveBoss.put(player.getUniqueId(), currentTime); - } else if (currentTime - playersAboveBoss.get(player.getUniqueId()) > 3000) { - // Knock player back after 3 seconds - knockPlayerBack(player); - playersAboveBoss.remove(playerId); - } - } else { - playersAboveBoss.remove(player.getUniqueId()); - } - } - } - - /** - * Knock player back - */ - private void knockPlayerBack(Player player) { - org.bukkit.util.Vector knockback = player.getLocation().toVector() - .subtract(bossEntity.getLocation().toVector()) - .normalize() - .multiply(3.0); - knockback.setY(1.5); - - player.setVelocity(knockback); - player.playSound(player.getLocation(), Sound.ENTITY_GENERIC_EXPLODE, 1.0f, 1.0f); - } - - /** - * Prevent player fortification - */ - private void preventPlayerFortification() { - if (bossEntity == null) return; - - Location bossLoc = bossEntity.getLocation(); - World world = bossLoc.getWorld(); - - // Break blocks in 3-block radius - for (int x = -3; x <= 3; x++) { - for (int y = -1; y <= 3; y++) { - for (int z = -3; z <= 3; z++) { - Location checkLoc = bossLoc.clone().add(x, y, z); - if (shouldBreakBlock(checkLoc.getBlock().getType())) { - checkLoc.getBlock().setType(Material.AIR); - } - } - } - } - } - - /** - * Check if block should be broken - */ - private boolean shouldBreakBlock(Material type) { - return type == Material.OAK_PLANKS || type == Material.COBBLESTONE || - type == Material.STONE_BRICKS || type == Material.IRON_BARS; - } - - /** - * Get current boss phase - */ - public int getBossPhase() { - return bossPhase; - } - - /** - * Get boss entity - */ - public Wither getBossEntity() { - return bossEntity; - } - - /** - * Check if boss is active - */ - public boolean isBossActive() { - return bossEntity != null && bossEntity.isValid(); - } - - /** - * Clean up all boss resources - */ - public void cleanup() { - // Cancel tasks - if (bossBarTask != null) bossBarTask.cancel(); - if (bossAITask != null) bossAITask.cancel(); - if (heightCheckTask != null) heightCheckTask.cancel(); - if (antiWallTask != null) antiWallTask.cancel(); - if (teleportTask != null) teleportTask.cancel(); - - // Remove boss bar - if (bossBar != null) { - // Note: Adventure API may have different method names - // bossBar.removeAllPlayers(); // Commented out for compatibility - } - - // Remove boss entity - if (bossEntity != null && bossEntity.isValid()) { - bossEntity.remove(); - } - - // Clear collections - playersAboveBoss.clear(); - playersNearBoss.clear(); - teleportCooldown.clear(); - playerArrowsShot.clear(); - - // Remove reference - bossEntity = null; - } -} \ No newline at end of file diff --git a/src/main/java/com/mmmm/story/listeners/Act2Listener.java b/src/main/java/com/mmmm/story/listeners/Act2Listener.java index 6d41226..b33d68c 100644 --- a/src/main/java/com/mmmm/story/listeners/Act2Listener.java +++ b/src/main/java/com/mmmm/story/listeners/Act2Listener.java @@ -1,5 +1,6 @@ package com.mmmm.story.listeners; +import com.mmmm.story.Cleanable; import com.mmmm.story.MmmmStoryPlugin; import com.mmmm.story.managers.ItemManager; import com.mmmm.story.managers.MessageManager; @@ -55,7 +56,7 @@ import java.util.Set; import java.util.UUID; -public class Act2Listener implements Listener { +public class Act2Listener implements Listener, Cleanable { private final MmmmStoryPlugin plugin; private BossBar boss1BossBar; @@ -3221,6 +3222,31 @@ public void onBoss1Death(EntityDeathEvent event) { } } + /** + * Release every runtime resource this listener owns. + * + *

Called on plugin shutdown as well as on {@code /reload}. Bukkit cancels the + * scheduler tasks on its own, but without this the boss bars stay stuck on the + * screens of players who survive a reload. + */ + @Override + public void cleanup() { + cleanupAllBossTasks(); + + // cleanupAllBossTasks() only clears the boss 2 bar - the boss 1 bar is normally + // removed by the death handler, which never runs when we shut down mid-fight. + if (boss1BossBar != null) { + for (Player player : plugin.getServer().getOnlinePlayers()) { + player.hideBossBar(boss1BossBar); + } + boss1BossBar = null; + } + + boss1Entity = null; + boss1Phase = 1; + boss1Warriors.clear(); + } + /** * Clean up all boss-related tasks when boss dies */ diff --git a/src/main/java/com/mmmm/story/listeners/Act3Listener.java b/src/main/java/com/mmmm/story/listeners/Act3Listener.java index 00a1819..292737e 100644 --- a/src/main/java/com/mmmm/story/listeners/Act3Listener.java +++ b/src/main/java/com/mmmm/story/listeners/Act3Listener.java @@ -162,7 +162,7 @@ private void summonBoss2(org.bukkit.entity.Item droppedItem) { // Spawn Boss 2 (Enderman boss) Location spawnLoc = location.clone().add(0, 3, 0); Enderman boss = (Enderman) world.spawnEntity(spawnLoc, EntityType.ENDERMAN); - boss.setCustomName(plugin.getMessageManager().getMessage("entities.end_guardian")); + boss.setCustomName(plugin.getMessageManager().getMessage("npc.entities.end_guardian")); boss.setCustomNameVisible(true); // Set attributes diff --git a/src/main/java/com/mmmm/story/listeners/PlayerListener.java b/src/main/java/com/mmmm/story/listeners/PlayerListener.java index efb91ce..47406e5 100644 --- a/src/main/java/com/mmmm/story/listeners/PlayerListener.java +++ b/src/main/java/com/mmmm/story/listeners/PlayerListener.java @@ -12,12 +12,14 @@ import org.bukkit.event.Listener; import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerRespawnEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.scheduler.BukkitTask; import java.util.ArrayList; import java.util.List; +import java.util.logging.Level; public class PlayerListener implements Listener { @@ -91,8 +93,7 @@ public void onPlayerJoin(PlayerJoinEvent event) { plugin.getActManager().startCampaign(); plugin.getLogger().info("=== STORY STARTED SUCCESSFULLY ==="); } catch (Exception e) { - plugin.getLogger().severe("=== ERROR STARTING STORY: " + e.getMessage()); - e.printStackTrace(); + plugin.getLogger().log(Level.SEVERE, "=== ERROR STARTING STORY", e); } } else { plugin.getLogger().info("Auto-start cancelled - conditions not met (Act: " + actNow + ", Players: " + playersNow + ")"); @@ -105,6 +106,17 @@ public void onPlayerJoin(PlayerJoinEvent event) { } } + /** + * Flush and evict the leaving player's cached profile. + * + *

{@code DataManager} loads profiles lazily and used to keep every one of them + * for the lifetime of the server, so the cache grew without bound. + */ + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + plugin.getDataManager().unloadPlayer(event.getPlayer().getUniqueId()); + } + @EventHandler public void onPlayerDeath(PlayerDeathEvent event) { Player player = event.getEntity(); diff --git a/src/main/java/com/mmmm/story/managers/DataManager.java b/src/main/java/com/mmmm/story/managers/DataManager.java index 0eafe27..04f9f57 100644 --- a/src/main/java/com/mmmm/story/managers/DataManager.java +++ b/src/main/java/com/mmmm/story/managers/DataManager.java @@ -9,9 +9,12 @@ import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; public class DataManager { @@ -21,7 +24,8 @@ public class DataManager { private File playersFolder; private FileConfiguration globalData; - private final Map playerData = new HashMap<>(); + /** Concurrent: read from scheduler tasks as well as the main thread. */ + private final Map playerData = new ConcurrentHashMap<>(); public DataManager(MmmmStoryPlugin plugin) { this.plugin = plugin; @@ -125,7 +129,52 @@ private void saveAllPlayers() { savePlayerData(uuid); } } - + + /** + * Save everything without stalling the server tick. + * + *

Bukkit configuration objects are not thread safe, so they are serialised to + * strings on the calling (main) thread; only the disk writes are handed to an + * async task. Used by the five-minute autosave, where the previous synchronous + * implementation wrote every cached player profile plus a backup copy inline. + */ + public void saveAsync() { + Map pending = new LinkedHashMap<>(); + pending.put(globalFile, globalData.saveToString()); + for (Map.Entry entry : playerData.entrySet()) { + pending.put(new File(playersFolder, entry.getKey() + ".yml"), entry.getValue().saveToString()); + } + + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { + for (Map.Entry entry : pending.entrySet()) { + writeFile(entry.getKey(), entry.getValue()); + } + }); + } + + private void writeFile(File file, String contents) { + try { + if (file.equals(globalFile) && globalFile.exists()) { + Files.copy(globalFile.toPath(), new File(dataFolder, "global.yml.backup").toPath(), + StandardCopyOption.REPLACE_EXISTING); + } + Files.writeString(file.toPath(), contents, StandardCharsets.UTF_8); + } catch (IOException e) { + plugin.getLogger().log(Level.SEVERE, "Failed to write " + file.getName(), e); + } + } + + /** + * Persist and drop a player's cached profile. + * + *

Called from {@code PlayerQuitEvent}. Without this the {@code playerData} cache + * grows for every player who has ever joined and is never reclaimed. + */ + public void unloadPlayer(UUID uuid) { + savePlayerData(uuid); + playerData.remove(uuid); + } + // Global data getters/setters public int getCurrentAct() { return globalData.getInt("act.current", 1); diff --git a/src/main/java/com/mmmm/story/managers/DialogManager.java b/src/main/java/com/mmmm/story/managers/DialogManager.java index 9fcedf7..5722450 100644 --- a/src/main/java/com/mmmm/story/managers/DialogManager.java +++ b/src/main/java/com/mmmm/story/managers/DialogManager.java @@ -266,7 +266,7 @@ public void run() { if (matchesConfiguredDelay || matchesTriggerText) { plugin.getLogger().info("[Dialog] Triggering messenger despawn for text: " + text); - plugin.getNpcManager().despawnMessenger(); + plugin.getNPCManager().despawnMessenger(); } } catch (Exception e) { plugin.getLogger().warning("[Dialog] Error while triggering messenger despawn: " + e.getMessage()); diff --git a/src/main/java/com/mmmm/story/managers/NPCManager.java b/src/main/java/com/mmmm/story/managers/NPCManager.java index ddd5ab2..bf39285 100644 --- a/src/main/java/com/mmmm/story/managers/NPCManager.java +++ b/src/main/java/com/mmmm/story/managers/NPCManager.java @@ -1155,8 +1155,7 @@ public void enhancedDespawnMessenger() { startEnhancedMessengerDespawn(npcId, npcLocation, animationDuration, particlesEnabled, cleanupRadius, finalCleanup, cleanupMethod); } catch (Exception e) { - plugin.getLogger().severe("[NPC] Error during enhanced despawn: " + e.getMessage()); - e.printStackTrace(); + plugin.getLogger().log(java.util.logging.Level.SEVERE, "[NPC] Error during enhanced despawn", e); // Fallback to simple removal removeNPC(npcId); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index acb5849..29e7256 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -159,6 +159,9 @@ acts: # Bug #4 & #5 Fix: Acts 4-5 portal blocking and spawn management act4: + # When false, artifacts are obtained only by searching chests (Act4Listener stays + # unregistered). Previously this was toggled by commenting out code. + autoSpawnArtifacts: false portalBlocking: enabled: true feedbackParticle: SMOKE # particle shown when portal blocked diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 4279ae2..76cd753 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -52,6 +52,8 @@ boss1: defeated_again: "§6§l⚔ Повелитель побежден снова!" wither_skull_attack: warning: "§5§l⚡ ПОВЕЛИТЕЛЬ ПРИЗЫВАЕТ ЧЕРЕПА ИССУШЕНИЯ!" + special_attack: + warning: "§5§l⚡ Босс готовит особую атаку! Отойдите от центра!" # Boss 2 - Nether Fiend boss2: @@ -104,6 +106,7 @@ chest: found_multiple: "§a§l✔ Найдено предметов: %count%!" lore_updated: "§7Описание предмета обновлено!" lore_added: "§7Добавлено описание предмета!" + lore_removed: "§7Описание предмета удалено!" enchantment_added: "§7Добавлено зачарование предмета!" enchantment_removed: "§7Удалено зачарование предмета!" durability_changed: "§7Прочность предмета изменена!" diff --git a/src/main/resources/messages_en.yml b/src/main/resources/messages_en.yml index 4d40fa5..61048c6 100644 --- a/src/main/resources/messages_en.yml +++ b/src/main/resources/messages_en.yml @@ -6,6 +6,11 @@ npc: messenger_name: "§6Messenger" direction_marker: "§6You received a direction marker! (Lasts 5 minutes)" + entities: + skeleton_lord: "§4§lSkeleton Lord" + nether_fiend: "§4§lNether Fiend" + end_guardian: "§5§lEnd Guardian" + crystal_guardian: "§5§lCrystal Guardian" # Act Manager act: @@ -133,6 +138,8 @@ chest: hide_enchantments_removed: "§7Item enchantments no longer hidden!" hide_tooltip_added: "§7Item tooltip hidden!" hide_tooltip_removed: "§7Item tooltip no longer hidden!" + hide_flags_added: "§7Item flags hidden!" + hide_flags_removed: "§7Item flags no longer hidden!" stored_success: "§a§l✔ Item stored in inventory!" stored_failed: "§c✗ Failed to store item in inventory!" retrieved_success: "§a§l✔ Item retrieved from inventory!" @@ -167,6 +174,8 @@ chest: enchanted_failed: "§c✗ Failed to enchant item!" repaired_success: "§a§l✔ Item processed on anvil!" repaired_failed: "§c✗ Failed to process item on anvil!" + anvil_success: "§a§l✔ Item processed on anvil!" + anvil_failed: "§c✗ Failed to process item on anvil!" grindstone_success: "§a§l✔ Item processed on grindstone!" grindstone_failed: "§c✗ Failed to process item on grindstone!" brewing_stand_success: "§a§l✔ Item processed on brewing stand!" @@ -250,7 +259,6 @@ chest: - "" - "▶ Drop (Q) on Dragon Egg" - "in Boss 2 Arena for summoning" - items: overworld_portal_key: name: "End Gates Key" lore: @@ -359,11 +367,6 @@ menu: all_ready: "§6§lAll players ready! Starting story..." waiting: "§eWaiting for player readiness..." -# Additional messages from English file, now in Russian -act5: - all_artifacts_collected: "§a§l✔ All artifacts collected!" - ritual_starting: "§d§l⚡ Ritual begins..." - # Spawn point management (Bug #5 Fix) spawn: saved: "§6Your respawn point has been saved." @@ -380,6 +383,7 @@ boss: events: wave_incoming: "§c§lSkeleton wave incoming! Defend yourselves until dawn!" wave_survived: "§aYou survived the skeleton warrior wave!" + phantom_burst: "§5§lThe Void has opened! Phantoms are approaching!" # Achievement messages (Bug #1 Fix) achievements: @@ -416,15 +420,6 @@ structures: line2: "§7End Portal" line3: "§eAct 5" -# NPC and Entity Names -npc: - messenger_name: "§6Messenger" -entities: - skeleton_lord: "§4§lSkeleton Lord" - nether_fiend: "§4§lNether Fiend" - end_guardian: "§5§lEnd Guardian" - crystal_guardian: "§5§lCrystal Guardian" - # Server Start Menu server_start_menu: title: "§6§lDialog Settings" diff --git a/src/test/java/com/mmmm/story/data/PlayerSettingsTest.java b/src/test/java/com/mmmm/story/data/PlayerSettingsTest.java new file mode 100644 index 0000000..b87d103 --- /dev/null +++ b/src/test/java/com/mmmm/story/data/PlayerSettingsTest.java @@ -0,0 +1,63 @@ +package com.mmmm.story.data; + +import com.mmmm.story.data.PlayerSettings.DialogSpeed; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PlayerSettingsTest { + + @Test + void defaultsToVisibleDialogsAtNormalSpeed() { + PlayerSettings settings = new PlayerSettings(); + + assertTrue(settings.isShowDialogs()); + assertEquals(DialogSpeed.NORMAL, settings.getDialogSpeed()); + assertEquals(1.0, settings.getSpeedMultiplier()); + } + + @Test + void toggleDialogsFlipsBackAndForth() { + PlayerSettings settings = new PlayerSettings(); + + settings.toggleDialogs(); + assertFalse(settings.isShowDialogs()); + + settings.toggleDialogs(); + assertTrue(settings.isShowDialogs()); + } + + @Test + void cycleSpeedWrapsThroughEveryValue() { + PlayerSettings settings = new PlayerSettings(true, DialogSpeed.SLOW); + + settings.cycleSpeed(); + assertEquals(DialogSpeed.NORMAL, settings.getDialogSpeed()); + + settings.cycleSpeed(); + assertEquals(DialogSpeed.FAST, settings.getDialogSpeed()); + + settings.cycleSpeed(); + assertEquals(DialogSpeed.SLOW, settings.getDialogSpeed()); + } + + @Test + void speedMultiplierTracksTheSelectedSpeed() { + PlayerSettings settings = new PlayerSettings(); + + settings.setDialogSpeed(DialogSpeed.SLOW); + assertEquals(1.5, settings.getSpeedMultiplier()); + + settings.setDialogSpeed(DialogSpeed.FAST); + assertEquals(0.75, settings.getSpeedMultiplier()); + } + + @Test + void fromStringIsCaseInsensitiveAndFallsBackToNormal() { + assertEquals(DialogSpeed.FAST, DialogSpeed.fromString("fast")); + assertEquals(DialogSpeed.SLOW, DialogSpeed.fromString("SlOw")); + assertEquals(DialogSpeed.NORMAL, DialogSpeed.fromString("not-a-speed")); + } +} diff --git a/src/test/java/com/mmmm/story/managers/MessageManagerTest.java b/src/test/java/com/mmmm/story/managers/MessageManagerTest.java index 9716d63..ed60378 100644 --- a/src/test/java/com/mmmm/story/managers/MessageManagerTest.java +++ b/src/test/java/com/mmmm/story/managers/MessageManagerTest.java @@ -4,12 +4,19 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; import static org.junit.jupiter.api.Assertions.*; @@ -48,4 +55,98 @@ public void testMessagesEnYmlSyntax(@TempDir Path tempDir) throws IOException { assertTrue(config.getStringList("chest.items.end_artifact_4.lore").size() > 0, "end_artifact_4 lore should not be empty"); assertTrue(config.getStringList("chest.items.end_artifact_5.lore").size() > 0, "end_artifact_5 lore should not be empty"); } + + /** + * The two locale files must expose exactly the same key set. + * + *

They had drifted apart in both directions: keys present only in Russian left + * English players on the Russian fallback, and {@code entities.end_guardian} + * existed only in English, so Russian players saw the raw key as the boss name. + */ + @Test + public void testLocaleFilesHaveIdenticalKeys() throws IOException { + Set ru = leafKeys(load("messages.yml")); + Set en = leafKeys(load("messages_en.yml")); + + Set missingFromEn = new TreeSet<>(ru); + missingFromEn.removeAll(en); + Set missingFromRu = new TreeSet<>(en); + missingFromRu.removeAll(ru); + + assertTrue(missingFromEn.isEmpty(), "Keys missing from messages_en.yml: " + missingFromEn); + assertTrue(missingFromRu.isEmpty(), "Keys missing from messages.yml: " + missingFromRu); + } + + /** + * A key defined twice silently discards the first definition. This is how the + * English {@code chest.items} block lost six story items and {@code act5} lost + * four messages. + */ + @Test + public void testNoDuplicateKeysInLocaleFiles() throws IOException { + for (String file : new String[]{"messages.yml", "messages_en.yml"}) { + List lines = readResourceLines(file); + Map seen = new HashMap<>(); + List path = new ArrayList<>(); + List indents = new ArrayList<>(); + + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i); + String stripped = line.stripLeading(); + if (stripped.isBlank() || stripped.startsWith("#") || stripped.startsWith("-")) { + continue; + } + int colon = stripped.indexOf(':'); + if (colon < 0) { + continue; + } + String name = stripped.substring(0, colon).strip(); + if (name.isEmpty() || name.contains("\"") || name.contains(" ")) { + continue; + } + + // Pop every entry at the same or deeper indentation - those siblings and + // children are closed by this line. + int indent = line.length() - stripped.length(); + while (!indents.isEmpty() && indents.get(indents.size() - 1) >= indent) { + indents.remove(indents.size() - 1); + path.remove(path.size() - 1); + } + path.add(name); + indents.add(indent); + + String full = String.join(".", path); + Integer first = seen.put(full, i + 1); + assertNull(first, "Duplicate key '" + full + "' in " + file + + " at lines " + first + " and " + (i + 1)); + } + } + } + + private YamlConfiguration load(String resource) throws IOException { + try (InputStream in = getClass().getClassLoader().getResourceAsStream(resource)) { + assertNotNull(in, resource + " should exist"); + return YamlConfiguration.loadConfiguration( + new InputStreamReader(in, StandardCharsets.UTF_8)); + } + } + + private List readResourceLines(String resource) throws IOException { + try (InputStream in = getClass().getClassLoader().getResourceAsStream(resource)) { + assertNotNull(in, resource + " should exist"); + return new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)) + .lines().toList(); + } + } + + /** Every path that holds a value rather than a nested section. */ + private Set leafKeys(YamlConfiguration config) { + Set leaves = new TreeSet<>(); + for (String key : config.getKeys(true)) { + if (!config.isConfigurationSection(key)) { + leaves.add(key); + } + } + return leaves; + } } \ No newline at end of file