diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
index e7ee21af..7e601786 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@ -1,5 +1,5 @@
name: Build
-on: [push]
+on: [ push ]
jobs:
build:
@@ -10,58 +10,67 @@ jobs:
steps:
- - name: Set up JDK 17
- uses: actions/setup-java@v1
+ - name: Set up JDK
+ uses: actions/setup-java@v5
with:
- java-version: 17
+ distribution: 'temurin'
+ java-version: |
+ 25
+ 21
- name: Cache Gradle
id: cache-gradle
- uses: actions/cache@v1
+ uses: actions/cache@v6
with:
path: ~/.gradle
key: ${{ runner.os }}-gradle
- - uses: actions/checkout@v1
+ - uses: actions/checkout@v7
- name: Grant execute permission to gradlew
run: chmod +x gradlew
- name: Build
- run: ./gradlew build :multiver:mc_1_17_R1:reobfJar :multiver:mc_1_18_R1:reobfJar :multiver:mc_1_18_R2:reobfJar :multiver:mc_1_19_R1:reobfJar :multiver:mc_1_19_R2:reobfJar :multiver:mc_1_19_R3:reobfJar :multiver:mc_1_20_R1:reobfJar :multiver:mc_1_20_R2:reobfJar :multiver:mc_1_20_R3:reobfJar shadowJar -x test
+ run: ./gradlew build -x test
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GH_URL: "https://api.github.com/"
+ MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
+ MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
- name: Upload build artifacts
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v7
with:
path: "**/build/libs"
release:
- needs: [build]
+ needs: [ build ]
if: github.ref == 'refs/heads/master' || 'refs/heads/beta' || github.ref == 'refs/heads/alpha'
runs-on: ubuntu-latest
steps:
- - name: Set up JDK 17
- uses: actions/setup-java@v1
+ - name: Set up JDK
+ uses: actions/setup-java@v5
with:
- java-version: 17
+ distribution: 'temurin'
+ java-version: |
+ 25
+ 21
- - name: Set up Node.js v18.x
- uses: actions/setup-node@v1
+ - name: Set up Node.js v20.x
+ uses: actions/setup-node@v6
with:
- node-version: "18.x"
+ node-version: "20.x"
- name: Cache Gradle
id: cache-gradle
- uses: actions/cache@v1
+ uses: actions/cache@v6
with:
path: ~/.gradle
key: ${{ runner.os }}-gradle
- - uses: actions/checkout@v1
+ - uses: actions/checkout@v7
- name: Grant execute permission for gradlew
run: chmod +x gradlew
diff --git a/.gitignore b/.gitignore
index 789aed76..d4c6a6aa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,229 +1,39 @@
-# Project exclude paths
-/.gradle/
-/build/
-/bukkit/build/
-/bukkit/build/classes/java/main/
-/core/build/
-/core/build/classes/java/main/
-/mc-1-17/.gradle/
-/mc-1-17/build/
-/mc-1-17/build/classes/java/main/
-/mc-1-18/.gradle/
-/mc-1-18/build/
-/mc-1-18/build/classes/java/main/
-/mc-1-18-2/.gradle/
-/mc-1-18-2/build/
-/mc-1-18-2/build/classes/java/main/
-/mc-1-19/.gradle/
-/mc-1-19/build/
-/mc-1-19/build/classes/java/main/
-/mc-1-19-3/.gradle/
-/mc-1-19-3/build/
-/mc-1-19-3/build/classes/java/main/
-
-# IntelliJ
-/.fleet/
-/.idea/dictionaries/
-/.idea/libraries/
-/.idea/*.xml
-*.iml
-
-# Gradle
-
.gradle
build/
+!gradle/wrapper/gradle-wrapper.jar
+!**/src/main/**/build/
+!**/src/test/**/build/
-# Server
-working/*
-**/debug/**/*
-!working/server.properties
-/.idea/
-
-# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
-# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
-
-# User-specific stuff
-.idea/**/workspace.xml
-.idea/**/tasks.xml
-.idea/**/usage.statistics.xml
-.idea/**/dictionaries
-.idea/**/shelf
-
-# Generated files
-.idea/**/contentModel.xml
-
-# Sensitive or high-churn files
-.idea/**/dataSources/
-.idea/**/dataSources.ids
-.idea/**/dataSources.local.xml
-.idea/**/sqlDataSources.xml
-.idea/**/dynamic.xml
-.idea/**/uiDesigner.xml
-.idea/**/dbnavigator.xml
-
-# Gradle
-.idea/**/gradle.xml
-.idea/**/libraries
-
-# Gradle and Maven with auto-import
-# When using Gradle or Maven with auto-import, you should exclude module files,
-# since they will be recreated, and may cause churn. Uncomment if using
-# auto-import.
-# .idea/artifacts
-# .idea/compiler.xml
-# .idea/jarRepositories.xml
-# .idea/modules.xml
-# .idea/*.iml
-# .idea/modules
-# *.iml
-# *.ipr
-
-# CMake
-cmake-build-*/
-
-# Mongo Explorer plugin
-.idea/**/mongoSettings.xml
-
-# File-based project format
+### IntelliJ IDEA ###
+.idea/*
*.iws
-
-# IntelliJ
+*.iml
+*.ipr
out/
-
-# mpeltonen/sbt-idea plugin
-.idea_modules/
-
-# JIRA plugin
-atlassian-ide-plugin.xml
-
-# Cursive Clojure plugin
-.idea/replstate.xml
-
-# Crashlytics plugin (for Android Studio and IntelliJ)
-com_crashlytics_export_strings.xml
-crashlytics.properties
-crashlytics-build.properties
-fabric.properties
-
-# Editor-based Rest Client
-.idea/httpRequests
-
-# Android studio 3.1+ serialized cache file
-.idea/caches/build_file_checksums.ser
-
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-lerna-debug.log*
-
-# Diagnostic reports (https://nodejs.org/api/report.html)
-report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
-
-# Runtime data
-pids
-*.pid
-*.seed
-*.pid.lock
-
-# Directory for instrumented libs generated by jscoverage/JSCover
-lib-cov
-
-# Coverage directory used by tools like istanbul
-coverage
-*.lcov
-
-# nyc test coverage
-.nyc_output
-
-# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
-.grunt
-
-# Bower dependency directory (https://bower.io/)
-bower_components
-
-# node-waf configuration
-.lock-wscript
-
-# Compiled binary addons (https://nodejs.org/api/addons.html)
-build/Release
-
-# Dependency directories
-node_modules/
-jspm_packages/
-
-# Snowpack dependency directory (https://snowpack.dev/)
-web_modules/
-
-# TypeScript cache
-*.tsbuildinfo
-
-# Optional npm cache directory
-.npm
-
-# Optional eslint cache
-.eslintcache
-
-# Microbundle cache
-.rpt2_cache/
-.rts2_cache_cjs/
-.rts2_cache_es/
-.rts2_cache_umd/
-
-# Optional REPL history
-.node_repl_history
-
-# Output of 'npm pack'
-*.tgz
-
-# Yarn Integrity file
-.yarn-integrity
-
-# dotenv environment variables file
-.env
-.env.test
-
-# parcel-bundler cache (https://parceljs.org/)
-.cache
-.parcel-cache
-
-# Next.js build output
-.next
-out
-
-# Nuxt.js build / generate output
-.nuxt
-dist
-
-# Gatsby files
-.cache/
-# Comment in the public line in if your project uses Gatsby and not Next.js
-# https://nextjs.org/blog/next-9-1#public-directory-support
-# public
-
-# vuepress build output
-.vuepress/dist
-
-# Serverless directories
-.serverless/
-
-# FuseBox cache
-.fusebox/
-
-# DynamoDB Local files
-.dynamodb/
-
-# TernJS port file
-.tern-port
-
-# Stores VSCode versions used for testing VSCode extensions
-.vscode-test
-
-# yarn v2
-
-.yarn/cache
-.yarn/unplugged
-.yarn/build-state.yml
-.pnp.*
\ No newline at end of file
+!**/src/main/**/out/
+!**/src/test/**/out/
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+!**/src/main/**/bin/
+!**/src/test/**/bin/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0cdd5d7e..c0eb28f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,176 @@
+# [8.0.0-beta.9](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.8...v8.0.0-beta.9) (2026-07-08)
+
+
+### Bug Fixes
+
+* catch specific NoSuchMethodError instead of generic Exception ([787f584](https://github.com/GeorgeV220/SkinOverlay/commit/787f584495e7f8ad876e9d66aa82716a22fcce93))
+* maven publish ([cb74575](https://github.com/GeorgeV220/SkinOverlay/commit/cb74575c68b3214b10e12b1ee3718af481aa2eb6))
+* use original property names when updating GameProfile properties ([eb3fc33](https://github.com/GeorgeV220/SkinOverlay/commit/eb3fc330ea0a8fa02873442f18be3cd0dea19387))
+
+
+### Features
+
+* add Minecraft 1.21.11 version support and refactor VersionRange ([f8e4a33](https://github.com/GeorgeV220/SkinOverlay/commit/f8e4a33aab07035592ec7107498cab9f0d5b5697))
+* add support for Minecraft 26.1 and 26.2 versions ([86bf989](https://github.com/GeorgeV220/SkinOverlay/commit/86bf989b8bf1a71dcac8085d843942681b267be9))
+
+# [8.0.0-beta.8](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.7...v8.0.0-beta.8) (2026-07-02)
+
+
+### Features
+
+* add placeholder field to skin configs and integrate with PlaceholderAPI ([2d04c3b](https://github.com/GeorgeV220/SkinOverlay/commit/2d04c3bcd5218d045b9636ab9327d7e9ec4b40c7))
+* improve placeholder API hook with configurable skin names ([71e990c](https://github.com/GeorgeV220/SkinOverlay/commit/71e990cc4b67af64dcb41776ddb7a484a2fb4012))
+
+# [8.0.0-beta.7](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.6...v8.0.0-beta.7) (2026-07-02)
+
+
+### Bug Fixes
+
+* add player placeholder to overlay reset success message ([0a83c6f](https://github.com/GeorgeV220/SkinOverlay/commit/0a83c6f434010e94f3b50c734239409d47f57019))
+* register PlaceholderAPI hook after plugin initialization ([b19c54f](https://github.com/GeorgeV220/SkinOverlay/commit/b19c54f765a60cee6244988219623400df435872))
+
+
+### Features
+
+* add multi-database support with MongoDB, MySQL, and SQLite ([c00a51f](https://github.com/GeorgeV220/SkinOverlay/commit/c00a51f36aa9b119200d28a2d158ad2c01b457e6))
+* implement SPlayer caching in PlayerProvider ([7e316b4](https://github.com/GeorgeV220/SkinOverlay/commit/7e316b43a2060cdb6a0f2ec4c9ba3a0e92a25bd6))
+* integrate Quark library manager for dependency loading ([030b71a](https://github.com/GeorgeV220/SkinOverlay/commit/030b71a683c9a3c6022a2f72a012948d07d623f6))
+* make Consumer parameter nullable in entity creation methods ([75d09ee](https://github.com/GeorgeV220/SkinOverlay/commit/75d09ee875a4792b0321e17719a3d235c519df1d))
+* simplify target player resolution in WearSubCommand ([44d73a4](https://github.com/GeorgeV220/SkinOverlay/commit/44d73a40e62eafdbd218c9f067d3d6b5b86b8296))
+
+# [8.0.0-beta.6](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.5...v8.0.0-beta.6) (2026-06-19)
+
+
+### Features
+
+* add Placeholder and PlaceholderUtils for dynamic message replacement ([c3f581e](https://github.com/GeorgeV220/SkinOverlay/commit/c3f581e8a00f3501094a292a00bd616006f51bfa))
+
+# [8.0.0-beta.5](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.4...v8.0.0-beta.5) (2026-06-18)
+
+
+### Bug Fixes
+
+* update repository URLs from GeorgeV22 to ShulkerLabs ([fecea1e](https://github.com/GeorgeV220/SkinOverlay/commit/fecea1ec7e85ee807330f8bc6f59914b1c65c551))
+
+# [8.0.0-beta.4](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.3...v8.0.0-beta.4) (2026-06-18)
+
+
+### Features
+
+* restructure data structures, storage, and task management ([d6519c9](https://github.com/GeorgeV220/SkinOverlay/commit/d6519c931d27534c406850d2c54b961a26a9b1ad))
+
+
+### BREAKING CHANGES
+
+* - Removed old `ConcurrentObjectMap`, `HashObjectMap`, `LinkedObjectMap`,
+ `ObjectMap`, `ObservableObjectMap`, `Pair`, `TreeObjectMap`,
+ `UnmodifiableObjectMap`, `PlayerEntityManager`, `SkinEntityManager`,
+ `PlayerFileManager`, `SkinFileManager`, `PairDocument`.
+- `Entity.getId()` renamed to `Entity.getUniqueId()`.
+- `EntityManagerRegistry` is now a singleton via `getInstance()` and
+ uses typed lookups via `getTyped(Class)`.
+- `EntityManager` load methods now return `CompletableFuture`.
+
+# [8.0.0-beta.3](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.2...v8.0.0-beta.3) (2025-11-30)
+
+
+* feat(core)!: introduce new command architecture and remove legacy message utilities ([f7768af](https://github.com/GeorgeV220/SkinOverlay/commit/f7768af1de07742f9f330c6b8b2fc462c8aaf25d))
+
+
+### Features
+
+* add command aliases for subcommands ([7a4f837](https://github.com/GeorgeV220/SkinOverlay/commit/7a4f8372629cd8252ab99fb05f9834f705b3c8d2))
+* add info subcommand to main command ([26939ea](https://github.com/GeorgeV220/SkinOverlay/commit/26939eae5a8477f986f5a77963f58547169fa673))
+* add no-op game profile provider and handle unsupported versions ([1dae36e](https://github.com/GeorgeV220/SkinOverlay/commit/1dae36eb1861b590be88349904e0cbe29b079924))
+* add reflect library and update package relocation paths ([7632063](https://github.com/GeorgeV220/SkinOverlay/commit/76320631a4f4e924b87ff9f968e73f244a75cc6b))
+* add SkinsRestorer integration and refactor skin applier ([b073d48](https://github.com/GeorgeV220/SkinOverlay/commit/b073d48ebd5f2ff70103d8f6b4b0e1f629cb5c6f))
+* add support for Minecraft 1.21.10-R0.1-SNAPSHOT ([85f5fce](https://github.com/GeorgeV220/SkinOverlay/commit/85f5fce25f26b8b9e94a43a07ec5b29ebaf700bf))
+* extend Minecraft 1.21 support range for Bukkit ([6942d02](https://github.com/GeorgeV220/SkinOverlay/commit/6942d021e1607a23bcb6c9e2a662a5fbf8cf5da0))
+
+
+### BREAKING CHANGES
+
+* - Removed legacy message system (MessageBuilder, MessageParser, MessagesUtil)
+- Removed PlayerOnly annotation
+- Replaced old message configuration with new MessageEntry-based system
+- Existing message configs and command usages must be updated to the new architecture
+
+feat(commands):
+- Added new annotations: Default, CommandTarget
+- Added MethodCommand for method-based command handling
+- Added registry system for extensible command targets (Registry, AbstractRegistry, CommandTargetRegistry)
+
+feat(messages):
+- Introduced new MessageEntry interface and MessageBuilder
+- Added MessagesRegistry for message management
+- Added CoreMessages and CommandMessages enums
+
+refactor(commands):
+- Reworked annotation processing with improved documentation and functionality
+- Enhanced CompletionEngine with a more robust resolver system
+- Added PreProcessor and PostProcessor interfaces
+- Rebuilt argument resolution and tab completion
+- Updated all command implementations and subcommands to the new system
+- Migrated all subcommands to use @Default
+- Improved command aliases, permissions, and descriptions
+
+refactor(config):
+- Updated CFG class with improved error handling
+- Removed legacy message configuration support
+
+refactor(utils):
+- Updated CustomData with cloning capabilities
+- Added Copyable interface and DeepCloner utility
+- General improvements across various utility functions
+
+chore(commands):
+- Refactored SkinOverlayMain and all subcommands to new architecture
+- Updated command completions and implementations
+
+Overall, this commit removes outdated messaging utilities and introduces a fully redesigned, extensible command and message system with better annotations, processors, registries, and utilities.
+
+# [8.0.0-beta.2](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.1...v8.0.0-beta.2) (2025-07-16)
+
+
+### Bug Fixes
+
+* **provider:** handle default skin not found case ([4ae9cd6](https://github.com/GeorgeV220/SkinOverlay/commit/4ae9cd62e22b184bf9c63f222ef2637b783f58eb))
+
+
+### Features
+
+* **logging:** add LoggerWrapper for unified logging integration ([82b1121](https://github.com/GeorgeV220/SkinOverlay/commit/82b112180486cca39592993afde599d7852c9af0))
+* **velocity:** add player event listeners and debug logging ([a626d79](https://github.com/GeorgeV220/SkinOverlay/commit/a626d793c6eb0161c0f2b8547486260d1f97b8a0))
+
+# [8.0.0-beta.1](https://github.com/GeorgeV220/SkinOverlay/compare/v7.1.0...v8.0.0-beta.1) (2025-07-15)
+
+
+### Bug Fixes
+
+* **completion:** handle empty args array in tab completion ([9dffca3](https://github.com/GeorgeV220/SkinOverlay/commit/9dffca35a088c4b460354a880a7ca8ebf71f19a1))
+* prevent saving entities in non-proxy mode when proxy enabled ([1381f51](https://github.com/GeorgeV220/SkinOverlay/commit/1381f515fd828813a4c9127c9879cd93b69bcab2))
+* **velocity:** pass plugin instance to audience provider ([2996a13](https://github.com/GeorgeV220/SkinOverlay/commit/2996a136fad4f9e99d0e560636700350f0ed650c))
+
+
+### Features
+
+* add Redis and PluginMessage support for skin synchronization ([a649393](https://github.com/GeorgeV220/SkinOverlay/commit/a64939330c35bbe5aa2cc3ef9fe912c57d2a504a))
+* conditionally initialize message handling based on proxy setting ([d77f772](https://github.com/GeorgeV220/SkinOverlay/commit/d77f772a828c7d02c88bc1a54102fb37660e6c4c))
+* **messaging:** recode messaging system with encryption and channels ([9deaaf1](https://github.com/GeorgeV220/SkinOverlay/commit/9deaaf1c25dfa24f9d47d8bf07004fd32dd25736))
+* **player:** delay publishPlayerJoin when PROXY enabled and not proxy ([07ae9a5](https://github.com/GeorgeV220/SkinOverlay/commit/07ae9a599559e6eb13b506cd1230a9a4f92527c3))
+* recode encryption and message handling ([0c901ef](https://github.com/GeorgeV220/SkinOverlay/commit/0c901ef9066b34dce5615c3d0bfb8503408380d9))
+* Recode of the entire plugin ([2ec6cf8](https://github.com/GeorgeV220/SkinOverlay/commit/2ec6cf855e7e29e9c157fff5e984a0a978dfe417))
+* Start recode ([6470152](https://github.com/GeorgeV220/SkinOverlay/commit/6470152d4d02eb8fb1faeff376539b9546efba98))
+* **utilities:** refactor message handling and add MessageParser ([2f35d09](https://github.com/GeorgeV220/SkinOverlay/commit/2f35d09962416ae0c759087cb37d21541c44f944))
+* Velocity support ([345924c](https://github.com/GeorgeV220/SkinOverlay/commit/345924c6c4e105ffb2129e4c2686a07cdfaec1c3))
+* **velocity:** add VelocityCommandManager initialization ([f9b59c5](https://github.com/GeorgeV220/SkinOverlay/commit/f9b59c5afc5c43b63d49dc1f8b5b71bcdecfe224))
+
+
+### BREAKING CHANGES
+
+* Old configs and save data may not be compatible with the new version
+* Removed all old classes for the recode
+
# [7.1.0](https://github.com/GeorgeV220/SkinOverlay/compare/v7.0.0...v7.1.0) (2024-01-04)
diff --git a/README.md b/README.md
index ef93f48f..4d7a6f82 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ You can have your project depend on SkinOverlay as a dependency through the foll
reposilite-repositoryGeorgeV22 Repository
- https://repo.georgev22.com/releases
+ https://repo.shulkerlabs.com/releases
@@ -26,7 +26,7 @@ You can have your project depend on SkinOverlay as a dependency through the foll
com.georgev22skinoverlay
- 7.1.0
+ 8.0.0-beta.9provided
@@ -40,12 +40,12 @@ You can include SkinOverlay into your gradle project using the following lines:
```groovy
repositories {
maven {
- url "https://repo.georgev22.com/releases"
+ url "https://repo.shulkerlabs.com/releases"
}
}
dependencies {
- compileOnly "com.georgev22:skinoverlay:7.1.0"
+ compileOnly "com.georgev22:skinoverlay:8.0.0-beta.9"
}
```
@@ -53,13 +53,13 @@ dependencies {
### Gradle
SkinOverlay can be built by running the following:
-`gradle clean build :multiver:mc_1_17_R1:reobfJar :multiver:mc_1_18_R1:reobfJar :multiver:mc_1_18_R2:reobfJar :multiver:mc_1_19_R1:reobfJar :multiver:mc_1_19_R2:reobfJar :multiver:mc_1_19_R3:reobfJar :multiver:mc_1_20_R1:reobfJar :multiver:mc_1_20_R2:reobfJar :multiver:mc_1_20_R3:reobfJar shadowJar`.
+`gradle clean build reobfJar shadowJar`.
The resultant jar is built and written
-to `build/libs/skinoverlay-{version}.jar`.
+to `{bukkit/velocity}build/libs/skinoverlay-{version}-{bukkit/velocity}.jar`.
The build directories can be cleaned instead using the `gradle clean` command.
-If you want to clean (install) and build the plugin use `gradle clean build :multiver:mc_1_17_R1:reobfJar :multiver:mc_1_18_R1:reobfJar :multiver:mc_1_18_R2:reobfJar :multiver:mc_1_19_R1:reobfJar :multiver:mc_1_19_R2:reobfJar :multiver:mc_1_19_R3:reobfJar :multiver:mc_1_20_R1:reobfJar :multiver:mc_1_20_R2:reobfJar :multiver:mc_1_20_R3:reobfJar shadowJar` command.
+If you want to clean (install) and build the plugin use `gradle clean build reobfJar shadowJar install` command.
# Contributing
diff --git a/build-info/build.gradle.kts b/build-info/build.gradle.kts
new file mode 100644
index 00000000..7aefe288
--- /dev/null
+++ b/build-info/build.gradle.kts
@@ -0,0 +1,42 @@
+import java.text.SimpleDateFormat
+import java.util.*
+
+plugins {
+ id("buildlogic.java-conventions")
+ id("org.jetbrains.gradle.plugin.idea-ext") version "1.1.10"
+ id("net.kyori.indra.git") version "3.1.3"
+ alias(libs.plugins.blossom)
+}
+
+sourceSets {
+ main {
+ blossom {
+ javaSources {
+ property("pluginId", project.property("pluginName").toString().lowercase())
+ property("pluginName", project.property("pluginName").toString())
+ property("version", version.toString())
+ property("description", rootProject.description ?: "unknown")
+ property("author", project.property("author").toString())
+ property("url", "https://github.com/GeorgeV220/SkinOverlay")
+ property("commit", indraGit.commit()?.name ?: "unknown")
+ property("branch", indraGit.branch()?.name ?: "unknown")
+ property("build_time", SimpleDateFormat("dd MMMM yyyy HH:mm:ss").format(Date()))
+ property("ci_name", getRunnerName())
+ property("ci_build_number", getBuildNumber())
+ }
+ }
+ }
+}
+
+fun getRunnerName(): String {
+ val githubActions = System.getenv("GITHUB_ACTIONS")
+ if (githubActions != null && githubActions == "true") {
+ return "github-actions"
+ }
+
+ return "local"
+}
+
+fun getBuildNumber(): String {
+ return System.getenv("BUILD_NUMBER") ?: System.getenv("GITHUB_RUN_NUMBER") ?: "local"
+}
\ No newline at end of file
diff --git a/build-info/src/main/java-templates/com/georgev22/skinoverlay/BuildParameters.java.peb b/build-info/src/main/java-templates/com/georgev22/skinoverlay/BuildParameters.java.peb
new file mode 100644
index 00000000..da31b09e
--- /dev/null
+++ b/build-info/src/main/java-templates/com/georgev22/skinoverlay/BuildParameters.java.peb
@@ -0,0 +1,15 @@
+package com.georgev22.skinoverlay;
+
+public class BuildParameters {
+ public static final String VERSION = "{{ version }}";
+ public static final String PLUGIN_NAME = "{{ pluginName }}";
+ public static final String PLUGIN_ID = "{{ pluginId }}";
+ public static final String AUTHOR = "{{ author }}";
+ public static final String DESCRIPTION = "{{ description }}";
+ public static final String URL = "{{ url }}";
+ public static final String COMMIT = "{{ commit }}";
+ public static final String BRANCH = "{{ branch }}";
+ public static final String BUILD_TIME = "{{ build_time }}";
+ public static final String CI_NAME = "{{ ci_name }}";
+ public static final String CI_BUILD_NUMBER = "{{ ci_build_number }}";
+}
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index 253a73d7..00000000
--- a/build.gradle
+++ /dev/null
@@ -1,183 +0,0 @@
-buildscript {
- repositories {
- mavenCentral()
- }
-}
-
-plugins {
- id 'com.github.johnrengelman.shadow' version '8.1.1'
- id 'io.freefair.lombok' version '8.0.1'
- id "net.kyori.blossom" version "1.3.1"
- id 'java'
- id 'jacoco'
- id 'idea'
-}
-
-apply from: "$rootDir/gradle/jacoco.gradle"
-apply from: "$rootDir/gradle/publish.gradle"
-
-if (project.hasProperty("local_script")) {
- apply from: file(local_script + "/build.local.gradle")
-}
-
-ext {
- mcVersion = project.property("mcVersion")
-}
-
-group project.property("group")
-
-allprojects {
- apply plugin: 'java'
- apply plugin: 'idea'
- apply plugin: 'io.freefair.lombok'
- apply plugin: 'com.github.johnrengelman.shadow'
- apply plugin: 'net.kyori.blossom'
-
- group project.property("group")
-
- repositories {
- mavenLocal()
- mavenCentral()
- maven { url = "https://repo.georgev22.com/releases/" }
- maven { url = "https://repo.georgev22.com/snapshots/" }
- maven { url = 'https://repo.codemc.org/repository/maven-public/' }
- maven { url = 'https://repo.papermc.io/repository/maven-public/' }
- maven { url = 'https://hub.spigotmc.org/nexus/content/repositories/public/' }
- maven { url = 'https://oss.sonatype.org/content/repositories/snapshots/' }
- maven { url = 'https://nexus.velocitypowered.com/repository/maven-public/' }
- maven { url = 'https://repo.dmulloy2.net/repository/public/' }
- maven { url = 'https://repo.inventivetalent.org/repository/public/' }
- maven { url = 'https://jitpack.io' }
- }
-
- dependencies {
- compileOnly "com.georgev22.library:database:${libraryVersion}"
- compileOnly "com.georgev22.library:maps:${libraryVersion}"
- compileOnly "com.georgev22.library:utilities:${libraryVersion}"
- compileOnly "com.georgev22.library:yaml:${libraryVersion}"
- compileOnly "com.georgev22.library:minecraft:${libraryVersion}"
-
- compileOnly "com.georgev22.api:libraryloader:${libraryLoaderVersion}"
-
- compileOnly "co.aikar:acf-paper:${acfVersion}"
- compileOnly "co.aikar:acf-bungee:${acfVersion}"
- compileOnly "co.aikar:acf-velocity:${acfVersion}"
-
- compileOnly "org.bstats:bstats-bukkit:${bstatsVersion}"
- compileOnly "org.bstats:bstats-bungeecord:${bstatsVersion}"
- compileOnly "org.bstats:bstats-velocity:${bstatsVersion}"
-
- compileOnly "net.skinsrestorer:skinsrestorer-api:${skinsRestorerVersion}"
-
- compileOnly "net.kyori:adventure-api:${adventureVersion}"
- compileOnly "net.kyori:adventure-text-serializer-legacy:${adventureVersion}"
-
- compileOnly "org.apache.logging.log4j:log4j-api:${log4jVersion}"
-
- compileOnly "com.google.code.gson:gson:${gsonVersion}"
- compileOnly "com.google.guava:guava:${guavaVersion}"
-
- compileOnly "org.jetbrains:annotations:${jetbrainsAnnotationsVersion}"
-
- compileOnly "commons-codec:commons-codec:${commonscodecVersion}"
- compileOnly "commons-io:commons-io:${commonsioVersion}"
- compileOnly "commons-lang:commons-lang:${commonslangVersion}"
-
- compileOnly "org.mongodb:mongodb-driver:${mongodbVersion}"
-
- compileOnly "com.mojang:authlib:${authlibVersion}"
-
- compileOnly "com.github.aematsubara:MineskinClient:${mineskinVersion}"
- }
-}
-
-repositories {
- mavenLocal()
- mavenCentral()
- maven { url = "https://repo.georgev22.com/releases/" }
- maven { url = "https://repo.georgev22.com/snapshots/" }
- maven { url = 'https://repo.codemc.org/repository/maven-public/' }
- maven { url = 'https://repo.papermc.io/repository/maven-public/' }
- maven { url = 'https://hub.spigotmc.org/nexus/content/repositories/public/' }
- maven { url = 'https://oss.sonatype.org/content/repositories/snapshots/' }
- maven { url = 'https://nexus.velocitypowered.com/repository/maven-public/' }
- maven { url = 'https://repo.dmulloy2.net/repository/public/' }
-}
-
-dependencies {
- implementation "com.georgev22.library:database:${libraryVersion}"
- implementation "com.georgev22.library:maps:${libraryVersion}"
- implementation "com.georgev22.library:utilities:${libraryVersion}"
- implementation "com.georgev22.library:yaml:${libraryVersion}"
- implementation "com.georgev22.library:minecraft:${libraryVersion}"
-
- implementation "com.georgev22.api:libraryloader:${libraryLoaderVersion}"
-
- implementation "co.aikar:acf-paper:${acfVersion}"
- implementation "co.aikar:acf-bungee:${acfVersion}"
- implementation "co.aikar:acf-velocity:${acfVersion}"
-
- implementation "org.bstats:bstats-bukkit:${bstatsVersion}"
- implementation "org.bstats:bstats-bungeecord:${bstatsVersion}"
- implementation "org.bstats:bstats-velocity:${bstatsVersion}"
-
- implementation "net.kyori:adventure-platform-bukkit:${adventurePlatformVersion}"
- implementation "net.kyori:adventure-platform-bungeecord:${adventurePlatformVersion}"
-
- implementation "com.google.code.gson:gson:${gsonVersion}"
-
- implementation "io.papermc:paperlib:${paperlibVersion}"
-
- implementation "com.github.aematsubara:MineskinClient:${mineskinVersion}"
-
- implementation project(path: ':core')
- implementation project(path: ':bukkit')
- implementation project(path: ':bungee')
- implementation project(path: ':velocity')
- implementation project(path: ':multiver:mc_1_17_R1', configuration: 'reobf')
- implementation project(path: ':multiver:mc_1_18_R1', configuration: 'reobf')
- implementation project(path: ':multiver:mc_1_18_R2', configuration: 'reobf')
- implementation project(path: ':multiver:mc_1_19_R1', configuration: 'reobf')
- implementation project(path: ':multiver:mc_1_19_R2', configuration: 'reobf')
- implementation project(path: ':multiver:mc_1_19_R3', configuration: 'reobf')
- implementation project(path: ':multiver:mc_1_20_R1', configuration: 'reobf')
- implementation project(path: ':multiver:mc_1_20_R2', configuration: 'reobf')
- implementation project(path: ':multiver:mc_1_20_R3', configuration: 'reobf')
-}
-
-shadowJar {
- archiveClassifier.set('')
- exclude("org/jsoup/**")
- relocate 'org.mineskin', "${packageName}.mineskin"
- relocate 'com.google.gson', "${packageName}.gson"
- relocate 'co.aikar.commands', "${packageName}.commands.acf"
- relocate 'co.aikar.locales', "${packageName}.commands.acf.locales"
- relocate 'org.bstats', "${packageName}.bstats"
- relocate 'com.georgev22.api.libraryloader', "${packageName}.libraryloader"
- relocate 'com.georgev22.library', "${packageName}.library"
- relocate 'org.yaml.snakeyaml', "${packageName}.snakeyaml"
- relocate 'io.papermc.lib', "${packageName}.paperlib"
-}
-
-jar {
- manifest {
- attributes["MixinConfigs"] = "mixins.skinoverlay.json"
- }
-}
-
-tasks.build.dependsOn(shadowJar)
-tasks.publish.dependsOn(shadowJar)
-
-processResources {
- filesMatching('**.yml') {
- project.properties.put('pluginName', this.pluginName)
- project.properties.put("version", this.version)
- project.properties.put("bungeeMain", this.bungeeMain)
- project.properties.put("author", this.author)
- expand project.properties
- filteringCharset 'UTF-8'
- expand project.properties
- }
-}
-
-defaultTasks 'build'
\ No newline at end of file
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
new file mode 100644
index 00000000..8156a89b
--- /dev/null
+++ b/buildSrc/build.gradle.kts
@@ -0,0 +1,7 @@
+plugins {
+ `kotlin-dsl`
+}
+
+repositories {
+ gradlePluginPortal()
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts
new file mode 100644
index 00000000..3313660f
--- /dev/null
+++ b/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts
@@ -0,0 +1,45 @@
+plugins {
+ `java-library`
+ `maven-publish`
+}
+
+if (file("$rootDir/build.local.gradle.kts").exists()) {
+ apply("$rootDir/build.local.gradle.kts")
+}
+
+repositories {
+ mavenLocal()
+ maven {
+ url = uri("https://maven-central.storage-download.googleapis.com/maven2/")
+ }
+ maven {
+ url = uri("https://repo.shulkerlabs.com/releases/")
+ }
+ maven {
+ url = uri("https://repo.shulkerlabs.com/snapshots/")
+ }
+ maven {
+ url = uri("https://repo.codemc.org/repository/maven-public/")
+ }
+ maven {
+ url = uri("https://repo.papermc.io/repository/maven-public/")
+ }
+ maven {
+ url = uri("https://libraries.minecraft.net/")
+ content {
+ includeGroup("com.mojang")
+ }
+ }
+ maven {
+ url = uri("https://hub.spigotmc.org/nexus/content/repositories/snapshots/")
+ }
+ maven {
+ url = uri("https://jitpack.io")
+ }
+ maven {
+ url = uri("https://repo.extendedclip.com/content/repositories/placeholderapi/")
+ }
+ maven {
+ url = uri("https://repo.inventivetalent.org/repository/public/")
+ }
+}
\ No newline at end of file
diff --git a/bukkit/build.gradle b/bukkit/build.gradle
deleted file mode 100644
index 302f0762..00000000
--- a/bukkit/build.gradle
+++ /dev/null
@@ -1,22 +0,0 @@
-repositories {
- maven { url 'https://jitpack.io' }
-}
-
-dependencies {
- compileOnly 'dev.folia:folia-api:1.19.4-R0.1-SNAPSHOT'
- compileOnly "io.papermc:paperlib:${paperlibVersion}"
- compileOnly project(path: ':core')
- compileOnly project(path: ':multiver:mc_1_17_R1', configuration: 'reobf')
- compileOnly project(path: ':multiver:mc_1_18_R1', configuration: 'reobf')
- compileOnly project(path: ':multiver:mc_1_18_R2', configuration: 'reobf')
- compileOnly project(path: ':multiver:mc_1_19_R1', configuration: 'reobf')
- compileOnly project(path: ':multiver:mc_1_19_R2', configuration: 'reobf')
- compileOnly project(path: ':multiver:mc_1_19_R3', configuration: 'reobf')
- compileOnly project(path: ':multiver:mc_1_20_R1', configuration: 'reobf')
- compileOnly project(path: ':multiver:mc_1_20_R2', configuration: 'reobf')
- compileOnly project(path: ':multiver:mc_1_20_R3', configuration: 'reobf')
-
- implementation "net.kyori:adventure-platform-bukkit:4.3.0"
-}
-
-defaultTasks 'build'
\ No newline at end of file
diff --git a/bukkit/build.gradle.kts b/bukkit/build.gradle.kts
new file mode 100644
index 00000000..f7ebfc0f
--- /dev/null
+++ b/bukkit/build.gradle.kts
@@ -0,0 +1,119 @@
+plugins {
+ id("buildlogic.java-conventions")
+ alias(libs.plugins.gradleup.shadow)
+ id("org.bxteam.quark") version "1.3.0"
+}
+
+apply(from = "$rootDir/gradle/publish.gradle")
+
+repositories {
+ maven {
+ url = uri("https://maven-central.storage-download.googleapis.com/maven2/")
+ }
+}
+
+dependencies {
+ compileOnly(libs.folia.api)
+ compileOnly(libs.placeholder.api)
+ compileOnly(libs.auth.lib.legacy)
+
+ quark(libs.bstats.bukkit)
+ quark(libs.adventure.platform.bukkit)
+ quark(libs.adventure.text.minimessage)
+ quark(libs.adventure.text.serializer.legacy)
+
+ quark(libs.hikari) {
+ exclude(group = "org.slf4j", module = "slf4j-api")
+ }
+
+ quark(libs.gson)
+
+ // MINESKIN CLIENT
+ quark(libs.mineskinclient.client) {
+ exclude(group = "com.google.code.gson", module = "gson")
+ exclude(group = "com.google.guava", module = "guava")
+ }
+ quark(libs.mineskinclient.java11) {
+ exclude(group = "com.google.code.gson", module = "gson")
+ exclude(group = "com.google.guava", module = "guava")
+ }
+
+ // YAML
+ quark(libs.yamlconfiguration) {
+ exclude(group = "org.slf4j", module = "slf4j-api")
+ }
+
+ quark(libs.jedis)
+
+ quark(libs.jspecify)
+ quark(libs.mongodb.driver.sync)
+
+ implementation(project(":common"))
+ implementation(project(":bukkit:versions:mc1_17", configuration = "reobf"))
+ implementation(project(":bukkit:versions:mc1_18", configuration = "reobf"))
+ implementation(project(":bukkit:versions:mc1_18_2", configuration = "reobf"))
+ implementation(project(":bukkit:versions:mc1_19", configuration = "reobf"))
+ implementation(project(":bukkit:versions:mc1_19_3", configuration = "reobf"))
+ implementation(project(":bukkit:versions:mc1_19_4", configuration = "reobf"))
+ implementation(project(":bukkit:versions:mc1_20", configuration = "reobf"))
+ implementation(project(":bukkit:versions:mc1_20_2", configuration = "reobf"))
+ implementation(project(":bukkit:versions:mc1_20_3", configuration = "reobf"))
+ implementation(project(":bukkit:versions:mc1_20_5", configuration = "reobf"))
+ implementation(project(":bukkit:versions:mc1_21", configuration = "reobf"))
+ implementation(project(":bukkit:versions:mc1_21_2", configuration = "reobf"))
+ implementation(project(":bukkit:versions:mc1_21_4", configuration = "reobf"))
+ implementation(project(":bukkit:versions:mc1_21_5", configuration = "reobf"))
+ implementation(project(":bukkit:versions:mc1_21_6", configuration = "reobf"))
+ implementation(project(":bukkit:versions:mc1_21_9", configuration = "reobf"))
+ implementation(project(":bukkit:versions:mc1_21_11", configuration = "reobf"))
+ implementation(project(":bukkit:versions:mc26_1"))
+}
+
+quark {
+ platform = "bukkit"
+ repositories {
+ includeProjectRepositories();
+ }
+
+ relocate("org.mineskin", "${project.property("packageName")}.lib.mineskin")
+ relocate("com.google.gson", "${project.property("packageName")}.lib.gson")
+ relocate("com.google.errorprone", "${project.property("packageName")}.lib.gson.errorprone")
+ relocate("com.zaxxer", "${project.property("packageName")}.lib.zaxxer")
+ relocate("org.bstats", "${project.property("packageName")}.lib.bstats")
+ relocate("org.bspfsystems.yamlconfiguration", "${project.property("packageName")}.lib.yaml")
+ relocate("org.yaml.snakeyaml", "${project.property("packageName")}.lib.yaml")
+ relocate("org.intellij.lang", "${project.property("packageName")}.lib.jetbrains")
+ relocate("org.jetbrains", "${project.property("packageName")}.lib.jetbrains")
+ relocate("org.json", "${project.property("packageName")}.lib.json")
+ relocate("org.apache.commons.pool2", "${project.property("packageName")}.lib.pool2")
+ relocate("net.kyori", "${project.property("packageName")}.lib.kyori")
+ relocate("redis.clients", "${project.property("packageName")}.lib.jedis")
+ relocate("org.bson", "${project.property("packageName")}.lib.bson")
+ relocate("com.mongodb", "${project.property("packageName")}.lib.mongodb")
+}
+
+configurations.configureEach {
+ resolutionStrategy {
+ force(libs.adventure.platform.bukkit)
+ force(libs.adventure.text.minimessage)
+ force(libs.adventure.text.serializer.legacy)
+ force(libs.folia.api)
+ dependencySubstitution {
+ substitute(module("org.spigotmc:spigot-api"))
+ .using(module(libs.folia.api.get().toString()))
+ substitute(module("org.bukkit:bukkit"))
+ .using(module(libs.folia.api.get().toString()))
+ substitute(module("io.papermc.paper:paper-api"))
+ .using(module(libs.folia.api.get().toString()))
+ }
+ }
+}
+
+tasks.shadowJar {
+ archiveBaseName.set("skinoverlay")
+ archiveClassifier.set("bukkit")
+}
+
+tasks.named("publish") {
+ dependsOn("shadowJar")
+}
\ No newline at end of file
diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java
index fabb11f9..30d43062 100644
--- a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java
+++ b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java
@@ -1,219 +1,111 @@
package com.georgev22.skinoverlay;
-import co.aikar.commands.PaperCommandManager;
-import com.georgev22.api.libraryloader.LibraryLoader;
-import com.georgev22.api.libraryloader.annotations.MavenLibrary;
-import com.georgev22.api.libraryloader.exceptions.InvalidDependencyException;
-import com.georgev22.api.libraryloader.exceptions.UnknownDependencyException;
-import com.georgev22.library.maps.ObservableObjectMap;
-import com.georgev22.library.minecraft.BukkitMinecraftUtils;
-import com.georgev22.library.minecraft.scheduler.MinecraftBukkitScheduler;
-import com.georgev22.library.minecraft.scheduler.MinecraftFoliaScheduler;
-import com.georgev22.library.minecraft.scheduler.MinecraftScheduler;
-import com.georgev22.skinoverlay.handler.handlers.*;
-import com.georgev22.skinoverlay.listeners.bukkit.DeveloperInformListener;
-import com.georgev22.skinoverlay.listeners.bukkit.PaperPlayerListeners;
+import com.georgev22.skinoverlay.hooks.placeholder.PlaceholderAPIHook;
+import com.georgev22.skinoverlay.refreshers.*;
+import com.georgev22.skinoverlay.command.BukkitCommandManager;
+import com.georgev22.skinoverlay.hooks.SkinHookNoop;
+import com.georgev22.skinoverlay.hooks.SkinsRestorerHook;
import com.georgev22.skinoverlay.listeners.bukkit.PlayerListeners;
-import com.georgev22.skinoverlay.utilities.BukkitPluginMessageUtils;
+import com.georgev22.skinoverlay.listeners.bukkit.PluginMessageListenerImpl;
+import com.georgev22.skinoverlay.messaging.MessageManagerNoop;
+import com.georgev22.skinoverlay.messaging.RedisManager;
+import com.georgev22.skinoverlay.player.SPlayer;
+import com.georgev22.skinoverlay.providers.*;
+import com.georgev22.skinoverlay.scheduler.MinecraftBukkitScheduler;
+import com.georgev22.skinoverlay.scheduler.MinecraftFoliaScheduler;
+import com.georgev22.skinoverlay.utilities.BukkitAdventure;
+import com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils;
+import com.georgev22.skinoverlay.utilities.SkinOverlayVersionResolver;
import com.georgev22.skinoverlay.utilities.config.OptionsUtil;
-import com.georgev22.skinoverlay.utilities.interfaces.SkinOverlayImpl;
-import com.georgev22.skinoverlay.utilities.player.PlayerObject;
-import com.georgev22.skinoverlay.utilities.player.PlayerObjectBukkit;
-import io.papermc.lib.PaperLib;
-import net.kyori.adventure.platform.bukkit.BukkitAudiences;
-import org.bstats.bukkit.Metrics;
-import org.bukkit.*;
-import org.bukkit.entity.Player;
-import org.bukkit.plugin.Plugin;
+import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
+import org.bxteam.quark.bukkit.BukkitLibraryManager;
-import java.io.File;
-import java.util.UUID;
-import java.util.logging.Logger;
+import static com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils.MinecraftVersion.getCurrent;
-import static com.georgev22.library.minecraft.BukkitMinecraftUtils.MinecraftVersion.*;
+public class SkinOverlayBukkit extends JavaPlugin {
-@MavenLibrary(groupId = "org.mongodb", artifactId = "mongo-java-driver", version = "3.12.7")
-@MavenLibrary(groupId = "mysql", artifactId = "mysql-connector-java", version = "8.0.22")
-@MavenLibrary(groupId = "org.xerial", artifactId = "sqlite-jdbc", version = "3.34.0")
-@MavenLibrary(groupId = "com.google.guava", artifactId = "guava", version = "30.1.1-jre")
-@MavenLibrary(groupId = "org.postgresql", artifactId = "postgresql", version = "42.2.18")
-@MavenLibrary(groupId = "commons-io", artifactId = "commons-io", version = "2.11.0")
-@MavenLibrary(groupId = "commons-codec", artifactId = "commons-codec", version = "1.15")
-@MavenLibrary(groupId = "commons-lang", artifactId = "commons-lang", version = "2.6")
-@MavenLibrary(groupId = "org.jsoup", artifactId = "jsoup", version = "1.15.3")
-@MavenLibrary("org.apache.commons:commons-lang3:3.12.0:https://repo1.maven.org/maven2/")
-@ApiStatus.Internal
-@ApiStatus.NonExtendable
-public class SkinOverlayBukkit extends JavaPlugin implements SkinOverlayImpl {
- private LibraryLoader libraryLoader;
+ private static SkinOverlayBukkit instance;
- private BukkitAudiences adventure;
- private SkinOverlay skinOverlay;
+ private final SkinOverlay skinOverlay = SkinOverlay.getInstance();
- private MinecraftScheduler scheduler;
-
- @Override
- public void onLoad() {
- try {
- if (getCurrentVersion().isBelow(V1_16_R3)) {
- this.libraryLoader = new LibraryLoader(this.getClassLoader(), this.getDataFolder());
- this.libraryLoader.loadAll(this, true);
- }
- } catch (InvalidDependencyException | UnknownDependencyException e) {
- throw new RuntimeException(e);
- }
- skinOverlay = new SkinOverlay(this);
- skinOverlay.onLoad();
+ public static SkinOverlayBukkit getInstance() {
+ return instance;
}
@Override
- public void onEnable() {
- if (BukkitMinecraftUtils.MinecraftVersion.getCurrentVersion().isBelow(V1_16_R1))
- this.adventure = BukkitAudiences.create(this);
- if (isFolia()) {
- this.scheduler = new MinecraftFoliaScheduler();
+ public void onLoad() {
+ instance = this;
+ BukkitLibraryManager libraryManager = new BukkitLibraryManager(this);
+ libraryManager.loadFromGradle();
+ this.skinOverlay.setPlugin(this);
+ this.skinOverlay.setLogger(this.getLogger());
+ this.skinOverlay.setDataFolder(this.getDataFolder());
+ this.skinOverlay.setCommandManager(new BukkitCommandManager(this));
+ this.skinOverlay.setPlayerProvider(new BukkitPlayerProvider());
+
+ if (Bukkit.getPluginManager().isPluginEnabled("SkinsRestorer")) {
+ this.skinOverlay.setSkinHook(new SkinsRestorerHook());
+ this.skinOverlay.setGameProfileProvider(new GameProfileProvider_SkinsRestorer());
+ this.skinOverlay.setSkinRefresher(new NoopSkinRefresher());
} else {
- this.scheduler = new MinecraftBukkitScheduler();
- }
- this.skinOverlay.setMinecraftScheduler(this.scheduler);
- switch (getCurrentVersion()) {
- case V1_17_R1 -> skinOverlay.setSkinHandler(new SkinHandler_1_17_R1());
- case V1_18_R1 -> skinOverlay.setSkinHandler(new SkinHandler_1_18_R1());
- case V1_18_R2 -> skinOverlay.setSkinHandler(new SkinHandler_1_18_R2());
- case V1_19_R1 -> skinOverlay.setSkinHandler(new SkinHandler_1_19_R1());
- case V1_19_R2 -> skinOverlay.setSkinHandler(new SkinHandler_1_19_R2());
- case V1_19_R3 -> skinOverlay.setSkinHandler(new SkinHandler_1_19_R3());
- case V1_20_R1 -> skinOverlay.setSkinHandler(new SkinHandler_1_20_R1());
- case V1_20_R2 -> skinOverlay.setSkinHandler(new SkinHandler_1_20_R2());
- case V1_20_R3 -> skinOverlay.setSkinHandler(new SkinHandler_1_20_R3());
- case UNKNOWN -> skinOverlay.setSkinHandler(new SkinHandler_Unsupported());
- default -> skinOverlay.setSkinHandler(new SkinHandler_Legacy());
- }
- skinOverlay.setCommandManager(new PaperCommandManager(this));
- skinOverlay.onEnable();
- BukkitMinecraftUtils.registerListeners(this, new PlayerListeners(), new DeveloperInformListener());
- if (PaperLib.isPaper() & getCurrentVersion().isAboveOrEqual(V1_15_R1))
- BukkitMinecraftUtils.registerListeners(this, new PaperPlayerListeners());
- if (OptionsUtil.PROXY.getBooleanValue()) {
- skinOverlay.setPluginMessageUtils(new BukkitPluginMessageUtils());
- Bukkit.getServer().getMessenger().registerIncomingPluginChannel(this, "skinoverlay:bungee", new PlayerListeners());
- Bukkit.getServer().getMessenger().registerOutgoingPluginChannel(this, "skinoverlay:message");
- }
- if (OptionsUtil.METRICS.getBooleanValue())
- new Metrics(this, 17474);
- if (!PaperLib.isPaper())
- PaperLib.suggestPaper(this);
- }
-
- @Override
- public void onDisable() {
- Bukkit.getServer().getMessenger().unregisterIncomingPluginChannel(this);
- Bukkit.getServer().getMessenger().unregisterOutgoingPluginChannel(this);
- skinOverlay.onDisable();
- if (this.adventure != null) {
- this.adventure.close();
- this.adventure = null;
- }
- if (getCurrentVersion().isBelow(V1_16_R3)) {
- try {
- this.libraryLoader.unloadAll();
- } catch (InvalidDependencyException e) {
- throw new RuntimeException(e);
+ this.skinOverlay.setSkinHook(new SkinHookNoop());
+ if (BukkitMinecraftUtils.isPaper() && getCurrent().isAtLeast(1, 20)) {
+ this.skinOverlay.setSkinRefresher(new NoopSkinRefresher());
+ this.skinOverlay.setGameProfileProvider(new PaperGameProfileProvider());
+ } else {
+ new SkinOverlayVersionResolver(this.skinOverlay).resolve();
}
}
- }
-
-
- @Override
- public Type type() {
- return Type.BUKKIT;
- }
-
- @Override
- public File dataFolder() {
- return this.getDataFolder();
- }
-
- @Override
- public Logger logger() {
- return this.getLogger();
- }
-
- @Override
- public Description description() {
- return new Description(this.getName(), this.getDescription().getVersion(), this.getDescription().getMain(), this.getDescription().getAuthors());
- }
-
- @Override
- public boolean enable(boolean enable) {
- this.setEnabled(enable);
- return enabled();
- }
- @Override
- public boolean enabled() {
- return this.isEnabled();
+ this.skinOverlay.setOnlineMode(Bukkit.getOnlineMode());
+ this.skinOverlay.setProxy(false);
+ this.skinOverlay.setScheduler(
+ BukkitMinecraftUtils.isFolia() ? new MinecraftFoliaScheduler() : new MinecraftBukkitScheduler()
+ );
+ this.skinOverlay.onLoad();
}
@Override
- public void saveResource(@NotNull String resource, boolean replace) {
- super.saveResource(resource, replace);
- }
-
- @Override
- public boolean onlineMode() {
- return Bukkit.getOnlineMode();
- }
-
- private final ObservableObjectMap players = new ObservableObjectMap<>();
+ public void onEnable() {
+ BukkitAdventure.init();
+ BukkitMinecraftUtils.registerListeners(
+ this,
+ new PlayerListeners()
+ );
- @Override
- public ObservableObjectMap onlinePlayers() {
- for (Player player : Bukkit.getOnlinePlayers()) {
- if (players.containsKey(player.getUniqueId())) {
- continue;
+ if (OptionsUtil.PROXY.getBooleanValue()) {
+ if (OptionsUtil.CONNECTION_TYPE.getStringValue().equalsIgnoreCase("PluginMessage")) {
+ this.skinOverlay.setMessageManager(new PluginMessageListenerImpl());
+ } else if (OptionsUtil.CONNECTION_TYPE.getStringValue().equalsIgnoreCase("Redis")) {
+ this.skinOverlay.setMessageManager(new RedisManager(
+ OptionsUtil.REDIS_HOST.getStringValue(),
+ OptionsUtil.REDIS_PORT.getIntValue(),
+ OptionsUtil.REDIS_PASSWORD.getStringValue()
+ ));
+ } else {
+ this.skinOverlay.setMessageManager(new MessageManagerNoop());
}
- players.append(player.getUniqueId(), new PlayerObjectBukkit(player));
+ skinOverlay.getMessageManager().subscribeSkinProperty((uuid, skin) -> {
+ SPlayer player = this.skinOverlay.getPlayerProvider().getSPlayer(uuid);
+ if (player != null) {
+ skinOverlay.getSkinProvider().setSkin(player, skin);
+ }
+ });
}
- return players;
- }
- @Override
- public boolean isPluginEnabled(String pluginName) {
- return this.getServer().getPluginManager().isPluginEnabled(pluginName);
- }
-
- @Override
- public JavaPlugin plugin() {
- return this;
- }
+ if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) {
+ this.skinOverlay.setPlaceholderHook(new PlaceholderAPIHook());
+ }
- @Override
- public Server serverImpl() {
- return this.getServer();
+ // call onEnable
+ skinOverlay.onEnable();
}
@Override
- public String serverVersion() {
- return Bukkit.getBukkitVersion();
- }
-
- public @NotNull BukkitAudiences adventure() {
- if (this.adventure == null) {
- throw new IllegalStateException("Tried to access Adventure when the plugin was disabled!");
- }
- return this.adventure;
+ public void onDisable() {
+ skinOverlay.onDisable();
+ BukkitAdventure.close();
}
- private boolean isFolia() {
- try {
- Class.forName("io.papermc.paper.threadedregions.RegionizedServer");
- return true;
- } catch (ClassNotFoundException e) {
- return false;
- }
- }
-}
\ No newline at end of file
+}
diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandIssuer.java b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandIssuer.java
new file mode 100644
index 00000000..b8757738
--- /dev/null
+++ b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandIssuer.java
@@ -0,0 +1,64 @@
+package com.georgev22.skinoverlay.command;
+
+import com.georgev22.skinoverlay.utilities.BukkitAdventure;
+import com.georgev22.skinoverlay.utilities.Utils;
+import net.kyori.adventure.audience.Audience;
+import net.kyori.adventure.platform.bukkit.BukkitAudiences;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public class BukkitCommandIssuer implements CommandIssuer {
+
+ private final CommandSender sender;
+
+ public BukkitCommandIssuer(@NotNull CommandSender sender) {
+ this.sender = sender;
+ }
+
+ @Override
+ public boolean isPlayer() {
+ return this.sender instanceof Player;
+ }
+
+ @Override
+ public Audience audience() {
+ BukkitAudiences audienceProvider = BukkitAdventure.getBukkitAudiences();
+ if (isPlayer()) {
+ return audienceProvider.player((Player) this.sender);
+ }
+ return audienceProvider.console();
+ }
+
+ @Override
+ public @NotNull T getIssuer() {
+ //noinspection unchecked
+ return (T) this.sender;
+ }
+
+ @Override
+ public boolean hasPermission(String permission) {
+ return this.sender.hasPermission(permission);
+ }
+
+ @Override
+ public boolean isOp() {
+ return this.sender.isOp();
+ }
+
+ @Override
+ public UUID getUniqueId() {
+ if (isPlayer()) {
+ return ((Player) this.sender).getUniqueId();
+ }
+ return Utils.generateUUID("SkinOverlayConsole");
+
+ }
+
+ @Override
+ public String getName() {
+ return this.sender.getName();
+ }
+}
diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandManager.java b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandManager.java
new file mode 100644
index 00000000..3ca92972
--- /dev/null
+++ b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandManager.java
@@ -0,0 +1,39 @@
+package com.georgev22.skinoverlay.command;
+
+import com.georgev22.skinoverlay.SkinOverlayBukkit;
+import org.bukkit.Bukkit;
+import org.bukkit.command.CommandMap;
+import org.jetbrains.annotations.NotNull;
+
+import java.lang.reflect.Field;
+import java.util.logging.Level;
+
+public class BukkitCommandManager extends CommandManager {
+
+ private final SkinOverlayBukkit plugin;
+
+ public BukkitCommandManager(SkinOverlayBukkit plugin) {
+ this.plugin = plugin;
+ }
+
+ @Override
+ protected void registerCommand0(@NotNull BaseCommand command) {
+ try {
+ CommandMap commandMap;
+ try {
+ commandMap = plugin.getServer().getCommandMap();
+ } catch (NoSuchMethodError e) {
+ Field commandMapField = Bukkit.getServer().getClass().getDeclaredField("commandMap");
+ commandMapField.setAccessible(true);
+ commandMap = (CommandMap) commandMapField.get(Bukkit.getServer());
+ }
+
+ for (String alias : command.getAliases(false)) {
+ BukkitPluginCommandWrapper wrapper = new BukkitPluginCommandWrapper(command, alias);
+ commandMap.register(plugin.getName(), wrapper);
+ }
+ } catch (ReflectiveOperationException exception) {
+ plugin.getLogger().log(Level.SEVERE, "Failed to register command: " + command.getClass().getName(), exception);
+ }
+ }
+}
diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitPluginCommandWrapper.java b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitPluginCommandWrapper.java
new file mode 100644
index 00000000..aa6ac44b
--- /dev/null
+++ b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitPluginCommandWrapper.java
@@ -0,0 +1,35 @@
+package com.georgev22.skinoverlay.command;
+
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BukkitPluginCommandWrapper extends Command {
+ private final BaseCommand baseCommand;
+
+ public BukkitPluginCommandWrapper(@NotNull BaseCommand command, String alias) {
+ super(alias);
+ this.baseCommand = command;
+ setAliases(new ArrayList<>(List.of(command.getAliases(false))));
+ setDescription(command.getDescription());
+ setUsage(command.getUsage());
+ setPermission(command.getPermission());
+ }
+
+ @Override
+ public boolean execute(@NotNull CommandSender sender, @NotNull String label, String @NotNull [] args) {
+ CommandIssuer commandIssuer = new BukkitCommandIssuer(sender);
+ CommandContext context = new CommandContext();
+ baseCommand.execute(commandIssuer, args, context);
+ return true;
+ }
+
+ @Override
+ public @NotNull List tabComplete(@NotNull CommandSender sender, @NotNull String alias, String @NotNull [] args) {
+ CommandIssuer commandIssuer = new BukkitCommandIssuer(sender);
+ return baseCommand.tabComplete(commandIssuer, args).stream().toList();
+ }
+}
\ No newline at end of file
diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderAPIHook.java b/bukkit/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderAPIHook.java
new file mode 100644
index 00000000..4f6153e6
--- /dev/null
+++ b/bukkit/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderAPIHook.java
@@ -0,0 +1,115 @@
+package com.georgev22.skinoverlay.hooks.placeholder;
+
+import com.georgev22.skinoverlay.BuildParameters;
+import com.georgev22.skinoverlay.SkinOverlay;
+import com.georgev22.skinoverlay.registry.EntityManagerRegistry;
+import com.georgev22.skinoverlay.storage.EntityManager;
+import com.georgev22.skinoverlay.storage.data.PlayerData;
+import com.georgev22.skinoverlay.utilities.config.OptionsUtil;
+import com.georgev22.skinoverlay.utilities.config.OverlayOptionsUtil;
+import com.georgev22.skinoverlay.utilities.config.SkinConfigurationFile;
+import me.clip.placeholderapi.expansion.PlaceholderExpansion;
+import net.kyori.adventure.audience.Audience;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.entity.Player;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+import java.util.Optional;
+
+public class PlaceholderAPIHook extends PlaceholderExpansion implements PlaceholderHook {
+
+ private final SkinOverlay skinOverlay = SkinOverlay.getInstance();
+
+ @Override
+ public String resolve(Audience audience, String input) {
+ if (audience == null) return input;
+ try {
+ return me.clip.placeholderapi.PlaceholderAPI
+ .setBracketPlaceholders(audience instanceof Player player ? player : null, input);
+ } catch (Throwable ignored) {
+ return input;
+ }
+ }
+
+ @Override
+ public @NonNull String getIdentifier() {
+ return BuildParameters.PLUGIN_NAME.toLowerCase();
+ }
+
+ @Override
+ public @NonNull String getAuthor() {
+ return BuildParameters.AUTHOR;
+ }
+
+ @Override
+ public @NonNull String getVersion() {
+ return BuildParameters.VERSION;
+ }
+
+ @Override
+ public boolean persist() {
+ return true;
+ }
+
+ @Override
+ public boolean canRegister() {
+ return true;
+ }
+
+ @Override
+ public @Nullable String onRequest(OfflinePlayer player, @NonNull String params) {
+ if (player == null || params.isEmpty()) return null;
+
+ Optional> entityManagerOpt = EntityManagerRegistry.getInstance().getTyped(PlayerData.class);
+ if (entityManagerOpt.isEmpty()) return null;
+
+ EntityManager entityManager = entityManagerOpt.get();
+
+ Optional playerDataOptional = entityManager.findById(player.getUniqueId());
+ if (playerDataOptional.isEmpty()) return null;
+
+ PlayerData playerData = playerDataOptional.get();
+
+ return handleParams(playerData, params);
+ }
+
+ private @Nullable String handleParams(PlayerData data, @NonNull String params) {
+ return switch (params.toLowerCase()) {
+ case "overlay" -> getOverlay(data);
+ case "overlay_raw" -> data.getCurrentSkin().getSkinParts().getSkinName();
+ default -> null;
+ };
+ }
+
+ private String getOverlay(@NonNull PlayerData playerData) {
+ String skinName = playerData.getCurrentSkin()
+ .getSkinParts()
+ .getSkinName();
+
+ if (skinName == null) {
+ return OptionsUtil.UNKNOWN_SKIN_NAME.getStringValue();
+ }
+
+ if (skinName.equalsIgnoreCase("default")) {
+ return OptionsUtil.DEFAULT_SKIN_NAME.getStringValue();
+ }
+
+ if (skinName.equalsIgnoreCase("custom")) {
+ return OptionsUtil.CUSTOM_SKIN_NAME.getStringValue();
+ }
+
+ if (skinName.startsWith("customURL-")) {
+ return OptionsUtil.CUSTOM_SKIN_URL_NAME.getStringValue();
+ }
+
+ SkinConfigurationFile file = skinOverlay.getSkinFileCache()
+ .getCacheSkinConfig(skinName);
+
+ if (file == null) {
+ return OptionsUtil.UNKNOWN_SKIN_NAME.getStringValue();
+ }
+
+ return OverlayOptionsUtil.PLACEHOLDER.getStringValue(file.getFileConfiguration());
+ }
+}
diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/DeveloperInformListener.java b/bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/DeveloperInformListener.java
deleted file mode 100644
index 91be1115..00000000
--- a/bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/DeveloperInformListener.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.georgev22.skinoverlay.listeners.bukkit;
-
-import com.georgev22.skinoverlay.SkinOverlay;
-import org.bukkit.event.EventHandler;
-import org.bukkit.event.Listener;
-import org.bukkit.event.player.PlayerJoinEvent;
-import org.jetbrains.annotations.ApiStatus;
-
-@ApiStatus.Internal
-@ApiStatus.NonExtendable
-public class DeveloperInformListener implements Listener {
-
- private final SkinOverlay skinOverlay = SkinOverlay.getInstance();
-
- @EventHandler
- private void onJoin(final PlayerJoinEvent e) {
- skinOverlay.getPlayer(e.getPlayer().getUniqueId()).orElseThrow().developerInform();
- }
-}
diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PaperPlayerListeners.java b/bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PaperPlayerListeners.java
deleted file mode 100644
index bc51c81a..00000000
--- a/bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PaperPlayerListeners.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package com.georgev22.skinoverlay.listeners.bukkit;
-
-import com.destroystokyo.paper.event.player.PlayerClientOptionsChangeEvent;
-import com.georgev22.skinoverlay.SkinOverlay;
-import com.georgev22.skinoverlay.event.events.player.PlayerSkinPartOptionsChangedEvent;
-import com.georgev22.skinoverlay.utilities.config.OptionsUtil;
-import org.bukkit.event.EventHandler;
-import org.bukkit.event.EventPriority;
-import org.bukkit.event.Listener;
-import org.jetbrains.annotations.ApiStatus;
-
-@ApiStatus.Internal
-@ApiStatus.NonExtendable
-public class PaperPlayerListeners implements Listener {
-
- private final SkinOverlay skinOverlay = SkinOverlay.getInstance();
-
- @EventHandler(priority = EventPriority.HIGHEST)
- public void onPLayerClientOptionsChange(PlayerClientOptionsChangeEvent event) {
- if (OptionsUtil.EXPERIMENTAL_FEATURES.getBooleanValue()) {
- if (!event.getPlayer().isOnline()) {
- return;
- }
-
- skinOverlay.getEventManager().callEvent(new PlayerSkinPartOptionsChangedEvent(
- skinOverlay.getPlayer(event.getPlayer().getUniqueId()).orElseThrow(),
- event.hasSkinPartsChanged(),
- true)
- );
- }
- }
-
-}
diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PlayerListeners.java b/bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PlayerListeners.java
index b46eba1f..3317697b 100644
--- a/bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PlayerListeners.java
+++ b/bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PlayerListeners.java
@@ -1,108 +1,36 @@
package com.georgev22.skinoverlay.listeners.bukkit;
-import com.georgev22.library.utilities.Utils;
import com.georgev22.skinoverlay.SkinOverlay;
-import com.georgev22.skinoverlay.event.events.player.PlayerObjectConnectionEvent;
-import com.georgev22.skinoverlay.handler.skin.SkinParts;
-import com.georgev22.skinoverlay.storage.data.Skin;
-import com.georgev22.skinoverlay.utilities.SerializableBufferedImage;
-import com.georgev22.skinoverlay.utilities.player.PlayerObject;
-import com.google.common.io.ByteArrayDataInput;
-import com.google.common.io.ByteStreams;
-import lombok.SneakyThrows;
+import com.georgev22.skinoverlay.event.events.player.SPlayerJoinEvent;
+import com.georgev22.skinoverlay.event.events.player.SPlayerLeaveEvent;
+import com.georgev22.skinoverlay.player.SPlayer;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
-import org.bukkit.plugin.messaging.PluginMessageListener;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-import javax.imageio.ImageIO;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.InputStream;
-import java.net.URL;
-import java.util.Objects;
-import java.util.UUID;
+public class PlayerListeners implements Listener {
-import static com.georgev22.skinoverlay.utilities.Utilities.decrypt;
+ private final SkinOverlay mainPlugin = SkinOverlay.getInstance();
-@ApiStatus.Internal
-@ApiStatus.NonExtendable
-public class PlayerListeners implements Listener, PluginMessageListener {
- private final SkinOverlay skinOverlay = SkinOverlay.getInstance();
-
- @EventHandler(priority = EventPriority.LOWEST)
- public void onJoin(PlayerJoinEvent playerJoinEvent) {
- skinOverlay.getEventManager().callEvent(
- new PlayerObjectConnectionEvent(
- skinOverlay.getPlayer(playerJoinEvent.getPlayer().getUniqueId()).orElseThrow(),
- PlayerObjectConnectionEvent.ConnectionType.CONNECT,
- true
- )
- );
- }
-
- @EventHandler(priority = EventPriority.LOWEST)
- public void onQuit(PlayerQuitEvent playerQuitEvent) {
- skinOverlay.getEventManager().callEvent(
- new PlayerObjectConnectionEvent(
- skinOverlay.getPlayer(playerQuitEvent.getPlayer().getUniqueId()).orElseThrow(),
- PlayerObjectConnectionEvent.ConnectionType.DISCONNECT,
- true
- )
- );
- }
-
- @SneakyThrows
- @Override
- public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, byte @NotNull [] message) {
- if (!channel.equalsIgnoreCase("skinoverlay:bungee")) {
- return;
+ @EventHandler(priority = EventPriority.HIGHEST)
+ public void onPlayerJoin(PlayerJoinEvent event) {
+ Player player = event.getPlayer();
+ SPlayer sPlayer = mainPlugin.getPlayerProvider().getSPlayer(player);
+ if (sPlayer != null) {
+ mainPlugin.getEventBus().post(new SPlayerJoinEvent(sPlayer));
}
- ByteArrayDataInput in = ByteStreams.newDataInput(message);
- String subChannel = in.readUTF();
- UUID uuid = UUID.fromString(Objects.requireNonNull(decrypt(in.readUTF())));
- Skin skin = (Skin) Utils.deserializeObjectFromString(Objects.requireNonNull(decrypt(in.readUTF())));
- boolean b = Boolean.parseBoolean(decrypt(in.readUTF()));
- PlayerObject playerObject = skinOverlay.getPlayer(uuid).orElseThrow();
- if (b) {
- skinOverlay.getSkinHandler().setSkin(playerObject, skin);
- } else {
- if (subChannel.equalsIgnoreCase("change")) {
- if (!skin.skinParts().getSkinName().contains("custom")) {
- skinOverlay.getSkinHandler().setSkin(playerObject, skin);
- } else {
- URL url = new URL(Objects.requireNonNull(skin.skinURL()));
- ByteArrayOutputStream output = new ByteArrayOutputStream();
-
- try (InputStream stream = url.openStream()) {
- byte[] buffer = new byte[4096];
+ }
- while (true) {
- int bytesRead = stream.read(buffer);
- if (bytesRead < 0) {
- break;
- }
- output.write(buffer, 0, bytesRead);
- }
- }
- skin.setSkinParts(new SkinParts(new SerializableBufferedImage(ImageIO.read(new ByteArrayInputStream(output.toByteArray()))), skin.skinParts().getSkinName()));
- skinOverlay.getSkinHandler().retrieveOrGenerateSkin(
- playerObject,
- () -> skin.skinParts().getFullSkin().getBufferedImage(),
- skin.skinParts()).thenAccept(userSkin -> {
- skinOverlay.getSkinHandler().setSkin(playerObject, skin);
- });
- }
- } else if (subChannel.equalsIgnoreCase("reset")) {
- skinOverlay.getSkinHandler().setSkin(playerObject, skin);
- }
+ @EventHandler(priority = EventPriority.HIGHEST)
+ public void onPlayerQuit(PlayerQuitEvent event) {
+ Player player = event.getPlayer();
+ SPlayer sPlayer = mainPlugin.getPlayerProvider().getSPlayer(player);
+ if (sPlayer != null) {
+ mainPlugin.getEventBus().post(new SPlayerLeaveEvent(sPlayer));
}
}
}
-
diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PluginMessageListenerImpl.java b/bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PluginMessageListenerImpl.java
new file mode 100644
index 00000000..6cce722e
--- /dev/null
+++ b/bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PluginMessageListenerImpl.java
@@ -0,0 +1,94 @@
+package com.georgev22.skinoverlay.listeners.bukkit;
+
+import com.georgev22.skinoverlay.messaging.MessageData;
+import com.georgev22.skinoverlay.messaging.MessageManager;
+import com.georgev22.skinoverlay.storage.data.Skin;
+import com.georgev22.skinoverlay.utilities.config.OptionsUtil;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.messaging.PluginMessageListener;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.UUID;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.logging.Level;
+
+public class PluginMessageListenerImpl extends MessageManager implements PluginMessageListener {
+ private BiConsumer handler = (uuid, skin) -> {
+ };
+
+ public PluginMessageListenerImpl() {
+ Bukkit.getMessenger().registerIncomingPluginChannel(this.mainPlugin.getPlugin(), CHANNEL_TO_BACKEND, this);
+ Bukkit.getMessenger().registerOutgoingPluginChannel(this.mainPlugin.getPlugin(), CHANNEL_FROM_BACKEND);
+ }
+
+ @Override
+ public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, byte @NotNull [] message) {
+ if (!channel.equals(CHANNEL_TO_BACKEND)) return;
+
+ @NotNull MessageData data;
+ try {
+ data = readByteArray(message);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ if (!data.subChannel().equalsIgnoreCase("skinupdate")) {
+ if (OptionsUtil.DEBUG.getBooleanValue()) {
+ this.mainPlugin.getLogger().warning("Unknown subchannel: " + data.subChannel());
+ }
+ return;
+ }
+ List parts = List.of(data.dataEntries());
+ if (parts.size() != 2) {
+ this.mainPlugin.getLogger().severe("Invalid skin property message: " + data);
+ return;
+ }
+
+ try {
+ UUID uuid = UUID.fromString(parts.get(0));
+ String base64Skin = parts.get(1);
+ Skin skin = Skin.fromBase64(base64Skin);
+
+ this.mainPlugin.getScheduler().runTask(mainPlugin.getPlugin(), () -> handler.accept(uuid, skin));
+ } catch (Exception e) {
+ this.mainPlugin.getLogger().log(Level.SEVERE, "Error handling skin property message: " + data, e);
+ }
+ }
+
+ @Override
+ public void publishSkinProperty(@NotNull UUID playerUUID, @NotNull Skin skin) {
+ // No need to send to this channel
+ }
+
+ @Override
+ public void publishPlayerJoin(@NotNull UUID playerUUID) {
+ Player player = Bukkit.getPlayer(playerUUID);
+ if (player == null) {
+ this.mainPlugin.getLogger().warning("Player " + playerUUID + " not found");
+ return;
+ }
+ String message = playerUUID.toString();
+ byte[] data = this.toByteArray("playerjoin", message);
+ player.sendPluginMessage(this.mainPlugin.getPlugin(), CHANNEL_FROM_BACKEND, data);
+ if (OptionsUtil.DEBUG.getBooleanValue())
+ this.mainPlugin.getLogger().info("Sent player join message to " + CHANNEL_FROM_BACKEND + ": " + message + " (" + data.length + " bytes) to " + player.getName());
+ }
+
+ @Override
+ public void subscribeSkinProperty(@NotNull BiConsumer handler) {
+ this.handler = handler;
+ }
+
+ @Override
+ public void subscribePlayerJoin(Consumer handler) {
+ // No need to listen to this channel
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/player/BukkitSPlayer.java b/bukkit/src/main/java/com/georgev22/skinoverlay/player/BukkitSPlayer.java
new file mode 100644
index 00000000..19897eda
--- /dev/null
+++ b/bukkit/src/main/java/com/georgev22/skinoverlay/player/BukkitSPlayer.java
@@ -0,0 +1,26 @@
+package com.georgev22.skinoverlay.player;
+
+import com.georgev22.skinoverlay.command.BukkitCommandIssuer;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+public class BukkitSPlayer extends BukkitCommandIssuer implements SPlayer {
+
+ private final Player player;
+
+ public BukkitSPlayer(@NotNull Player player) {
+ super(player);
+ this.player = player;
+ }
+
+ @Override
+ public boolean isOnline() {
+ return this.player.isOnline();
+ }
+
+ @Override
+ public T getPlayer() {
+ //noinspection unchecked
+ return (T) this.player;
+ }
+}
diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitLegacyGameProfileProvider.java b/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitLegacyGameProfileProvider.java
new file mode 100644
index 00000000..18f5c1f7
--- /dev/null
+++ b/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitLegacyGameProfileProvider.java
@@ -0,0 +1,65 @@
+package com.georgev22.skinoverlay.providers;
+
+import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap;
+import com.georgev22.skinoverlay.datastructures.maps.ObjectMap;
+import com.georgev22.skinoverlay.player.SPlayer;
+import com.georgev22.skinoverlay.skin.SGameProfile;
+import com.georgev22.skinoverlay.skin.SProperty;
+import com.mojang.authlib.GameProfile;
+import com.mojang.authlib.properties.Property;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Level;
+
+import static com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils.MinecraftReflection.obcClass;
+import static com.georgev22.skinoverlay.utilities.Utils.Reflection.fetchMethodAndInvoke;
+
+public class BukkitLegacyGameProfileProvider extends GameProfileProvider {
+
+ @Override
+ public GameProfile getInternalGameProfile(@NotNull SPlayer player) {
+ try {
+ Class> craftPlayerClass = obcClass("entity.CraftPlayer");
+ Player bukkitPlayer = player.getPlayer();
+ return (GameProfile) fetchMethodAndInvoke(craftPlayerClass, "getProfile", bukkitPlayer, new Object[]{}, new Class[]{});
+ } catch (Exception e) {
+ skinOverlay.getLogger().log(Level.SEVERE, "Error while trying to retrieve internal game profile: ", e);
+ skinOverlay.getLogger().info("Trying to create a new game profile...");
+ GameProfile gameProfile = new GameProfile(player.getUniqueId(), player.getName());
+ if (!gameProfile.getProperties().containsKey("textures")) {
+ try {
+ SProperty property = this.skinOverlay.getSkinProvider().getSkin(player);
+ gameProfile.getProperties().put("textures", new Property(property.value(), property.signature()));
+ } catch (IOException | ExecutionException | InterruptedException exception) {
+ skinOverlay.getLogger().log(Level.SEVERE, "Error while trying to apply new texture: ", exception);
+ }
+ }
+ return gameProfile;
+ }
+ }
+
+ @Override
+ public SGameProfile getGameProfile(@NotNull SPlayer player) {
+ if (sGameProfiles.containsKey(player)) {
+ return sGameProfiles.get(player);
+ }
+ ObjectMap propertyObjectMap = new HashObjectMap<>();
+ GameProfile gameProfile = this.getInternalGameProfile(player);
+ gameProfile.getProperties().forEach((name, property) -> propertyObjectMap
+ .append(name, new SProperty(property.getName(), property.getValue(), property.getSignature())));
+ SGameProfile sGameProfile = new SGameProfile(gameProfile.getName(), gameProfile.getId(), propertyObjectMap);
+ return sGameProfiles.append(player, sGameProfile).get(player);
+ }
+
+ @Override
+ public void applyUpdatedGameProfile(@NotNull SPlayer player) {
+ GameProfile internalGameProfile = this.getInternalGameProfile(player);
+ internalGameProfile.getProperties().removeAll("textures");
+ SGameProfile gameProfile = this.getGameProfile(player);
+ gameProfile.getProperties().forEach((name, property) -> internalGameProfile.getProperties()
+ .put(name, new Property(property.name(), property.value(), property.signature())));
+ }
+}
diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitPlayerProvider.java b/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitPlayerProvider.java
new file mode 100644
index 00000000..12f76d1c
--- /dev/null
+++ b/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitPlayerProvider.java
@@ -0,0 +1,90 @@
+package com.georgev22.skinoverlay.providers;
+
+import com.georgev22.skinoverlay.command.CommandIssuer;
+import com.georgev22.skinoverlay.player.BukkitSPlayer;
+import com.georgev22.skinoverlay.player.SPlayer;
+import org.bukkit.Bukkit;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jspecify.annotations.NonNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+public class BukkitPlayerProvider extends PlayerProvider {
+
+ private SPlayer getOrCreate(@NonNull Player player) {
+ return playerCache.computeIfAbsent(
+ player.getUniqueId(),
+ uuid -> new BukkitSPlayer(player)
+ );
+ }
+
+ @Override
+ public SPlayer getSPlayer(@NonNull Object player) {
+ if (player instanceof Player bukkitPlayer) {
+ return getOrCreate(bukkitPlayer);
+ }
+
+ if (player instanceof OfflinePlayer offlinePlayer) {
+ if (offlinePlayer.isOnline() && offlinePlayer.getPlayer() != null) {
+ return getOrCreate(offlinePlayer.getPlayer());
+ }
+ return null;
+ }
+
+ if (player instanceof String name) {
+ return getSPlayer(name);
+ }
+
+ if (player instanceof UUID uuid) {
+ return getSPlayer(uuid);
+ }
+
+ if (player instanceof CommandIssuer commandIssuer) {
+ return getSPlayer(commandIssuer);
+ }
+
+ return null;
+ }
+
+ @Override
+ public SPlayer getSPlayer(@NotNull CommandIssuer commandIssuer) {
+ if (!commandIssuer.isPlayer()) {
+ return null;
+ }
+
+ return getOrCreate(commandIssuer.getIssuer());
+ }
+
+ @Override
+ public SPlayer getSPlayer(@NonNull String name) {
+ Player player = Bukkit.getPlayer(name);
+ if (player != null) {
+ return getOrCreate(player);
+ }
+
+ return null;
+ }
+
+ @Override
+ public SPlayer getSPlayer(@NonNull UUID uuid) {
+ Player player = Bukkit.getPlayer(uuid);
+ if (player != null) {
+ return getOrCreate(player);
+ }
+
+ return null;
+ }
+
+ @Override
+ public @NonNull List getOnlinePlayers() {
+ return new ArrayList<>(
+ Bukkit.getOnlinePlayers().stream()
+ .map(this::getOrCreate)
+ .toList()
+ );
+ }
+}
\ No newline at end of file
diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/providers/PaperGameProfileProvider.java b/bukkit/src/main/java/com/georgev22/skinoverlay/providers/PaperGameProfileProvider.java
new file mode 100644
index 00000000..6946e1d3
--- /dev/null
+++ b/bukkit/src/main/java/com/georgev22/skinoverlay/providers/PaperGameProfileProvider.java
@@ -0,0 +1,43 @@
+package com.georgev22.skinoverlay.providers;
+
+import com.destroystokyo.paper.profile.PlayerProfile;
+import com.destroystokyo.paper.profile.ProfileProperty;
+import com.georgev22.skinoverlay.player.SPlayer;
+import com.georgev22.skinoverlay.skin.SGameProfile;
+import com.georgev22.skinoverlay.skin.SProperty;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+public class PaperGameProfileProvider extends GameProfileProvider {
+
+ @Override
+ public PlayerProfile getInternalGameProfile(@NotNull SPlayer player) {
+ Player bukkitPlayer = player.getPlayer();
+ return bukkitPlayer.getPlayerProfile();
+ }
+
+ @Override
+ public SGameProfile getGameProfile(@NotNull SPlayer player) {
+ if (sGameProfiles.containsKey(player)) {
+ return sGameProfiles.get(player);
+ }
+ SGameProfile gameProfile = new SGameProfile(player.getName(), player.getUniqueId());
+ PlayerProfile playerProfile = getInternalGameProfile(player);
+ playerProfile.getProperties().forEach(profileProperty -> gameProfile.addProperty(
+ profileProperty.getName(), new SProperty(profileProperty.getName(), profileProperty.getValue(), profileProperty.getSignature())
+ ));
+ return sGameProfiles.append(player, gameProfile).get(player);
+ }
+
+ @Override
+ public void applyUpdatedGameProfile(@NotNull SPlayer player) {
+ Player bukkitPlayer = player.getPlayer();
+ PlayerProfile playerProfile = bukkitPlayer.getPlayerProfile();
+ playerProfile.getProperties().removeIf(profileProperty -> profileProperty.getName().equalsIgnoreCase("textures"));
+ SGameProfile gameProfile = this.getGameProfile(player);
+ gameProfile.getProperties()
+ .forEach((name, property) -> playerProfile.getProperties()
+ .add(new ProfileProperty(name, property.value(), property.signature())));
+ bukkitPlayer.setPlayerProfile(playerProfile);
+ }
+}
diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_Legacy.java b/bukkit/src/main/java/com/georgev22/skinoverlay/refreshers/LegacySkinRefresher.java
similarity index 71%
rename from bukkit/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_Legacy.java
rename to bukkit/src/main/java/com/georgev22/skinoverlay/refreshers/LegacySkinRefresher.java
index 000118b4..03039254 100644
--- a/bukkit/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_Legacy.java
+++ b/bukkit/src/main/java/com/georgev22/skinoverlay/refreshers/LegacySkinRefresher.java
@@ -1,18 +1,14 @@
-package com.georgev22.skinoverlay.handler.handlers;
+package com.georgev22.skinoverlay.refreshers;
-import com.georgev22.library.exceptions.ReflectionException;
-import com.georgev22.skinoverlay.handler.SGameProfile;
-import com.georgev22.skinoverlay.storage.data.Skin;
-import com.georgev22.skinoverlay.utilities.player.PlayerObject;
+import com.georgev22.skinoverlay.exceptions.ReflectionException;
+import com.georgev22.skinoverlay.player.SPlayer;
import com.google.common.collect.ImmutableList;
import com.google.common.hash.Hashing;
-import com.mojang.authlib.GameProfile;
import org.bukkit.Location;
import org.bukkit.entity.Player;
-import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
-import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
@@ -20,16 +16,14 @@
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
-import static com.georgev22.library.minecraft.BukkitMinecraftUtils.MinecraftReflection.getNMSClass;
-import static com.georgev22.library.minecraft.BukkitMinecraftUtils.MinecraftReflection.getOBCClass;
-import static com.georgev22.library.utilities.Utils.Reflection.*;
+import static com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils.MinecraftReflection.*;
+import static com.georgev22.skinoverlay.utilities.Utils.Reflection.*;
+
+@SuppressWarnings("deprecation")
+public class LegacySkinRefresher extends SkinRefresher {
-@ApiStatus.Internal
-@ApiStatus.NonExtendable
-public class SkinHandler_Legacy extends SkinHandler_Unsupported {
private final Class> playOutRespawn;
private final Class> playOutPlayerInfo;
private final Class> playOutPosition;
@@ -39,13 +33,13 @@ public class SkinHandler_Legacy extends SkinHandler_Unsupported {
private Enum> removePlayerEnum;
private Enum> addPlayerEnum;
- public SkinHandler_Legacy() {
+ public LegacySkinRefresher() {
try {
- packet = getNMSClass("Packet", "net.minecraft.network.protocol.Packet");
- playOutHeldItemSlot = getNMSClass("PacketPlayOutHeldItemSlot", "net.minecraft.network.protocol.game.PacketPlayOutHeldItemSlot");
- playOutPosition = getNMSClass("PacketPlayOutPosition", "net.minecraft.network.protocol.game.PacketPlayOutPosition");
- playOutPlayerInfo = getNMSClass("PacketPlayOutPlayerInfo", "net.minecraft.network.protocol.game.PacketPlayOutPlayerInfo");
- playOutRespawn = getNMSClass("PacketPlayOutRespawn", "net.minecraft.network.protocol.game.PacketPlayOutRespawn");
+ packet = nmsClass("Packet", "net.minecraft.network.protocol.Packet");
+ playOutHeldItemSlot = nmsClass("PacketPlayOutHeldItemSlot", "net.minecraft.network.protocol.game.PacketPlayOutHeldItemSlot");
+ playOutPosition = nmsClass("PacketPlayOutPosition", "net.minecraft.network.protocol.game.PacketPlayOutPosition");
+ playOutPlayerInfo = nmsClass("PacketPlayOutPlayerInfo", "net.minecraft.network.protocol.game.PacketPlayOutPlayerInfo");
+ playOutRespawn = nmsClass("PacketPlayOutRespawn", "net.minecraft.network.protocol.game.PacketPlayOutRespawn");
try {
removePlayerEnum = getEnum(playOutPlayerInfo, "EnumPlayerInfoAction", "REMOVE_PLAYER");
@@ -62,7 +56,7 @@ public SkinHandler_Legacy() {
addPlayerEnum = getEnum(playOutPlayerInfo, "Action", "ADD_PLAYER");
} catch (Exception e3) {
try {
- Class> enumPlayerInfoAction = getNMSClass("EnumPlayerInfoAction", null);
+ Class> enumPlayerInfoAction = nmsClass("EnumPlayerInfoAction", null);
removePlayerEnum = getEnum(enumPlayerInfoAction, "REMOVE_PLAYER");
addPlayerEnum = getEnum(enumPlayerInfoAction, "ADD_PLAYER");
@@ -73,18 +67,44 @@ public SkinHandler_Legacy() {
}
}
- getHandleMethod = getOBCClass("entity.CraftPlayer").getDeclaredMethod("getHandle");
+ getHandleMethod = obcClass("entity.CraftPlayer").getDeclaredMethod("getHandle");
} catch (Exception e) {
throw new RuntimeException(e);
}
-
}
@Override
- public CompletableFuture updateSkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) {
+ public void refresh(@NotNull SPlayer player) {
+ this.skinOverlay.getScheduler().createDelayedTask(this.skinOverlay.getPlugin(), () -> {
+ Player bukkitPlayer = player.getPlayer();
+ bukkitPlayer.hidePlayer(bukkitPlayer);
+ bukkitPlayer.showPlayer(bukkitPlayer);
+ this.sendPackets(player).handleAsync((result, throwable) -> {
+ if (throwable != null) {
+ skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable);
+ return false;
+ }
+ return result;
+ }).thenAccept(result -> this.skinOverlay.getScheduler().runTask(this.skinOverlay.getPlugin(), () -> {
+ if (result)
+ skinOverlay.getPlayerProvider().getOnlinePlayers().stream().filter(online -> online != player).forEach(online -> {
+ Player p = online.getPlayer();
+ p.hidePlayer(bukkitPlayer);
+ p.showPlayer(bukkitPlayer);
+ });
+ }));
+ }, 20L);
+ }
+
+ private void sendPacket(Object playerConnection, Object packet) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
+ fetchMethodAndInvoke(playerConnection.getClass(), "sendPacket", playerConnection, new Object[]{packet}, new Class>[]{this.packet});
+ }
+
+ @Contract("_ -> new")
+ protected @NotNull CompletableFuture sendPackets(@NotNull SPlayer player) {
return CompletableFuture.supplyAsync(() -> {
try {
- Player player = playerObject.player();
+ Player bukkitPlayer = player.getPlayer();
final Object entityPlayer = getHandleMethod.invoke(player);
Object removePlayer;
@@ -94,8 +114,8 @@ public CompletableFuture updateSkin(@NotNull PlayerObject playerObject,
addPlayer = invokeConstructor(playOutPlayerInfo, addPlayerEnum, ImmutableList.of(entityPlayer));
} catch (ReflectionException e) {
int ping = (int) fetchField(entityPlayer.getClass(), entityPlayer, "ping");
- removePlayer = invokeConstructor(playOutPlayerInfo, player.getPlayerListName(), false, 9999);
- addPlayer = invokeConstructor(playOutPlayerInfo, player.getPlayerListName(), true, ping);
+ removePlayer = invokeConstructor(playOutPlayerInfo, bukkitPlayer.getPlayerListName(), false, 9999);
+ addPlayer = invokeConstructor(playOutPlayerInfo, bukkitPlayer.getPlayerListName(), true, ping);
}
Object world;
@@ -141,8 +161,8 @@ public CompletableFuture updateSkin(@NotNull PlayerObject playerObject,
Object playerIntManager = getFieldByType(entityPlayer, "PlayerInteractManager");
Enum> enumGamemode = (Enum>) fetchMethodAndInvoke(playerIntManager.getClass(), "getGameMode", playerIntManager, new Object[0], new Class[0]);
- int gamemodeId = player.getGameMode().getValue();
- int dimension = player.getWorld().getEnvironment().getId();
+ //noinspection UnstableApiUsage
+ int dimension = bukkitPlayer.getWorld().getEnvironment().getId();
Object respawn;
try {
@@ -154,7 +174,7 @@ public CompletableFuture updateSkin(@NotNull PlayerObject playerObject,
dimensionManager = getFieldByType(worldObject, "DimensionManager");
} catch (ReflectionException e) {
try {
- Class> dimensionManagerClass = getNMSClass("DimensionManager", "net.minecraft.world.level.dimension.DimensionManager");
+ Class> dimensionManagerClass = nmsClass("DimensionManager", "net.minecraft.world.level.dimension.DimensionManager");
for (Method m : dimensionManagerClass.getDeclaredMethods()) {
if (m.getReturnType() == dimensionManagerClass && m.getParameterCount() == 1 && m.getParameterTypes()[0] == Integer.TYPE) {
@@ -177,7 +197,7 @@ public CompletableFuture updateSkin(@NotNull PlayerObject playerObject,
try {
respawn = invokeConstructor(playOutRespawn, dimensionManager, worldType, enumGamemode);
} catch (Exception ignored3) {
- long seedEncrypted = Hashing.sha256().hashString(String.valueOf(player.getWorld().getSeed()), StandardCharsets.UTF_8).asLong();
+ long seedEncrypted = Hashing.sha256().hashString(String.valueOf(bukkitPlayer.getWorld().getSeed()), StandardCharsets.UTF_8).asLong();
try {
respawn = invokeConstructor(playOutRespawn, dimensionManager, seedEncrypted, worldType, enumGamemode);
} catch (Exception ignored5) {
@@ -204,7 +224,7 @@ public CompletableFuture updateSkin(@NotNull PlayerObject playerObject,
}
}
- Location l = player.getLocation();
+ Location l = bukkitPlayer.getLocation();
Object pos;
try {
pos = invokeConstructor(playOutPosition, l.getX(), l.getY(), l.getZ(), l.getYaw(), l.getPitch(), new HashSet>(), 0);
@@ -216,7 +236,7 @@ public CompletableFuture updateSkin(@NotNull PlayerObject playerObject,
}
}
- Object slot = invokeConstructor(playOutHeldItemSlot, player.getInventory().getHeldItemSlot());
+ Object slot = invokeConstructor(playOutHeldItemSlot, bukkitPlayer.getInventory().getHeldItemSlot());
Object playerConnection = getFieldByType(entityPlayer, "PlayerConnection");
sendPacket(playerConnection, removePlayer);
@@ -230,13 +250,13 @@ public CompletableFuture updateSkin(@NotNull PlayerObject playerObject,
sendPacket(playerConnection, slot);
fetchMethodAndInvoke(player.getClass(), "updateScaledHealth", player, new Object[0], new Class[0]);
- player.updateInventory();
+ bukkitPlayer.updateInventory();
fetchMethodAndInvoke(entityPlayer.getClass(), "triggerHealthUpdate", entityPlayer, new Object[0], new Class[0]);
if (player.isOp()) {
- this.skinOverlay.getMinecraftScheduler().runTask(this.skinOverlay.getSkinOverlay().plugin(), () -> {
- player.setOp(false);
- player.setOp(true);
+ this.skinOverlay.getScheduler().runTask(this.skinOverlay.getPlugin(), () -> {
+ bukkitPlayer.setOp(false);
+ bukkitPlayer.setOp(true);
});
}
return true;
@@ -244,52 +264,8 @@ public CompletableFuture updateSkin(@NotNull PlayerObject playerObject,
NoSuchFieldException e) {
throw new RuntimeException(e);
}
- }, runnable -> this.skinOverlay.getMinecraftScheduler().runTask(this.skinOverlay.getSkinOverlay().plugin(), runnable));
+ }, runnable -> this.skinOverlay.getScheduler().runTask(this.skinOverlay.getPlugin(), runnable));
}
- @Override
- public void applySkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) {
- this.skinOverlay.getMinecraftScheduler().createDelayedTask(this.skinOverlay.getSkinOverlay().plugin(), () -> {
- Player player = playerObject.player();
- player.hidePlayer(player);
- player.showPlayer(player);
- skinOverlay.getSkinHandler().updateSkin(playerObject, skin).handleAsync((aBoolean, throwable) -> {
- if (throwable != null) {
- skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable);
- return false;
- }
- return aBoolean;
- }).thenAccept(aBoolean -> this.skinOverlay.getMinecraftScheduler().runTask(this.skinOverlay.getSkinOverlay().plugin(), () -> {
- if (aBoolean)
- skinOverlay.onlinePlayers().stream().filter(playerObjects -> playerObjects != playerObject).forEach(playerObjects -> {
- Player p = playerObjects.player();
- p.hidePlayer(player);
- p.showPlayer(player);
- });
- }));
- }, 20L);
- }
-
- @Override
- public GameProfile getInternalGameProfile(@NotNull PlayerObject playerObject) throws IOException, ExecutionException, InterruptedException {
- try {
- Class> craftPlayerClass = getOBCClass("entity.CraftPlayer");
- Player player = playerObject.player();
- return (GameProfile) fetchMethodAndInvoke(craftPlayerClass, "getProfile", player, new Object[]{}, new Class[]{});
- } catch (Exception e) {
- return super.getInternalGameProfile(playerObject);
- }
- }
- @Override
- public SGameProfile getGameProfile(@NotNull PlayerObject playerObject) throws IOException, ExecutionException, InterruptedException {
- if (sGameProfiles.containsKey(playerObject)) {
- return sGameProfiles.get(playerObject);
- }
- return sGameProfiles.append(playerObject, wrapper(this.getInternalGameProfile(playerObject))).get(playerObject);
- }
-
- private void sendPacket(Object playerConnection, Object packet) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
- fetchMethodAndInvoke(playerConnection.getClass(), "sendPacket", playerConnection, new Object[]{packet}, new Class>[]{this.packet});
- }
}
diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/scheduler/MinecraftBukkitScheduler.java b/bukkit/src/main/java/com/georgev22/skinoverlay/scheduler/MinecraftBukkitScheduler.java
new file mode 100644
index 00000000..45947ce7
--- /dev/null
+++ b/bukkit/src/main/java/com/georgev22/skinoverlay/scheduler/MinecraftBukkitScheduler.java
@@ -0,0 +1,303 @@
+package com.georgev22.skinoverlay.scheduler;
+
+import org.bukkit.Bukkit;
+import org.bukkit.Chunk;
+import org.bukkit.Location;
+import org.bukkit.World;
+import org.bukkit.entity.Entity;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.scheduler.BukkitTask;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Supplier;
+
+@ApiStatus.NonExtendable
+public class MinecraftBukkitScheduler implements MinecraftScheduler {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask runTask(Plugin plugin, Runnable task) {
+ return new BukkitSchedulerTask(Bukkit.getScheduler().runTask(plugin, task));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture runTask(Plugin plugin, Supplier task) {
+ CompletableFuture future = new CompletableFuture<>();
+ Bukkit.getScheduler().runTask(plugin, () -> {
+ try {
+ T result = task.get();
+ future.complete(result);
+ } catch (Exception e) {
+ future.completeExceptionally(e);
+ }
+ });
+
+ return future;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask runAsyncTask(Plugin plugin, Runnable task) {
+ return new BukkitSchedulerTask(Bukkit.getScheduler().runTaskAsynchronously(plugin, task));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture runAsyncTask(Plugin plugin, Supplier task) {
+ CompletableFuture future = new CompletableFuture<>();
+ Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
+ try {
+ T result = task.get();
+ future.complete(result);
+ } catch (Exception e) {
+ future.completeExceptionally(e);
+ }
+ });
+
+ return future;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask createDelayedTask(Plugin plugin, Runnable task, long delay) {
+ return new BukkitSchedulerTask(Bukkit.getScheduler().runTaskLater(plugin, task, delay));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture createDelayedTask(Plugin plugin, Supplier task, long delay) {
+ CompletableFuture future = new CompletableFuture<>();
+ Bukkit.getScheduler().runTaskLater(plugin, () -> {
+ try {
+ T result = task.get();
+ future.complete(result);
+ } catch (Exception e) {
+ future.completeExceptionally(e);
+ }
+ }, delay);
+
+ return future;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask createRepeatingTask(Plugin plugin, Runnable task, long delay, long period) {
+ return new BukkitSchedulerTask(Bukkit.getScheduler().runTaskTimer(plugin, task, delay, period));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask createAsyncDelayedTask(Plugin plugin, Runnable task, long delay) {
+ return new BukkitSchedulerTask(Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, task, delay));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture createAsyncDelayedTask(Plugin plugin, Supplier task, long delay) {
+ CompletableFuture future = new CompletableFuture<>();
+ Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, () -> {
+ try {
+ T result = task.get();
+ future.complete(result);
+ } catch (Exception e) {
+ future.completeExceptionally(e);
+ }
+ }, delay);
+
+ return future;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask createAsyncRepeatingTask(Plugin plugin, Runnable task, long delay, long period) {
+ return new BukkitSchedulerTask(Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, task, delay, period));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask createDelayedTaskForWorld(Plugin plugin, Runnable task, World world, @NotNull Chunk chunk, long delay) {
+ return new BukkitSchedulerTask(Bukkit.getScheduler().runTaskLater(plugin, task, delay));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture createDelayedTaskForWorld(Plugin plugin, Supplier task, World world, @NotNull Chunk chunk, long delay) {
+ return this.createDelayedTask(plugin, task, delay);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask createDelayedForLocation(Plugin plugin, Runnable task, Location location, long delay) {
+ return new BukkitSchedulerTask(Bukkit.getScheduler().runTaskLater(plugin, task, delay));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture createDelayedForLocation(Plugin plugin, Supplier task, Location location, long delay) {
+ return this.createDelayedTask(plugin, task, delay);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask createDelayedForEntity(Plugin plugin, Runnable task, Runnable retired, Entity entity, long delay) {
+ return new BukkitSchedulerTask(Bukkit.getScheduler().runTaskLater(plugin, task, delay));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture createDelayedForEntity(Plugin plugin, Supplier task, Runnable retired, Entity entity, long delay) {
+ return this.createDelayedTask(plugin, task, delay);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask createTaskForWorld(Plugin plugin, Runnable task, World world, @NotNull Chunk chunk) {
+ return new BukkitSchedulerTask(Bukkit.getScheduler().runTask(plugin, task));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture createTaskForWorld(Plugin plugin, Supplier task, World world, @NotNull Chunk chunk) {
+ return this.runTask(plugin, task);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask createTaskForLocation(Plugin plugin, Runnable task, Location location) {
+ return new BukkitSchedulerTask(Bukkit.getScheduler().runTask(plugin, task));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture createTaskForLocation(Plugin plugin, Supplier task, Location location) {
+ return this.runTask(plugin, task);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask createTaskForEntity(Plugin plugin, Runnable task, Runnable retired, Entity entity) {
+ return new BukkitSchedulerTask(Bukkit.getScheduler().runTask(plugin, task));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture createTaskForEntity(Plugin plugin, Supplier task, Runnable retired, Entity entity) {
+ return this.runTask(plugin, task);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask createRepeatingTaskForWorld(Plugin plugin, Runnable task, World world, @NotNull Chunk chunk, long delay, long period) {
+ return new BukkitSchedulerTask(Bukkit.getScheduler().runTaskTimer(plugin, task, delay, period));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask createRepeatingTaskForLocation(Plugin plugin, Runnable task, Location location, long delay, long period) {
+ return new BukkitSchedulerTask(Bukkit.getScheduler().runTaskTimer(plugin, task, delay, period));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask createRepeatingTaskForEntity(Plugin plugin, Runnable task, Runnable retired, Entity entity, long delay, long period) {
+ return new BukkitSchedulerTask(Bukkit.getScheduler().runTaskTimer(plugin, task, delay, period));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void cancelTasks(Plugin plugin) {
+ Bukkit.getScheduler().cancelTasks(plugin);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public MinecraftScheduler getScheduler() {
+ return this;
+ }
+
+ public static class BukkitSchedulerTask implements SchedulerTask {
+
+ private final BukkitTask bukkitTask;
+
+ public BukkitSchedulerTask(BukkitTask bukkitTask) {
+ this.bukkitTask = bukkitTask;
+ }
+
+ @Override
+ public void cancel() {
+ bukkitTask.cancel();
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return bukkitTask.isCancelled();
+ }
+
+ @Override
+ public int getTaskId() {
+ return bukkitTask.getTaskId();
+ }
+
+ @Override
+ public boolean isRunning() {
+ return Bukkit.getScheduler().isCurrentlyRunning(bukkitTask.getTaskId());
+ }
+ }
+}
diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/scheduler/MinecraftFoliaScheduler.java b/bukkit/src/main/java/com/georgev22/skinoverlay/scheduler/MinecraftFoliaScheduler.java
new file mode 100644
index 00000000..c64cadbf
--- /dev/null
+++ b/bukkit/src/main/java/com/georgev22/skinoverlay/scheduler/MinecraftFoliaScheduler.java
@@ -0,0 +1,356 @@
+package com.georgev22.skinoverlay.scheduler;
+
+import io.papermc.paper.threadedregions.scheduler.ScheduledTask;
+import org.bukkit.Bukkit;
+import org.bukkit.Chunk;
+import org.bukkit.Location;
+import org.bukkit.World;
+import org.bukkit.entity.Entity;
+import org.bukkit.plugin.Plugin;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+@ApiStatus.NonExtendable
+public class MinecraftFoliaScheduler implements MinecraftScheduler {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask runTask(Plugin plugin, Runnable task) {
+ return new FoliaSchedulerTask(Bukkit.getGlobalRegionScheduler().run(plugin, (scheduledTask) -> task.run()));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture runTask(Plugin plugin, Supplier task) {
+ CompletableFuture future = new CompletableFuture<>();
+ Bukkit.getGlobalRegionScheduler().run(plugin, scheduledTask -> {
+ try {
+ T result = task.get();
+ future.complete(result);
+ } catch (Exception e) {
+ future.completeExceptionally(e);
+ }
+ });
+ return future;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask runAsyncTask(Plugin plugin, Runnable task) {
+ return new FoliaSchedulerTask(Bukkit.getAsyncScheduler().runNow(plugin, (scheduledTask) -> task.run()));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture runAsyncTask(Plugin plugin, Supplier task) {
+ CompletableFuture future = new CompletableFuture<>();
+ Bukkit.getAsyncScheduler().runNow(plugin, scheduledTask -> {
+ try {
+ T result = task.get();
+ future.complete(result);
+ } catch (Exception e) {
+ future.completeExceptionally(e);
+ }
+ });
+ return future;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask createDelayedTask(Plugin plugin, Runnable task, long delay) {
+ return new FoliaSchedulerTask(Bukkit.getGlobalRegionScheduler().runDelayed(plugin, (scheduledTask) -> task.run(), delay));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture createDelayedTask(Plugin plugin, Supplier task, long delay) {
+ CompletableFuture future = new CompletableFuture<>();
+ Bukkit.getGlobalRegionScheduler().runDelayed(plugin, scheduledTask -> {
+ try {
+ T result = task.get();
+ future.complete(result);
+ } catch (Exception e) {
+ future.completeExceptionally(e);
+ }
+ }, delay);
+ return future;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask createRepeatingTask(Plugin plugin, Runnable task, long delay, long period) {
+ return new FoliaSchedulerTask(Bukkit.getGlobalRegionScheduler().runAtFixedRate(plugin, (scheduledTask) -> task.run(), delay, period));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask createAsyncDelayedTask(Plugin plugin, Runnable task, long delay) {
+ return new FoliaSchedulerTask(Bukkit.getAsyncScheduler().runDelayed(plugin, (scheduledTask) -> task.run(), (delay / 20), TimeUnit.SECONDS));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture createAsyncDelayedTask(Plugin plugin, Supplier task, long delay) {
+ CompletableFuture future = new CompletableFuture<>();
+ Bukkit.getAsyncScheduler().runDelayed(plugin, scheduledTask -> {
+ try {
+ T result = task.get();
+ future.complete(result);
+ } catch (Exception e) {
+ future.completeExceptionally(e);
+ }
+ }, (delay / 20), TimeUnit.SECONDS);
+ return future;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask createAsyncRepeatingTask(Plugin plugin, Runnable task, long delay, long period) {
+ return new FoliaSchedulerTask(Bukkit.getAsyncScheduler().runAtFixedRate(plugin, (scheduledTask) -> task.run(), (delay / 20), (period / 20), TimeUnit.SECONDS));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public SchedulerTask createDelayedTaskForWorld(Plugin plugin, Runnable task, World world, @NotNull Chunk chunk, long delay) {
+ return new FoliaSchedulerTask(Bukkit.getRegionScheduler().runDelayed(plugin, world, chunk.getX(), chunk.getZ(), (scheduledTask) -> task.run(), delay));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture createDelayedTaskForWorld(Plugin plugin, Supplier task, World world, @NotNull Chunk chunk, long delay) {
+ CompletableFuture future = new CompletableFuture<>();
+ Bukkit.getRegionScheduler().runDelayed(
+ plugin,
+ world,
+ chunk.getX(),
+ chunk.getZ(),
+ scheduledTask -> {
+ try {
+ T result = task.get();
+ future.complete(result);
+ } catch (Exception e) {
+ future.completeExceptionally(e);
+ }
+ }, delay);
+ return future;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public SchedulerTask createDelayedForLocation(Plugin plugin, Runnable task, Location location, long delay) {
+ return new FoliaSchedulerTask(Bukkit.getRegionScheduler().runDelayed(plugin, location, (scheduledTask) -> task.run(), delay));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture createDelayedForLocation(Plugin plugin, Supplier task, Location location, long delay) {
+ CompletableFuture future = new CompletableFuture<>();
+ Bukkit.getRegionScheduler().runDelayed(plugin, location, scheduledTask -> {
+ try {
+ T result = task.get();
+ future.complete(result);
+ } catch (Exception e) {
+ future.completeExceptionally(e);
+ }
+ }, delay);
+ return future;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask createDelayedForEntity(Plugin plugin, Runnable task, Runnable retired, @NotNull Entity entity, long delay) {
+ return new FoliaSchedulerTask(entity.getScheduler().runDelayed(plugin, (scheduledTask) -> task.run(), retired, delay));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture createDelayedForEntity(Plugin plugin, Supplier task, Runnable retired, @NotNull Entity entity, long delay) {
+ CompletableFuture future = new CompletableFuture<>();
+ entity.getScheduler().runDelayed(plugin, scheduledTask -> {
+ try {
+ T result = task.get();
+ future.complete(result);
+ } catch (Exception e) {
+ future.completeExceptionally(e);
+ }
+ }, retired, delay);
+ return future;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public SchedulerTask createTaskForWorld(Plugin plugin, Runnable task, World world, @NotNull Chunk chunk) {
+ return new FoliaSchedulerTask(Bukkit.getRegionScheduler().run(plugin, world, chunk.getX(), chunk.getZ(), (scheduledTask) -> task.run()));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture createTaskForWorld(Plugin plugin, Supplier task, World world, @NotNull Chunk chunk) {
+ CompletableFuture future = new CompletableFuture<>();
+ Bukkit.getRegionScheduler().run(plugin, world, chunk.getX(), chunk.getZ(), scheduledTask -> {
+ try {
+ T result = task.get();
+ future.complete(result);
+ } catch (Exception e) {
+ future.completeExceptionally(e);
+ }
+ });
+ return future;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public SchedulerTask createTaskForLocation(Plugin plugin, Runnable task, Location location) {
+ return new FoliaSchedulerTask(Bukkit.getRegionScheduler().run(plugin, location, (scheduledTask) -> task.run()));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture createTaskForLocation(Plugin plugin, Supplier task, Location location) {
+ CompletableFuture future = new CompletableFuture<>();
+ Bukkit.getRegionScheduler().run(plugin, location, scheduledTask -> {
+ try {
+ T result = task.get();
+ future.complete(result);
+ } catch (Exception e) {
+ future.completeExceptionally(e);
+ }
+ });
+ return future;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask createTaskForEntity(Plugin plugin, Runnable task, Runnable retired, @NotNull Entity entity) {
+ return new FoliaSchedulerTask(entity.getScheduler().run(plugin, (scheduledTask) -> task.run(), retired));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture createTaskForEntity(Plugin plugin, Supplier task, Runnable retired, @NotNull Entity entity) {
+ CompletableFuture future = new CompletableFuture<>();
+ entity.getScheduler().run(plugin, scheduledTask -> {
+ try {
+ T result = task.get();
+ future.complete(result);
+ } catch (Exception e) {
+ future.completeExceptionally(e);
+ }
+ }, retired);
+ return future;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public SchedulerTask createRepeatingTaskForWorld(Plugin plugin, Runnable task, World world, @NotNull Chunk chunk, long delay, long period) {
+ return new FoliaSchedulerTask(Bukkit.getRegionScheduler().runAtFixedRate(plugin, world, chunk.getX(), chunk.getZ(), (scheduledTask) -> task.run(), delay, period));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public SchedulerTask createRepeatingTaskForLocation(Plugin plugin, Runnable task, Location location, long delay, long period) {
+ return new FoliaSchedulerTask(Bukkit.getRegionScheduler().runAtFixedRate(plugin, location, (scheduledTask) -> task.run(), delay, period));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SchedulerTask createRepeatingTaskForEntity(Plugin plugin, Runnable task, Runnable retired, @NotNull Entity entity, long delay, long period) {
+ return new FoliaSchedulerTask(entity.getScheduler().runAtFixedRate(plugin, (scheduledTask) -> task.run(), retired, delay, period));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void cancelTasks(Plugin plugin) {
+ Bukkit.getGlobalRegionScheduler().cancelTasks(plugin);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public MinecraftScheduler getScheduler() {
+ return this;
+ }
+
+ public static class FoliaSchedulerTask implements SchedulerTask {
+
+ private final ScheduledTask scheduledTask;
+
+ public FoliaSchedulerTask(ScheduledTask scheduledTask) {
+ this.scheduledTask = scheduledTask;
+ }
+
+ @Override
+ public void cancel() {
+ scheduledTask.cancel();
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return scheduledTask.isCancelled();
+ }
+
+ @Override
+ public int getTaskId() {
+ return 0;
+ }
+
+ @Override
+ public boolean isRunning() {
+ return switch (scheduledTask.getExecutionState()) {
+ case IDLE, RUNNING -> true;
+ default -> false;
+ };
+ }
+ }
+}
diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitAdventure.java b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitAdventure.java
new file mode 100644
index 00000000..7b0f399f
--- /dev/null
+++ b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitAdventure.java
@@ -0,0 +1,27 @@
+package com.georgev22.skinoverlay.utilities;
+
+import com.georgev22.skinoverlay.SkinOverlay;
+import com.georgev22.skinoverlay.SkinOverlayBukkit;
+import net.kyori.adventure.platform.bukkit.BukkitAudiences;
+
+public class BukkitAdventure {
+
+ private static BukkitAudiences bukkitAudiences;
+
+ public static void init() {
+ bukkitAudiences = BukkitAudiences.create(SkinOverlayBukkit.getInstance());
+ SkinOverlay.getInstance().setConsoleAudience(bukkitAudiences.console());
+ }
+
+ public static BukkitAudiences getBukkitAudiences() {
+ return bukkitAudiences;
+ }
+
+ public static void close() {
+ if (bukkitAudiences != null) {
+ bukkitAudiences.close();
+ bukkitAudiences = null;
+ }
+ }
+
+}
diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java
new file mode 100644
index 00000000..5c9e6168
--- /dev/null
+++ b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java
@@ -0,0 +1,938 @@
+package com.georgev22.skinoverlay.utilities;
+
+import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap;
+import com.georgev22.skinoverlay.utilities.color.Color;
+import com.google.common.collect.Lists;
+import org.bukkit.*;
+import org.bukkit.command.CommandSender;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.event.Listener;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.permissions.ServerOperator;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.plugin.PluginManager;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.bukkit.util.io.BukkitObjectInputStream;
+import org.bukkit.util.io.BukkitObjectOutputStream;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import static com.georgev22.skinoverlay.utilities.Utils.hasClass;
+
+public class BukkitMinecraftUtils {
+
+ private static boolean join = false;
+ private static String disableJoinMessage = "";
+
+ public static boolean isList(final @NotNull FileConfiguration file, final String path) {
+ return Utils.isList(file.get(path));
+ }
+
+ public static void broadcastMsg(final String input) {
+ //noinspection deprecation
+ Bukkit.broadcastMessage(colorize(input));
+ }
+
+ public static void printMsg(final String input) {
+ Bukkit.getConsoleSender().sendMessage(colorize(input));
+ }
+
+ public static void broadcastMsg(final @NotNull List input) {
+ input.forEach(BukkitMinecraftUtils::broadcastMsg);
+ }
+
+ public static void broadcastMsg(final @NotNull String... input) {
+ Arrays.stream(input).forEach(BukkitMinecraftUtils::broadcastMsg);
+ }
+
+ public static void broadcastMsg(final Object input) {
+ broadcastMsg(String.valueOf(input));
+ }
+
+ public static void printMsg(final @NotNull List input) {
+ input.forEach(BukkitMinecraftUtils::printMsg);
+ }
+
+ public static void printMsg(final @NotNull String... input) {
+ Arrays.stream(input).forEach(BukkitMinecraftUtils::printMsg);
+ }
+
+ public static void printMsg(final Object input) {
+ printMsg(String.valueOf(input));
+ }
+
+ public static void msg(final CommandSender target, final String message, final Map map,
+ final boolean ignoreCase) {
+ msg(target, placeholderAPI(target, message, map, ignoreCase));
+ }
+
+ public static void msg(final CommandSender target, final List message, final Map map,
+ final boolean ignoreCase) {
+ msg(target, placeholderAPI(target, message, map, ignoreCase));
+ }
+
+ public static void msg(final CommandSender target, final String[] message, final Map map,
+ final boolean ignoreCase) {
+ msg(target, placeholderAPI(target, message, map, ignoreCase));
+ }
+
+ public static void msg(final CommandSender target, final FileConfiguration file, final String path) {
+ msg(target, file, path, null, false);
+ }
+
+ public static void msg(final CommandSender target, final FileConfiguration file, final String path,
+ final Map map, final boolean replace) {
+ if (file == null) {
+ throw new IllegalArgumentException("The file can't be null");
+ }
+ if (path == null) {
+ throw new IllegalArgumentException("The path can't be null");
+ }
+
+ if (!file.isSet(path)) {
+ throw new IllegalArgumentException("The path: " + path + " doesn't exist.");
+ }
+
+ if (isList(file, path)) {
+ msg(target, file.getStringList(path), map, replace);
+ } else {
+ msg(target, file.getString(path), map, replace);
+ }
+ }
+
+ public static void msg(final CommandSender target, final String message) {
+ if (target == null) {
+ throw new IllegalArgumentException("The target can't be null");
+ }
+ if (message == null) {
+ return;
+ }
+ target.sendMessage(colorize(message));
+ }
+
+ public static void msg(final CommandSender target, final String... message) {
+ if (target == null) {
+ throw new IllegalArgumentException("The target can't be null");
+ }
+ if (message == null || message.length == 0) {
+ return;
+ }
+ if (Arrays.stream(message).anyMatch(Objects::isNull)) {
+ throw new IllegalArgumentException("The string array can't have null elements");
+ }
+ target.sendMessage(colorize(message));
+ }
+
+ public static void msg(final CommandSender target, final List message) {
+ if (target == null) {
+ throw new IllegalArgumentException("The target can't be null");
+ }
+ if (message == null || message.isEmpty()) {
+ return;
+ }
+ if (message.stream().anyMatch(Objects::isNull)) {
+ throw new IllegalArgumentException("The list can't have null elements");
+ }
+ msg(target, message.toArray(new String[0]));
+ }
+
+ /**
+ * Returns a translated string.
+ *
+ * @param msg The message to be translated
+ * @return A translated message
+ */
+ public static @NotNull String colorize(final String msg) {
+ String unEditedMessage = msg;
+ if (unEditedMessage == null) {
+ throw new IllegalArgumentException("The string can't be null!");
+ }
+ Pattern pattern = Pattern.compile("#[a-fA-F0-9]{6}");
+ Matcher matcher = pattern.matcher(unEditedMessage);
+ while (matcher.find()) {
+ String hexCode = unEditedMessage.substring(matcher.start(), matcher.end());
+ String replaceSharp = hexCode.replace('#', 'x');
+
+ char[] ch = replaceSharp.toCharArray();
+ StringBuilder builder = new StringBuilder();
+ for (char c : ch) {
+ builder.append("&").append(c);
+ }
+
+ unEditedMessage = unEditedMessage.replace(hexCode, builder.toString());
+ matcher = pattern.matcher(unEditedMessage);
+ }
+ //noinspection deprecation
+ return ChatColor.translateAlternateColorCodes('&', unEditedMessage);
+ }
+
+ public static String stripColor(final String msg) {
+ if (msg == null) {
+ throw new IllegalArgumentException("The string can't be null!");
+ }
+ //noinspection deprecation
+ return ChatColor.stripColor(msg);
+ }
+
+ /**
+ * Returns a translated string array.
+ *
+ * @param array Array of messages
+ * @return A translated message array
+ */
+ public static String @NotNull [] colorize(final String... array) {
+ if (array == null) {
+ throw new IllegalArgumentException("The string array can't be null!");
+ }
+ if (Arrays.stream(array).anyMatch(Objects::isNull)) {
+ throw new IllegalArgumentException("The string array can't have null elements!");
+ }
+ final String[] newarr = Arrays.copyOf(array, array.length);
+ for (int i = 0; i < newarr.length; i++) {
+ newarr[i] = colorize(newarr[i]);
+ }
+ return newarr;
+ }
+
+ public static String @NotNull [] stripColor(final String... array) {
+ if (array == null) {
+ throw new IllegalArgumentException("The string array can't be null!");
+ }
+ if (Arrays.stream(array).anyMatch(Objects::isNull)) {
+ throw new IllegalArgumentException("The string array can't have null elements!");
+ }
+ final String[] newarr = Arrays.copyOf(array, array.length);
+ for (int i = 0; i < newarr.length; i++) {
+ newarr[i] = stripColor(newarr[i]);
+ }
+ return newarr;
+ }
+
+ /**
+ * Returns a translated string collection.
+ *
+ * @param coll The collection to be translated
+ * @return A translated message
+ */
+ public static @NotNull List colorize(final List coll) {
+ if (coll == null) {
+ throw new IllegalArgumentException("The string collection can't be null!");
+ }
+ if (coll.stream().anyMatch(Objects::isNull)) {
+ throw new IllegalArgumentException("The string collection can't have null elements!");
+ }
+ final List newColl = Lists.newArrayList(coll);
+ newColl.replaceAll(BukkitMinecraftUtils::colorize);
+ return newColl;
+ }
+
+ /**
+ * Converts a String List that contains color codes to Color List
+ *
+ * @param list the String List that contains the color codes
+ * @return the new Color List with the colors of the input Color String List
+ */
+ public static @NotNull List colorsStringListToColorList(@NotNull List list) {
+ return colorsStringListToColorList(list.toArray(new String[0]));
+ }
+
+ /**
+ * Converts a String Array that contains color codes to Color List
+ *
+ * @param array the String Array that contains the color codes
+ * @return the new Color List with the colors of the input Color String Array
+ */
+ public static @NotNull List colorsStringListToColorList(String @NotNull ... array) {
+ List colorList = Lists.newArrayList();
+ for (String str : array) {
+ colorList.add(Color.from(str));
+ }
+ return colorList;
+ }
+
+ public static @NotNull List stripColor(final List coll) {
+ if (coll == null) {
+ throw new IllegalArgumentException("The string collection can't be null!");
+ }
+ if (coll.stream().anyMatch(Objects::isNull)) {
+ throw new IllegalArgumentException("The string collection can't have null elements!");
+ }
+ final List newColl = Lists.newArrayList(coll);
+ newColl.replaceAll(BukkitMinecraftUtils::stripColor);
+ return newColl;
+ }
+
+ public static void debug(final String name, String version, final Map map, String @NotNull ... messages) {
+ for (final String msg : messages) {
+ BukkitMinecraftUtils.printMsg(Utils.placeHolder("[" + name + "] [Debug] [Version: " + version + "] " + msg, map, false));
+ }
+ }
+
+ public static void debug(final String name, String version, String... messages) {
+ debug(name, version, new HashObjectMap<>(), messages);
+ }
+
+ public static void debug(final String name, String version, @NotNull List messages) {
+ debug(name, version, new HashObjectMap<>(), messages.toArray(new String[0]));
+ }
+
+ public static void debug(final JavaPlugin plugin, final Map map, String @NotNull ... messages) {
+ for (final String msg : messages) {
+ //noinspection deprecation
+ BukkitMinecraftUtils.printMsg(Utils.placeHolder("[" + plugin.getDescription().getName() + "] [Debug] [Version: " + plugin.getDescription().getVersion() + "] " + msg, map, false));
+ }
+ }
+
+ public static void debug(final JavaPlugin plugin, String... messages) {
+ debug(plugin, new HashObjectMap<>(), messages);
+ }
+
+ public static void debug(final JavaPlugin plugin, @NotNull List messages) {
+ debug(plugin, new HashObjectMap<>(), messages.toArray(new String[0]));
+ }
+
+ public static ItemStack @NotNull [] getItems(final @NotNull ItemStack item, int amount) {
+
+ final int maxSize = item.getMaxStackSize();
+ if (amount <= maxSize) {
+ item.setAmount(Math.max(amount, 1));
+ return new ItemStack[]{item};
+ }
+ final List resultItems = Lists.newArrayList();
+ do {
+ item.setAmount(Math.min(amount, maxSize));
+ resultItems.add(new ItemStack(item));
+ amount = amount >= maxSize ? amount - maxSize : 0;
+ } while (amount != 0);
+ return resultItems.toArray(new ItemStack[0]);
+ }
+
+ public static @NotNull String getProgressBar(double current, double max, int totalBars, String symbol, String completedColor,
+ String notCompletedColor) {
+ final double percent = (float) Math.min(current, max) / max;
+ final int progressBars = (int) (totalBars * percent);
+ final int leftOver = totalBars - progressBars;
+
+ return colorize(completedColor) +
+ String.valueOf(symbol).repeat(Math.max(0, progressBars)) +
+ colorize(notCompletedColor) +
+ String.valueOf(symbol).repeat(Math.max(0, leftOver));
+ }
+
+ public static @NotNull ItemStack resetItemMeta(final @NotNull ItemStack item) {
+ final ItemStack copy = item.clone();
+ copy.setItemMeta(Bukkit.getItemFactory().getItemMeta(copy.getType()));
+ return copy;
+ }
+
+ /**
+ * Register listeners
+ *
+ * @param listeners Class that implements Listener interface
+ */
+ public static void registerListeners(Plugin plugin, Listener @NotNull ... listeners) {
+ final PluginManager pm = Bukkit.getPluginManager();
+ for (final Listener listener : listeners) {
+ pm.registerEvents(listener, plugin);
+ }
+ }
+
+ /**
+ * Run the commands from config
+ *
+ * @param s Command to run
+ * @since v5.0
+ */
+ public static void runCommand(Plugin plugin, String s) {
+ Bukkit.getScheduler().runTask(plugin, () -> {
+ if (s == null)
+ return;
+ Bukkit.dispatchCommand(Bukkit.getConsoleSender(), s);
+ });
+ }
+
+ /**
+ * Kick all players.
+ *
+ * @param kickMessage The kick message to display.
+ * @since v5.0
+ */
+ public static void kickAll(Plugin plugin, String kickMessage) {
+ Bukkit.getScheduler().runTask(plugin, () -> Bukkit.getOnlinePlayers().forEach(player -> player.kickPlayer(colorize(kickMessage))));
+ }
+
+ /**
+ * Disallow or allow the player login to the server with a custom message.
+ *
+ * @param b True -> disallow player login. False -> allow player login.
+ * @param message The message to display when the player is disallowed to login.
+ * @since v5.0
+ */
+ public static void disallowLogin(boolean b, String message) {
+ join = b;
+ disableJoinMessage = message;
+ }
+
+ /**
+ * @return true if the player login is disallowed or false if the player login is allowed.
+ * @since v5.0
+ */
+ public static boolean isLoginDisallowed() {
+ return join;
+ }
+
+ /**
+ * @return The message to display when the player is disallowed to login.
+ * @since v5.0
+ */
+ public static String getDisallowLoginMessage() {
+ return disableJoinMessage;
+ }
+
+ /**
+ * Gets a list of ItemStacks from Base64 string.
+ *
+ * @param data Base64 string to convert to ItemStack list.
+ * @return ItemStack list created from the Base64 string.
+ */
+ @Contract("null -> new")
+ public static @Nullable List itemStackListFromBase64(String data) {
+ if (data == null || data.isEmpty()) {
+ return Lists.newArrayList();
+ }
+ try {
+ ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64Coder.decodeLines(data));
+ BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream);
+ ItemStack[] items = new ItemStack[dataInput.readInt()];
+
+ for (int i = 0; i < items.length; i++) {
+ items[i] = (ItemStack) dataInput.readObject();
+ }
+
+ dataInput.close();
+ return Arrays.asList(items);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ /**
+ * A method to serialize an {@link ItemStack} list to Base64 String.
+ *
+ * @param items to turn into a Base64 String.
+ * @return Base64 string of the items.
+ */
+ public static @NotNull String itemStackListToBase64(List items) {
+ try {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream);
+
+ dataOutput.writeInt(items.size());
+
+ for (ItemStack item : items) {
+ dataOutput.writeObject(item);
+ }
+
+ dataOutput.close();
+ return Base64Coder.encodeLines(outputStream.toByteArray());
+ } catch (Exception e) {
+ throw new IllegalStateException("Unable to save item stacks.", e);
+ }
+ }
+
+ /**
+ * Check if a username belongs to a premium account
+ *
+ * @param username player name
+ * @return boolean
+ */
+ public static boolean isUsernamePremium(String username) {
+ try {
+ URL url = new URL("https://api.mojang.com/users/profiles/minecraft/" + username);
+ BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
+ String line;
+ StringBuilder result = new StringBuilder();
+ while ((line = in.readLine()) != null) {
+ result.append(line);
+ }
+ return !result.toString().isEmpty();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ return false;
+ }
+
+ /**
+ * Translates all the placeholders of the string from the map
+ *
+ * @param target target for the placeholders.
+ * @param str the input string to translate the placeholders on
+ * @param map the map that contains all the placeholders with the replacement
+ * @param ignoreCase if it is true all the placeholders will be replaced
+ * in ignore case
+ * @return the new string with the placeholders replaced
+ */
+ public static String placeholderAPI(final ServerOperator target, String str, final Map map, final boolean ignoreCase) {
+ if (target == null) {
+ throw new IllegalArgumentException("The target can't be null");
+ }
+ if (str == null) {
+ throw new IllegalArgumentException("The string can't be null!");
+ }
+ if (map == null) {
+ try {
+ if (target instanceof OfflinePlayer offlinePlayer) {
+ return me.clip.placeholderapi.PlaceholderAPI.setBracketPlaceholders(offlinePlayer, str);
+ }
+ return str;
+ } catch (Throwable error) {
+ return str;
+ }
+ }
+ for (final Map.Entry entry : map.entrySet()) {
+ str = ignoreCase ? Utils.replaceIgnoreCase(str, entry.getKey(), entry.getValue())
+ : str.replace(entry.getKey(), entry.getValue());
+ }
+ try {
+ if (target instanceof OfflinePlayer offlinePlayer) {
+ return me.clip.placeholderapi.PlaceholderAPI.setBracketPlaceholders(offlinePlayer, str);
+ }
+ return str;
+ } catch (Throwable error) {
+ return str;
+ }
+ }
+
+ /**
+ * Translates all the placeholders of the string from the map
+ *
+ * @param target target for the placeholders.
+ * @param array the input array of string to translate the placeholders on
+ * @param map the map that contains all the placeholders with the replacement
+ * @param ignoreCase if it is true all the placeholders will be replaced
+ * in ignore case
+ * @return the new string array with the placeholders replaced
+ */
+ public static String @NotNull [] placeholderAPI(final ServerOperator target, final String[] array, final Map map, final boolean ignoreCase) {
+ if (array == null) throw new IllegalArgumentException("The string array can't be null!");
+ if (Arrays.stream(array).anyMatch(Objects::isNull))
+ throw new IllegalArgumentException("The string array can't have null elements!");
+ final String[] newArray = Arrays.copyOf(array, array.length);
+ for (int i = 0; i < newArray.length; i++) {
+ newArray[i] = placeholderAPI(target, newArray[i], map, ignoreCase);
+ }
+ return newArray;
+ }
+
+ /**
+ * Translates all the placeholders of the string from the map
+ *
+ * @param target target for the placeholders.
+ * @param coll the input string list to translate the placeholders on
+ * @param map the map that contains all the placeholders with the replacement
+ * @param ignoreCase if it is true all the placeholders will be replaced
+ * in ignore case
+ * @return the new string list with the placeholders replaced
+ */
+ public static List placeholderAPI(final ServerOperator target, final List coll, final Map map,
+ final boolean ignoreCase) {
+ if (coll == null) throw new IllegalArgumentException("The string collection can't be null!");
+ if (coll.stream().anyMatch(Objects::isNull))
+ throw new IllegalArgumentException("The string collection can't have null elements!");
+ return coll.stream().map(str -> placeholderAPI(target, str, map, ignoreCase)).collect(Collectors.toList());
+ }
+
+
+ /**
+ * Compares two Chunks to check if they are the same.
+ *
+ * @param chunkA The first Chunk to compare.
+ * @param chunkB The second Chunk to compare.
+ * @return `true` if the Chunks are equal, otherwise `false`.
+ */
+ public static boolean compareChunks(final @NotNull Chunk chunkA, final @NotNull Chunk chunkB) {
+ if (!chunkA.getWorld().equals(chunkB.getWorld())) {
+ return false;
+ }
+ if (chunkA.getX() != chunkB.getX()) {
+ return false;
+ }
+ return chunkA.getZ() == chunkB.getZ();
+ }
+
+ /**
+ * Compares two ItemStacks to check if they are the same.
+ *
+ * @param item1 The first ItemStack to compare.
+ * @param item2 The second ItemStack to compare.
+ * @return `true` if the ItemStacks are equal, otherwise `false`.
+ */
+ public static boolean compareItemStacks(ItemStack item1, ItemStack item2) {
+ if (item1 == null || item2 == null) {
+ return false;
+ }
+
+ if (item1 == item2) {
+ return true;
+ }
+
+ if (item1.getType() != item2.getType()) {
+ return false;
+ }
+
+ if (item1.getDurability() != item2.getDurability()) {
+ return false;
+ }
+
+ if (item1.hasItemMeta() != item2.hasItemMeta()) {
+ return false;
+ }
+
+ if (item1.hasItemMeta() && item2.hasItemMeta()) {
+ ItemMeta meta1 = item1.getItemMeta();
+ ItemMeta meta2 = item2.getItemMeta();
+
+ if (!Bukkit.getItemFactory().equals(meta1, meta2)) {
+ return false;
+ }
+ }
+
+ return item1.getAmount() == item2.getAmount();
+ }
+
+ /**
+ * Checks if the chunk containing the specified `location` is loaded in the world.
+ *
+ * @param loc The location to check.
+ * @return `true` if the chunk is loaded, otherwise `false`.
+ */
+ public static boolean isChunkLoaded(final Location loc) {
+ if (loc == null) {
+ return false;
+ }
+ if (loc.getWorld() == null) {
+ return false;
+ }
+ return loc.getWorld().isChunkLoaded(loc.getBlockX() >> 4, loc.getBlockZ() >> 4);
+ }
+
+ /**
+ * Checks if the current environment is "Folia" by attempting to load the "io.papermc.paper.threadedregions.RegionizedServer" class.
+ *
+ * @return `true` if the environment is "Folia," otherwise `false`.
+ */
+ public static boolean isFolia() {
+ return hasClass("io.papermc.paper.threadedregions.RegionizedServer");
+ }
+
+ /**
+ * Checks if the current environment is "Paper" by attempting to load the "com.destroystokyo.paper.PaperConfig" or "io.papermc.paper.configuration.Configuration" class.
+ *
+ * @return `true` if the environment is "Paper," otherwise `false`.
+ */
+ public static boolean isPaper() {
+ return (hasClass("com.destroystokyo.paper.PaperConfig")
+ || hasClass("io.papermc.paper.configuration.Configuration"));
+ }
+
+ /**
+ * Represents a Minecraft server version using a numeric format (major.minor.patch).
+ */
+ public static final class MinecraftVersion implements Comparable {
+ private static final Pattern VERSION_PATTERN =
+ Pattern.compile("^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?");
+
+ private final int major;
+ private final int minor;
+ private final int patch;
+ private final String nmsPackage;
+
+ /**
+ * The current server version, parsed once during class initialization.
+ */
+ private static final MinecraftVersion CURRENT;
+
+ static {
+ CURRENT = parse(Bukkit.getServer().getBukkitVersion());
+ }
+
+ /**
+ * Constructs a new {@link MinecraftVersion}.
+ *
+ * @param major the major version (e.g. 1 or 26)
+ * @param minor the minor version (e.g. 21 or 1)
+ * @param patch the patch version (e.g. 4 or 11)
+ */
+ public MinecraftVersion(int major, int minor, int patch) {
+ this.major = major;
+ this.minor = minor;
+ this.patch = patch;
+ this.nmsPackage = parseNMSPackage();
+ }
+
+ public MinecraftVersion(int major, int minor, int patch, String nmsPackage) {
+ this.major = major;
+ this.minor = minor;
+ this.patch = patch;
+ this.nmsPackage = nmsPackage;
+ }
+
+ /**
+ * Returns the current Minecraft server version.
+ *
+ * @return the parsed server version
+ */
+ public static MinecraftVersion getCurrent() {
+ return CURRENT;
+ }
+
+ /**
+ * Parses a Bukkit version string into a {@link MinecraftVersion}.
+ *
+ * Examples of supported formats:
+ *
+ *
{@code 1.21.4-R0.1-SNAPSHOT}
+ *
{@code 26.1.1}
+ *
+ *
+ * @param bukkitVersion the raw version string from Bukkit
+ * @return a parsed {@link MinecraftVersion}, or {@code 0.0.0} if parsing fails
+ */
+ public static @NotNull MinecraftVersion parse(String bukkitVersion) {
+ Matcher matcher = VERSION_PATTERN.matcher(bukkitVersion);
+
+ if (!matcher.find()) {
+ return new MinecraftVersion(0, 0, 0, "unknown");
+ }
+
+ int major = Integer.parseInt(matcher.group(1));
+ int minor = matcher.group(2) != null ? Integer.parseInt(matcher.group(2)) : 0;
+ int patch = matcher.group(3) != null ? Integer.parseInt(matcher.group(3)) : 0;
+
+ return new MinecraftVersion(major, minor, patch, parseNMSPackage());
+ }
+
+ private static String parseNMSPackage() {
+ try {
+ String packageName = Bukkit.getServer()
+ .getClass()
+ .getPackage()
+ .getName();
+
+ String[] parts = packageName.split("\\.");
+
+ return parts[parts.length - 1];
+ } catch (Exception e) {
+ return "unknown";
+ }
+ }
+
+ /**
+ * Compares this version to another version.
+ *
+ * @param other the other version
+ * @return a negative value if lower, positive if higher, 0 if equal
+ */
+ @Override
+ public int compareTo(@NonNull MinecraftVersion other) {
+ if (this.major != other.major) {
+ return Integer.compare(this.major, other.major);
+ }
+ if (this.minor != other.minor) {
+ return Integer.compare(this.minor, other.minor);
+ }
+ return Integer.compare(this.patch, other.patch);
+ }
+
+ /**
+ * Checks if this version is greater than or equal to the given version.
+ * Patch is ignored (assumes 0).
+ *
+ * @param major the major version
+ * @param minor the minor version
+ * @return {@code true} if this version is >= given version
+ */
+ public boolean isAtLeast(int major, int minor) {
+ return compareTo(new MinecraftVersion(major, minor, 0)) >= 0;
+ }
+
+ /**
+ * Checks if this version is greater than or equal to the given version.
+ *
+ * @param major the major version
+ * @param minor the minor version
+ * @param patch the patch version
+ * @return {@code true} if this version is >= given version
+ */
+ public boolean isAtLeast(int major, int minor, int patch) {
+ return compareTo(new MinecraftVersion(major, minor, patch)) >= 0;
+ }
+
+ /**
+ * Checks if this version is strictly lower than the given version.
+ * Patch is ignored (assumes 0).
+ *
+ * @param major the major version
+ * @param minor the minor version
+ * @return {@code true} if this version is {@code <} given version
+ */
+ public boolean isBelow(int major, int minor) {
+ return isBelow(major, minor, 0);
+ }
+
+
+ public boolean isBelow(int major, int minor, int patch) {
+ return compareTo(new MinecraftVersion(major, minor, patch)) < 0;
+ }
+
+ /**
+ * Checks if this version is within a range:
+ * {@code [min, max)} (inclusive lower bound, exclusive upper bound).
+ *
+ * @param minMajor minimum major version
+ * @param minMinor minimum minor version
+ * @param maxMajor maximum major version
+ * @param maxMinor maximum minor version
+ * @return {@code true} if within the specified range
+ */
+ public boolean isBetween(
+ int minMajor, int minMinor,
+ int maxMajor, int maxMinor
+ ) {
+ return isAtLeast(minMajor, minMinor)
+ && isBelow(maxMajor, maxMinor);
+ }
+
+ public boolean isBetween(int minMajor, int minMinor, int minPatch, int maxMajor, int maxMinor, int maxPatch) {
+ return isAtLeast(minMajor, minMinor, minPatch)
+ && isBelow(maxMajor, maxMinor, maxPatch);
+ }
+
+ public boolean isVersion(int major, int minor, Integer patch) {
+ return this.major == major
+ && this.minor == minor
+ && (patch == null || this.patch == patch);
+ }
+
+ /**
+ * @return the major version
+ */
+ public int getMajor() {
+ return major;
+ }
+
+ /**
+ * @return the minor version
+ */
+ public int getMinor() {
+ return minor;
+ }
+
+ /**
+ * @return the patch version
+ */
+ public int getPatch() {
+ return patch;
+ }
+
+ public String getNMSPackage() {
+ return nmsPackage;
+ }
+
+ /**
+ * Returns the version in {@code major.minor.patch} format.
+ *
+ * @return string representation of this version
+ */
+ @Override
+ public String toString() {
+ return major + "." + minor + "." + patch;
+ }
+ }
+
+ public static final class MinecraftReflection {
+
+ private static final String NET_MINECRAFT_PACKAGE = "net.minecraft";
+ private static final String OBC_PACKAGE = "org.bukkit.craftbukkit";
+
+ private static final String NMS_SERVER_BASE = NET_MINECRAFT_PACKAGE + ".server";
+
+ private static final MinecraftVersion VERSION = MinecraftVersion.getCurrent();
+ private static final boolean REPACKAGED_NMS = VERSION.isAtLeast(1, 17);
+ private static final boolean REPACKAGED_OBC = VERSION.isAtLeast(1, 20, 5) && isPaper();
+
+ private static String resolveNmsPackage() {
+ return REPACKAGED_NMS ? NMS_SERVER_BASE : NMS_SERVER_BASE + "." + VERSION.getNMSPackage();
+ }
+
+ private static String resolveObcPackage() {
+ return REPACKAGED_OBC ? OBC_PACKAGE : OBC_PACKAGE + "." + VERSION.getNMSPackage();
+ }
+
+ private static ClassLoader loader() {
+ return Bukkit.class.getClassLoader();
+ }
+
+ @Contract(pure = true)
+ public static @NonNull String nmsClassName(String className) {
+ return resolveNmsPackage() + "." + className;
+ }
+
+ public static @NonNull Class> nmsClass(String className) throws ClassNotFoundException {
+ return Class.forName(nmsClassName(className), false, loader());
+ }
+
+ public static Optional> nmsOptionalClass(String className) {
+ return Utils.Reflection.optionalClass(nmsClassName(className), loader());
+ }
+
+ public static @NonNull Class> nmsClass(String className, String fullName) throws ClassNotFoundException {
+ return Class.forName(REPACKAGED_NMS ? fullName : nmsClassName(className), false, loader());
+ }
+
+ public static Optional> nmsOptionalClass(String className, String fullName) {
+ return Utils.Reflection.optionalClass(
+ REPACKAGED_NMS ? fullName : nmsClassName(className),
+ loader()
+ );
+ }
+
+ @Contract(pure = true)
+ public static @NonNull String obcClassName(String className) {
+ return resolveObcPackage() + "." + className;
+ }
+
+ public static @NonNull Class> obcClass(String className) throws ClassNotFoundException {
+ return Class.forName(obcClassName(className), false, loader());
+ }
+
+ public static Optional> obcOptionalClass(String className) {
+ return Utils.Reflection.optionalClass(obcClassName(className), loader());
+ }
+
+ public static boolean isNmsRepackaged() {
+ return REPACKAGED_NMS;
+ }
+
+ public static boolean isObcRepackaged() {
+ return REPACKAGED_OBC;
+ }
+ }
+
+}
diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitPluginMessageUtils.java b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitPluginMessageUtils.java
deleted file mode 100644
index a29b3e05..00000000
--- a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitPluginMessageUtils.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.georgev22.skinoverlay.utilities;
-
-import com.georgev22.skinoverlay.utilities.player.PlayerObject;
-import org.bukkit.Bukkit;
-import org.bukkit.entity.Player;
-import org.jetbrains.annotations.NotNull;
-
-public class BukkitPluginMessageUtils extends PluginMessageUtils {
-
- public void sendDataToServer(@NotNull String subChannel, String... dataArray) {
- Bukkit.getServer().sendPluginMessage(skinOverlay.getSkinOverlay().plugin(), getChannel(), this.toByteArray(subChannel, dataArray));
- }
-
- @Override
- public void sendDataToPlayer(@NotNull String subChannel, @NotNull PlayerObject player, String... dataArray) {
- ((Player) player.player()).sendPluginMessage(skinOverlay.getSkinOverlay().plugin(), getChannel(), this.toByteArray(subChannel, dataArray));
- }
-}
diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/SkinOverlayVersionResolver.java b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/SkinOverlayVersionResolver.java
new file mode 100644
index 00000000..f31f7e53
--- /dev/null
+++ b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/SkinOverlayVersionResolver.java
@@ -0,0 +1,31 @@
+package com.georgev22.skinoverlay.utilities;
+
+import com.georgev22.skinoverlay.SkinOverlay;
+import com.georgev22.skinoverlay.providers.GameProfileProviderNoop;
+import com.georgev22.skinoverlay.refreshers.NoopSkinRefresher;
+
+public final class SkinOverlayVersionResolver {
+
+ private final SkinOverlay skinOverlay;
+
+ public SkinOverlayVersionResolver(SkinOverlay skinOverlay) {
+ this.skinOverlay = skinOverlay;
+ }
+
+ public void resolve() {
+ BukkitMinecraftUtils.MinecraftVersion current = BukkitMinecraftUtils.MinecraftVersion.getCurrent();
+ for (VersionRange range : VersionRegistry.RANGES) {
+ if (range.matches(current)) {
+ skinOverlay.setGameProfileProvider(range.provider().get());
+ skinOverlay.setSkinRefresher(range.refresher().get());
+ return;
+ }
+ }
+
+ skinOverlay.setSkinRefresher(new NoopSkinRefresher());
+ skinOverlay.setGameProfileProvider(new GameProfileProviderNoop());
+
+ skinOverlay.getLogger().info("SkinOverlay does not support " + current);
+ skinOverlay.getLogger().info("Install SkinsRestorer in order for SkinOverlay to use their API to apply skins.");
+ }
+}
\ No newline at end of file
diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRange.java b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRange.java
new file mode 100644
index 00000000..0301f680
--- /dev/null
+++ b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRange.java
@@ -0,0 +1,54 @@
+package com.georgev22.skinoverlay.utilities;
+
+import com.georgev22.skinoverlay.providers.GameProfileProvider;
+import com.georgev22.skinoverlay.refreshers.SkinRefresher;
+import com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils.MinecraftVersion;
+import org.jspecify.annotations.NonNull;
+
+import java.util.function.Supplier;
+
+/**
+ * Represents a supported Minecraft version range and the components that should
+ * be used for that range.
+ *
+ *
A {@code VersionRange} defines the minimum and maximum Minecraft versions
+ * supported by a specific {@link GameProfileProvider} and {@link SkinRefresher}.
+ * The providers are created lazily using {@link Supplier} instances, allowing
+ * implementations to be initialized only when required.
+ *
+ * @param min the minimum supported Minecraft version (inclusive)
+ * @param max the maximum supported Minecraft version (inclusive)
+ * @param provider supplier that creates the {@link GameProfileProvider} for this range
+ * @param refresher supplier that creates the {@link SkinRefresher} for this range
+ */
+public record VersionRange(
+ MinecraftVersion min,
+ MinecraftVersion max,
+ Supplier provider,
+ Supplier refresher
+) {
+
+ /**
+ * Creates a version range that only matches a single Minecraft version.
+ *
+ * @param version the Minecraft version supported by this range
+ * @param provider supplier that creates the {@link GameProfileProvider} for this version
+ * @param refresher supplier that creates the {@link SkinRefresher} for this version
+ */
+ public VersionRange(MinecraftVersion version, Supplier provider, Supplier refresher) {
+ this(version, version, provider, refresher);
+ }
+
+ /**
+ * Checks whether the specified Minecraft version is contained within this range.
+ *
+ *
The comparison is inclusive, meaning that versions equal to {@link #min()}
+ * or {@link #max()} will match.
+ * This method is intended for internal use only. The plugin automatically
+ * manages placeholder hook initialization and registration during its
+ * lifecycle.
+ *
+ * Calling this method manually is strongly discouraged unless you fully
+ * understand the placeholder hook lifecycle. In particular, registration
+ * should be handled manually by the caller, as the plugin's normal
+ * registration process occurs during {@code onEnable()}. Replacing the
+ * hook after or during this process without properly registering it may
+ * result in errors or undefined behavior.
+ *
+ * If a custom hook is supplied, it is the caller's responsibility to ensure
+ * that the hook is correctly initialized and that
+ * {@link PlaceholderHook#isRegistered()} returns {@code true} before the
+ * hook is used.
+ *
+ * This method should only be called after the plugin has completed its
+ * enable phase.
+ *
+ * @param placeholderHook the placeholder hook to use
+ */
+ @ApiStatus.Internal
+ public void setPlaceholderHook(PlaceholderHook placeholderHook) {
+ this.placeholderHook = placeholderHook;
+ }
+
+ /**
+ * Checks if the server is in online mode.
+ *
+ * @return true if online mode, false otherwise
+ */
+ public boolean isOnlineMode() {
+ return isOnlineMode;
+ }
+
+ /**
+ * Sets the server online mode.
+ *
+ * @param isOnlineMode true for online mode, false otherwise
+ */
+ public void setOnlineMode(boolean isOnlineMode) {
+ this.isOnlineMode = isOnlineMode;
+ }
+
+ /**
+ * Checks if the server is running as a proxy.
+ *
+ * @return true if proxy, false otherwise
+ */
+ public boolean isProxy() {
+ return isProxy;
+ }
+
+ /**
+ * Sets whether the server is running as a proxy.
+ *
+ * @param isProxy true for proxy, false otherwise
+ */
+ public void setProxy(boolean isProxy) {
+ this.isProxy = isProxy;
+ }
+
+ /**
+ * Gets the SkinFileCache instance.
+ *
+ * @return the SkinFileCache
+ */
+ public SkinFileCache getSkinFileCache() {
+ return skinFileCache;
+ }
+
+ /**
+ * Gets the EventBus instance.
+ *
+ * @return the EventBus
+ */
+ public EventBus getEventBus() {
+ return eventBus;
+ }
+
+ /**
+ * Sets the MessageManager instance.
+ *
+ * @param messageManager the MessageManager
+ */
+ public void setMessageManager(MessageManager messageManager) {
+ this.messageManager = messageManager;
+ }
+
+ /**
+ * Gets the MessageManager instance.
+ *
+ * @return the MessageManager
+ */
+ public MessageManager getMessageManager() {
+ return messageManager;
+ }
+
+ private void registerEntityManager(EntityManagerRegistry entityManagerRegistry,
+ ManagedEntity extends Entity> managedEntity,
+ EntityManager> entityManager) {
+ try {
+ entityManagerRegistry.register(managedEntity.type(), entityManager);
+ this.getLogger().info(
+ "Registered " + managedEntity.type().getSimpleName() + " entity manager");
+ } catch (Exception e) {
+ this.getLogger().log(Level.SEVERE,
+ "Failed to register " + managedEntity.type().getSimpleName() + " entity manager", e);
+ }
+ }
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/BaseCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/BaseCommand.java
new file mode 100644
index 00000000..89a33129
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/BaseCommand.java
@@ -0,0 +1,371 @@
+package com.georgev22.skinoverlay.command;
+
+import com.georgev22.skinoverlay.SkinOverlay;
+import com.georgev22.skinoverlay.command.annotation.*;
+import com.georgev22.skinoverlay.command.processors.PostProcessor;
+import com.georgev22.skinoverlay.command.processors.PreProcessor;
+import com.georgev22.skinoverlay.message.Placeholder;
+import com.georgev22.skinoverlay.message.messages.CommandMessages;
+import com.georgev22.skinoverlay.registry.CommandTargetRegistry;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.*;
+import java.util.logging.Level;
+
+/**
+ * Base class for all commands in the Core plugin.
+ *
Tab-completion using ArgumentResolvers or default completion engine
+ *
+ */
+@SuppressWarnings("unused")
+public abstract class BaseCommand {
+
+ private String[] aliases;
+ protected String[] subCommandAliases;
+ protected String description;
+ protected String usage;
+ protected String[] targets;
+ protected String permission;
+ private ExecutionType executionType;
+
+ private final List preprocessors = new ArrayList<>();
+ private final List postprocessors = new ArrayList<>();
+ private final Map subcommands = new HashMap<>();
+
+ /**
+ * Constructs a new BaseCommand, scanning annotations for metadata and subcommands.
+ */
+ public BaseCommand() {
+ Class> clazz = getClass();
+
+ CommandAlias alias = getAnnotation(CommandAlias.class, null, clazz);
+ if (alias != null) aliases = alias.value();
+
+ Subcommand sub = getAnnotation(Subcommand.class, null, clazz);
+ if (sub != null) subCommandAliases = sub.value();
+
+ Description desc = getAnnotation(Description.class, null, clazz);
+ if (desc != null) description = desc.value();
+
+ Usage usageAnn = getAnnotation(Usage.class, null, clazz);
+ if (usageAnn != null) usage = usageAnn.value();
+
+ CommandTarget targetAnn = getAnnotation(CommandTarget.class, null, clazz);
+ targets = (targetAnn != null) ? targetAnn.value() : new String[]{"any"};
+
+ Permission perm = getAnnotation(Permission.class, null, clazz);
+ if (perm != null) permission = perm.value();
+
+ Execution exec = getAnnotation(Execution.class, null, clazz);
+ if (exec != null) executionType = exec.value();
+
+ for (Method method : clazz.getDeclaredMethods()) {
+ if (method.isAnnotationPresent(Subcommand.class)) {
+ this.addSubcommand(new MethodCommand(this, method));
+ } else if (method.isAnnotationPresent(Default.class)) {
+ // Default handler (root or inside a subcommand)
+ MethodCommand cmd = new MethodCommand(this, method);
+ subcommands.put("__default__", cmd);
+ }
+ }
+ }
+
+ /**
+ * Executes this command with the given sender and arguments.
+ * Handles pre-processing, permission checks, async execution, and subcommand delegation.
+ *
+ * @param sender the command issuer
+ * @param args the command arguments
+ */
+ public void execute(CommandIssuer sender, String @NotNull [] args, CommandContext context) {
+ // Run global and local preprocessors
+ SkinOverlay.getInstance().getCommandManager().getGlobalPreprocessors().forEach(pre -> pre.process(() -> this, sender, context));
+ preprocessors.forEach(pre -> pre.process(() -> this, sender, context));
+
+ // Player-only check
+ if (!CommandTargetRegistry.getInstance().matches(targets, sender)) {
+ CommandMessages.COMMAND_TARGET_DENIED.msg(sender,
+ Placeholder.builder(sender.audience())
+ .placeholder("%command%", context.getData().getOr("command", ""))
+ .placeholder("%targets%", String.join(", ", targets))
+ .build()
+ );
+ return;
+ }
+
+ // Permission check
+ if (permission != null && !permission.isEmpty() && !sender.hasPermission(permission)) {
+ CommandMessages.COMMAND_PERMISSION_DENIED.msg(sender,
+ Placeholder.builder(sender.audience())
+ .placeholder("%command%", context.getData().getOr("command", ""))
+ .build());
+ return;
+ }
+
+ Runnable task = () -> {
+ try {
+ if (!executeSubcommand(sender, args, context)) {
+ // If no subcommand found, run "default" (if any)
+ BaseCommand def = subcommands.get("__default__");
+ if (def != null) {
+ def.execute(sender, args, context);
+ } else if (!handle(sender, args, context)) {
+ CommandMessages.COMMAND_USAGE.msg(sender,
+ Placeholder.builder(sender.audience())
+ .placeholder("%command%", context.getData().getOr("command", ""))
+ .placeholder("%usage%", usage).build());
+ }
+ }
+
+ // Run postprocessors
+ runPostprocessors(sender, context);
+ } catch (Exception e) {
+ CommandMessages.COMMAND_ERROR.msg(sender,
+ Placeholder.builder(sender.audience())
+ .placeholder("%command%", context.getData().getOr("command", ""))
+ .build());
+ SkinOverlay.getInstance().getLogger()
+ .log(Level.WARNING, "Error while executing command", e);
+ }
+ };
+
+ if (executionType == ExecutionType.ASYNC) {
+ SkinOverlay.getInstance().getScheduler()
+ .runAsyncTask(SkinOverlay.getInstance(), task);
+ } else {
+ task.run();
+ }
+ }
+
+ /**
+ * Executes a subcommand (nested BaseCommand or method-based) if it exists.
+ *
+ * @param sender the command issuer
+ * @param args the arguments
+ * @return true if a subcommand was executed, false otherwise
+ */
+ private boolean executeSubcommand(CommandIssuer sender, String[] args, CommandContext context) {
+ if (args.length == 0) return false;
+
+ BaseCommand sub = subcommands.get(args[0].toLowerCase());
+ if (sub != null) {
+ sub.execute(sender, Arrays.copyOfRange(args, 1, args.length), context);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Runs post-processing hooks after command execution.
+ *
+ * @param sender the command issuer
+ * @param context the command context
+ */
+ private void runPostprocessors(CommandIssuer sender, CommandContext context) {
+ SkinOverlay.getInstance().getCommandManager().getGlobalPostprocessors().forEach(post -> post.process(() -> this, sender, context));
+ postprocessors.forEach(post -> post.process(() -> this, sender, context));
+ }
+
+ /**
+ * Provides tab-completion suggestions for this command.
+ *
+ * @param sender the command issuer
+ * @param args the current arguments
+ * @return a collection of completion strings
+ */
+ public Collection tabComplete(CommandIssuer sender, String @NotNull [] args) {
+ if (args.length == 0) {
+ // Try default handler if there’s no explicit subcommand
+ MethodCommand defaultCmd = (MethodCommand) subcommands.get("__default__");
+ if (defaultCmd != null) {
+ return defaultCmd.tabComplete(sender, args);
+ }
+ return getAvailableSubcommands(sender);
+ }
+
+ if (args.length == 1) {
+ String current = args[0].toLowerCase();
+ List matches = new ArrayList<>();
+
+ for (String alias : subcommands.keySet()) {
+ if (!alias.equalsIgnoreCase("__default__") && alias.startsWith(current)) {
+ BaseCommand sub = subcommands.get(alias);
+ if (sub != null && (sub.getPermission() == null || sender.hasPermission(sub.getPermission()))) {
+ matches.add(alias);
+ }
+ }
+ }
+
+ if (!matches.isEmpty()) {
+ return matches;
+ }
+
+ MethodCommand defaultCmd = (MethodCommand) subcommands.get("__default__");
+ if (defaultCmd != null) {
+ return defaultCmd.tabComplete(sender, args);
+ }
+
+ return getAvailableSubcommands(sender);
+ }
+
+ BaseCommand sub = subcommands.get(args[0].toLowerCase());
+ if (sub != null && (sub.getPermission() == null || sender.hasPermission(sub.getPermission()))) {
+ String[] subArgs = Arrays.copyOfRange(args, 1, args.length);
+ return sub.tabComplete(sender, subArgs);
+ }
+
+ if (this instanceof MethodCommand methodCmd) {
+ return methodCmd.tabComplete(sender, args);
+ }
+
+ MethodCommand defaultCmd = (MethodCommand) subcommands.get("__default__");
+ if (defaultCmd != null) {
+ return defaultCmd.tabComplete(sender, args);
+ }
+
+ return CompletionEngine.resolveCompletions(getClass(), sender, args);
+ }
+
+ /**
+ * Lists all available subcommands for this command for tab-completion.
+ */
+ private @NotNull Collection getAvailableSubcommands(CommandIssuer sender) {
+ List available = new ArrayList<>();
+ for (Map.Entry entry : subcommands.entrySet()) {
+ String alias = entry.getKey();
+ BaseCommand sub = entry.getValue();
+
+ if ("__default__".equals(alias)) continue;
+ if (sub.getPermission() == null || sender.hasPermission(sub.getPermission())) {
+ available.add(alias);
+ }
+ }
+ return available;
+ }
+
+ /**
+ * Adds a nested BaseCommand as a subcommand.
+ *
+ * @param subCommand the subcommand to add
+ */
+ public void addSubcommand(@NotNull BaseCommand subCommand) {
+ if (subCommand.subCommandAliases.length == 0 && !(subCommand instanceof MethodCommand)) {
+ throw new IllegalArgumentException("Subcommand must define at least one alias");
+ }
+ for (String alias : subCommand.subCommandAliases) {
+ subcommands.put(alias.toLowerCase(), subCommand);
+ }
+ if (subCommand instanceof MethodCommand && ((MethodCommand) subCommand).isDefault()) {
+ subCommand.subcommands.put("__default__", subCommand);
+ }
+ if (subCommand.aliases != null && subCommand.aliases.length != 0)
+ SkinOverlay.getInstance().getCommandManager().registerCommand(subCommand);
+ }
+
+ /**
+ * Adds a preprocessor for this command.
+ *
+ * @param preprocessor the preprocessor to add
+ */
+ public void addPreprocessor(PreProcessor preprocessor) {
+ if (!preprocessors.contains(preprocessor)) preprocessors.add(preprocessor);
+ }
+
+ /**
+ * Adds a postprocessor for this command.
+ *
+ * @param postprocessor the postprocessor to add
+ */
+ public void addPostprocessor(PostProcessor postprocessor) {
+ if (!postprocessors.contains(postprocessor)) postprocessors.add(postprocessor);
+ }
+
+ /**
+ * Returns aliases for this command or subcommand.
+ *
+ * @param isSubcommand true if the aliases are for a subcommand
+ */
+ public @NotNull String[] getAliases(boolean isSubcommand) {
+ return isSubcommand ? subCommandAliases == null ? new String[0] : subCommandAliases : aliases == null ? new String[0] : aliases;
+ }
+
+ /**
+ * Returns usage string, if defined.
+ */
+ public String getUsage() {
+ return usage;
+ }
+
+ /**
+ * Returns permission string, if defined.
+ */
+ public String getPermission() {
+ return permission;
+ }
+
+ /**
+ * Returns command description, if defined.
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Returns the command execution type (SYNC or ASYNC).
+ */
+ public ExecutionType getExecutionType() {
+ return executionType;
+ }
+
+ /**
+ * Handles the execution of this command.
+ *
+ * This method is invoked when the command is executed and there is no specific method
+ * annotated with {@link Default} to handle it. Implementations of this method should
+ * process the provided command arguments and perform the desired action.
+ *
+ *
+ * Use this method for general command handling logic when a dedicated default handler
+ * method is not present.
+ *