diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 12f335f..3952713 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,32 +21,33 @@ concurrency: jobs: build: - name: Build + test on JDK 21 + name: Build + test on JDK 25 runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7.0.1 - - name: Set up JDK 21 - uses: actions/setup-java@v4 + - name: Set up JDK 25 + uses: actions/setup-java@v6.2.0 with: - java-version: '21' + java-version: '25' distribution: 'temurin' - name: Set up Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v6.4.1 with: - gradle-home-cache-cleanup: true + cache-cleanup: 'always' - name: Build + tests (all modules) + shadowJar run: ./gradlew --no-daemon build :plugin:shadowJar - name: Publish test report if: always() - uses: mikepenz/action-junit-report@v5 + uses: mikepenz/action-junit-report@v6.7.1 with: report_paths: '**/build/test-results/test/TEST-*.xml' fail_on_failure: true + annotate_only: true require_tests: false - name: Resolve plugin JAR path @@ -62,7 +63,7 @@ jobs: echo "javadoc=$(ls api/build/libs/AbstractMenus-api-*-javadoc.jar | head -n1)" >> "$GITHUB_OUTPUT" - name: Upload plugin shadow JAR - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7.0.2 with: name: AbstractMenus-jar path: ${{ steps.plugin_jar.outputs.path }} @@ -70,7 +71,7 @@ jobs: retention-days: 14 - name: Upload api artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7.0.2 with: name: AbstractMenus-api path: | diff --git a/.github/workflows/publish-api.yml b/.github/workflows/publish-api.yml index e3113f2..5e7b386 100644 --- a/.github/workflows/publish-api.yml +++ b/.github/workflows/publish-api.yml @@ -20,18 +20,18 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout (release tag) - uses: actions/checkout@v4 + uses: actions/checkout@v7.0.1 with: ref: ${{ github.event.release.tag_name || inputs.tag }} - - name: Set up JDK 21 - uses: actions/setup-java@v4 + - name: Set up JDK 25 + uses: actions/setup-java@v6.2.0 with: - java-version: '21' + java-version: '25' distribution: 'temurin' - name: Set up Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v6.4.1 - name: Publish :api run: ./gradlew --no-daemon :api:publish diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b77bde6..922b3a3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,19 +19,19 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout (release tag) - uses: actions/checkout@v4 + uses: actions/checkout@v7.0.1 with: ref: ${{ github.event.release.tag_name || inputs.tag }} fetch-depth: 0 - - name: Set up JDK 21 - uses: actions/setup-java@v4 + - name: Set up JDK 25 + uses: actions/setup-java@v6.2.0 with: - java-version: '21' + java-version: '25' distribution: 'temurin' - name: Set up Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v6.4.1 - name: Verify tag matches project version run: | @@ -46,7 +46,7 @@ jobs: exit 1 fi - - name: Build shadow plugin + api artifacts (skip tests โ€” already run on the tagged commit by build.yml) + - name: Build shadow plugin + api artifacts (skip tests - already run on the tagged commit by build.yml) run: ./gradlew --no-daemon :plugin:shadowJar :api:build - name: Resolve artifact paths @@ -63,7 +63,7 @@ jobs: echo "api_doc=$API_DOC" >> "$GITHUB_OUTPUT" - name: Attach JARs to release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3.0.4 with: tag_name: ${{ github.event.release.tag_name || inputs.tag }} files: | diff --git a/CLAUDE.md b/CLAUDE.md index ae430f8..1c0302f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,17 +4,17 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -AbstractMenus is a GUI plugin for SpigotMC/PaperMC Minecraft servers (1.20.6+) that allows creating custom inventory-based menus. Written in Java 21, it uses Gradle with PaperMC Paperweight userdev and Shadow plugin. +AbstractMenus is a GUI plugin for PaperMC/Folia Minecraft servers (26.1+) that allows creating custom inventory-based menus. Written in Java 25, it uses Gradle 9.9.1 with the Paper 26.3 API and Shadow plugin. ## Build Commands ```bash ./gradlew build # Compile and run tests -./gradlew shadowJar # Create fat JAR with bundled dependencies (output: build/libs/AbstractMenus-.jar) -./gradlew test # Run JUnit 5 tests only +./gradlew shadowJar # Create fat JAR with bundled dependencies (output: build/dist/AbstractMenus-.jar) +./gradlew test # Run JUnit 6 tests only ``` -Tests are in `src/test/java/` using JUnit 5 (Jupiter). There are currently 4 test classes. Run a single test with: +Tests are in `plugin/src/test/java/` using JUnit 6 (Jupiter), Mockito, and MockBukkit 26.3. Run a single test with: ```bash ./gradlew test --tests "ru.abstractmenus.TestValueComparator" ``` @@ -65,11 +65,11 @@ Menu (API interface) Menus and config use HOCON format (not YAML). The internal HOCON library is at `ru.abstractmenus.hocon`. ### Key Dependencies -- **API module**: `com.github.AbstractMenus:api` (separate repo, pulled via JitPack) +- **API module**: Local `:api` subproject (`ru.abstractmenus:api`) - **Lombok**: Used throughout for `@Getter`/`@Setter`/`@AllArgsConstructor` - **Kyori Adventure + MiniMessage**: Rich text component API - **FoliaLib**: Thread-safe task scheduling (Folia compatibility) -- **Paperweight userdev**: NMS access with Mojang mappings +- **Paper API**: Compile-only Paper 26.3 API dependency ### Adding a New Action/Rule/Property/Activator 1. Create class in the appropriate `data/` subpackage diff --git a/README.md b/README.md index 9efef4b..ef21374 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ license version -minecraft -paper -java +minecraft +paper +java folia AbstractMenus is a GUI plugin for Paper / Folia Minecraft servers that lets @@ -13,9 +13,7 @@ HOCON configuration. Menus, rules, actions, activators, and item properties are all first-class, extensible via an open type registry, and render through a cached, MiniMessage-aware pipeline. -The plugin supports **Minecraft 1.20.6 onwards**. Older versions were dropped -to keep the codebase free of backwards-compatibility wrappers and to track the -current Paper API surface. +The plugin supports **Minecraft 26.1 onwards** and requires **Java 25**. The build targets the Paper 26.3 API. ## ๐Ÿ”ญ Table of contents - [Features](#-features) @@ -62,47 +60,95 @@ current Paper API surface. ## ๐Ÿงฐ Item properties -Item properties are configured per menu item. The `tooltipDisplay` property -wraps the Paper `tooltip_display` data component (Minecraft 1.21.5+). +Item properties are configured per menu item on Minecraft 26.x. Namespaced keys containing `:` must be quoted in HOCON; keys without a namespace use `minecraft`. -Hide an item's tooltip entirely: +`tooltipDisplay` can hide an entire tooltip or selected data components, alongside normal name and lore properties: ```hocon -material: DIAMOND_SWORD +name: "Bundle" tooltipDisplay { - hideTooltip: true + hideTooltip: false + hiddenComponents: [BUNDLE_CONTENTS, "minecraft:enchantments"] } +lore: ["Contents stay hidden"] ``` -Hide the tooltip of specific components, e.g. only the Bundle contents: +Set `hideTooltip: true` to hide the whole tooltip. Component names accept uppercase, lowercase and namespaced keys. Unknown components fail at menu load. Hidden components are merged with existing flags, so `tooltipDisplay` and `flags` can appear in either order. + +`itemModel` selects a resource-pack item model; `tooltipStyle` selects its tooltip sprites: ```hocon -material: BUNDLE -tooltipDisplay { - hiddenComponents: [ - BUNDLE_CONTENTS - ] -} +itemModel: "example:menu/cake" +tooltipStyle: "example:menu/gold" ``` -Several components at once: +`maxStackSize` accepts integers from 1 through 99, with invalid constants rejected at menu load. It also accepts integer placeholders; results outside that range log a warning and leave the existing stack limit unchanged. Stack sizes above one cannot be combined with the `max_damage` component, so use them on items without durability: ```hocon -tooltipDisplay { - hiddenComponents: [ - BUNDLE_CONTENTS, - ATTRIBUTE_MODIFIERS, - ENCHANTMENTS - ] +material: CAKE +maxStackSize: 99 +count: 64 +``` + +`rarity` accepts `COMMON`, `UNCOMMON`, `RARE` or `EPIC`, including enum placeholders: + +```hocon +rarity: RARE +``` + +`model` keeps the integer format used by existing menus, mapping it to one float in the custom model data component: + +```hocon +model: 123 +``` + +It also accepts an object containing any of the four custom model data lists. Floats, flags, strings and colors support placeholders; colors accept hex, RGB strings or dye names: + +```hocon +model { + floats: [1.0, 2.5] + flags: [true, false] + strings: ["cake"] + colors: ["#ff8000"] } ``` -`hiddenComponents` accepts component keys case-insensitively and in -namespaced form (`BUNDLE_CONTENTS`, `bundle_contents`, -`"minecraft:bundle_contents"`). Unknown component names fail loudly at menu load. -Namespaced keys contain `:`, which HOCON only allows inside quotes, so write -them as `"minecraft:bundle_contents"`. Namespaced keys without an explicit -namespace are assumed to be `minecraft:`. +`glow: true` forces the enchantment glint without adding an enchantment; `false` leaves the item's glint unchanged: + +```hocon +glow: true +``` + +`flags` hides the matching tooltip components and preserves attribute modifiers, including those configured with `attributeModifier`: + +```hocon +material: DIAMOND_SWORD +flags: HIDE_ATTRIBUTES +``` + +`count` is passed through unchanged, including amounts above the item's maximum stack size: + +```hocon +material: DIAMOND_SWORD +count: 64 +``` + +`data` remains accepted as a byte-valued alias for item damage through `Damageable`. It no longer reads or writes legacy material data; use `damage` for new menus. + +## Upgrading + +This version supports Minecraft 26.1+ and Java 25 only. + +`glow` no longer adds an Unbreaking enchantment, and `flags` no longer writes default attribute modifiers. Items handed to players by older versions, such as menu opener items and quest tokens, that carry those old enchantments or modifiers no longer match updated item definitions in `clickItem` activators, `heldItem` and `inventoryItem` rules, or `itemRemove`. Reissue those affected items after updating. + +The built-in placeholder values have changed: + +- `block_data` returns the full block state string, such as `minecraft:oak_log[axis=y]`, instead of a legacy numeric data byte. +- `item_data` returns the item's damage value, or `0` for items without damage. +- `item_localized_name` returns an empty string. +- `block_biome` returns the registry key path in uppercase, such as `PLAINS`. + +`count` is passed through unchanged. ## ๐Ÿ”— Links - [SpigotMC](https://www.spigotmc.org/resources/abstract-menus-an-advanced-gui-plugin.75107/) @@ -110,9 +156,9 @@ namespace are assumed to be `minecraft:`. - [Documentation](https://abstractmenus.github.io/docs/index.html) ## ๐Ÿงฐ Requirements -- **JDK 21+** (target and source) -- **Gradle 9.x** (wrapper included) -- **Paper** or Folia, **1.20.6 โ€“ 1.21.11+** (built against 1.21.11 dev bundle) +- **JDK 25+** (target and source) +- **Gradle 9.9.1** (wrapper included) +- **Paper** or Folia, **26.1+** (built against the Paper 26.3 API) ## ๐Ÿ”จ Build Clone and build a distributable shaded JAR: @@ -131,7 +177,7 @@ Other useful tasks: ```bash ./gradlew build # compile + tests -./gradlew test # JUnit 5 + Mockito tests +./gradlew test # JUnit 6 + Mockito tests ./gradlew test --tests "ru.abstractmenus.util.TestStringUtil" ./gradlew jmh -Pbench=PlaceholderReplace # JMH micro-benchmarks ``` @@ -199,23 +245,16 @@ build, and concurrent map access. ``` ## ๐Ÿงช Tests -~190 unit and integration tests under `src/test/java/`, covering pure-logic -utilities, the HOCON type deserializers, the copy-on-write item contract, the -MiniMessage legacy color path, and Mockito-based regression pinning for -Bukkit API fixes. +Unit and integration tests under `plugin/src/test/java/`, covering pure-logic utilities, the HOCON type deserializers, the copy-on-write item contract, the MiniMessage legacy color path, and Mockito-based regression pinning for Bukkit API fixes. ```bash ./gradlew test ``` -MockBukkit-backed integration tests are scaffolded but currently `@Disabled` -due to an upstream MockBukkit โ†” Paper 1.21.11 registry-bootstrap collision. +MockBukkit 26.3 integration tests verify that the plugin enables, initializes its menu manager, and disables cleanly. ## ๐Ÿ‘ช Contributions and feedback -If you have suggestions for improvement or want to report a bug, feel free -to create an issue or a pull request in the project repository. The -codebase follows standard Java style, uses Lombok for boilerplate, and -targets Java 21 (pattern matching, records, switch expressions welcome). +If you have suggestions for improvement or want to report a bug, feel free to create an issue or a pull request in the project repository. The codebase follows standard Java style, uses Lombok for boilerplate, and targets Java 25 (pattern matching, records, switch expressions welcome). ## ๐Ÿ™Œ Contributors diff --git a/about.md b/about.md index dc53d04..850eea3 100644 --- a/about.md +++ b/about.md @@ -2,12 +2,12 @@ ## Project Summary -AbstractMenus is a GUI plugin for SpigotMC/PaperMC/Folia Minecraft servers that allows creating custom inventory-based menus configured via HOCON files. Players interact with these menus through various activators (commands, items, events), and server admins define menu layouts, rules, and actions declaratively. +AbstractMenus is a GUI plugin for PaperMC/Folia Minecraft servers that allows creating custom inventory-based menus configured via HOCON files. Players interact with these menus through various activators (commands, items, events), and server admins define menu layouts, rules, and actions declaratively. -- **Target MC version:** 1.21.11 -- **Minimum MC version:** 1.20.6 -- **Java version:** 21 -- **Build system:** Gradle 8.8 with Paperweight userdev + Shadow plugin +- **Target MC version:** 26.3 +- **Minimum MC version:** 26.1 +- **Java version:** 25 +- **Build system:** Gradle 9.9.1 with Paper API + Shadow plugin - **Configuration format:** HOCON (bundled library at `ru.abstractmenus.hocon`) ## Architecture Layers @@ -22,7 +22,7 @@ Services Layer (MenuManager, BungeeManager, HeadAnimManager, ProfileStorage, Var Data Layer (Actions, Rules, Activators, ItemProps, Catalogs - ~164 files) | v -API Module (com.github.AbstractMenus:api - external, JitPack) +API Module (:api - local Gradle subproject) ``` ## Plugin Lifecycle @@ -97,17 +97,18 @@ Abstracted through interfaces in `handlers/` package, accessed via static `Handl ## NMS Layer -Version-specific code for features not available in Bukkit API: -- **Book:** `Book_1_15` uses `player.openBook()` (modern API). Legacy versions (1.8-1.14) use NMS reflection - **to be removed** (min version 1.20.6+). -- **Title:** `SenderModern` uses `player.sendTitle()`. `SenderLegacy` uses NMS packets - **to be removed**. -- **ActionBar:** `ActionBar_1_9` uses Spigot `sendMessage(ChatMessageType.ACTION_BAR)`. `ActionBar_1_8` uses NMS - **to be removed**. +Wrappers use the current server APIs without Minecraft version checks. + +- **Book:** `Book_1_15` uses `player.openBook()`. +- **Title:** `SenderModern` uses `player.sendTitle()`. +- **ActionBar:** `ActionBar_1_9` uses Spigot `sendMessage(ChatMessageType.ACTION_BAR)`. ## Key Dependencies | Dependency | Role | |---|---| -| `paper-api` / `paperDevBundle` | Server API + NMS access via Mojang mappings | -| `AbstractMenus:api` | Core API interfaces (Menu, Item, Rule, Action, etc.) | +| `paper-api` | Compile-only Paper 26.3 server API | +| `:api` | Core API interfaces (Menu, Item, Rule, Action, etc.) | | `Lombok` | Boilerplate reduction (@Getter, @Setter, @AllArgsConstructor) | | `Kyori Adventure + MiniMessage` | Rich text component API, MiniMessage format support | | `FoliaLib` | Thread-safe task scheduling (Folia compatibility) | @@ -121,16 +122,13 @@ Version-specific code for features not available in Bukkit API: - `VariablesDao.connection`: Single SQLite `Connection` used from multiple threads without synchronization - `AbstractMenu.placedItems`: Lazy init `if (null) new HashMap<>()` is not thread-safe - `BungeeManager.servers`: `HashSet` accessed from message handler and scheduler threads -- Static singletons (`NMS.minorVersion`, `Title.sender`, `ActionBar.bar`) lack `volatile` keyword ### Memory Leaks - `HeadAnimManager.currentFrames`: ConcurrentHashMap entries accumulate indefinitely - `AbstractMenu.placedItems`: Not cleared after `dropPlaced()` ### Deprecated/Legacy Code -- NMS reflection code for MC versions < 1.20.6 (6 files to remove) - `Skulls.createSkullItem()`: Uses deprecated `ItemStack(Material, int, short)` constructor -- `NMS.sendPacket()`: Legacy reflection-based packet sending ### Removed: RuleJS (Nashorn) The `js` rule type (JavaScript evaluation via Nashorn engine) has been **removed**. It was a security risk (arbitrary code execution) and Nashorn was removed from JDK in Java 15+. Use the `if` rule with `BooleanEvaluator` as an alternative for conditional logic. diff --git a/api/build.gradle b/api/build.gradle index 43bc7df..e7aa961 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -20,14 +20,14 @@ repositories { } dependencies { - compileOnly 'io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT' + compileOnly 'io.papermc.paper:paper-api:26.3.build.+' api 'com.github.AbstractMenus:hocon:1.0.6' - compileOnly 'org.projectlombok:lombok:1.18.44' - annotationProcessor 'org.projectlombok:lombok:1.18.44' + compileOnly 'org.projectlombok:lombok:1.18.48' + annotationProcessor 'org.projectlombok:lombok:1.18.48' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.14.3' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.14.3' + testImplementation 'org.junit.jupiter:junit-jupiter-api:6.1.3' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:6.1.3' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/build.gradle b/build.gradle index 2417ef7..d07085c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ // Common configuration applied to every subproject. -// Subproject-specific plugins (java-library, shadow, paperweight) stay in subprojects. +// Subproject-specific plugins (java-library, shadow) stay in subprojects. plugins { // Root needs the base lifecycle ('build', 'assemble', 'clean') so we @@ -14,8 +14,8 @@ allprojects { repositories { mavenLocal() mavenCentral() - maven { url 'https://repo.papermc.io/repository/maven-public/' } - maven { url 'https://jitpack.io' } + maven { url = 'https://repo.papermc.io/repository/maven-public/' } + maven { url = 'https://jitpack.io' } } } @@ -23,10 +23,10 @@ subprojects { apply plugin: 'java' java { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_25 + targetCompatibility = JavaVersion.VERSION_25 toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(25) } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e644113..51cbd2a 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2dcec85..bdc1ff0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,10 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.0-bin.zip +distributionSha256Sum=d8a18a7a841dd5df825d35a685fad6f2d7f12ac0c280e57b67c27b4a520ba228 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.9.1-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index b740cf1..249efbb 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright ยฉ 2015-2021 the original authors. +# Copyright ยฉ 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,10 +15,12 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -27,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,7 @@ done # 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 +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -112,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -170,7 +171,6 @@ fi # 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" ) @@ -203,15 +203,14 @@ fi 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, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 25da30d..3185a43 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,16 +13,45 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -43,13 +72,14 @@ 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 +1>&2 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. -goto fail +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -57,36 +87,26 @@ 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 +1>&2 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. -goto fail +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel :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 +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel -:omega +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/jitpack.yml b/jitpack.yml index 8e1a95a..c9f8338 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,8 +1,8 @@ jdk: - - openjdk21 + - openjdk25 before_install: - - sdk install java 21.0.3-tem < /dev/null || true + - sdk install java 25.0.5.0+8-tem < /dev/null || true install: - ./gradlew :api:publishToMavenLocal -x test diff --git a/plugin/build.gradle b/plugin/build.gradle index ef92f88..8e692d1 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -1,50 +1,38 @@ -import proguard.gradle.ProGuardTask - -buildscript { - repositories { - mavenCentral() - mavenLocal() - } - dependencies { - classpath 'com.guardsquare:proguard-gradle:7.9.1' - } -} - plugins { - id 'com.gradleup.shadow' version '9.4.1' - id 'io.papermc.paperweight.userdev' version '2.0.0-beta.21' + id 'com.gradleup.shadow' version '9.6.1' } // Plugin-specific repos not already in root repositories { - maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' } - maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } - maven { url 'https://maven.enginehub.org/repo/' } - maven { url 'https://repo.extendedclip.com/content/repositories/placeholderapi/' } - maven { url 'https://mvn.lumine.io/repository/maven-public/' } + maven { url = 'https://repo.tcoded.com/releases' } + maven { url = 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' } + maven { url = 'https://oss.sonatype.org/content/repositories/snapshots' } + maven { url = 'https://maven.enginehub.org/repo/' } + maven { url = 'https://repo.extendedclip.com/content/repositories/placeholderapi/' } + maven { url = 'https://mvn.lumine.io/repository/maven-public/' } maven { - url 'https://repo.codemc.org/repository/maven-public/' + url = 'https://repo.codemc.org/repository/maven-public/' metadataSources { artifact() } } maven { - url 'https://repo.codemc.org/repository/maven-snapshots/' + url = 'https://repo.codemc.org/repository/maven-snapshots/' metadataSources { artifact() } } maven { - url 'https://repo.codemc.org/repository/maven-releases/' + url = 'https://repo.codemc.org/repository/maven-releases/' metadataSources { artifact() } } } dependencies { - compileOnly 'org.projectlombok:lombok:1.18.44' - annotationProcessor 'org.projectlombok:lombok:1.18.44' + compileOnly 'org.projectlombok:lombok:1.18.48' + annotationProcessor 'org.projectlombok:lombok:1.18.48' - paperweight.paperDevBundle("1.21.11-R0.1-SNAPSHOT") + compileOnly 'io.papermc.paper:paper-api:26.3.build.+' implementation project(':api') - implementation 'com.fathzer:javaluator:3.0.6' + implementation 'com.fathzer:javaluator:3.0.9' // Adventure (Component, MiniMessage, LegacyComponentSerializer) is // bundled INSIDE Paper itself (via paper-api's BOM). Pull it in @@ -52,14 +40,14 @@ dependencies { // ~2.7 MB of duplicate Adventure classes into the shadow jar. // adventure-platform-bukkit was used by nothing in our code so it is // dropped entirely. - compileOnly 'net.kyori:adventure-text-minimessage:4.26.1' + compileOnly 'net.kyori:adventure-text-minimessage:5.2.0' - implementation "com.github.technicallycoded:FoliaLib:0.4.4" + implementation "com.tcoded:FoliaLib:0.5.2" shadow files('libs/MMOItems-6.5.4.jar') shadow files('libs/citizens-2.0.jar') - shadow ('de.tr7zw:item-nbt-api-plugin:2.15.7') { + shadow ('de.tr7zw:item-nbt-api-plugin:2.16.1') { exclude group: 'org.bstats', module: 'bstats-bukkit' } shadow ('com.arcaniax:HeadDatabase-API:1.3.2') { @@ -72,13 +60,13 @@ dependencies { shadow ('net.luckperms:api:5.5') { exclude group: 'org.bstats', module: 'bstats-bukkit' } - shadow ('com.sk89q.worldguard:worldguard-bukkit:7.0.0') { + shadow ('com.sk89q.worldguard:worldguard-bukkit:7.0.21') { exclude group: 'org.bstats', module: 'bstats-bukkit' exclude group: 'org.bukkit' exclude group: 'io.papermc.paper' } - compileOnly 'me.clip:placeholderapi:2.12.2' + compileOnly 'me.clip:placeholderapi:2.12.3' shadow ('io.lumine:MythicLib:1.0.12-SNAPSHOT') { exclude group: 'org.bstats', module: 'bstats-bukkit' @@ -86,20 +74,23 @@ dependencies { shadow ('com.github.LoneDev6:api-itemsadder:3.6.1') { exclude group: 'org.bstats', module: 'bstats-bukkit' } - shadow ('net.skinsrestorer:skinsrestorer-api:15.10.0') { + shadow ('net.skinsrestorer:skinsrestorer-api:15.12.6') { exclude group: 'org.bstats', module: 'bstats-bukkit' } - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.14.3' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.14.3' + testImplementation 'io.papermc.paper:paper-api:26.3.build.+' + + testImplementation 'org.junit.jupiter:junit-jupiter-api:6.1.3' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:6.1.3' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - testImplementation 'org.mockbukkit.mockbukkit:mockbukkit-v1.21:4.108.0' - testImplementation 'org.mockito:mockito-core:5.15.2' - testImplementation 'org.mockito:mockito-junit-jupiter:5.15.2' + testRuntimeOnly 'org.xerial:sqlite-jdbc:3.54.1.0' + testImplementation 'org.mockbukkit.mockbukkit:mockbukkit-v26.3:4.120.0' + testImplementation 'org.mockito:mockito-core:5.24.0' + testImplementation 'org.mockito:mockito-junit-jupiter:5.24.0' testImplementation 'org.openjdk.jmh:jmh-core:1.37' testAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37' - testCompileOnly 'org.projectlombok:lombok:1.18.44' - testAnnotationProcessor 'org.projectlombok:lombok:1.18.44' + testCompileOnly 'org.projectlombok:lombok:1.18.48' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.48' } // The :plugin module ships as a fat jar named AbstractMenus-.jar. diff --git a/plugin/libs/paper-1.20.6-147.jar b/plugin/libs/paper-1.20.6-147.jar deleted file mode 100644 index 81237d0..0000000 Binary files a/plugin/libs/paper-1.20.6-147.jar and /dev/null differ diff --git a/plugin/proguard.txt b/plugin/proguard.txt deleted file mode 100644 index 696f8be..0000000 --- a/plugin/proguard.txt +++ /dev/null @@ -1,62 +0,0 @@ -# Include java runtime classes - --libraryjars /lib/rt.jar -#-libraryjars /jmods - --dontwarn -#-dontoptimize - --optimizations !method/inlining/unique --optimizations !class/merging/* - --keep public class ru.abstractmenus.AbstractMenus --keep class ru.abstractmenus.Metrics{*;} --keep class ru.abstractmenus.Metrics$*{*;} --keep class ru.abstractmenus.api.**{*;} --keep class ru.abstractmenus.hocon.api.**{*;} - --keepclassmembers public class ru.abstractmenus.AbstractMenus{ - private ; -} - --keepclassmembernames public class ru.abstractmenus.AbstractMenus{ - private ; -} - --keepclassmembers public class ru.abstractmenus.events.RegionEvent{ - public static org.bukkit.event.HandlerList getHandlerList(); -} - --keepclassmembers class ** { - @org.bukkit.event.EventHandler - ; - @org.bukkit.event.EventHandler - ; -} - --keepattributes *Annotation* --keepattributes EventHandler --keepattributes LineNumberTable, Signature, Exceptions, InnerClasses, EnclosingMethod - --keepclassmembernames class * { - java.lang.Class class$(java.lang.String); - java.lang.Class class$(java.lang.String, boolean); -} - --keepclasseswithmembernames class * { - native ; -} - --keepclassmembers enum * { - public static **[] values(); - public static ** valueOf(java.lang.String); -} - --keepclassmembers class * implements java.io.Serializable { - static final long serialVersionUID; - private static final java.io.ObjectStreamField[] serialPersistentFields; - private void writeObject(java.io.ObjectOutputStream); - private void readObject(java.io.ObjectInputStream); - java.lang.Object writeReplace(); - java.lang.Object readResolve(); -} \ No newline at end of file diff --git a/plugin/src/main/java/ru/abstractmenus/AbstractMenus.java b/plugin/src/main/java/ru/abstractmenus/AbstractMenus.java index 4af53f8..001d057 100644 --- a/plugin/src/main/java/ru/abstractmenus/AbstractMenus.java +++ b/plugin/src/main/java/ru/abstractmenus/AbstractMenus.java @@ -4,7 +4,6 @@ import lombok.Getter; import lombok.Setter; import org.bukkit.Bukkit; -import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.ServicePriority; @@ -48,17 +47,13 @@ import ru.abstractmenus.util.TimeUtil; import ru.abstractmenus.util.bukkit.BukkitTasks; import ru.abstractmenus.util.bukkit.Events; -import ru.abstractmenus.util.proxy.ClassInfo; import ru.abstractmenus.variables.VariableManagerImpl; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Objects; import java.util.Optional; @Getter -public final class AbstractMenus extends JavaPlugin { +public class AbstractMenus extends JavaPlugin { private static AbstractMenus instance; @@ -103,8 +98,8 @@ public void onLoad() { public void onEnable() { try { instance = this; - isProxyMode = determineProxy(); Logger.set(getLogger()); + isProxyMode = determineProxy(); metrics = new Metrics(this); foliaLib = new FoliaLib(this); @@ -281,31 +276,8 @@ public static AbstractMenus instance() { return instance; } - // from SkinRestorer. - // - // YamlConfiguration.loadConfiguration below reads spigot.yml / paper.yml - // synchronously on the main thread. These files are <10 KB on a real - // server and the call only fires once, during onEnable, before any - // gameplay can be affected. Acceptable startup-only IO. public boolean determineProxy() { - Path spigotFile = Paths.get("spigot.yml"); - Path paperFile = Paths.get("paper.yml"); - - if (Optional.of(getConfig()).map(config -> - config.getBoolean("settings.bungeecord")).orElse(false)) { - return true; - } else if (ClassInfo.get().isSpigot() // Only consider files if classes for that platform are present - && Files.exists(spigotFile) - && YamlConfiguration.loadConfiguration(spigotFile.toFile()) - .getBoolean("settings.bungeecord")) { - return true; - } else if (Optional.of(Bukkit.spigot().getPaperConfig()).map(config -> - config.getBoolean("settings.velocity-support.enabled") - || config.getBoolean("proxies.velocity.enabled")).orElse(false)) { - return true; - } else return ClassInfo.get().isPaper() // Only consider files if classes for that platform are present - && Files.exists(paperFile) - && YamlConfiguration.loadConfiguration(paperFile.toFile()) - .getBoolean("settings.velocity-support.enabled"); + return getConfig().getBoolean("settings.bungeecord") + || getServer().getServerConfig().isProxyEnabled(); } } diff --git a/plugin/src/main/java/ru/abstractmenus/Metrics.java b/plugin/src/main/java/ru/abstractmenus/Metrics.java index 6abeae9..a04fd06 100644 --- a/plugin/src/main/java/ru/abstractmenus/Metrics.java +++ b/plugin/src/main/java/ru/abstractmenus/Metrics.java @@ -7,7 +7,6 @@ import lombok.Getter; import org.bukkit.Bukkit; import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.RegisteredServiceProvider; import org.bukkit.plugin.ServicePriority; @@ -221,17 +220,7 @@ public JsonObject getPluginData() { @SuppressWarnings("UnstableApiUsage") private JsonObject getServerData() { // Minecraft specific data - int playerAmount; - try { - // Around MC 1.8 the return type was changed to a collection from an array, - // This fixes java.lang.NoSuchMethodError: org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection; - Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers"); - playerAmount = onlinePlayersMethod.getReturnType().equals(Collection.class) - ? ((Collection) onlinePlayersMethod.invoke(Bukkit.getServer())).size() - : ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length; - } catch (Exception e) { - playerAmount = Bukkit.getOnlinePlayers().size(); // Just use the new method if the Reflection failed - } + int playerAmount = Bukkit.getOnlinePlayers().size(); int onlineMode = Bukkit.getOnlineMode() ? 1 : 0; String bukkitVersion = Bukkit.getVersion(); String bukkitName = Bukkit.getName(); diff --git a/plugin/src/main/java/ru/abstractmenus/command/CommandManager.java b/plugin/src/main/java/ru/abstractmenus/command/CommandManager.java index d454e51..317517e 100644 --- a/plugin/src/main/java/ru/abstractmenus/command/CommandManager.java +++ b/plugin/src/main/java/ru/abstractmenus/command/CommandManager.java @@ -2,11 +2,10 @@ import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; -import org.bukkit.command.SimpleCommandMap; +import org.bukkit.command.CommandMap; import org.bukkit.plugin.Plugin; import ru.abstractmenus.command.bukkit.CommandWrapper; -import java.lang.reflect.Method; import java.util.*; import java.util.stream.Collectors; @@ -14,19 +13,12 @@ public final class CommandManager { private final Plugin plugin; private final Map commands = new HashMap<>(); - private SimpleCommandMap commandMap; + private final CommandMap commandMap; public CommandManager(Plugin plugin) { this.plugin = plugin; - try { - String version = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3]; - Class craftServerClass = Class.forName(String.format("org.bukkit.craftbukkit.%s.CraftServer", version)); - Method getCommandMap = craftServerClass.getDeclaredMethod("getCommandMap"); - commandMap = (SimpleCommandMap) getCommandMap.invoke(Bukkit.getServer()); - } catch (Exception e) { - commandMap = (SimpleCommandMap) Bukkit.getCommandMap(); - } + commandMap = Bukkit.getCommandMap(); } public Command get(String name) { diff --git a/plugin/src/main/java/ru/abstractmenus/core/CoreItemPropsBundle.java b/plugin/src/main/java/ru/abstractmenus/core/CoreItemPropsBundle.java index ce4d247..1a12f53 100644 --- a/plugin/src/main/java/ru/abstractmenus/core/CoreItemPropsBundle.java +++ b/plugin/src/main/java/ru/abstractmenus/core/CoreItemPropsBundle.java @@ -35,6 +35,10 @@ import ru.abstractmenus.data.properties.PropNameLight; import ru.abstractmenus.data.properties.PropLoreLight; import ru.abstractmenus.data.properties.PropTooltipDisplay; +import ru.abstractmenus.data.properties.PropItemModel; +import ru.abstractmenus.data.properties.PropTooltipStyle; +import ru.abstractmenus.data.properties.PropMaxStackSize; +import ru.abstractmenus.data.properties.PropRarity; /** * Core item property registrations. Mirrors {@code ItemProps.init()} before @@ -69,6 +73,10 @@ void register(AbstractMenusApi api, MenuExtension owner) { api.itemProperties().register("bookData", PropBookData.class, new PropBookData.Serializer(), owner); api.itemProperties().register("bannerData", PropBannerData.class, new PropBannerData.Serializer(), owner); api.itemProperties().register("shieldData", PropShieldData.class, new PropShieldData.Serializer(), owner); + api.itemProperties().register("itemModel", PropItemModel.class, new PropItemModel.Serializer(), owner); + api.itemProperties().register("tooltipStyle", PropTooltipStyle.class, new PropTooltipStyle.Serializer(), owner); + api.itemProperties().register("maxStackSize", PropMaxStackSize.class, new PropMaxStackSize.Serializer(), owner); + api.itemProperties().register("rarity", PropRarity.class, new PropRarity.Serializer(), owner); api.itemProperties().register("model", PropModel.class, new PropModel.Serializer(), owner); api.itemProperties().register("enchantStore", PropEnchantStore.class, new PropEnchantStore.Serializer(), owner); api.itemProperties().register("recipes", PropKnowledgeBook.class, new PropKnowledgeBook.Serializer(), owner); diff --git a/plugin/src/main/java/ru/abstractmenus/data/BannerData.java b/plugin/src/main/java/ru/abstractmenus/data/BannerData.java index 854c329..f77329b 100644 --- a/plugin/src/main/java/ru/abstractmenus/data/BannerData.java +++ b/plugin/src/main/java/ru/abstractmenus/data/BannerData.java @@ -4,12 +4,15 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import ru.abstractmenus.hocon.api.ConfigNode; -import ru.abstractmenus.hocon.api.serialize.NodeSerializeException; -import ru.abstractmenus.hocon.api.serialize.NodeSerializer; +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; import org.bukkit.DyeColor; +import org.bukkit.NamespacedKey; import org.bukkit.block.banner.Pattern; import org.bukkit.block.banner.PatternType; +import ru.abstractmenus.hocon.api.ConfigNode; +import ru.abstractmenus.hocon.api.serialize.NodeSerializeException; +import ru.abstractmenus.hocon.api.serialize.NodeSerializer; import java.util.ArrayList; import java.util.List; @@ -50,7 +53,55 @@ private List parseFromJson(ConfigNode node, String json) throws NodeSer String pattern = object.get("Pattern").getAsString(); byte colorByte = object.get("Color").getAsByte(); - PatternType patternType = PatternType.getByIdentifier(pattern); + // Banner JSON uses compact pattern identifiers. + String patternKey = switch (pattern) { + case "b" -> "base"; + case "bl" -> "square_bottom_left"; + case "br" -> "square_bottom_right"; + case "tl" -> "square_top_left"; + case "tr" -> "square_top_right"; + case "bs" -> "stripe_bottom"; + case "ts" -> "stripe_top"; + case "ls" -> "stripe_left"; + case "rs" -> "stripe_right"; + case "cs" -> "stripe_center"; + case "ms" -> "stripe_middle"; + case "drs" -> "stripe_downright"; + case "dls" -> "stripe_downleft"; + case "ss" -> "small_stripes"; + case "cr" -> "cross"; + case "sc" -> "straight_cross"; + case "bt" -> "triangle_bottom"; + case "tt" -> "triangle_top"; + case "bts" -> "triangles_bottom"; + case "tts" -> "triangles_top"; + case "ld" -> "diagonal_left"; + case "rd" -> "diagonal_up_right"; + case "lud" -> "diagonal_up_left"; + case "rud" -> "diagonal_right"; + case "mc" -> "circle"; + case "mr" -> "rhombus"; + case "vh" -> "half_vertical"; + case "hh" -> "half_horizontal"; + case "vhr" -> "half_vertical_right"; + case "hhb" -> "half_horizontal_bottom"; + case "bo" -> "border"; + case "cbo" -> "curly_border"; + case "cre" -> "creeper"; + case "gra" -> "gradient"; + case "gru" -> "gradient_up"; + case "bri" -> "bricks"; + case "sku" -> "skull"; + case "flo" -> "flower"; + case "moj" -> "mojang"; + case "glb" -> "globe"; + case "pig" -> "piglin"; + case "flw" -> "flow"; + case "gus" -> "guster"; + default -> pattern; + }; + NamespacedKey key = NamespacedKey.fromString(patternKey); + PatternType patternType = key == null ? null : RegistryAccess.registryAccess().getRegistry(RegistryKey.BANNER_PATTERN).get(key); DyeColor color = DyeColor.getByWoolData(colorByte); if (patternType == null) throw new NodeSerializeException(node, "Cannot serialize banner pattern from JSON"); diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropCount.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropCount.java index ebc02bf..9db5aa2 100644 --- a/plugin/src/main/java/ru/abstractmenus/data/properties/PropCount.java +++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropCount.java @@ -31,7 +31,6 @@ public boolean isApplyMeta() { @Override public void apply(ItemStack itemStack, ItemMeta meta, Player player, Menu menu) { itemStack.setAmount(amount.getInt(player, menu)); - } public static class Serializer implements NodeSerializer { diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropDamage.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropDamage.java index 6dcf20f..fdeba8d 100644 --- a/plugin/src/main/java/ru/abstractmenus/data/properties/PropDamage.java +++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropDamage.java @@ -14,17 +14,8 @@ public class PropDamage implements ItemProperty { private final TypeInt damage; - private static boolean legacy; - static { - try { - Class.forName("org.bukkit.inventory.meta.Damageable"); - } catch (Exception e) { - legacy = true; - } - } - - private PropDamage(TypeInt damage){ + private PropDamage(TypeInt damage) { this.damage = damage; } @@ -35,20 +26,13 @@ public boolean canReplaceMaterial() { @Override public boolean isApplyMeta() { - return !legacy; + return true; } @Override public void apply(ItemStack itemStack, ItemMeta meta, Player player, Menu menu) { - int dmg = damage.getInt(player, menu); - - if (legacy) { - itemStack.setDurability((short) dmg); - } else { - if (meta instanceof Damageable) { - Damageable damageable = (Damageable) meta; - damageable.setDamage(dmg); - } + if (meta instanceof Damageable damageable) { + damageable.setDamage(damage.getInt(player, menu)); } } diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropData.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropData.java index 1b314ba..613f7f7 100644 --- a/plugin/src/main/java/ru/abstractmenus/data/properties/PropData.java +++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropData.java @@ -5,6 +5,7 @@ import ru.abstractmenus.hocon.api.serialize.NodeSerializer; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.Damageable; import org.bukkit.inventory.meta.ItemMeta; import ru.abstractmenus.api.inventory.Menu; import ru.abstractmenus.api.inventory.ItemProperty; @@ -25,14 +26,14 @@ public boolean canReplaceMaterial() { @Override public boolean isApplyMeta() { - return false; + return true; } @Override public void apply(ItemStack itemStack, ItemMeta meta, Player player, Menu menu) { - byte resolved = data.getByte(player, menu); - itemStack.getData().setData(resolved); - itemStack.setDurability(resolved); + if (meta instanceof Damageable damageable) { + damageable.setDamage(data.getByte(player, menu)); + } } public static class Serializer implements NodeSerializer { diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropFlags.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropFlags.java index e99838d..2f525f6 100644 --- a/plugin/src/main/java/ru/abstractmenus/data/properties/PropFlags.java +++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropFlags.java @@ -1,13 +1,9 @@ package ru.abstractmenus.data.properties; -import com.google.common.collect.Multimap; -import org.bukkit.attribute.Attribute; -import org.bukkit.attribute.AttributeModifier; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; -import ru.abstractmenus.api.Logger; import ru.abstractmenus.api.inventory.ItemProperty; import ru.abstractmenus.api.inventory.Menu; import ru.abstractmenus.datatype.TypeEnum; @@ -16,7 +12,6 @@ import ru.abstractmenus.hocon.api.serialize.NodeSerializer; import ru.abstractmenus.util.StringUtil; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -42,28 +37,10 @@ public boolean isApplyMeta() { @Override public void apply(ItemStack itemStack, ItemMeta meta, Player player, Menu menu) { for (TypeEnum flag : flags) { - Method method = getMethodForAttributeModifiers(itemStack); - if (method != null) { - try { - @SuppressWarnings("unchecked") - Multimap attributeModifiers = (Multimap) method.invoke(itemStack.getType()); - meta.setAttributeModifiers(attributeModifiers); - } catch (Exception e) { - Logger.warning("Failed to apply attribute modifiers: " + e.getMessage()); - } - } meta.addItemFlags(flag.getEnum(ItemFlag.class, player, menu)); } } - private Method getMethodForAttributeModifiers(ItemStack itemStack) { - try { - return itemStack.getType().getClass().getMethod("getDefaultAttributeModifiers"); - } catch (NoSuchMethodException e) { - return null; - } - } - public static class Serializer implements NodeSerializer { @Override @@ -96,4 +73,4 @@ private TypeEnum getItemFlag(ConfigNode node, String value) throws Nod } } } -} \ No newline at end of file +} diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropGlow.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropGlow.java index 6f64920..dfa2375 100644 --- a/plugin/src/main/java/ru/abstractmenus/data/properties/PropGlow.java +++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropGlow.java @@ -4,9 +4,7 @@ import ru.abstractmenus.hocon.api.ConfigNode; import ru.abstractmenus.hocon.api.serialize.NodeSerializeException; import ru.abstractmenus.hocon.api.serialize.NodeSerializer; -import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import ru.abstractmenus.api.inventory.Menu; @@ -16,18 +14,8 @@ public class PropGlow implements ItemProperty { private final TypeBool glow; - private Enchantment durabilityEnch; - - private PropGlow(TypeBool glow){ + private PropGlow(TypeBool glow) { this.glow = glow; - - try { - durabilityEnch = (Enchantment) Enchantment.class - .getField("DURABILITY") - .get(null); - } catch (Throwable t) { - // Ignore - } } @Override @@ -42,14 +30,8 @@ public boolean isApplyMeta() { @Override public void apply(ItemStack itemStack, ItemMeta meta, Player player, Menu menu) { - if (glow.getBool(player, null)) { - if (durabilityEnch != null) { - meta.addEnchant(durabilityEnch, 1, true); - } else { - meta.addEnchant(Enchantment.UNBREAKING, 1, true); - } - - meta.addItemFlags(ItemFlag.HIDE_ENCHANTS); + if (glow.getBool(player, menu)) { + meta.setEnchantmentGlintOverride(true); } } diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropItemModel.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropItemModel.java new file mode 100644 index 0000000..649fd0e --- /dev/null +++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropItemModel.java @@ -0,0 +1,47 @@ +package ru.abstractmenus.data.properties; + +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import ru.abstractmenus.api.inventory.ItemProperty; +import ru.abstractmenus.api.inventory.Menu; +import ru.abstractmenus.hocon.api.ConfigNode; +import ru.abstractmenus.hocon.api.serialize.NodeSerializeException; +import ru.abstractmenus.hocon.api.serialize.NodeSerializer; + +public class PropItemModel implements ItemProperty { + + private final NamespacedKey itemModel; + + private PropItemModel(NamespacedKey itemModel) { + this.itemModel = itemModel; + } + + @Override + public boolean canReplaceMaterial() { + return false; + } + + @Override + public boolean isApplyMeta() { + return true; + } + + @Override + public void apply(ItemStack itemStack, ItemMeta meta, Player player, Menu menu) { + meta.setItemModel(itemModel); + } + + public static class Serializer implements NodeSerializer { + + @Override + public PropItemModel deserialize(Class type, ConfigNode node) throws NodeSerializeException { + NamespacedKey key = NamespacedKey.fromString(node.getString("")); + if (key == null) { + throw new NodeSerializeException(node, "Invalid namespaced key"); + } + return new PropItemModel(key); + } + } +} diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropMaxStackSize.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropMaxStackSize.java new file mode 100644 index 0000000..e63dde1 --- /dev/null +++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropMaxStackSize.java @@ -0,0 +1,57 @@ +package ru.abstractmenus.data.properties; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import ru.abstractmenus.api.Logger; +import ru.abstractmenus.api.inventory.ItemProperty; +import ru.abstractmenus.api.inventory.Menu; +import ru.abstractmenus.datatype.DataType; +import ru.abstractmenus.datatype.TypeInt; +import ru.abstractmenus.hocon.api.ConfigNode; +import ru.abstractmenus.hocon.api.serialize.NodeSerializeException; +import ru.abstractmenus.hocon.api.serialize.NodeSerializer; + +public class PropMaxStackSize implements ItemProperty { + + private final TypeInt maxStackSize; + + private PropMaxStackSize(TypeInt maxStackSize) { + this.maxStackSize = maxStackSize; + } + + @Override + public boolean canReplaceMaterial() { + return false; + } + + @Override + public boolean isApplyMeta() { + return true; + } + + @Override + public void apply(ItemStack itemStack, ItemMeta meta, Player player, Menu menu) { + int size = maxStackSize.getInt(player, menu); + if (size < 1 || size > 99) { + Logger.warning("Skipping maxStackSize value outside 1..99: " + size); + return; + } + meta.setMaxStackSize(size); + } + + public static class Serializer implements NodeSerializer { + + @Override + public PropMaxStackSize deserialize(Class type, ConfigNode node) throws NodeSerializeException { + TypeInt value = new TypeInt.Serializer().deserialize(TypeInt.class, node); + if (!DataType.hasPlaceholder(node.getString(""))) { + int size = value.getInt(null, null); + if (size < 1 || size > 99) { + throw new NodeSerializeException(node, "maxStackSize must be between 1 and 99"); + } + } + return new PropMaxStackSize(value); + } + } +} diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropModel.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropModel.java index 794a786..2f5ee0e 100644 --- a/plugin/src/main/java/ru/abstractmenus/data/properties/PropModel.java +++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropModel.java @@ -1,21 +1,38 @@ package ru.abstractmenus.data.properties; -import ru.abstractmenus.hocon.api.ConfigNode; -import ru.abstractmenus.hocon.api.serialize.NodeSerializeException; -import ru.abstractmenus.hocon.api.serialize.NodeSerializer; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; -import ru.abstractmenus.api.inventory.Menu; +import org.bukkit.inventory.meta.components.CustomModelDataComponent; +import ru.abstractmenus.api.AbstractMenusApi; import ru.abstractmenus.api.inventory.ItemProperty; +import ru.abstractmenus.api.inventory.Menu; +import ru.abstractmenus.datatype.DataType; +import ru.abstractmenus.datatype.TypeBool; +import ru.abstractmenus.datatype.TypeColor; +import ru.abstractmenus.datatype.TypeFloat; import ru.abstractmenus.datatype.TypeInt; +import ru.abstractmenus.hocon.api.ConfigNode; +import ru.abstractmenus.hocon.api.serialize.NodeSerializeException; +import ru.abstractmenus.hocon.api.serialize.NodeSerializer; + +import java.util.ArrayList; +import java.util.List; public class PropModel implements ItemProperty { private final TypeInt model; + private final List floats; + private final List flags; + private final List strings; + private final List colors; - private PropModel(TypeInt model){ + private PropModel(TypeInt model, List floats, List flags, List strings, List colors) { this.model = model; + this.floats = floats; + this.flags = flags; + this.strings = strings; + this.colors = colors; } @Override @@ -30,19 +47,43 @@ public boolean isApplyMeta() { @Override public void apply(ItemStack itemStack, ItemMeta meta, Player player, Menu menu) { - try{ - meta.setCustomModelData(model.getInt(player, menu)); - } catch (Exception e){ - /* Ignore */ - } + CustomModelDataComponent component = meta.getCustomModelDataComponent(); + component.setFloats(model != null ? List.of((float) model.getInt(player, menu)) : floats.stream().map(value -> value.getFloat(player, menu)).toList()); + component.setFlags(flags.stream().map(value -> value.getBool(player, menu)).toList()); + component.setStrings(strings.stream().map(value -> DataType.hasPlaceholder(value) ? AbstractMenusApi.get().providers().placeholders().resolve().replace(player, value) : value).toList()); + component.setColors(colors.stream().map(value -> value.getColor(player, menu)).toList()); + meta.setCustomModelDataComponent(component); } public static class Serializer implements NodeSerializer { @Override public PropModel deserialize(Class type, ConfigNode node) throws NodeSerializeException { - return new PropModel(node.getValue(TypeInt.class)); + if (node.isPrimitive()) { + return new PropModel(new TypeInt.Serializer().deserialize(TypeInt.class, node), List.of(), List.of(), List.of(), List.of()); + } + if (node.isList() || node.rawValue() == null) { + throw new NodeSerializeException(node, "model must be an integer or an object"); + } + return new PropModel(null, + readList(node.node("floats"), TypeFloat.class, new TypeFloat.Serializer()), + readList(node.node("flags"), TypeBool.class, new TypeBool.Serializer()), + readList(node.node("strings"), String.class, (stringType, entry) -> entry.getString()), + readList(node.node("colors"), TypeColor.class, new TypeColor.Serializer())); } + private List readList(ConfigNode node, Class type, NodeSerializer serializer) throws NodeSerializeException { + if (node.rawValue() == null) { + return List.of(); + } + if (!node.isList()) { + throw new NodeSerializeException(node, "Model component field must be a list"); + } + List values = new ArrayList<>(); + for (ConfigNode entry : node.childrenList()) { + values.add(serializer.deserialize(type, entry)); + } + return values; + } } } diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropRarity.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropRarity.java new file mode 100644 index 0000000..b6531f4 --- /dev/null +++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropRarity.java @@ -0,0 +1,55 @@ +package ru.abstractmenus.data.properties; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemRarity; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import ru.abstractmenus.api.inventory.ItemProperty; +import ru.abstractmenus.api.inventory.Menu; +import ru.abstractmenus.datatype.DataType; +import ru.abstractmenus.datatype.TypeEnum; +import ru.abstractmenus.hocon.api.ConfigNode; +import ru.abstractmenus.hocon.api.serialize.NodeSerializeException; +import ru.abstractmenus.hocon.api.serialize.NodeSerializer; + +import java.util.Locale; + +public class PropRarity implements ItemProperty { + + private final TypeEnum rarity; + + private PropRarity(TypeEnum rarity) { + this.rarity = rarity; + } + + @Override + public boolean canReplaceMaterial() { + return false; + } + + @Override + public boolean isApplyMeta() { + return true; + } + + @Override + public void apply(ItemStack itemStack, ItemMeta meta, Player player, Menu menu) { + meta.setRarity(rarity.getEnum(ItemRarity.class, player, menu)); + } + + public static class Serializer implements NodeSerializer { + + @Override + public PropRarity deserialize(Class type, ConfigNode node) throws NodeSerializeException { + String value = node.getString(""); + if (DataType.hasPlaceholder(value)) { + return new PropRarity(new TypeEnum<>(value)); + } + try { + return new PropRarity(new TypeEnum<>(ItemRarity.valueOf(value.toUpperCase(Locale.ROOT)))); + } catch (IllegalArgumentException e) { + throw new NodeSerializeException(node, "Unknown item rarity '" + value + "'"); + } + } + } +} diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropTooltipDisplay.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropTooltipDisplay.java index cb3159b..c3347c4 100644 --- a/plugin/src/main/java/ru/abstractmenus/data/properties/PropTooltipDisplay.java +++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropTooltipDisplay.java @@ -3,6 +3,7 @@ import io.papermc.paper.datacomponent.DataComponentType; import io.papermc.paper.datacomponent.DataComponentTypes; import io.papermc.paper.datacomponent.item.TooltipDisplay; +import org.bukkit.NamespacedKey; import org.bukkit.Registry; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -14,25 +15,10 @@ import ru.abstractmenus.hocon.api.serialize.NodeSerializeException; import ru.abstractmenus.hocon.api.serialize.NodeSerializer; -import java.util.Collections; -import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Locale; -import java.util.Map; import java.util.Set; -/** - * Item property controlling the {@code tooltip_display} data component - * ({@link TooltipDisplay}). Hides a tooltip entirely via {@code hideTooltip} - * or hides the tooltip of specific components via {@code hiddenComponents}. - * - *
{@code
- * tooltipDisplay {
- *     hideTooltip: false
- *     hiddenComponents: [BUNDLE_CONTENTS, ENCHANTMENTS]
- * }
- * }
- */ public class PropTooltipDisplay implements ItemProperty { private final TypeBool hideTooltip; @@ -50,7 +36,7 @@ public boolean canReplaceMaterial() { @Override public boolean isApplyMeta() { - return true; + return false; } @Override @@ -69,9 +55,13 @@ private void tooltipDisplay(ItemStack itemStack, Player player, Menu menu) { ? hideTooltip.getBool(player, menu) : (existing != null && existing.hideTooltip()); - Set hidden = (hiddenComponents != null) - ? hiddenComponents - : (existing != null ? existing.hiddenComponents() : Collections.emptySet()); + Set hidden = new LinkedHashSet<>(); + if (existing != null) { + hidden.addAll(existing.hiddenComponents()); + } + if (hiddenComponents != null) { + hidden.addAll(hiddenComponents); + } itemStack.setData(DataComponentTypes.TOOLTIP_DISPLAY, TooltipDisplay.tooltipDisplay() .hideTooltip(hide) @@ -112,65 +102,11 @@ public PropTooltipDisplay deserialize(Class type, ConfigNode node) throws NodeSe } - /** - * Test hook: replaces the live {@link Registry#DATA_COMPONENT_TYPE} source. - * Accepts any {@link Iterable} so tests do not need to mock the Registry - * class itself (its static initializer requires a live Paper server). - */ - static void configureRegistry(Iterable registry) { - registryOverride = registry; - cache = null; - } - - /** - * Test hook: restores the live registry source and clears the cache. - */ - static void resetRegistry() { - registryOverride = null; - cache = null; - } - - static DataComponentType resolveComponent(String raw) { + private static DataComponentType resolveComponent(String raw) { if (raw == null) { return null; } - String key = raw.trim().toLowerCase(Locale.ROOT); - int colon = key.indexOf(':'); - if (colon >= 0) { - key = key.substring(colon + 1); - } - if (key.isEmpty()) { - return null; - } - return registryMap().get(key); - } - - private static volatile Iterable registryOverride; - private static volatile Map cache; - - private static Map registryMap() { - Map local = cache; - if (local != null) { - return local; - } - Iterable source = registryOverride; - if (source == null) { - try { - source = Registry.DATA_COMPONENT_TYPE; - } catch (Throwable t) { - source = null; - } - } - Map built = new HashMap<>(); - if (source != null) { - for (DataComponentType type : source) { - if (type.getKey() == null) { - continue; - } - built.put(type.getKey().getKey().toLowerCase(Locale.ROOT), type); - } - } - cache = built; - return built; + NamespacedKey key = NamespacedKey.fromString(raw.trim().toLowerCase(Locale.ROOT)); + return key == null ? null : Registry.DATA_COMPONENT_TYPE.get(key); } } diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropTooltipStyle.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropTooltipStyle.java new file mode 100644 index 0000000..14a1a93 --- /dev/null +++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropTooltipStyle.java @@ -0,0 +1,47 @@ +package ru.abstractmenus.data.properties; + +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import ru.abstractmenus.api.inventory.ItemProperty; +import ru.abstractmenus.api.inventory.Menu; +import ru.abstractmenus.hocon.api.ConfigNode; +import ru.abstractmenus.hocon.api.serialize.NodeSerializeException; +import ru.abstractmenus.hocon.api.serialize.NodeSerializer; + +public class PropTooltipStyle implements ItemProperty { + + private final NamespacedKey tooltipStyle; + + private PropTooltipStyle(NamespacedKey tooltipStyle) { + this.tooltipStyle = tooltipStyle; + } + + @Override + public boolean canReplaceMaterial() { + return false; + } + + @Override + public boolean isApplyMeta() { + return true; + } + + @Override + public void apply(ItemStack itemStack, ItemMeta meta, Player player, Menu menu) { + meta.setTooltipStyle(tooltipStyle); + } + + public static class Serializer implements NodeSerializer { + + @Override + public PropTooltipStyle deserialize(Class type, ConfigNode node) throws NodeSerializeException { + NamespacedKey key = NamespacedKey.fromString(node.getString("")); + if (key == null) { + throw new NodeSerializeException(node, "Invalid namespaced key"); + } + return new PropTooltipStyle(key); + } + } +} diff --git a/plugin/src/main/java/ru/abstractmenus/extractors/BlockExtractor.java b/plugin/src/main/java/ru/abstractmenus/extractors/BlockExtractor.java index d54b35f..d7aad22 100644 --- a/plugin/src/main/java/ru/abstractmenus/extractors/BlockExtractor.java +++ b/plugin/src/main/java/ru/abstractmenus/extractors/BlockExtractor.java @@ -1,8 +1,12 @@ package ru.abstractmenus.extractors; +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; import org.bukkit.block.Block; import ru.abstractmenus.api.ValueExtractor; +import java.util.Locale; + public class BlockExtractor implements ValueExtractor { public static final BlockExtractor INSTANCE = new BlockExtractor(); @@ -15,14 +19,14 @@ public String extract(Object obj, String placeholder) { return switch (placeholder) { default -> null; case "block_type" -> block.getType().toString(); - case "block_data" -> String.valueOf(block.getData()); // 1.12- + case "block_data" -> block.getBlockData().getAsString(); case "block_world" -> block.getWorld().getName(); case "block_x" -> String.valueOf(block.getX()); case "block_y" -> String.valueOf(block.getY()); case "block_z" -> String.valueOf(block.getZ()); case "block_power" -> String.valueOf(block.getBlockPower()); case "block_temp" -> String.valueOf(block.getTemperature()); - case "block_biome" -> block.getBiome().name(); + case "block_biome" -> RegistryAccess.registryAccess().getRegistry(RegistryKey.BIOME).getKeyOrThrow(block.getBiome()).getKey().toUpperCase(Locale.ROOT); }; } diff --git a/plugin/src/main/java/ru/abstractmenus/extractors/ItemStackExtractor.java b/plugin/src/main/java/ru/abstractmenus/extractors/ItemStackExtractor.java index 56f982f..c3fb06f 100644 --- a/plugin/src/main/java/ru/abstractmenus/extractors/ItemStackExtractor.java +++ b/plugin/src/main/java/ru/abstractmenus/extractors/ItemStackExtractor.java @@ -1,6 +1,7 @@ package ru.abstractmenus.extractors; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.Damageable; import org.bukkit.inventory.meta.ItemMeta; import ru.abstractmenus.api.ValueExtractor; import ru.abstractmenus.util.bukkit.ItemUtil; @@ -16,7 +17,7 @@ public String extract(Object obj, String placeholder) { String result = switch (placeholder) { case "item_type" -> item.getType().toString(); - case "item_data" -> String.valueOf(item.getData().getData()); // 1.12- + case "item_data" -> String.valueOf(item.getItemMeta() instanceof Damageable damageable ? damageable.getDamage() : 0); case "item_amount" -> String.valueOf(item.getAmount()); case "item_max_stack" -> String.valueOf(item.getMaxStackSize()); case "item_serialized" -> ItemUtil.encodeStack(item); @@ -32,7 +33,7 @@ public String extract(Object obj, String placeholder) { if (meta != null) { return switch (placeholder) { case "item_display_name" -> meta.getDisplayName(); - case "item_localized_name" -> meta.getLocalizedName(); + case "item_localized_name" -> ""; case "item_model" -> String.valueOf(meta.getCustomModelData()); // 1.14+ default -> ""; }; diff --git a/plugin/src/main/java/ru/abstractmenus/impl/AbstractMenusApiImpl.java b/plugin/src/main/java/ru/abstractmenus/impl/AbstractMenusApiImpl.java index a982dc9..b1d5dab 100644 --- a/plugin/src/main/java/ru/abstractmenus/impl/AbstractMenusApiImpl.java +++ b/plugin/src/main/java/ru/abstractmenus/impl/AbstractMenusApiImpl.java @@ -113,12 +113,6 @@ public Optional getOpenedMenu(Player player) { @Override public String apiVersion() { - // Paper 1.21+ exposes getPluginMeta().getVersion(); fall back to - // getDescription().getVersion() if the new API isn't present. - try { - return plugin.getPluginMeta().getVersion(); - } catch (NoSuchMethodError | AbstractMethodError ignored) { - return plugin.getDescription().getVersion(); - } + return plugin.getPluginMeta().getVersion(); } } diff --git a/plugin/src/main/java/ru/abstractmenus/menu/AbstractMenu.java b/plugin/src/main/java/ru/abstractmenus/menu/AbstractMenu.java index 7d05bc5..ae1a1cd 100644 --- a/plugin/src/main/java/ru/abstractmenus/menu/AbstractMenu.java +++ b/plugin/src/main/java/ru/abstractmenus/menu/AbstractMenu.java @@ -2,6 +2,7 @@ import lombok.Getter; import lombok.Setter; +import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.entity.Player; @@ -358,8 +359,8 @@ protected int getFreeSlot() { } protected void createInventory(Player player, InventoryHolder holder) { - String title = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, this.title); - title = MiniMessageUtil.parseToLegacy(title); + String replacedTitle = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, this.title); + Component title = MiniMessageUtil.parseToComponent(replacedTitle); if (this.type != null) { this.inventory = Bukkit.createInventory(holder, type, title); diff --git a/plugin/src/main/java/ru/abstractmenus/serializers/BannerPatternSerializer.java b/plugin/src/main/java/ru/abstractmenus/serializers/BannerPatternSerializer.java index fc06528..94115ea 100644 --- a/plugin/src/main/java/ru/abstractmenus/serializers/BannerPatternSerializer.java +++ b/plugin/src/main/java/ru/abstractmenus/serializers/BannerPatternSerializer.java @@ -1,12 +1,17 @@ package ru.abstractmenus.serializers; -import ru.abstractmenus.hocon.api.ConfigNode; -import ru.abstractmenus.hocon.api.serialize.NodeSerializeException; -import ru.abstractmenus.hocon.api.serialize.NodeSerializer; +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; import org.bukkit.DyeColor; +import org.bukkit.NamespacedKey; import org.bukkit.block.banner.Pattern; import org.bukkit.block.banner.PatternType; +import ru.abstractmenus.hocon.api.ConfigNode; +import ru.abstractmenus.hocon.api.serialize.NodeSerializeException; +import ru.abstractmenus.hocon.api.serialize.NodeSerializer; + +import java.util.Locale; public class BannerPatternSerializer implements NodeSerializer { @@ -18,9 +23,9 @@ public Pattern deserialize(Class token, ConfigNode node) throws NodeSer PatternType type; DyeColor color; - try { - type = PatternType.valueOf(typeStr); - } catch (IllegalArgumentException e) { + NamespacedKey key = NamespacedKey.fromString(typeStr.toLowerCase(Locale.ROOT)); + type = key == null ? null : RegistryAccess.registryAccess().getRegistry(RegistryKey.BANNER_PATTERN).get(key); + if (type == null) { throw new NodeSerializeException(node.node("type"), "Cannot find banner pattern type: " + typeStr); } diff --git a/plugin/src/main/java/ru/abstractmenus/services/MenuManager.java b/plugin/src/main/java/ru/abstractmenus/services/MenuManager.java index a53d4e8..086e4ec 100644 --- a/plugin/src/main/java/ru/abstractmenus/services/MenuManager.java +++ b/plugin/src/main/java/ru/abstractmenus/services/MenuManager.java @@ -22,7 +22,6 @@ import ru.abstractmenus.menu.AbstractMenu; import ru.abstractmenus.util.FileUtils; import ru.abstractmenus.api.Logger; -import ru.abstractmenus.util.NMS; import ru.abstractmenus.util.bukkit.Skulls; import ru.abstractmenus.util.bukkit.BukkitTasks; import ru.abstractmenus.util.bukkit.TaskHandle; @@ -361,8 +360,6 @@ private void unregisterActivators() { private void loadExampleMenus() throws Exception { if (!Files.exists(menuFolder)) { - int version = NMS.getMinorVersion(); - Files.createDirectory(menuFolder); ConfigurationLoader.builder() @@ -372,18 +369,8 @@ private void loadExampleMenus() throws Exception { .build() .load(); - String animMenuPath; - - if (version >= 21) { - animMenuPath = "/1_21/menu_anim.conf"; - } else if (version >= 13) { - animMenuPath = "/1_13/menu_anim.conf"; - } else { - animMenuPath = "/menu_anim.conf"; - } - ConfigurationLoader.builder() - .source(ConfigSources.resource(animMenuPath, plugin) + .source(ConfigSources.resource("/menu_anim.conf", plugin) .copyTo(menuFolder)) .serializers(AbstractMenusApi.get().serializers()) .build() @@ -452,4 +439,4 @@ public void run() { } } -} \ No newline at end of file +} diff --git a/plugin/src/main/java/ru/abstractmenus/util/LegacyColorTagReplacer.java b/plugin/src/main/java/ru/abstractmenus/util/LegacyColorTagReplacer.java index 43dd075..72e0f16 100644 --- a/plugin/src/main/java/ru/abstractmenus/util/LegacyColorTagReplacer.java +++ b/plugin/src/main/java/ru/abstractmenus/util/LegacyColorTagReplacer.java @@ -1,6 +1,7 @@ package ru.abstractmenus.util; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -25,7 +26,7 @@ final class LegacyColorTagReplacer { static final char SECTION_CHAR = '\u00a7'; - private static final Pattern COLOR_PATTERN = Pattern.compile(SECTION_CHAR + "([0-9a-fk-rx])"); + private static final Pattern COLOR_PATTERN = Pattern.compile(SECTION_CHAR + "x(?:" + SECTION_CHAR + "[0-9a-f]){6}|" + SECTION_CHAR + "#([0-9a-f]{6})|" + SECTION_CHAR + "([0-9a-fk-rx])", Pattern.CASE_INSENSITIVE); private static final Map COLOR_TAGS = new HashMap<>(); static { @@ -67,7 +68,10 @@ static String replaceLegacyTags(String input) { StringBuilder sb = new StringBuilder(input.length() + 32); do { - String replacement = COLOR_TAGS.get(matcher.group(1)); + String code = matcher.group(2); + String replacement = code != null + ? COLOR_TAGS.get(code.toLowerCase(Locale.ROOT)) + : "<#" + matcher.group().replace(String.valueOf(SECTION_CHAR), "").substring(1) + ">"; // Replacement values contain '<' and '>' but neither '$' nor '\\', // so they are safe as appendReplacement literals without quoting. matcher.appendReplacement(sb, replacement != null ? replacement : matcher.group()); diff --git a/plugin/src/main/java/ru/abstractmenus/util/MiniMessageUtil.java b/plugin/src/main/java/ru/abstractmenus/util/MiniMessageUtil.java index 67e51aa..4197ee2 100644 --- a/plugin/src/main/java/ru/abstractmenus/util/MiniMessageUtil.java +++ b/plugin/src/main/java/ru/abstractmenus/util/MiniMessageUtil.java @@ -5,6 +5,7 @@ import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import org.bukkit.entity.Player; import ru.abstractmenus.MainConfig; +import ru.abstractmenus.api.text.Colors; import java.util.ArrayList; import java.util.List; @@ -44,6 +45,10 @@ public static List parseToLegacy(List input) { return replacer.parseToLegacy(input); } + public static Component parseToComponent(String input) { + return input == null ? null : replacer.parseToComponent(Colors.of(input)); + } + private interface Replacer { void sendParsed(List input, Player player); @@ -52,6 +57,8 @@ private interface Replacer { List parseToLegacy(List input); + Component parseToComponent(String input); + } private static class ActiveReplacer implements Replacer { @@ -69,7 +76,7 @@ public void sendParsed(List input, Player player) { for (String line : input) { if (line != null) { - send(parse(LegacyColorTagReplacer.replaceLegacyTags(line)), player); + send(parseToComponent(line), player); } } } @@ -79,7 +86,7 @@ public String parseToLegacy(String input) { if (input == null) return null; if (input.isEmpty()) return input; - return serializer.serialize(parse(LegacyColorTagReplacer.replaceLegacyTags(input))); + return serializer.serialize(parseToComponent(input)); } @Override @@ -98,8 +105,9 @@ private void send(Component component, Player player) { player.sendMessage(component); } - private Component parse(String input) { - return miniMessage.deserialize(input); + @Override + public Component parseToComponent(String input) { + return miniMessage.deserialize(LegacyColorTagReplacer.replaceLegacyTags(input)); } } @@ -119,6 +127,11 @@ public String parseToLegacy(String input) { public List parseToLegacy(List input) { return input; } + + @Override + public Component parseToComponent(String input) { + return LegacyComponentSerializer.legacySection().deserialize(input); + } } } diff --git a/plugin/src/main/java/ru/abstractmenus/util/NMS.java b/plugin/src/main/java/ru/abstractmenus/util/NMS.java deleted file mode 100644 index b7bfcea..0000000 --- a/plugin/src/main/java/ru/abstractmenus/util/NMS.java +++ /dev/null @@ -1,30 +0,0 @@ -package ru.abstractmenus.util; - -import org.bukkit.Bukkit; - -public final class NMS { - - private static volatile int minorVersion = -1; - - private NMS() {} - - public static String getVersion() { - try { - // Paper now has API to get version string in format .. - // Replace `.` to `_` for backward compatibility - return Bukkit.getMinecraftVersion().replace('.', '_'); - } catch (Throwable t) { - return Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3]; - } - } - - public static int getMinorVersion() { - if (minorVersion == -1) { - String ver = getVersion(); - String[] arr = ver.split("_"); - minorVersion = arr.length > 1 ? Integer.parseInt(arr[1]) : -1; - } - - return minorVersion; - } -} diff --git a/plugin/src/main/java/ru/abstractmenus/util/bukkit/Skulls.java b/plugin/src/main/java/ru/abstractmenus/util/bukkit/Skulls.java index 9fbec18..8995876 100644 --- a/plugin/src/main/java/ru/abstractmenus/util/bukkit/Skulls.java +++ b/plugin/src/main/java/ru/abstractmenus/util/bukkit/Skulls.java @@ -6,22 +6,19 @@ import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.SkullMeta; -import ru.abstractmenus.api.Logger; import java.util.Locale; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; public final class Skulls { + private static final Pattern PLAYER_NAME = Pattern.compile("[!-~]{1,16}"); + private static final Map skullCache = new ConcurrentHashMap<>(); - /** - * Cache of resolved player-skin skulls keyed by lowercased player name. - * Populated lazily by {@link #getPlayerSkull(String)} and invalidated by - * {@link #invalidatePlayerSkull(String)} (called from PlayerJoinEvent so a - * rejoining player gets fresh skin data on the next query). - */ + // PlayerJoinEvent invalidates cached heads so online skin changes are picked up. private static final Map playerSkullCache = new ConcurrentHashMap<>(); private Skulls() { @@ -71,24 +68,16 @@ public static ItemStack getCustomSkull(com.destroystokyo.paper.profile.PlayerPro } public static ItemStack getPlayerSkull(String playerName) { - if (playerName == null) return null; + if (playerName == null || !PLAYER_NAME.matcher(playerName).matches()) return null; String key = playerName.toLowerCase(Locale.ROOT); ItemStack cached = playerSkullCache.get(key); if (cached != null) return cached.clone(); - Player player = Bukkit.getPlayer(playerName); - - if (player == null) { - Logger.info("Player '" + playerName + "' is not online or not found."); - return null; - } - - PlayerProfile profile = player.getPlayerProfile(); - - if (profile == null) { - Logger.info("PlayerProfile for '" + playerName + "' not found."); - return null; + Player player = Bukkit.getPlayerExact(playerName); + PlayerProfile profile = player != null ? player.getPlayerProfile() : null; + if (profile == null || !profile.hasTextures()) { + profile = Bukkit.createProfileExact(null, playerName); } ItemStack result = getCustomSkull(profile); diff --git a/plugin/src/main/java/ru/abstractmenus/util/proxy/ClassInfo.java b/plugin/src/main/java/ru/abstractmenus/util/proxy/ClassInfo.java deleted file mode 100644 index b97b264..0000000 --- a/plugin/src/main/java/ru/abstractmenus/util/proxy/ClassInfo.java +++ /dev/null @@ -1,30 +0,0 @@ -package ru.abstractmenus.util.proxy; - -import lombok.Data; - -/** - * From SkinRestorer - */ -@Data -public class ClassInfo { - private static final ClassInfo INSTANCE = new ClassInfo(); - private final boolean craftBukkit; - private final boolean spigot; - private final boolean paper; - private final boolean folia; - private final boolean bungeecord; - private final boolean velocity; - - private ClassInfo() { - spigot = ReflectionUtil.classExists("org.spigotmc.SpigotConfig"); - paper = ReflectionUtil.classExists("com.destroystokyo.paper.PaperConfig", "io.papermc.paper.configuration.Configuration"); - craftBukkit = ReflectionUtil.classExists("org.bukkit.Bukkit"); - folia = ReflectionUtil.classExists("io.papermc.paper.threadedregions.RegionizedServerInitEvent"); - bungeecord = ReflectionUtil.classExists("net.md_5.bungee.BungeeCord"); - velocity = ReflectionUtil.classExists("com.velocitypowered.proxy.Velocity"); - } - - public static ClassInfo get() { - return INSTANCE; - } -} \ No newline at end of file diff --git a/plugin/src/main/java/ru/abstractmenus/util/proxy/ReflectionUtil.java b/plugin/src/main/java/ru/abstractmenus/util/proxy/ReflectionUtil.java deleted file mode 100644 index 112ef3e..0000000 --- a/plugin/src/main/java/ru/abstractmenus/util/proxy/ReflectionUtil.java +++ /dev/null @@ -1,30 +0,0 @@ -package ru.abstractmenus.util.proxy; - -/** - * From SkinRestorer - */ -public class ReflectionUtil { - - private ReflectionUtil() { - } - - public static boolean classExists(String clazz) { - try { - Class.forName(clazz); - return true; - } catch (ClassNotFoundException e) { - return false; - } - } - - public static boolean classExists(String... classNames) { - for (String className : classNames) { - if (classExists(className)) { - return true; - } - } - - return false; - } - -} \ No newline at end of file diff --git a/plugin/src/main/resources/1_13/menu_anim.conf b/plugin/src/main/resources/1_13/menu_anim.conf deleted file mode 100644 index 6029bc5..0000000 --- a/plugin/src/main/resources/1_13/menu_anim.conf +++ /dev/null @@ -1,186 +0,0 @@ -# -# Menu example with simple animation -# Here is used a templates system. We recommend you to check the docs to understand this code -# - -title: "&eRoulette" -size: 3 - -activators { - command: "anim" -} - -# Looping this animation -loop: true - -# A frames of animation -frames: [ - { - delay: 1 - clear: false - items: [ - ${wood} { slot: 10 }, - ${iron} { slot: 11 }, - ${beef} { slot: 12 }, - ${stone} { slot: 13 }, - ${diamond} { slot: 14 }, - ${grass} { slot: 15 }, - ${leggings} { slot: 16 } - ] - }, - { - delay: 1 - clear: false - items: [ - ${leggings} { slot: 10 }, - ${wood} { slot: 11 }, - ${iron} { slot: 12 }, - ${beef} { slot: 13 }, - ${stone} { slot: 14 }, - ${diamond} { slot: 15 }, - ${grass} { slot: 16 } - ] - }, - { - delay: 1 - clear: false - items: [ - ${grass} { slot: 10 }, - ${leggings} { slot: 11 }, - ${wood} { slot: 12 }, - ${iron} { slot: 13 }, - ${beef} { slot: 14 }, - ${stone} { slot: 15 }, - ${diamond} { slot: 16 } - ] - }, - { - delay: 1 - clear: false - items: [ - ${diamond} { slot: 10 }, - ${grass} { slot: 11 }, - ${leggings} { slot: 12 }, - ${wood} { slot: 13 }, - ${iron} { slot: 14 }, - ${beef} { slot: 15 }, - ${stone} { slot: 16 } - ] - }, - { - delay: 1 - clear: false - items: [ - ${stone} { slot: 10 }, - ${diamond} { slot: 11 }, - ${grass} { slot: 12 }, - ${leggings} { slot: 13 }, - ${wood} { slot: 14 }, - ${iron} { slot: 15 }, - ${beef} { slot: 16 } - ] - }, - { - delay: 1 - clear: false - items: [ - ${beef} { slot: 10 }, - ${stone} { slot: 11 }, - ${diamond} { slot: 12 }, - ${grass} { slot: 13 }, - ${leggings} { slot: 14 }, - ${wood} { slot: 15 }, - ${iron} { slot: 16 } - ] - }, - { - delay: 1 - clear: false - items: [ - ${iron} { slot: 10 }, - ${beef} { slot: 11 }, - ${stone} { slot: 12 }, - ${diamond} { slot: 13 }, - ${grass} { slot: 14 }, - ${leggings} { slot: 15 }, - ${wood} { slot: 16 } - ] - } -] - -# Static background (white stained glass pane) -items: [ - ${whitePane} { slot: "0-9" }, - ${whitePane} { slot: "17-26" } -] - -# A templates for some roulette items - -whitePane { - material: WHITE_STAINED_GLASS_PANE - name: " " -} - -wood { - material: OAK_WOOD - name: " " - click { - itemAdd { - material: OAK_WOOD - } - } -} - -beef { - material: BEEF - click { - itemAdd { - material: BEEF - } - } -} - -stone{ - material: STONE - click { - itemAdd { - material: STONE - } - } -} - -grass { - material: GRASS - click { - itemAdd { - material: GRASS - } - } -} - -iron { - material: IRON_INGOT - click { - itemAdd { - material: IRON_INGOT - } - } -} - -leggings { - material: LEATHER_LEGGINGS - click { - itemAdd { - material: LEATHER_LEGGINGS - } - } -} - -diamond { - material: DIAMOND - click { - itemAdd { - material: DIAMOND - } - } -} \ No newline at end of file diff --git a/plugin/src/main/resources/1_21/menu_anim.conf b/plugin/src/main/resources/1_21/menu_anim.conf deleted file mode 100644 index 83019ac..0000000 --- a/plugin/src/main/resources/1_21/menu_anim.conf +++ /dev/null @@ -1,186 +0,0 @@ -# -# Menu example with simple animation -# Here is used a templates system. We recommend you to check the docs to understand this code -# - -title: "&eRoulette" -size: 3 - -activators { - command: "anim" -} - -# Looping this animation -loop: true - -# A frames of animation -frames: [ - { - delay: 1 - clear: false - items: [ - ${wood} { slot: 10 }, - ${iron} { slot: 11 }, - ${beef} { slot: 12 }, - ${stone} { slot: 13 }, - ${diamond} { slot: 14 }, - ${grass} { slot: 15 }, - ${leggings} { slot: 16 } - ] - }, - { - delay: 1 - clear: false - items: [ - ${leggings} { slot: 10 }, - ${wood} { slot: 11 }, - ${iron} { slot: 12 }, - ${beef} { slot: 13 }, - ${stone} { slot: 14 }, - ${diamond} { slot: 15 }, - ${grass} { slot: 16 } - ] - }, - { - delay: 1 - clear: false - items: [ - ${grass} { slot: 10 }, - ${leggings} { slot: 11 }, - ${wood} { slot: 12 }, - ${iron} { slot: 13 }, - ${beef} { slot: 14 }, - ${stone} { slot: 15 }, - ${diamond} { slot: 16 } - ] - }, - { - delay: 1 - clear: false - items: [ - ${diamond} { slot: 10 }, - ${grass} { slot: 11 }, - ${leggings} { slot: 12 }, - ${wood} { slot: 13 }, - ${iron} { slot: 14 }, - ${beef} { slot: 15 }, - ${stone} { slot: 16 } - ] - }, - { - delay: 1 - clear: false - items: [ - ${stone} { slot: 10 }, - ${diamond} { slot: 11 }, - ${grass} { slot: 12 }, - ${leggings} { slot: 13 }, - ${wood} { slot: 14 }, - ${iron} { slot: 15 }, - ${beef} { slot: 16 } - ] - }, - { - delay: 1 - clear: false - items: [ - ${beef} { slot: 10 }, - ${stone} { slot: 11 }, - ${diamond} { slot: 12 }, - ${grass} { slot: 13 }, - ${leggings} { slot: 14 }, - ${wood} { slot: 15 }, - ${iron} { slot: 16 } - ] - }, - { - delay: 1 - clear: false - items: [ - ${iron} { slot: 10 }, - ${beef} { slot: 11 }, - ${stone} { slot: 12 }, - ${diamond} { slot: 13 }, - ${grass} { slot: 14 }, - ${leggings} { slot: 15 }, - ${wood} { slot: 16 } - ] - } -] - -# Static background (white stained glass pane) -items: [ - ${whitePane} { slot: "0-9" }, - ${whitePane} { slot: "17-26" } -] - -# A templates for some roulette items - -whitePane { - material: WHITE_STAINED_GLASS_PANE - name: " " -} - -wood { - material: OAK_WOOD - name: " " - click { - itemAdd { - material: OAK_WOOD - } - } -} - -beef { - material: BEEF - click { - itemAdd { - material: BEEF - } - } -} - -stone{ - material: STONE - click { - itemAdd { - material: STONE - } - } -} - -grass { - material: GRASS_BLOCK - click { - itemAdd { - material: GRASS_BLOCK - } - } -} - -iron { - material: IRON_INGOT - click { - itemAdd { - material: IRON_INGOT - } - } -} - -leggings { - material: LEATHER_LEGGINGS - click { - itemAdd { - material: LEATHER_LEGGINGS - } - } -} - -diamond { - material: DIAMOND - click { - itemAdd { - material: DIAMOND - } - } -} \ No newline at end of file diff --git a/plugin/src/main/resources/menu.conf b/plugin/src/main/resources/menu.conf index 271f239..a3eb96f 100644 --- a/plugin/src/main/resources/menu.conf +++ b/plugin/src/main/resources/menu.conf @@ -4,10 +4,28 @@ activators { command: "menu" } items: [ + { + slot: 3 + material: DIAMOND_SWORD + name: "&b&lDisplay sword" + count: 64 + glow: true + flags: HIDE_ATTRIBUTES + }, { slot: 4 material: CAKE name: "&6&lClick me" + itemModel: "minecraft:cake" + tooltipStyle: "minecraft:default" + maxStackSize: 99 + rarity: RARE + model { + floats: [1.0, 2.5] + flags: [true, false] + strings: ["cake"] + colors: ["#ff8000"] + } lore: "&aI really want you click me" click { message: "&aHi! Thanks for the click. Hold your cake." @@ -22,10 +40,10 @@ items: [ slot: 5 material: BUNDLE name: "&6&lBundle" + model: 123 tooltipDisplay { - hiddenComponents: [ - BUNDLE_CONTENTS - ] + hideTooltip: false + hiddenComponents: [BUNDLE_CONTENTS] } }, { diff --git a/plugin/src/main/resources/menu_anim.conf b/plugin/src/main/resources/menu_anim.conf index 0314954..83019ac 100644 --- a/plugin/src/main/resources/menu_anim.conf +++ b/plugin/src/main/resources/menu_anim.conf @@ -118,30 +118,29 @@ items: [ whitePane { material: WHITE_STAINED_GLASS_PANE - data: 0 name: " " } wood { - material: OAK_LOG + material: OAK_WOOD name: " " click { itemAdd { - material: OAK_LOG + material: OAK_WOOD } } } -beef{ - material: COOKED_BEEF +beef { + material: BEEF click { itemAdd { - material: COOKED_BEEF + material: BEEF } } } -stone { +stone{ material: STONE click { itemAdd { diff --git a/plugin/src/main/resources/plugin.yml b/plugin/src/main/resources/plugin.yml index 2d951ea..2cf2190 100644 --- a/plugin/src/main/resources/plugin.yml +++ b/plugin/src/main/resources/plugin.yml @@ -1,7 +1,7 @@ main: 'ru.abstractmenus.AbstractMenus' name: 'AbstractMenus' version: ${version} -api-version: '1.21' +api-version: '26.1' authors: ['Nanit', 'BrainRTP', 'WhyZerVellasskx'] folia-supported: true softdepend: diff --git a/plugin/src/test/java/ru/abstractmenus/addon/AddonManagerIntegrationTest.java b/plugin/src/test/java/ru/abstractmenus/addon/AddonManagerIntegrationTest.java index f09fed4..2a0899e 100644 --- a/plugin/src/test/java/ru/abstractmenus/addon/AddonManagerIntegrationTest.java +++ b/plugin/src/test/java/ru/abstractmenus/addon/AddonManagerIntegrationTest.java @@ -40,8 +40,7 @@ * {@code unloadAll()} strips the registration. * *

Uses a pure-unit {@link StubApi} + the package-private test-only - * {@code AddonManager(Path, AbstractMenusApi)} constructor, since MockBukkit - * does not work with Paper 1.21.11 (see {@code CoreExtensionTest}). + * {@code AddonManager(Path, AbstractMenusApi)} constructor. */ class AddonManagerIntegrationTest { diff --git a/plugin/src/test/java/ru/abstractmenus/core/CoreExtensionTest.java b/plugin/src/test/java/ru/abstractmenus/core/CoreExtensionTest.java index e6f0243..0cb2021 100644 --- a/plugin/src/test/java/ru/abstractmenus/core/CoreExtensionTest.java +++ b/plugin/src/test/java/ru/abstractmenus/core/CoreExtensionTest.java @@ -34,9 +34,7 @@ * *

This test does NOT boot the full plugin โ€” it constructs a lightweight * {@link StubApi} backed by real {@link TypeRegistryImpl} instances and invokes - * {@code CoreExtension.onEnable()} directly, bypassing the MockBukkit - * incompatibility with Paper 1.21.11 that prevents the full plugin from loading - * in tests (see {@link ru.abstractmenus.integration.TestPluginLifecycle}). + * {@code CoreExtension.onEnable()} directly. * *

Activator count note: {@link CoreActivatorsBundle} conditionally * registers "clickNPC" (Citizens) and "regionJoin"/"regionLeave" (WorldGuard) @@ -52,6 +50,7 @@ class CoreExtensionTest { @BeforeEach void setUp() { + ru.abstractmenus.api.Logger.set(java.util.logging.Logger.getLogger(CoreExtensionTest.class.getName())); // CoreActivatorsBundle calls AbstractMenus.checkDependency() which calls // Bukkit.getServer().getPluginManager().isPluginEnabled(...). // Mock the static Bukkit.getServer() so the call doesn't NPE. @@ -90,8 +89,8 @@ void coreExtension_registersExpectedRules() { @Test void coreExtension_registersExpectedItemProperties() { new CoreExtension().onEnable(api); - assertEquals(33, api.itemProperties().keys().size(), - "expected 33 core item-property types (matches Task 10 migration count)"); + assertEquals(37, api.itemProperties().keys().size(), + "expected 37 core item-property types"); } @Test diff --git a/plugin/src/test/java/ru/abstractmenus/data/properties/TestPropCount.java b/plugin/src/test/java/ru/abstractmenus/data/properties/TestPropCount.java new file mode 100644 index 0000000..5832b32 --- /dev/null +++ b/plugin/src/test/java/ru/abstractmenus/data/properties/TestPropCount.java @@ -0,0 +1,52 @@ +package ru.abstractmenus.data.properties; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; +import ru.abstractmenus.datatype.TypeInt; +import ru.abstractmenus.datatype.TypeMaterial; +import ru.abstractmenus.menu.item.SimpleItem; +import ru.abstractmenus.hocon.api.serialize.NodeSerializers; +import ru.abstractmenus.hocon.api.ConfigurationLoader; +import ru.abstractmenus.hocon.api.source.ConfigSources; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; + +class TestPropCount { + + @BeforeEach + void setUp() { + MockBukkit.mock(); + } + + @AfterEach + void tearDown() { + MockBukkit.unmock(); + } + + @Test + void buildKeepsConfiguredCountOnUnstackableItem() throws Exception { + NodeSerializers serializers = NodeSerializers.defaults(); + serializers.register(TypeInt.class, new TypeInt.Serializer()); + serializers.register(TypeMaterial.class, new TypeMaterial.Serializer()); + var root = ConfigurationLoader.builder() + .serializers(serializers) + .source(ConfigSources.inputStream("test", new ByteArrayInputStream("material: DIAMOND_SWORD\ncount: 3".getBytes(StandardCharsets.UTF_8)))) + .build().load(); + SimpleItem template = new SimpleItem(); + template.addProperty("material", new PropMaterial.Serializer().deserialize(PropMaterial.class, root.node("material"))); + template.addProperty("count", new PropCount.Serializer().deserialize(PropCount.class, root.node("count"))); + + ItemStack sword = template.build(null, null); + + assertNotNull(sword); + assertEquals(Material.DIAMOND_SWORD, sword.getType()); + assertEquals(3, sword.getAmount()); + } +} diff --git a/plugin/src/test/java/ru/abstractmenus/data/properties/TestPropFlags.java b/plugin/src/test/java/ru/abstractmenus/data/properties/TestPropFlags.java new file mode 100644 index 0000000..687e53a --- /dev/null +++ b/plugin/src/test/java/ru/abstractmenus/data/properties/TestPropFlags.java @@ -0,0 +1,51 @@ +package ru.abstractmenus.data.properties; + +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeModifier; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; +import ru.abstractmenus.hocon.api.ConfigurationLoader; +import ru.abstractmenus.hocon.api.source.ConfigSources; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; + +class TestPropFlags { + + @BeforeEach + void setUp() { + MockBukkit.mock(); + } + + @AfterEach + void tearDown() { + MockBukkit.unmock(); + } + + @Test + void hidingAttributesPreservesCustomModifiers() throws Exception { + var root = ConfigurationLoader.builder() + .source(ConfigSources.inputStream("test", new ByteArrayInputStream("flags: HIDE_ATTRIBUTES".getBytes(StandardCharsets.UTF_8)))) + .build().load(); + PropFlags property = new PropFlags.Serializer().deserialize(PropFlags.class, root.node("flags")); + ItemStack sword = new ItemStack(Material.DIAMOND_SWORD); + ItemMeta meta = sword.getItemMeta(); + AttributeModifier modifier = new AttributeModifier(NamespacedKey.minecraft("test_damage"), 12, AttributeModifier.Operation.ADD_NUMBER); + meta.addAttributeModifier(Attribute.ATTACK_DAMAGE, modifier); + + property.apply(sword, meta, null, null); + sword.setItemMeta(meta); + + assertTrue(sword.getItemMeta().hasItemFlag(ItemFlag.HIDE_ATTRIBUTES)); + assertEquals(java.util.List.of(modifier), java.util.List.copyOf(sword.getItemMeta().getAttributeModifiers(Attribute.ATTACK_DAMAGE))); + } +} diff --git a/plugin/src/test/java/ru/abstractmenus/data/properties/TestPropMaxStackSize.java b/plugin/src/test/java/ru/abstractmenus/data/properties/TestPropMaxStackSize.java new file mode 100644 index 0000000..18d0b52 --- /dev/null +++ b/plugin/src/test/java/ru/abstractmenus/data/properties/TestPropMaxStackSize.java @@ -0,0 +1,72 @@ +package ru.abstractmenus.data.properties; + +import org.bukkit.inventory.meta.ItemMeta; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; +import ru.abstractmenus.api.AbstractMenusApi; +import ru.abstractmenus.api.Logger; +import ru.abstractmenus.menu.item.SimpleItem; +import ru.abstractmenus.hocon.api.ConfigNode; +import ru.abstractmenus.hocon.api.ConfigurationLoader; +import ru.abstractmenus.hocon.api.serialize.NodeSerializeException; +import ru.abstractmenus.hocon.api.source.ConfigSources; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class TestPropMaxStackSize { + + @Test + void rejectsOutOfRangeAtMenuLoad() throws Exception { + for (int size : new int[]{0, -1, 100, Integer.MAX_VALUE}) { + ConfigNode node = loadValue(size); + assertThrows(NodeSerializeException.class, () -> new PropMaxStackSize.Serializer().deserialize(PropMaxStackSize.class, node)); + } + } + + @Test + void acceptsBoundsAndNormalStackSize() throws Exception { + for (int size : new int[]{1, 64, 99}) { + PropMaxStackSize property = new PropMaxStackSize.Serializer().deserialize(PropMaxStackSize.class, loadValue(size)); + ItemMeta meta = mock(ItemMeta.class); + property.apply(null, meta, null, null); + verify(meta).setMaxStackSize(size); + } + } + + @Test + void invalidPlaceholderValueWarnsAndKeepsTheItemAndExistingLimit() throws Exception { + MockBukkit.mock(); + AbstractMenusApi api = mock(AbstractMenusApi.class, RETURNS_DEEP_STUBS); + try (var apiLookup = mockStatic(AbstractMenusApi.class); var logging = mockStatic(Logger.class)) { + apiLookup.when(AbstractMenusApi::get).thenReturn(api); + for (int size : new int[]{0, 100}) { + when(api.providers().placeholders().resolve().replace(null, "%stack_size%")).thenReturn(String.valueOf(size)); + SimpleItem template = new SimpleItem(); + template.addProperty("initialStackSize", new PropMaxStackSize.Serializer().deserialize(PropMaxStackSize.class, loadValue(32))); + template.addProperty("maxStackSize", new PropMaxStackSize.Serializer().deserialize(PropMaxStackSize.class, loadNode("\"%stack_size%\""))); + + var item = template.build(null, null); + + assertNotNull(item); + assertEquals(32, item.getMaxStackSize()); + logging.verify(() -> Logger.warning("Skipping maxStackSize value outside 1..99: " + size)); + } + } finally { + MockBukkit.unmock(); + } + } + + private static ConfigNode loadValue(int size) throws Exception { + return loadNode(String.valueOf(size)); + } + + private static ConfigNode loadNode(String value) throws Exception { + return ConfigurationLoader.builder() + .source(ConfigSources.inputStream("test", new ByteArrayInputStream(("value: " + value).getBytes(StandardCharsets.UTF_8)))) + .build().load().node("value"); + } +} diff --git a/plugin/src/test/java/ru/abstractmenus/data/properties/TestPropModel.java b/plugin/src/test/java/ru/abstractmenus/data/properties/TestPropModel.java new file mode 100644 index 0000000..4cd7d1c --- /dev/null +++ b/plugin/src/test/java/ru/abstractmenus/data/properties/TestPropModel.java @@ -0,0 +1,68 @@ +package ru.abstractmenus.data.properties; + +import org.bukkit.Color; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.components.CustomModelDataComponent; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; +import ru.abstractmenus.hocon.api.ConfigNode; +import ru.abstractmenus.hocon.api.ConfigurationLoader; +import ru.abstractmenus.hocon.api.source.ConfigSources; +import ru.abstractmenus.menu.item.SimpleItem; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class TestPropModel { + + @BeforeEach + void setUp() { + MockBukkit.mock(); + } + + @AfterEach + void tearDown() { + MockBukkit.unmock(); + } + + @Test + void legacyIntegerBecomesSingleFloat() throws Exception { + CustomModelDataComponent model = build("model: 123"); + assertEquals(List.of(123.0f), model.getFloats()); + assertTrue(model.getFlags().isEmpty()); + assertTrue(model.getStrings().isEmpty()); + assertTrue(model.getColors().isEmpty()); + } + + @Test + void objectRetainsAllFourTypedLists() throws Exception { + CustomModelDataComponent model = build(""" + model { + floats: [1, 2.5] + flags: [true, false] + strings: ["cake", "bundle"] + colors: ["#ff8000", "0, 64, 255"] + } + """); + assertEquals(List.of(1.0f, 2.5f), model.getFloats()); + assertEquals(List.of(true, false), model.getFlags()); + assertEquals(List.of("cake", "bundle"), model.getStrings()); + assertEquals(List.of(Color.fromRGB(255, 128, 0), Color.fromRGB(0, 64, 255)), model.getColors()); + } + + private static CustomModelDataComponent build(String hocon) throws Exception { + ConfigNode root = ConfigurationLoader.builder() + .source(ConfigSources.inputStream("test", new ByteArrayInputStream(hocon.getBytes(StandardCharsets.UTF_8)))) + .build().load(); + SimpleItem template = new SimpleItem(); + template.addProperty("model", new PropModel.Serializer().deserialize(PropModel.class, root.node("model"))); + ItemStack item = template.build(null, null); + assertNotNull(item); + return item.getItemMeta().getCustomModelDataComponent(); + } +} diff --git a/plugin/src/test/java/ru/abstractmenus/data/properties/TestPropTooltipDisplay.java b/plugin/src/test/java/ru/abstractmenus/data/properties/TestPropTooltipDisplay.java index f820dbf..b2571da 100644 --- a/plugin/src/test/java/ru/abstractmenus/data/properties/TestPropTooltipDisplay.java +++ b/plugin/src/test/java/ru/abstractmenus/data/properties/TestPropTooltipDisplay.java @@ -1,194 +1,116 @@ package ru.abstractmenus.data.properties; -import io.papermc.paper.datacomponent.DataComponentType; -import org.bukkit.NamespacedKey; +import io.papermc.paper.datacomponent.DataComponentTypes; +import io.papermc.paper.datacomponent.item.TooltipDisplay; +import org.bukkit.inventory.ItemStack; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import ru.abstractmenus.datatype.TypeBool; +import org.mockbukkit.mockbukkit.MockBukkit; +import ru.abstractmenus.api.text.Colors; import ru.abstractmenus.hocon.api.ConfigNode; import ru.abstractmenus.hocon.api.ConfigurationLoader; import ru.abstractmenus.hocon.api.serialize.NodeSerializeException; import ru.abstractmenus.hocon.api.source.ConfigSources; +import ru.abstractmenus.menu.item.SimpleItem; +import ru.abstractmenus.util.MiniMessageUtil; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; -import java.lang.reflect.Field; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.List; import java.util.Set; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -/** - * Tests the {@code tooltipDisplay} item property's config parsing and its - * {@code hiddenComponents} component resolver. - * - *

The resolver is backed by the injectable {@link Registry#DATA_COMPONENT_TYPE} - * source, so case-insensitive / namespaced resolution and unknown-component - * handling are covered without a live server (see - * {@code PropTooltipDisplay.configureRegistry}). - * - *

Full {@code apply()} verification needs the Paper {@code TooltipDisplay} - * builder bridge, which requires a booted server (MockBukkit cannot bootstrap - * Paper 1.21.11) โ€” see README's Tests section. - */ class TestPropTooltipDisplay { - private static final Map COMPONENTS = new LinkedHashMap<>(); - @BeforeEach void setUp() { - COMPONENTS.clear(); - COMPONENTS.put("bundle_contents", component("bundle_contents")); - COMPONENTS.put("enchantments", component("enchantments")); - COMPONENTS.put("attribute_modifiers", component("attribute_modifiers")); - COMPONENTS.put("stored_enchantments", component("stored_enchantments")); - COMPONENTS.put("custom_model_data", component("custom_model_data")); - - PropTooltipDisplay.configureRegistry(COMPONENTS.values()); + MockBukkit.mock(); + MiniMessageUtil.init(false); + Colors.init(false); } @AfterEach void tearDown() { - PropTooltipDisplay.resetRegistry(); - } - - // ----- resolver ----- - - @Test - void resolvesUppercaseName() { - assertSame(COMPONENTS.get("bundle_contents"), - PropTooltipDisplay.resolveComponent("BUNDLE_CONTENTS")); - } - - @Test - void resolvesLowercaseName() { - assertSame(COMPONENTS.get("bundle_contents"), - PropTooltipDisplay.resolveComponent("bundle_contents")); - } - - @Test - void resolvesNamespacedKey() { - assertSame(COMPONENTS.get("bundle_contents"), - PropTooltipDisplay.resolveComponent("minecraft:bundle_contents")); - } - - @Test - void resolveIsCaseInsensitiveForNamespacedInput() { - assertSame(COMPONENTS.get("bundle_contents"), - PropTooltipDisplay.resolveComponent("MINECRAFT:BUNDLE_CONTENTS")); - } - - @Test - void unknownComponentResolvesToNull() { - assertNull(PropTooltipDisplay.resolveComponent("NOPE_NOT_A_COMPONENT")); - } - - @Test - void nullComponentResolvesToNull() { - assertNull(PropTooltipDisplay.resolveComponent(null)); - assertNull(PropTooltipDisplay.resolveComponent(" ")); - } - - // ----- serialization ----- - - @Test - void parsesHideTooltipTrue() throws Exception { - PropTooltipDisplay prop = build("tooltipDisplay { hideTooltip: true }"); - TypeBool hide = (TypeBool) field(prop, "hideTooltip"); - assertNotNull(hide); - assertTrue(hide.getBool(null, null)); + MockBukkit.unmock(); } @Test - void parsesHideTooltipFalse() throws Exception { - PropTooltipDisplay prop = build("tooltipDisplay { hideTooltip: false }"); - TypeBool hide = (TypeBool) field(prop, "hideTooltip"); - assertNotNull(hide); - assertFalse(hide.getBool(null, null)); + void survivesMetaPropertiesOnBothSidesWhenBuildingMenuItem() throws Exception { + ConfigNode root = loadNode(""" + name: "Named bundle" + tooltipDisplay { + hideTooltip: true + hiddenComponents: [BUNDLE_CONTENTS, "minecraft:enchantments"] + } + lore: ["Still named and hidden"] + """); + SimpleItem template = new SimpleItem(); + template.addProperty("name", new PropName.Serializer().deserialize(PropName.class, root.node("name"))); + PropTooltipDisplay tooltipProperty = spy(new PropTooltipDisplay.Serializer().deserialize(PropTooltipDisplay.class, root.node("tooltipDisplay"))); + // MockBukkit retains component data when stale meta is applied, so also verify the flush boundary. + doAnswer(invocation -> { + ItemStack stack = invocation.getArgument(0); + assertEquals("Named bundle", stack.getItemMeta().getDisplayName()); + return invocation.callRealMethod(); + }).when(tooltipProperty).apply(any(), any(), any(), any()); + template.addProperty("tooltipDisplay", tooltipProperty); + template.addProperty("lore", new PropLore.Serializer().deserialize(PropLore.class, root.node("lore"))); + + ItemStack item = template.build(null, null); + + assertNotNull(item); + assertEquals("Named bundle", item.getItemMeta().getDisplayName()); + assertEquals(List.of("Still named and hidden"), item.getItemMeta().getLore()); + TooltipDisplay tooltip = item.getData(DataComponentTypes.TOOLTIP_DISPLAY); + assertNotNull(tooltip); + assertTrue(tooltip.hideTooltip()); + assertEquals(Set.of(DataComponentTypes.BUNDLE_CONTENTS, DataComponentTypes.ENCHANTMENTS), tooltip.hiddenComponents()); } @Test - void parsesHiddenComponentsCaseInsensitive() throws Exception { - PropTooltipDisplay prop = build("tooltipDisplay { hiddenComponents: [BUNDLE_CONTENTS] }"); - Set hidden = hidden(prop); - assertEquals(Set.of(COMPONENTS.get("bundle_contents")), hidden); + void hiddenComponentsPreserveFlagsAppliedBeforeTooltipDisplay() throws Exception { + ConfigNode root = loadNode(""" + flags: HIDE_ENCHANTS + tooltipDisplay { hiddenComponents: [BUNDLE_CONTENTS] } + """); + SimpleItem template = new SimpleItem(); + template.addProperty("flags", new PropFlags.Serializer().deserialize(PropFlags.class, root.node("flags"))); + PropTooltipDisplay tooltipProperty = spy(new PropTooltipDisplay.Serializer().deserialize(PropTooltipDisplay.class, root.node("tooltipDisplay"))); + // MockBukkit stores flags separately, so mirror Paper's tooltip component conversion at this boundary. + doAnswer(invocation -> { + ItemStack stack = invocation.getArgument(0); + assertTrue(stack.getItemMeta().hasItemFlag(org.bukkit.inventory.ItemFlag.HIDE_ENCHANTS)); + stack.setData(DataComponentTypes.TOOLTIP_DISPLAY, TooltipDisplay.tooltipDisplay().hiddenComponents(Set.of(DataComponentTypes.ENCHANTMENTS)).build()); + return invocation.callRealMethod(); + }).when(tooltipProperty).apply(any(), any(), any(), any()); + template.addProperty("tooltipDisplay", tooltipProperty); + + ItemStack item = template.build(null, null); + + assertNotNull(item); + TooltipDisplay tooltip = item.getData(DataComponentTypes.TOOLTIP_DISPLAY); + assertNotNull(tooltip); + assertEquals(Set.of(DataComponentTypes.ENCHANTMENTS, DataComponentTypes.BUNDLE_CONTENTS), tooltip.hiddenComponents()); } @Test - void parsesHiddenComponentsNamespaced() throws Exception { - PropTooltipDisplay prop = build("tooltipDisplay { hiddenComponents: [\"minecraft:bundle_contents\"] }"); - assertEquals(Set.of(COMPONENTS.get("bundle_contents")), hidden(prop)); - } - - @Test - void parsesMultipleHiddenComponents() throws Exception { - PropTooltipDisplay prop = build( - "tooltipDisplay { hiddenComponents: [BUNDLE_CONTENTS, ENCHANTMENTS, attribute_modifiers] }"); - assertEquals(Set.of( - COMPONENTS.get("bundle_contents"), - COMPONENTS.get("enchantments"), - COMPONENTS.get("attribute_modifiers")), hidden(prop)); - } - - @Test - void unknownComponentThrows() throws Exception { - ConfigNode root = loadNode("tooltipDisplay { hiddenComponents: [FUCKING_UNKNOWN_COMPONENT] }"); + void rejectsUnknownComponentAndNonList() throws Exception { + ConfigNode root = loadNode(""" + unknown { hiddenComponents: [UNKNOWN_COMPONENT] } + invalid { hiddenComponents: BUNDLE_CONTENTS } + """); PropTooltipDisplay.Serializer serializer = new PropTooltipDisplay.Serializer(); - assertThrows(NodeSerializeException.class, () -> - serializer.deserialize(PropTooltipDisplay.class, root.node("tooltipDisplay"))); - } - - @Test - void emptyHiddenComponentsIsAccepted() throws Exception { - PropTooltipDisplay prop = build("tooltipDisplay { hiddenComponents: [] }"); - assertEquals(Set.of(), hidden(prop)); - } - - @Test - void emptyTooltipDisplayLeavesFieldsNull() throws Exception { - PropTooltipDisplay prop = build("tooltipDisplay { }"); - assertNull(field(prop, "hideTooltip")); - assertNull(field(prop, "hiddenComponents")); - } - - @Test - void hideTooltipOnlyLeavesHiddenNull() throws Exception { - PropTooltipDisplay prop = build("tooltipDisplay { hideTooltip: false }"); - assertNull(field(prop, "hiddenComponents")); - } - - // ----- helpers ----- - - private static PropTooltipDisplay build(String hocon) throws Exception { - ConfigNode root = loadNode(hocon); - return new PropTooltipDisplay.Serializer().deserialize(PropTooltipDisplay.class, root.node("tooltipDisplay")); - } - - @SuppressWarnings("unchecked") - private static Set hidden(PropTooltipDisplay prop) throws Exception { - return (Set) field(prop, "hiddenComponents"); - } - - private static Object field(Object target, String name) throws Exception { - Field f = PropTooltipDisplay.class.getDeclaredField(name); - f.setAccessible(true); - return f.get(target); + assertThrows(NodeSerializeException.class, () -> serializer.deserialize(PropTooltipDisplay.class, root.node("unknown"))); + assertThrows(NodeSerializeException.class, () -> serializer.deserialize(PropTooltipDisplay.class, root.node("invalid"))); } private static ConfigNode loadNode(String hocon) throws Exception { - ByteArrayInputStream in = new ByteArrayInputStream(hocon.getBytes(StandardCharsets.UTF_8)); return ConfigurationLoader.builder() - .source(ConfigSources.inputStream("test", in)) - .build() - .load(); - } - - private static DataComponentType component(String key) { - DataComponentType type = mock(DataComponentType.class); - when(type.getKey()).thenReturn(NamespacedKey.minecraft(key)); - return type; + .source(ConfigSources.inputStream("test", new ByteArrayInputStream(hocon.getBytes(StandardCharsets.UTF_8)))) + .build().load(); } } diff --git a/plugin/src/test/java/ru/abstractmenus/integration/TestPluginLifecycle.java b/plugin/src/test/java/ru/abstractmenus/integration/TestPluginLifecycle.java index 0640d44..f34982b 100644 --- a/plugin/src/test/java/ru/abstractmenus/integration/TestPluginLifecycle.java +++ b/plugin/src/test/java/ru/abstractmenus/integration/TestPluginLifecycle.java @@ -1,27 +1,19 @@ package ru.abstractmenus.integration; +import io.papermc.paper.configuration.ServerConfiguration; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockbukkit.mockbukkit.MockBukkit; import org.mockbukkit.mockbukkit.ServerMock; import ru.abstractmenus.AbstractMenus; import ru.abstractmenus.services.MenuManager; +import ru.abstractmenus.util.bukkit.BukkitTasks; +import ru.abstractmenus.util.bukkit.Events; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; -/** - * First-level MockBukkit smoke tests: the plugin boots, registers its services, - * and shuts down cleanly. Heavier flow tests (open menu, click, actions) follow. - * - * Currently disabled: MockBukkit 4.108.0 + Paper 1.21.11-R0.1-SNAPSHOT fails during - * {@code MockBukkit.mock()} with {@code PaperRegistryAccess: RegistryKeyImpl[key=minecraft:attribute] - * points to a registry that is not available yet} (TagsMock.loadRegistry โ†’ Registry.{@literal }). - * Upstream compatibility issue โ€” re-enable once MockBukkit tracks the current Paper build - * or once we pin Paper to a version MockBukkit supports. - */ -@Disabled("MockBukkit 4.108 vs Paper 1.21.11 registry bootstrap incompatibility") class TestPluginLifecycle { private ServerMock server; @@ -29,13 +21,26 @@ class TestPluginLifecycle { @BeforeEach void startServer() { - server = MockBukkit.mock(); + server = MockBukkit.mock(new ServerMock() { + private final ServerConfiguration configuration = mock(ServerConfiguration.class); + + @Override + public ServerConfiguration getServerConfig() { + return configuration; + } + }); plugin = MockBukkit.load(AbstractMenus.class); } @AfterEach void stopServer() { - MockBukkit.unmock(); + try { + MockBukkit.unmock(); + } finally { + BukkitTasks.setFoliaLib(null); + BukkitTasks.setPlugin(null); + Events.setPlugin(null); + } } @Test diff --git a/plugin/src/test/java/ru/abstractmenus/menu/TestInventoryTitles.java b/plugin/src/test/java/ru/abstractmenus/menu/TestInventoryTitles.java new file mode 100644 index 0000000..492972a --- /dev/null +++ b/plugin/src/test/java/ru/abstractmenus/menu/TestInventoryTitles.java @@ -0,0 +1,77 @@ +package ru.abstractmenus.menu; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.plugin.ServicePriority; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.inventory.InventoryMock; +import ru.abstractmenus.api.AbstractMenusApi; +import ru.abstractmenus.api.text.Colors; +import ru.abstractmenus.util.MiniMessageUtil; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +class TestInventoryTitles { + + private Player player; + + @BeforeEach + void startServer() { + var server = MockBukkit.mock(); + var api = mock(AbstractMenusApi.class, RETURNS_DEEP_STUBS); + when(api.providers().placeholders().resolve().replace(any(), anyString())) + .thenAnswer(call -> call.getArgument(1).replace("%title%", "Menu")); + server.getServicesManager().register(AbstractMenusApi.class, api, MockBukkit.createMockPlugin(), ServicePriority.Normal); + player = server.addPlayer(); + Colors.init(false); + } + + @AfterEach + void stopServer() { + MiniMessageUtil.init(false); + MockBukkit.unmock(); + } + + @Test + void legacyTitleProducesComponentWithAndWithoutMiniMessage() { + for (boolean miniMessage : new boolean[]{false, true}) { + MiniMessageUtil.init(miniMessage); + assertEquals(Component.text("Menu", NamedTextColor.GREEN, TextDecoration.BOLD), titleOf("&a&l%title%", null)); + } + } + + @Test + void miniMessageTitleRetainsComponentEventsAndFormattingForTypedInventory() { + MiniMessageUtil.init(true); + Component expected = Component.text("Menu", NamedTextColor.GOLD, TextDecoration.BOLD) + .hoverEvent(Component.text("Details")); + assertEquals(expected, titleOf("%title%", InventoryType.HOPPER)); + } + + @Test + void hexTitlesPreserveRgbColors() { + Component expected = Component.text("Menu", TextColor.color(0x12abef)); + for (boolean miniMessage : new boolean[]{false, true}) { + MiniMessageUtil.init(miniMessage); + assertEquals(expected, titleOf("&x&1&2&a&b&e&f%title%", null)); + } + MiniMessageUtil.init(true); + assertEquals(expected, titleOf("<#12abef>%title%", null)); + } + + private Component titleOf(String title, InventoryType type) { + var menu = new SimpleMenu(title, 9); + menu.setType(type); + menu.createInventory(player, menu); + return ((InventoryMock) menu.getInventory()).getCustomTitle(); + } +} diff --git a/plugin/src/test/java/ru/abstractmenus/util/bukkit/TestSkulls.java b/plugin/src/test/java/ru/abstractmenus/util/bukkit/TestSkulls.java new file mode 100644 index 0000000..f787a8a --- /dev/null +++ b/plugin/src/test/java/ru/abstractmenus/util/bukkit/TestSkulls.java @@ -0,0 +1,83 @@ +package ru.abstractmenus.util.bukkit; + +import com.destroystokyo.paper.profile.ProfileProperty; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.SkullMeta; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; +import ru.abstractmenus.listeners.PlayerListener; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class TestSkulls { + + private ServerMock server; + + @BeforeEach + void startServer() { + server = MockBukkit.mock(new ServerMock() { + @Override + public OfflinePlayer getOfflinePlayer(String name) { + throw new AssertionError("Head creation must not perform a blocking offline player lookup"); + } + }); + Skulls.clearCache(); + } + + @AfterEach + void stopServer() { + Skulls.clearCache(); + MockBukkit.unmock(); + } + + @Test + void unknownOfflineOwnerProducesNameOnlyHeadWithoutMojangLookup() { + try (var mojang = mockStatic(MojangApi.class)) { + ItemStack head = Skulls.getPlayerSkull("OfflineSkin"); + + assertNotNull(head); + assertEquals(Material.PLAYER_HEAD, head.getType()); + var profile = ((SkullMeta) head.getItemMeta()).getPlayerProfile(); + assertNotNull(profile); + assertEquals("OfflineSkin", profile.getName()); + assertNull(profile.getId()); + assertTrue(profile.getProperties().isEmpty()); + mojang.verifyNoInteractions(); + } + } + + @Test + void joinInvalidatesCaseInsensitiveCacheAndPreservesOnlineSkin() { + ItemStack offline = Skulls.getPlayerSkull("OfflineSkin"); + offline.setAmount(5); + assertEquals(1, Skulls.getPlayerSkull("offlineskin").getAmount()); + + var player = server.addPlayer("OfflineSkin"); + var profile = player.getPlayerProfile(); + var texture = new ProfileProperty("textures", "custom-skin", "signature"); + profile.setProperty(texture); + player.setPlayerProfile(profile); + new PlayerListener().onPlayerJoin(new org.bukkit.event.player.PlayerJoinEvent(player, (net.kyori.adventure.text.Component) null)); + + ItemStack online = Skulls.getPlayerSkull("OFFLINESKIN"); + assertNotNull(online); + var skullProfile = ((SkullMeta) online.getItemMeta()).getPlayerProfile(); + assertNotNull(skullProfile); + assertEquals(player.getUniqueId(), skullProfile.getId()); + assertTrue(skullProfile.getProperties().contains(texture)); + } + + @Test + void invalidOwnerKeepsDefaultHeadPath() { + assertNull(Skulls.getPlayerSkull(null)); + assertNull(Skulls.getPlayerSkull("")); + assertNull(Skulls.getPlayerSkull("%missing owner%")); + assertNull(Skulls.getPlayerSkull("longerThanSixteenCharacters")); + } +}