From 447dcb668042d917d2158e3b46dd4cbc18cff57b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Jul 2026 14:31:02 +0000 Subject: [PATCH 1/2] Fix build, shutdown cleanup and player data lifecycle The project did not build: de.eisi05:NpcApi-Paper is published on JitPack but no such repository was declared, so dependency resolution failed outright. The CI workflow was an unmodified GitHub template that would have failed anyway -- JDK 11 against a Java 21 source level, and an `mvn deploy` step with no distributionManagement in the POM. Build: - Declare the JitPack repository so NpcApi-Paper resolves. - CI: JDK 21, build on push and pull_request (not only on release), upload the jar as an artifact, and attach it to releases instead of deploying nowhere. - Pin maven-surefire-plugin 3.2.5. Maven < 3.9 defaults to surefire 2.12.4, which cannot run JUnit 5 and reports "no tests to run" instead of failing. - Relocate the shaded NpcApi classes so the plugin cannot clash with another plugin bundling the same library. - Drop dev.folia:folia-api (zero usages, and pinned to 1.20.1 in a 1.21.1 project) and the test-scope spigot-api 1.21.3 that conflicted with paper-api 1.21.1; paper-api already covers the Bukkit API the tests use. - Restrict resource filtering to plugin.yml, the only resource using ${}. - Restore .gitignore, deleted in 5088e94. Shutdown: - Add the Cleanable interface and collect registered components into a list that onDisable() walks. Bukkit cancels scheduler tasks by itself, but boss bars stayed pinned to the screens of players who survived a /reload while the summoned boss stayed in the world untracked. - Stop localising the disable log line: messageManager is null when startup failed early, and the NPE masked the original error. Player data: - Evict a player's cached profile on quit. DataManager loaded profiles lazily and never removed them, so the cache grew for every player who ever joined. - Back the cache with a ConcurrentHashMap; it is read from scheduler tasks. - Add saveAsync() for the five-minute autosave: configurations are serialised on the main thread, only the disk writes move off it. The old path wrote every cached profile plus a backup copy inline on the server tick. Cleanup: - Remove src/main/NpcApi-Paper-master/, a vendored copy of the upstream Gradle project (47 files, incl. a gradle-wrapper.jar). Maven only compiles src/main/java, so it never took part in the build. - Remove bosses/backup/OriginalBoss2Backup.java; git history covers this. - Register TextureTestCommand, declared in plugin.yml but never bound. - Gate Act4Listener behind act4.autoSpawnArtifacts instead of a commented-out registration line. - Fail loudly when a command is missing from plugin.yml rather than NPE. - Drop the duplicate getNpcManager() getter; keep getNPCManager(). - Replace printStackTrace() with logger calls that keep the stack trace. - Correct README claims that did not match the code: storage is YAML, not JSON, and saving was neither async nor cleaning tasks up until now. Add PlayerSettingsTest covering the dialog speed cycle and fromString fallback. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017441xEm5EainbMSewf8u6e --- .github/workflows/maven-publish.yml | 69 +- .gitignore | 43 + README.md | 12 +- pom.xml | 104 +- .../.github/ISSUE_TEMPLATE/bug_report.md | 38 - .../.github/ISSUE_TEMPLATE/feature_request.md | 20 - src/main/NpcApi-Paper-master/.gitignore | 5 - src/main/NpcApi-Paper-master/LICENSE | 21 - src/main/NpcApi-Paper-master/README.md | 233 ----- src/main/NpcApi-Paper-master/build.gradle | 67 -- .../NpcApi-Paper-master/gradle.properties | 1 - .../gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 - src/main/NpcApi-Paper-master/gradlew | 249 ----- src/main/NpcApi-Paper-master/gradlew.bat | 92 -- src/main/NpcApi-Paper-master/jitpack.yml | 3 - src/main/NpcApi-Paper-master/settings.gradle | 1 - .../main/java/de/eisi05/npc/api/NpcApi.java | 131 --- .../eisi05/npc/api/enums/ClickActionType.java | 39 - .../java/de/eisi05/npc/api/enums/Result.java | 7 - .../de/eisi05/npc/api/enums/SkinParts.java | 50 - .../npc/api/events/NpcInteractEvent.java | 96 -- .../npc/api/interfaces/NpcClickAction.java | 44 - .../api/listeners/ChangeWorldListener.java | 17 - .../npc/api/listeners/ConnectionListener.java | 38 - .../api/listeners/NpcInteractListener.java | 16 - .../de/eisi05/npc/api/manager/NpcManager.java | 119 --- .../eisi05/npc/api/manager/TeamManager.java | 48 - .../eisi05/npc/api/objects/CustomNameTag.java | 290 ------ .../java/de/eisi05/npc/api/objects/NPC.java | 897 ------------------ .../de/eisi05/npc/api/objects/NpcConfig.java | 150 --- .../de/eisi05/npc/api/objects/NpcHolder.java | 62 -- .../de/eisi05/npc/api/objects/NpcOption.java | 591 ------------ .../java/de/eisi05/npc/api/objects/Skin.java | 260 ----- .../de/eisi05/npc/api/pathfinding/AStar.java | 385 -------- .../de/eisi05/npc/api/pathfinding/Path.java | 196 ---- .../npc/api/pathfinding/PathfindingUtils.java | 97 -- .../npc/api/pathfinding/PathingResult.java | 44 - .../de/eisi05/npc/api/pathfinding/Tile.java | 284 ------ .../de/eisi05/npc/api/scheduler/Tasks.java | 66 -- .../eisi05/npc/api/utils/ItemSerializer.java | 206 ---- .../de/eisi05/npc/api/utils/ObjectSaver.java | 164 ---- .../de/eisi05/npc/api/utils/PacketReader.java | 160 ---- .../de/eisi05/npc/api/utils/Reflections.java | 406 -------- .../de/eisi05/npc/api/utils/TriFunction.java | 24 - .../java/de/eisi05/npc/api/utils/Var.java | 103 -- .../de/eisi05/npc/api/utils/Versions.java | 199 ---- .../npc/api/wrapper/enums/ChatFormat.java | 58 -- .../api/wrapper/packets/AnimatePacket.java | 29 - .../wrapper/packets/SetEntityDataPacket.java | 13 - .../wrapper/packets/SetPlayerTeamPacket.java | 24 - src/main/java/com/mmmm/story/Cleanable.java | 27 + .../java/com/mmmm/story/MmmmStoryPlugin.java | 132 ++- .../bosses/backup/OriginalBoss2Backup.java | 393 -------- .../mmmm/story/listeners/Act2Listener.java | 28 +- .../mmmm/story/listeners/PlayerListener.java | 16 +- .../com/mmmm/story/managers/DataManager.java | 53 +- .../mmmm/story/managers/DialogManager.java | 2 +- .../com/mmmm/story/managers/NPCManager.java | 3 +- src/main/resources/config.yml | 3 + .../mmmm/story/data/PlayerSettingsTest.java | 63 ++ 61 files changed, 444 insertions(+), 6554 deletions(-) create mode 100644 .gitignore delete mode 100644 src/main/NpcApi-Paper-master/.github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 src/main/NpcApi-Paper-master/.github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 src/main/NpcApi-Paper-master/.gitignore delete mode 100644 src/main/NpcApi-Paper-master/LICENSE delete mode 100644 src/main/NpcApi-Paper-master/README.md delete mode 100644 src/main/NpcApi-Paper-master/build.gradle delete mode 100644 src/main/NpcApi-Paper-master/gradle.properties delete mode 100644 src/main/NpcApi-Paper-master/gradle/wrapper/gradle-wrapper.jar delete mode 100644 src/main/NpcApi-Paper-master/gradle/wrapper/gradle-wrapper.properties delete mode 100644 src/main/NpcApi-Paper-master/gradlew delete mode 100644 src/main/NpcApi-Paper-master/gradlew.bat delete mode 100644 src/main/NpcApi-Paper-master/jitpack.yml delete mode 100644 src/main/NpcApi-Paper-master/settings.gradle delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/NpcApi.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/enums/ClickActionType.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/enums/Result.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/enums/SkinParts.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/events/NpcInteractEvent.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/interfaces/NpcClickAction.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/listeners/ChangeWorldListener.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/listeners/ConnectionListener.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/listeners/NpcInteractListener.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/manager/NpcManager.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/manager/TeamManager.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/CustomNameTag.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NPC.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NpcConfig.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NpcHolder.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NpcOption.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/Skin.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/AStar.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/Path.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/PathfindingUtils.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/PathingResult.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/Tile.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/scheduler/Tasks.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/ItemSerializer.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/ObjectSaver.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/PacketReader.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/Reflections.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/TriFunction.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/Var.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/Versions.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/enums/ChatFormat.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/packets/AnimatePacket.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/packets/SetEntityDataPacket.java delete mode 100644 src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/packets/SetPlayerTeamPacket.java create mode 100644 src/main/java/com/mmmm/story/Cleanable.java delete mode 100644 src/main/java/com/mmmm/story/bosses/backup/OriginalBoss2Backup.java create mode 100644 src/test/java/com/mmmm/story/data/PlayerSettingsTest.java 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 e6441136f3d4ba8a0da8d277868979cfbc8ad796..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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
NnX)EH+Nua)3Y(c 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 '} - 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/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/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")); + } +} From 903be12dc9bb707ab710521d594914a6b9f69898 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Jul 2026 14:35:55 +0000 Subject: [PATCH 2/2] Fix locale files losing messages to duplicate YAML keys messages_en.yml defined three keys twice. YAML keeps only the last definition, so each duplicate silently discarded the block above it: - chest.items (lines 194 and 253) dropped six story items - stabilization_core, act1_skeleton_key, boss1_material, boss1_catalyst, boss1_summon_key and boss2_structure_key - leaving English players with the Russian fallback for every one of their names and lore lines. - act5 (lines 71 and 363) dropped exit_blocked, too_far, artifacts_count and returned_overworld. - npc (lines 6 and 420) dropped direction_marker. The entity names had also drifted apart structurally: Russian nests them under npc.entities, English had them at the top level as entities. Act3Listener:165 read entities.end_guardian, a path that only ever existed in the English file, so Russian players saw the boss named "entities.end_guardian" - the raw key that MessageManager returns when a lookup misses. The other two call sites already used npc.entities, so English now follows that shape and the listener was corrected. With the duplicates merged and the remaining gaps translated in both directions, the two files expose an identical set of 322 keys. Two regression tests cover this: one asserts the locale key sets match, the other rejects duplicate keys. Both were checked against the pre-fix file to confirm they fail on the bugs described above. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017441xEm5EainbMSewf8u6e --- .../mmmm/story/listeners/Act3Listener.java | 2 +- src/main/resources/messages.yml | 3 + src/main/resources/messages_en.yml | 25 ++--- .../story/managers/MessageManagerTest.java | 101 ++++++++++++++++++ 4 files changed, 115 insertions(+), 16 deletions(-) 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/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/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