diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index e7ee21af..a292fa85 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,63 @@ jobs: steps: - - name: Set up JDK 17 - uses: actions/setup-java@v1 + - name: Set up JDK 21 + uses: actions/setup-java@v4 with: - java-version: 17 + distribution: 'temurin' + java-version: '21' - name: Cache Gradle id: cache-gradle - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: ~/.gradle key: ${{ runner.os }}-gradle - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - 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@v4 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 21 + uses: actions/setup-java@v4 with: - java-version: 17 + distribution: 'temurin' + java-version: '21' - - name: Set up Node.js v18.x - uses: actions/setup-node@v1 + - name: Set up Node.js v20.x + uses: actions/setup-node@v4 with: - node-version: "18.x" + node-version: "20.x" - name: Cache Gradle id: cache-gradle - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: ~/.gradle key: ${{ runner.os }}-gradle - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - 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..48c02363 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,103 @@ +# [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..569e4735 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ You can have your project depend on SkinOverlay as a dependency through the foll com.georgev22 skinoverlay - 7.1.0 + 8.0.0-beta.3 provided @@ -45,7 +45,7 @@ repositories { } dependencies { - compileOnly "com.georgev22:skinoverlay:7.1.0" + compileOnly "com.georgev22:skinoverlay:8.0.0-beta.3" } ``` @@ -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..cdd1a1df --- /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) + 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..e1360543 --- /dev/null +++ b/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts @@ -0,0 +1,46 @@ +plugins { + `java-library` + `maven-publish` +} + +if (file("$rootDir/build.local.gradle.kts").exists()) { + apply("$rootDir/build.local.gradle.kts") +} + +repositories { + mavenLocal() + mavenCentral() + maven { + url = uri("https://repo.georgev22.com/releases/") + } + maven { + url = uri("https://repo.georgev22.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://jcenter.bintray.com/") + } + 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..22d7e9d1 --- /dev/null +++ b/bukkit/build.gradle.kts @@ -0,0 +1,82 @@ +plugins { + id("buildlogic.java-conventions") + alias(libs.plugins.gradleup.shadow) +} + +apply(from = "$rootDir/gradle/publish.gradle") + +repositories { + mavenCentral() +} + +dependencies { + compileOnly(libs.folia.api) + compileOnly(libs.placeholder.api) + compileOnly(libs.auth.lib.legacy) + + implementation(libs.bstats.bukkit) + implementation(libs.adventure.platform.bukkit) + implementation(libs.adventure.text.minimessage) + implementation(libs.adventure.text.serializer.legacy) + implementation(libs.reflect) + + implementation(project(":common")) { + exclude(group = "org.slf4j", module = "slf4j-api") + } + implementation(project(":bukkit:versions:mc1_17_R1", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_18_R1", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_18_R2", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_19_R1", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_19_R2", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_19_R3", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_20_R1", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_20_R2", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_20_R3", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_20_R4", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_21_R1", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_21_R2", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_21_R3", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_21_R4", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_21_R5", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_21_R6", configuration = "reobf")) +} + +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") + 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("net.lenni0451.reflect", "${project.property("packageName")}.lib.reflect") +} + +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..5667b3d3 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java @@ -1,219 +1,171 @@ 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.appliers.*; +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.BukkitMinecraftUtils; 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 java.io.File; -import java.util.UUID; -import java.util.logging.Logger; +import static com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils.MinecraftVersion.V1_20_R1; +import static com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils.MinecraftVersion.getCurrentVersion; -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 BukkitAudiences adventure; - private SkinOverlay skinOverlay; - - private MinecraftScheduler scheduler; + private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); @Override public void onLoad() { - try { - if (getCurrentVersion().isBelow(V1_16_R3)) { - this.libraryLoader = new LibraryLoader(this.getClassLoader(), this.getDataFolder()); - this.libraryLoader.loadAll(this, true); + 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 (BukkitMinecraftUtils.isPaper() && getCurrentVersion().isAboveOrEqual(V1_20_R1)) { + this.skinOverlay.setSkinApplier(new NoopSkinApplier()); + this.skinOverlay.setGameProfileProvider(new PaperGameProfileProvider()); + } else { + switch (getCurrentVersion()) { + case V1_8_R1, V1_8_R2, V1_8_R3, V1_9_R1, V1_9_R2, V1_10_R1, V1_11_R1, V1_12_R1, V1_13_R1, V1_13_R2, + V1_14_R1, V1_15_R1, V1_16_R1, V1_16_R2, V1_16_R3 -> { + this.skinOverlay.setGameProfileProvider(new BukkitLegacyGameProfileProvider()); + this.skinOverlay.setSkinApplier(new LegacySkinApplier()); + } + case V1_17_R1 -> { + this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_17_R1()); + this.skinOverlay.setSkinApplier(new SkinApplier_1_17_R1()); + } + case V1_18_R1 -> { + this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_18_R1()); + this.skinOverlay.setSkinApplier(new SkinApplier_1_18_R1()); + } + case V1_18_R2 -> { + this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_18_R2()); + this.skinOverlay.setSkinApplier(new SkinApplier_1_18_R2()); + } + case V1_19_R1 -> { + this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_19_R1()); + this.skinOverlay.setSkinApplier(new SkinApplier_1_19_R1()); + } + case V1_19_R2 -> { + this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_19_R2()); + this.skinOverlay.setSkinApplier(new SkinApplier_1_19_R2()); + } + case V1_19_R3 -> { + this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_19_R3()); + this.skinOverlay.setSkinApplier(new SkinApplier_1_19_R3()); + } + case V1_20_R1 -> { + this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_20_R1()); + this.skinOverlay.setSkinApplier(new SkinApplier_1_20_R1()); + } + case V1_20_R2 -> { + this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_20_R2()); + this.skinOverlay.setSkinApplier(new SkinApplier_1_20_R2()); + } + case V1_20_R3 -> { + this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_20_R3()); + this.skinOverlay.setSkinApplier(new SkinApplier_1_20_R3()); + } + case V1_21_R1 -> { + this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_21_R1()); + this.skinOverlay.setSkinApplier(new SkinApplier_1_21_R1()); + } + case V1_21_R2 -> { + this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_21_R2()); + this.skinOverlay.setSkinApplier(new SkinApplier_1_21_R2()); + } + case V1_21_R3 -> { + this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_21_R3()); + this.skinOverlay.setSkinApplier(new SkinApplier_1_21_R3()); + } + case V1_21_R4 -> { + this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_21_R4()); + this.skinOverlay.setSkinApplier(new SkinApplier_1_21_R4()); + } + case V1_21_R5 -> { + this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_21_R5()); + this.skinOverlay.setSkinApplier(new SkinApplier_1_21_R5()); + } + case V1_21_R6 -> { + this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_21_R6()); + this.skinOverlay.setSkinApplier(new SkinApplier_1_21_R6()); + } + default -> { + this.skinOverlay.setSkinApplier(new NoopSkinApplier()); + this.skinOverlay.setGameProfileProvider(new GameProfileProviderNoop()); + this.getLogger().info("SkinOverlay does not support " + Bukkit.getBukkitVersion()); + } } - } catch (InvalidDependencyException | UnknownDependencyException e) { - throw new RuntimeException(e); } - skinOverlay = new SkinOverlay(this); - skinOverlay.onLoad(); - } - @Override - public void onEnable() { - if (BukkitMinecraftUtils.MinecraftVersion.getCurrentVersion().isBelow(V1_16_R1)) - this.adventure = BukkitAudiences.create(this); - if (isFolia()) { - this.scheduler = new MinecraftFoliaScheduler(); + if (Bukkit.getPluginManager().isPluginEnabled("SkinsRestorer")) { + this.skinOverlay.setSkinHook(new SkinsRestorerHook()); + this.skinOverlay.setGameProfileProvider(new GameProfileProvider_SkinsRestorer()); + this.skinOverlay.setSkinApplier(new NoopSkinApplier()); } 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; + this.skinOverlay.setSkinHook(new SkinHookNoop()); } - if (getCurrentVersion().isBelow(V1_16_R3)) { - try { - this.libraryLoader.unloadAll(); - } catch (InvalidDependencyException e) { - throw new RuntimeException(e); - } + this.skinOverlay.setOnlineMode(Bukkit.getOnlineMode()); + this.skinOverlay.setProxy(false); + if (BukkitMinecraftUtils.isFolia()) { + this.skinOverlay.setScheduler( + new MinecraftFoliaScheduler() + ); + } else { + this.skinOverlay.setScheduler( + new MinecraftBukkitScheduler() + ); } - } - - - @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()); + this.skinOverlay.onLoad(); } @Override - public boolean enable(boolean enable) { - this.setEnabled(enable); - return enabled(); - } - - @Override - public boolean enabled() { - return this.isEnabled(); - } - - @Override - public void saveResource(@NotNull String resource, boolean replace) { - super.saveResource(resource, replace); - } - - @Override - public boolean onlineMode() { - return Bukkit.getOnlineMode(); - } + public void onEnable() { + this.skinOverlay.setAudienceProvider(BukkitAudiences.create(this)); - private final ObservableObjectMap players = new ObservableObjectMap<>(); + 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.getSkinApplier().setSkin(player, skin); + } + }); } - return players; - } - - @Override - public boolean isPluginEnabled(String pluginName) { - return this.getServer().getPluginManager().isPluginEnabled(pluginName); - } - - @Override - public JavaPlugin plugin() { - return this; - } - - @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; - } - - private boolean isFolia() { - try { - Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); - return true; - } catch (ClassNotFoundException e) { - return false; - } + public void onDisable() { + skinOverlay.onDisable(); } -} \ No newline at end of file +} diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_Legacy.java b/bukkit/src/main/java/com/georgev22/skinoverlay/appliers/LegacySkinApplier.java similarity index 77% rename from bukkit/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_Legacy.java rename to bukkit/src/main/java/com/georgev22/skinoverlay/appliers/LegacySkinApplier.java index 000118b4..bce0e1f2 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_Legacy.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/appliers/LegacySkinApplier.java @@ -1,18 +1,14 @@ -package com.georgev22.skinoverlay.handler.handlers; +package com.georgev22.skinoverlay.appliers; -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,15 @@ 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.getNMSClass; +import static com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils.MinecraftReflection.getOBCClass; +import static com.georgev22.skinoverlay.utilities.Utils.Reflection.*; + +@SuppressWarnings("deprecation") +public class LegacySkinApplier extends SkinApplier { -@ApiStatus.Internal -@ApiStatus.NonExtendable -public class SkinHandler_Legacy extends SkinHandler_Unsupported { private final Class playOutRespawn; private final Class playOutPlayerInfo; private final Class playOutPosition; @@ -39,7 +34,7 @@ public class SkinHandler_Legacy extends SkinHandler_Unsupported { private Enum removePlayerEnum; private Enum addPlayerEnum; - public SkinHandler_Legacy() { + public LegacySkinApplier() { try { packet = getNMSClass("Packet", "net.minecraft.network.protocol.Packet"); playOutHeldItemSlot = getNMSClass("PacketPlayOutHeldItemSlot", "net.minecraft.network.protocol.game.PacketPlayOutHeldItemSlot"); @@ -77,14 +72,40 @@ public SkinHandler_Legacy() { } catch (Exception e) { throw new RuntimeException(e); } - } @Override - public CompletableFuture updateSkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { + public void applySkin(@NotNull SPlayer player) { + this.skinOverlay.getScheduler().createDelayedTask(this.skinOverlay.getPlugin(), () -> { + Player bukkitPlayer = player.getPlayer(); + bukkitPlayer.hidePlayer(bukkitPlayer); + bukkitPlayer.showPlayer(bukkitPlayer); + skinOverlay.getSkinApplier().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 +115,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 +162,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 { @@ -177,7 +198,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 +225,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 +237,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 +251,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 +265,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/command/BukkitCommandIssuer.java b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandIssuer.java new file mode 100644 index 00000000..53b25c20 --- /dev/null +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandIssuer.java @@ -0,0 +1,68 @@ +package com.georgev22.skinoverlay.command; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.utilities.Utils; +import net.kyori.adventure.text.Component; +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 void sendMessage(@NotNull String message) { + this.sender.sendMessage(message); + } + + @Override + public void sendMessage(@NotNull Component component) { + if (!isPlayer()) { + SkinOverlay.getInstance().getAudienceProvider().console().sendMessage(component); + return; + } + SkinOverlay.getInstance().getAudienceProvider().player(this.getUniqueId()).sendMessage(component); + } + + @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..5e5874f9 --- /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 (Exception 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/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..ffda4fb5 --- /dev/null +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitLegacyGameProfileProvider.java @@ -0,0 +1,64 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.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.getOBCClass; +import static com.georgev22.skinoverlay.utilities.Utils.Reflection.fetchMethodAndInvoke; + +public class BukkitLegacyGameProfileProvider extends GameProfileProvider { + + @Override + public GameProfile getInternalGameProfile(@NotNull SPlayer player) { + try { + Class craftPlayerClass = getOBCClass("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((s, property) -> propertyObjectMap.append(s, new SProperty(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((s, property) -> internalGameProfile.getProperties() + .put(s, new Property(s, 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..b861f7f5 --- /dev/null +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitPlayerProvider.java @@ -0,0 +1,85 @@ +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 java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class BukkitPlayerProvider extends PlayerProvider { + + @Override + public SPlayer getSPlayer(Object player) { + if (player instanceof Player bukkitPlayer) { + return new BukkitSPlayer(bukkitPlayer); + } + if (player instanceof OfflinePlayer offlinePlayer) { + if (offlinePlayer.isOnline() && offlinePlayer.getPlayer() != null) { + return new BukkitSPlayer(offlinePlayer.getPlayer()); + } + throw new IllegalArgumentException("Player " + offlinePlayer.getName() + " is offline"); + } + if (player instanceof String name) { + Player onlinePlayer = Bukkit.getPlayer(name); + if (onlinePlayer != null) { + return new BukkitSPlayer(onlinePlayer); + } + throw new IllegalArgumentException("Player " + name + " is null"); + } + if (player instanceof UUID uuid) { + Player onlinePlayer = Bukkit.getPlayer(uuid); + if (onlinePlayer != null) { + return new BukkitSPlayer(onlinePlayer); + } + throw new IllegalArgumentException("Player " + uuid + " is null"); + } + if (player instanceof CommandIssuer commandIssuer) { + if (!commandIssuer.isPlayer()) { + throw new IllegalArgumentException("CommandIssuer is not a player"); + } + return new BukkitSPlayer(commandIssuer.getIssuer()); + } + return null; + } + + @Override + public SPlayer getSPlayer(@NotNull CommandIssuer commandIssuer) { + if (!commandIssuer.isPlayer()) { + throw new IllegalArgumentException("CommandIssuer is not a player"); + } + return new BukkitSPlayer(commandIssuer.getIssuer()); + } + + @Override + public SPlayer getSPlayer(String name) { + Player onlinePlayer = Bukkit.getPlayer(name); + if (onlinePlayer != null) { + return new BukkitSPlayer(onlinePlayer); + } + throw new IllegalArgumentException("Player " + name + " is null"); + } + + @Override + public SPlayer getSPlayer(UUID uuid) { + Player onlinePlayer = Bukkit.getPlayer(uuid); + if (onlinePlayer != null) { + return new BukkitSPlayer(onlinePlayer); + } + throw new IllegalArgumentException("Player " + uuid + " is null"); + } + + @Override + public List getOnlinePlayers() { + return new ArrayList<>( + Bukkit.getOnlinePlayers().stream() + .map(BukkitSPlayer::new) + .toList() + ); + } +} 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..c6f2b257 --- /dev/null +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/providers/PaperGameProfileProvider.java @@ -0,0 +1,42 @@ +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.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((s, property) -> playerProfile.getProperties().add(new ProfileProperty(s, property.value(), property.signature()))); + bukkitPlayer.setPlayerProfile(playerProfile); + } +} 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/BukkitMinecraftUtils.java b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java new file mode 100644 index 00000000..b333f932 --- /dev/null +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java @@ -0,0 +1,904 @@ +package com.georgev22.skinoverlay.utilities; + +import com.georgev22.skinoverlay.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.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")); + } + + public enum MinecraftVersion { + V1_7_R1(new SubVersionRange("1.7", 2, 4)), + V1_7_R2(new SubVersionRange("1.7", 5, 7)), + V1_7_R3(new SubVersionRange("1.7", 8, 9)), + V1_7_R4(new SubVersionRange("1.7", 10)), + V1_8_R1(new SubVersionRange("1.8", 0, 1)), + V1_8_R2(new SubVersionRange("1.8", 3, 3)), + V1_8_R3(new SubVersionRange("1.8", 4, 9)), + V1_9_R1(new SubVersionRange("1.9", 0, 2)), + V1_9_R2(new SubVersionRange("1.9", 4)), + V1_10_R1(new SubVersionRange("1.10", 0, 2)), + V1_11_R1(new SubVersionRange("1.11", 0, 2)), + V1_12_R1(new SubVersionRange("1.12", 0, 2)), + V1_13_R1(new SubVersionRange("1.13", 0, 1)), + V1_13_R2(new SubVersionRange("1.13", 2)), + V1_14_R1(new SubVersionRange("1.14", 0, 4)), + V1_15_R1(new SubVersionRange("1.15", 0, 2)), + V1_16_R1(new SubVersionRange("1.16", 0, 1)), + V1_16_R2(new SubVersionRange("1.16", 2, 3)), + V1_16_R3(new SubVersionRange("1.16", 4, 5)), + V1_17_R1(new SubVersionRange("1.17", 0, 1)), + V1_18_R1(new SubVersionRange("1.18", 0, 1)), + V1_18_R2(new SubVersionRange("1.18", 2)), + V1_19_R1(new SubVersionRange("1.19", 0, 2)), + V1_19_R2(new SubVersionRange("1.19", 3)), + V1_19_R3(new SubVersionRange("1.19", 4)), + V1_20_R1(new SubVersionRange("1.20", 0, 1)), + V1_20_R2(new SubVersionRange("1.20", 2)), + V1_20_R3(new SubVersionRange("1.20", 3, 4)), + V1_20_R4(new SubVersionRange("1.20", 5, 6)), + V1_21_R1(new SubVersionRange("1.21", 0, 1)), + V1_21_R2(new SubVersionRange("1.21", 2, 3)), + V1_21_R3(new SubVersionRange("1.21", 4)), + V1_21_R4(new SubVersionRange("1.21", 5)), + V1_21_R5(new SubVersionRange("1.21", 6, 8)), + V1_21_R6(new SubVersionRange("1.21", 9, 10)), + UNKNOWN(new SubVersionRange("UNKNOWN", 0, 0)), + ; + + private static MinecraftVersion currentVersion; + + private static int versionNumber, releaseNumber; + + static { + try { + String bukkitVersion = Bukkit.getServer().getBukkitVersion(); + String[] versionParts = bukkitVersion.split("-")[0].split("\\."); + if (versionParts.length >= 2) { + int majorVersion = Integer.parseInt(versionParts[0]); + int minorVersion = Integer.parseInt(versionParts[1]); + int patchVersion = versionParts.length >= 3 ? Integer.parseInt(versionParts[2]) : 0; + for (MinecraftVersion version : MinecraftVersion.values()) { + if (version.subVersionRange.version.equals(majorVersion + "." + minorVersion) && + patchVersion >= version.subVersionRange.start && patchVersion <= version.subVersionRange.end) { + currentVersion = version; + versionNumber = Integer.parseInt(currentVersion.name().split("_")[1]); + releaseNumber = Integer.parseInt(currentVersion.name().split("R")[1]); + break; + } + } + } + if (currentVersion == null) { + currentVersion = UNKNOWN; + } + } catch (Exception e) { + currentVersion = UNKNOWN; + } + } + + private final SubVersionRange subVersionRange; + + MinecraftVersion(SubVersionRange subVersionRange) { + this.subVersionRange = subVersionRange; + } + + /** + * Returns the current minecraft server version. + * + * @return the current minecraft server version. + */ + public static MinecraftVersion getCurrentVersion() { + return currentVersion; + } + + /** + * Get the version number of the Minecraft server. + * + * @return The version number of the Minecraft server. + */ + public static int getVersionNumber() { + return versionNumber; + } + + /** + * Get the release number of the Minecraft server. + * + * @return The release number of the Minecraft server. + */ + public static int getReleaseNumber() { + return releaseNumber; + } + + @Contract(pure = true) + public static @NotNull String getCurrentVersionName() { + return currentVersion.name(); + } + + @Contract(pure = true) + public static @NotNull String getCurrentVersionNameVtoLowerCase() { + return currentVersion.name().replace("V", "v"); + } + + /** + * Check if the version is above or equal. + * + * @param minecraftVersion The {@link MinecraftVersion} to be checked. + * @return if the minecraft version is above or equal. + */ + public boolean isAboveOrEqual(@NotNull MinecraftVersion minecraftVersion) { + return this.ordinal() >= minecraftVersion.ordinal(); + } + + /** + * Check if the version is above. + * + * @param minecraftVersion The {@link MinecraftVersion} to be checked. + * @return if the minecraft version is above. + */ + public boolean isAbove(@NotNull MinecraftVersion minecraftVersion) { + return this.ordinal() > minecraftVersion.ordinal(); + } + + /** + * Check if the version is below or equal. + * + * @param minecraftVersion The {@link MinecraftVersion} to be checked. + * @return if the minecraft version is below or equal. + */ + public boolean isBelowOrEqual(@NotNull MinecraftVersion minecraftVersion) { + return this.ordinal() <= minecraftVersion.ordinal(); + } + + /** + * Check if the version is below. + * + * @param minecraftVersion The {@link MinecraftVersion} to be checked. + * @return if the minecraft version is below. + */ + public boolean isBelow(@NotNull MinecraftVersion minecraftVersion) { + return this.ordinal() < minecraftVersion.ordinal(); + } + + /** + * Check if the version is equal. + * + * @param minecraftVersion The {@link MinecraftVersion} to be checked. + * @return if the minecraft version is equal. + */ + public boolean isEqual(@NotNull MinecraftVersion minecraftVersion) { + return this.ordinal() == minecraftVersion.ordinal(); + } + + /** + * Get the sub version range. + * + * @return The sub version range. + **/ + public SubVersionRange getSubVersionRange() { + return subVersionRange; + } + + public static class SubVersionRange { + private final String version; + private final int start; + private final int end; + + SubVersionRange(String version, int patch) { + this(version, patch, patch); + } + + SubVersionRange(String version, int start, int end) { + this.version = version; + this.start = start; + this.end = end; + } + + public String getVersion() { + return version; + } + + public int getStart() { + return start; + } + + public int getEnd() { + return end; + } + } + } + + public static class MinecraftReflection { + + public static final String NET_MINECRAFT_PACKAGE = "net.minecraft"; + public static final String ORG_BUKKIT_CRAFTBUKKIT_PACKAGE = "org.bukkit.craftbukkit"; + public static final String NET_MINECRAFT_SERVER_PACKAGE = NET_MINECRAFT_PACKAGE + ".server"; + + private static volatile Object theUnsafe; + + public static boolean isRepackaged() { + return MinecraftVersion.getCurrentVersion().isAboveOrEqual(MinecraftVersion.V1_17_R1); + } + + @Contract(pure = true) + public static @NotNull String getNMSClassName(String className) { + return NET_MINECRAFT_SERVER_PACKAGE + '.' + MinecraftVersion.getCurrentVersionNameVtoLowerCase() + '.' + className; + } + + public static @NotNull Class getNMSClass(String className) throws ClassNotFoundException { + return Class.forName(getNMSClassName(className)); + } + + public static Optional> getNMSOptionalClass(String className) { + return Utils.Reflection.optionalClass(getNMSClassName(className), Bukkit.class.getClassLoader()); + } + + public static @NotNull String getNMSClassName(String className, String fullClassName) { + return isRepackaged() ? fullClassName : getNMSClassName(className); + } + + public static @NotNull Class getNMSClass(String className, String fullClassName) throws ClassNotFoundException { + return isRepackaged() ? Class.forName(fullClassName) : getNMSClass(className); + } + + public static Optional> getNMSOptionalClass(String className, String fullClassName) { + return isRepackaged() ? Utils.Reflection.optionalClass(fullClassName, Bukkit.class.getClassLoader()) : getNMSOptionalClass(className); + } + + @Contract(pure = true) + public static @NotNull String getOBCClassName(String className) { + return ORG_BUKKIT_CRAFTBUKKIT_PACKAGE + '.' + MinecraftVersion.getCurrentVersionNameVtoLowerCase() + '.' + className; + } + + public static @NotNull Class getOBCClass(String className) throws ClassNotFoundException { + return Class.forName(getOBCClassName(className)); + } + + public static Optional> getOBCOptionalClass(String className) { + return Utils.Reflection.optionalClass(getOBCClassName(className), Bukkit.class.getClassLoader()); + } + } + +} 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/player/PlayerObjectBukkit.java b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/player/PlayerObjectBukkit.java deleted file mode 100644 index 4c638193..00000000 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/player/PlayerObjectBukkit.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.georgev22.skinoverlay.utilities.player; - -import com.georgev22.library.maps.ObjectMap; -import com.georgev22.library.minecraft.BukkitMinecraftUtils; -import com.georgev22.skinoverlay.SkinOverlay; -import net.kyori.adventure.audience.Audience; -import org.bukkit.OfflinePlayer; - -import java.util.List; -import java.util.Objects; -import java.util.UUID; - -public class PlayerObjectBukkit extends PlayerObject { - private final OfflinePlayer offlinePlayer; - - public PlayerObjectBukkit(final OfflinePlayer offlinePlayer) { - this.offlinePlayer = offlinePlayer; - } - - @Override - public OfflinePlayer player() { - return this.offlinePlayer; - } - - @Override - public Audience audience() { - return BukkitMinecraftUtils.MinecraftVersion.getCurrentVersion().isBelow(BukkitMinecraftUtils.MinecraftVersion.V1_16_R1) ? SkinOverlay.getInstance().getSkinOverlay().adventure().player(player().getUniqueId()) : player().getPlayer(); - } - - @Override - public UUID playerUUID() { - return this.offlinePlayer.getUniqueId(); - } - - @Override - public String playerName() { - return this.offlinePlayer.getName(); - } - - @Override - public void sendMessage(String input) { - BukkitMinecraftUtils.msg(Objects.requireNonNull(offlinePlayer.getPlayer()), input); - } - - @Override - public void sendMessage(List input) { - BukkitMinecraftUtils.msg(Objects.requireNonNull(offlinePlayer.getPlayer()), input); - } - - @Override - public void sendMessage(String... input) { - BukkitMinecraftUtils.msg(Objects.requireNonNull(offlinePlayer.getPlayer()), input); - } - - @Override - public void sendMessage(String input, ObjectMap placeholders, boolean ignoreCase) { - BukkitMinecraftUtils.msg(Objects.requireNonNull(offlinePlayer.getPlayer()), input, placeholders, ignoreCase); - } - - @Override - public void sendMessage(List input, ObjectMap placeholders, boolean ignoreCase) { - BukkitMinecraftUtils.msg(Objects.requireNonNull(offlinePlayer.getPlayer()), input, placeholders, ignoreCase); - } - - @Override - public void sendMessage(String[] input, ObjectMap placeholders, boolean ignoreCase) { - BukkitMinecraftUtils.msg(Objects.requireNonNull(offlinePlayer.getPlayer()), input, placeholders, ignoreCase); - } - - @Override - public boolean isOnline() { - return offlinePlayer.isOnline(); - } - - @Override - public boolean permission(String permission) { - return isOnline() && player().getPlayer().hasPermission(permission); - } -} diff --git a/bukkit/versions/mc1_17_R1/build.gradle.kts b/bukkit/versions/mc1_17_R1/build.gradle.kts new file mode 100644 index 00000000..33464ff2 --- /dev/null +++ b/bukkit/versions/mc1_17_R1/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(libs.plugins.paperweight.userdev) +} + +dependencies { + paperweight.paperDevBundle("1.17.1-R0.1-SNAPSHOT") + compileOnly(project(":common")) +} + +tasks.reobfJar { + paperweight { + reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.REOBF_PRODUCTION + } +} + +tasks.assemble { + dependsOn(tasks.reobfJar) +} \ No newline at end of file diff --git a/multiver/mc_1_17_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_17_R1.java b/bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_17_R1.java similarity index 51% rename from multiver/mc_1_17_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_17_R1.java rename to bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_17_R1.java index c8804d0e..4b74ae03 100644 --- a/multiver/mc_1_17_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_17_R1.java +++ b/bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_17_R1.java @@ -1,11 +1,7 @@ -package com.georgev22.skinoverlay.handler.handlers; +package com.georgev22.skinoverlay.appliers; import com.georgev22.skinoverlay.exceptions.SkinException; -import com.georgev22.skinoverlay.handler.SGameProfile; -import com.georgev22.skinoverlay.handler.SkinHandler; -import com.georgev22.skinoverlay.storage.data.Skin; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import com.mojang.authlib.GameProfile; +import com.georgev22.skinoverlay.player.SPlayer; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientboundPlayerInfoPacket; import net.minecraft.network.protocol.game.ClientboundRespawnPacket; @@ -24,27 +20,49 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -import static com.georgev22.skinoverlay.handler.handlers.SkinHandler_Unsupported.wrapper; +public class SkinApplier_1_17_R1 extends SkinApplier { -public final class SkinHandler_1_17_R1 extends SkinHandler { + @Override + public void applySkin(@NotNull SPlayer player) { + this.skinOverlay.getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { + Player bukkitPlayer = player.getPlayer(); + bukkitPlayer.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + bukkitPlayer.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + skinOverlay.getSkinApplier().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(skinOverlay.getPlugin(), () -> { + if (result) + skinOverlay.getPlayerProvider().getOnlinePlayers().stream() + .filter(onlinePlayer -> onlinePlayer != player).forEach(onlinePlayer -> { + Player p = onlinePlayer.getPlayer(); + p.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + p.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + }); + })); + }, 20L); + } @Override - public CompletableFuture updateSkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { + protected @NotNull CompletableFuture sendPackets(@NotNull SPlayer player) { return CompletableFuture.supplyAsync(() -> { try { - Player player = playerObject.player(); - final CraftPlayer craftPlayer = (CraftPlayer) player; + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; final ServerPlayer entityPlayer = craftPlayer.getHandle(); ServerLevel world = entityPlayer.getLevel(); - ServerPlayerGameMode gamemode = entityPlayer.gameMode; + ServerPlayerGameMode gameMode = entityPlayer.gameMode; ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( world.dimensionType(), world.dimension(), BiomeManager.obfuscateSeed(world.getSeed()), - gamemode.getGameModeForPlayer(), - gamemode.getPreviousGameModeForPlayer(), + gameMode.getGameModeForPlayer(), + gameMode.getPreviousGameModeForPlayer(), world.isDebug(), world.isFlat(), true); @@ -56,7 +74,7 @@ public CompletableFuture updateSkin(@NotNull PlayerObject playerObject, entityPlayer.onUpdateAbilities(); - entityPlayer.connection.teleport(player.getLocation()); + entityPlayer.connection.teleport(bukkitPlayer.getLocation()); entityPlayer.resetSentInfo(); @@ -73,50 +91,10 @@ public CompletableFuture updateSkin(@NotNull PlayerObject playerObject, } catch (Exception exception) { throw new SkinException(exception); } - }, runnable -> this.skinOverlay.getMinecraftScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); - } - - @Override - public @NotNull GameProfile getInternalGameProfile(@NotNull PlayerObject playerObject) { - Player player = playerObject.player(); - final CraftPlayer craftPlayer = (CraftPlayer) player; - final ServerPlayer entityPlayer = craftPlayer.getHandle(); - return entityPlayer.getGameProfile(); - } - - @Override - public SGameProfile getGameProfile(@NotNull PlayerObject playerObject) { - if (sGameProfiles.containsKey(playerObject)) { - return sGameProfiles.get(playerObject); - } - return sGameProfiles.append(playerObject, wrapper(this.getInternalGameProfile(playerObject))).get(playerObject); + }, runnable -> this.skinOverlay.getScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); } private void sendPacket(@NotNull ServerPlayer player, Packet packet) { player.connection.send(packet); } - - @Override - public void applySkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - this.skinOverlay.getMinecraftScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { - Player player = playerObject.player(); - player.hidePlayer(skinOverlay.getSkinOverlay().plugin(), player); - player.showPlayer(skinOverlay.getSkinOverlay().plugin(), 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(skinOverlay.getPlugin(), () -> { - if (aBoolean) - skinOverlay.onlinePlayers().stream().filter(playerObjects -> playerObjects != playerObject).forEach(playerObjects -> { - Player p = playerObjects.player(); - p.hidePlayer(skinOverlay.getPlugin(), player); - p.showPlayer(skinOverlay.getPlugin(), player); - }); - })); - }, 20L); - } } - diff --git a/bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_17_R1.java b/bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_17_R1.java new file mode 100644 index 00000000..186130c3 --- /dev/null +++ b/bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_17_R1.java @@ -0,0 +1,44 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.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 net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.v1_17_R1.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class GameProfileProvider_1_17_R1 extends GameProfileProvider { + @Override + public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + return entityPlayer.getGameProfile(); + } + + @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((s, property) -> propertyObjectMap.append(s, new SProperty(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((s, property) -> internalGameProfile.getProperties() + .put(s, new Property(s, property.value(), property.signature()))); + } +} diff --git a/bukkit/versions/mc1_18_R1/build.gradle.kts b/bukkit/versions/mc1_18_R1/build.gradle.kts new file mode 100644 index 00000000..12bb57db --- /dev/null +++ b/bukkit/versions/mc1_18_R1/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(libs.plugins.paperweight.userdev) +} + +dependencies { + paperweight.paperDevBundle("1.18-R0.1-SNAPSHOT") + compileOnly(project(":common")) +} + +tasks.reobfJar { + paperweight { + reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.REOBF_PRODUCTION + } +} + +tasks.assemble { + dependsOn(tasks.reobfJar) +} \ No newline at end of file diff --git a/multiver/mc_1_18_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_18_R1.java b/bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_18_R1.java similarity index 51% rename from multiver/mc_1_18_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_18_R1.java rename to bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_18_R1.java index a6a09749..6c3449c6 100644 --- a/multiver/mc_1_18_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_18_R1.java +++ b/bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_18_R1.java @@ -1,11 +1,7 @@ -package com.georgev22.skinoverlay.handler.handlers; +package com.georgev22.skinoverlay.appliers; import com.georgev22.skinoverlay.exceptions.SkinException; -import com.georgev22.skinoverlay.handler.SGameProfile; -import com.georgev22.skinoverlay.handler.SkinHandler; -import com.georgev22.skinoverlay.storage.data.Skin; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import com.mojang.authlib.GameProfile; +import com.georgev22.skinoverlay.player.SPlayer; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientboundPlayerInfoPacket; import net.minecraft.network.protocol.game.ClientboundRespawnPacket; @@ -24,27 +20,49 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -import static com.georgev22.skinoverlay.handler.handlers.SkinHandler_Unsupported.wrapper; +public class SkinApplier_1_18_R1 extends SkinApplier { -public final class SkinHandler_1_18_R1 extends SkinHandler { + @Override + public void applySkin(@NotNull SPlayer player) { + this.skinOverlay.getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { + Player bukkitPlayer = player.getPlayer(); + bukkitPlayer.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + bukkitPlayer.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + skinOverlay.getSkinApplier().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(skinOverlay.getPlugin(), () -> { + if (result) + skinOverlay.getPlayerProvider().getOnlinePlayers().stream() + .filter(onlinePlayer -> onlinePlayer != player).forEach(onlinePlayer -> { + Player p = onlinePlayer.getPlayer(); + p.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + p.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + }); + })); + }, 20L); + } @Override - public CompletableFuture updateSkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { + protected @NotNull CompletableFuture sendPackets(@NotNull SPlayer player) { return CompletableFuture.supplyAsync(() -> { try { - Player player = playerObject.player(); - final CraftPlayer craftPlayer = (CraftPlayer) player; + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; final ServerPlayer entityPlayer = craftPlayer.getHandle(); ServerLevel world = entityPlayer.getLevel(); - ServerPlayerGameMode gamemode = entityPlayer.gameMode; + ServerPlayerGameMode gameMode = entityPlayer.gameMode; ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( world.dimensionType(), world.dimension(), BiomeManager.obfuscateSeed(world.getSeed()), - gamemode.getGameModeForPlayer(), - gamemode.getPreviousGameModeForPlayer(), + gameMode.getGameModeForPlayer(), + gameMode.getPreviousGameModeForPlayer(), world.isDebug(), world.isFlat(), true); @@ -56,7 +74,7 @@ public CompletableFuture updateSkin(@NotNull PlayerObject playerObject, entityPlayer.onUpdateAbilities(); - entityPlayer.connection.teleport(player.getLocation()); + entityPlayer.connection.teleport(bukkitPlayer.getLocation()); entityPlayer.resetSentInfo(); @@ -73,50 +91,10 @@ public CompletableFuture updateSkin(@NotNull PlayerObject playerObject, } catch (Exception exception) { throw new SkinException(exception); } - }, runnable -> this.skinOverlay.getMinecraftScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); - } - - @Override - public void applySkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - this.skinOverlay.getMinecraftScheduler().getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { - Player player = playerObject.player(); - player.hidePlayer(skinOverlay.getSkinOverlay().plugin(), player); - player.showPlayer(skinOverlay.getSkinOverlay().plugin(), 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(skinOverlay.getPlugin(), () -> { - if (aBoolean) - skinOverlay.onlinePlayers().stream().filter(playerObjects -> playerObjects != playerObject).forEach(playerObjects -> { - Player p = playerObjects.player(); - p.hidePlayer(skinOverlay.getPlugin(), player); - p.showPlayer(skinOverlay.getPlugin(), player); - }); - })); - }, 20L); - } - - @Override - public @NotNull GameProfile getInternalGameProfile(@NotNull PlayerObject playerObject) { - Player player = playerObject.player(); - final CraftPlayer craftPlayer = (CraftPlayer) player; - final ServerPlayer entityPlayer = craftPlayer.getHandle(); - return entityPlayer.getGameProfile(); - } - - @Override - public SGameProfile getGameProfile(@NotNull PlayerObject playerObject) { - if (sGameProfiles.containsKey(playerObject)) { - return sGameProfiles.get(playerObject); - } - return sGameProfiles.append(playerObject, wrapper(this.getInternalGameProfile(playerObject))).get(playerObject); + }, runnable -> this.skinOverlay.getScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); } private void sendPacket(@NotNull ServerPlayer player, Packet packet) { player.connection.send(packet); } } - diff --git a/bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18_R1.java b/bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18_R1.java new file mode 100644 index 00000000..760b4efb --- /dev/null +++ b/bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18_R1.java @@ -0,0 +1,44 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.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 net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.v1_18_R1.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class GameProfileProvider_1_18_R1 extends GameProfileProvider { + @Override + public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + return entityPlayer.getGameProfile(); + } + + @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((s, property) -> propertyObjectMap.append(s, new SProperty(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((s, property) -> internalGameProfile.getProperties() + .put(s, new Property(s, property.value(), property.signature()))); + } +} diff --git a/bukkit/versions/mc1_18_R2/build.gradle.kts b/bukkit/versions/mc1_18_R2/build.gradle.kts new file mode 100644 index 00000000..ef4c730d --- /dev/null +++ b/bukkit/versions/mc1_18_R2/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(libs.plugins.paperweight.userdev) +} + +dependencies { + paperweight.paperDevBundle("1.18.2-R0.1-SNAPSHOT") + compileOnly(project(":common")) +} + +tasks.reobfJar { + paperweight { + reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.REOBF_PRODUCTION + } +} + +tasks.assemble { + dependsOn(tasks.reobfJar) +} \ No newline at end of file diff --git a/bukkit/versions/mc1_18_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_18_R2.java b/bukkit/versions/mc1_18_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_18_R2.java new file mode 100644 index 00000000..4e1c2ae1 --- /dev/null +++ b/bukkit/versions/mc1_18_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_18_R2.java @@ -0,0 +1,103 @@ +package com.georgev22.skinoverlay.appliers; + +import com.georgev22.skinoverlay.exceptions.SkinException; +import com.georgev22.skinoverlay.player.SPlayer; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoPacket; +import net.minecraft.network.protocol.game.ClientboundRespawnPacket; +import net.minecraft.network.protocol.game.ClientboundUpdateMobEffectPacket; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.ServerPlayerGameMode; +import net.minecraft.server.players.PlayerList; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.level.biome.BiomeManager; +import org.bukkit.craftbukkit.v1_18_R2.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; + +public class SkinApplier_1_18_R2 extends SkinApplier { + + @Override + public void applySkin(@NotNull SPlayer player) { + this.skinOverlay.getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { + Player bukitPlayer = player.getPlayer(); + bukitPlayer.hidePlayer(skinOverlay.getPlugin(), bukitPlayer); + bukitPlayer.showPlayer(skinOverlay.getPlugin(), bukitPlayer); + skinOverlay.getSkinApplier().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(skinOverlay.getPlugin(), () -> { + if (result) + skinOverlay.getPlayerProvider().getOnlinePlayers().stream() + .filter(onlinePlayer -> onlinePlayer != player).forEach(onlinePlayer -> { + Player p = onlinePlayer.getPlayer(); + p.hidePlayer(skinOverlay.getPlugin(), bukitPlayer); + p.showPlayer(skinOverlay.getPlugin(), bukitPlayer); + }); + })); + }, 20L); + } + + @Override + protected @NotNull CompletableFuture sendPackets(@NotNull SPlayer player) { + return CompletableFuture.supplyAsync(() -> { + try { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + + ServerLevel world = entityPlayer.getLevel(); + ServerPlayerGameMode gameMode = entityPlayer.gameMode; + + ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( + world.dimensionTypeRegistration(), + world.dimension(), + BiomeManager.obfuscateSeed(world.getSeed()), + gameMode.getGameModeForPlayer(), + gameMode.getPreviousGameModeForPlayer(), + world.isDebug(), + world.isFlat(), + true); + + sendPacket(entityPlayer, new ClientboundPlayerInfoPacket(ClientboundPlayerInfoPacket.Action.REMOVE_PLAYER, + List.of(entityPlayer))); + sendPacket(entityPlayer, new ClientboundPlayerInfoPacket(ClientboundPlayerInfoPacket.Action.ADD_PLAYER, + List.of(entityPlayer))); + + sendPacket(entityPlayer, respawn); + + entityPlayer.onUpdateAbilities(); + + entityPlayer.connection.teleport(bukkitPlayer.getLocation()); + + entityPlayer.resetSentInfo(); + + PlayerList playerList = entityPlayer.server.getPlayerList(); + playerList.sendPlayerPermissionLevel(entityPlayer); + playerList.sendLevelInfo(entityPlayer, world); + playerList.sendAllPlayerInfo(entityPlayer); + + for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { + ClientboundUpdateMobEffectPacket effect = new ClientboundUpdateMobEffectPacket(entityPlayer.getId(), mobEffect); + sendPacket(entityPlayer, effect); + } + return true; + } catch (Exception exception) { + throw new SkinException(exception); + } + }, runnable -> this.skinOverlay.getScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); + } + + + private void sendPacket(@NotNull ServerPlayer player, Packet packet) { + player.connection.send(packet); + } +} diff --git a/bukkit/versions/mc1_18_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18_R2.java b/bukkit/versions/mc1_18_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18_R2.java new file mode 100644 index 00000000..45c9f2ab --- /dev/null +++ b/bukkit/versions/mc1_18_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18_R2.java @@ -0,0 +1,44 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.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 net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.v1_18_R2.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class GameProfileProvider_1_18_R2 extends GameProfileProvider { + @Override + public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + return entityPlayer.getGameProfile(); + } + + @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((s, property) -> propertyObjectMap.append(s, new SProperty(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((s, property) -> internalGameProfile.getProperties() + .put(s, new Property(s, property.value(), property.signature()))); + } +} diff --git a/bukkit/versions/mc1_19_R1/build.gradle.kts b/bukkit/versions/mc1_19_R1/build.gradle.kts new file mode 100644 index 00000000..8ef35f8a --- /dev/null +++ b/bukkit/versions/mc1_19_R1/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(libs.plugins.paperweight.userdev) +} + +dependencies { + paperweight.paperDevBundle("1.19-R0.1-SNAPSHOT") + compileOnly(project(":common")) +} + +tasks.reobfJar { + paperweight { + reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.REOBF_PRODUCTION + } +} + +tasks.assemble { + dependsOn(tasks.reobfJar) +} \ No newline at end of file diff --git a/bukkit/versions/mc1_19_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_19_R1.java b/bukkit/versions/mc1_19_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_19_R1.java new file mode 100644 index 00000000..bc93ad11 --- /dev/null +++ b/bukkit/versions/mc1_19_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_19_R1.java @@ -0,0 +1,104 @@ +package com.georgev22.skinoverlay.appliers; + +import com.georgev22.skinoverlay.exceptions.SkinException; +import com.georgev22.skinoverlay.player.SPlayer; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoPacket; +import net.minecraft.network.protocol.game.ClientboundRespawnPacket; +import net.minecraft.network.protocol.game.ClientboundUpdateMobEffectPacket; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.ServerPlayerGameMode; +import net.minecraft.server.players.PlayerList; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.level.biome.BiomeManager; +import org.bukkit.craftbukkit.v1_19_R1.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; + +public class SkinApplier_1_19_R1 extends SkinApplier { + + @Override + public void applySkin(@NotNull SPlayer player) { + this.skinOverlay.getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { + Player bukkitPlayer = player.getPlayer(); + bukkitPlayer.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + bukkitPlayer.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + skinOverlay.getSkinApplier().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(skinOverlay.getPlugin(), () -> { + if (result) + skinOverlay.getPlayerProvider().getOnlinePlayers().stream() + .filter(onlinePlayer -> onlinePlayer != player).forEach(onlinePlayer -> { + Player p = onlinePlayer.getPlayer(); + p.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + p.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + }); + })); + }, 20L); + } + + @Override + protected @NotNull CompletableFuture sendPackets(@NotNull SPlayer player) { + return CompletableFuture.supplyAsync(() -> { + try { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + + ServerLevel world = entityPlayer.getLevel(); + ServerPlayerGameMode gameMode = entityPlayer.gameMode; + + ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( + world.dimensionTypeId(), + world.dimension(), + BiomeManager.obfuscateSeed(world.getSeed()), + gameMode.getGameModeForPlayer(), + gameMode.getPreviousGameModeForPlayer(), + world.isDebug(), + world.isFlat(), + true, + entityPlayer.getLastDeathLocation() + ); + + sendPacket(entityPlayer, new ClientboundPlayerInfoPacket(ClientboundPlayerInfoPacket.Action.REMOVE_PLAYER, + List.of(entityPlayer))); + sendPacket(entityPlayer, new ClientboundPlayerInfoPacket(ClientboundPlayerInfoPacket.Action.ADD_PLAYER, + List.of(entityPlayer))); + + sendPacket(entityPlayer, respawn); + + entityPlayer.onUpdateAbilities(); + + entityPlayer.connection.teleport(bukkitPlayer.getLocation()); + + entityPlayer.resetSentInfo(); + + PlayerList playerList = entityPlayer.server.getPlayerList(); + playerList.sendPlayerPermissionLevel(entityPlayer); + playerList.sendLevelInfo(entityPlayer, world); + playerList.sendAllPlayerInfo(entityPlayer); + + for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { + ClientboundUpdateMobEffectPacket effect = new ClientboundUpdateMobEffectPacket(entityPlayer.getId(), mobEffect); + sendPacket(entityPlayer, effect); + } + return true; + } catch (Exception exception) { + throw new SkinException(exception); + } + }, runnable -> this.skinOverlay.getScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); + } + + private void sendPacket(@NotNull ServerPlayer player, Packet packet) { + player.connection.send(packet); + } +} diff --git a/bukkit/versions/mc1_19_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_R1.java b/bukkit/versions/mc1_19_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_R1.java new file mode 100644 index 00000000..1634c68c --- /dev/null +++ b/bukkit/versions/mc1_19_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_R1.java @@ -0,0 +1,44 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.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 net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.v1_19_R1.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class GameProfileProvider_1_19_R1 extends GameProfileProvider { + @Override + public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + return entityPlayer.getGameProfile(); + } + + @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((s, property) -> propertyObjectMap.append(s, new SProperty(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((s, property) -> internalGameProfile.getProperties() + .put(s, new Property(s, property.value(), property.signature()))); + } +} diff --git a/bukkit/versions/mc1_19_R2/build.gradle.kts b/bukkit/versions/mc1_19_R2/build.gradle.kts new file mode 100644 index 00000000..ef89f1e5 --- /dev/null +++ b/bukkit/versions/mc1_19_R2/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(libs.plugins.paperweight.userdev) +} + +dependencies { + paperweight.paperDevBundle("1.19.3-R0.1-SNAPSHOT") + compileOnly(project(":common")) +} + +tasks.reobfJar { + paperweight { + reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.REOBF_PRODUCTION + } +} + +tasks.assemble { + dependsOn(tasks.reobfJar) +} \ No newline at end of file diff --git a/bukkit/versions/mc1_19_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_19_R2.java b/bukkit/versions/mc1_19_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_19_R2.java new file mode 100644 index 00000000..e45071fe --- /dev/null +++ b/bukkit/versions/mc1_19_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_19_R2.java @@ -0,0 +1,103 @@ +package com.georgev22.skinoverlay.appliers; + +import com.georgev22.skinoverlay.exceptions.SkinException; +import com.georgev22.skinoverlay.player.SPlayer; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import net.minecraft.network.protocol.game.ClientboundRespawnPacket; +import net.minecraft.network.protocol.game.ClientboundUpdateMobEffectPacket; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.ServerPlayerGameMode; +import net.minecraft.server.players.PlayerList; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.level.biome.BiomeManager; +import org.bukkit.craftbukkit.v1_19_R2.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; + +public class SkinApplier_1_19_R2 extends SkinApplier { + + @Override + public void applySkin(@NotNull SPlayer player) { + this.skinOverlay.getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { + Player bukkitPlayer = player.getPlayer(); + bukkitPlayer.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + bukkitPlayer.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + skinOverlay.getSkinApplier().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(skinOverlay.getPlugin(), () -> { + if (result) + skinOverlay.getPlayerProvider().getOnlinePlayers().stream() + .filter(onlinePlayer -> onlinePlayer != player).forEach(onlinePlayer -> { + Player p = onlinePlayer.getPlayer(); + p.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + p.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + }); + })); + }, 20L); + } + + @Override + protected @NotNull CompletableFuture sendPackets(@NotNull SPlayer player) { + return CompletableFuture.supplyAsync(() -> { + try { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + + ServerLevel world = entityPlayer.getLevel(); + ServerPlayerGameMode gameMode = entityPlayer.gameMode; + + ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( + world.dimensionTypeId(), + world.dimension(), + BiomeManager.obfuscateSeed(world.getSeed()), + gameMode.getGameModeForPlayer(), + gameMode.getPreviousGameModeForPlayer(), + world.isDebug(), + world.isFlat(), + ClientboundRespawnPacket.KEEP_ALL_DATA, + entityPlayer.getLastDeathLocation() + ); + + sendPacket(entityPlayer, new ClientboundPlayerInfoRemovePacket(List.of(player.getUniqueId()))); + sendPacket(entityPlayer, ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityPlayer))); + + sendPacket(entityPlayer, respawn); + + entityPlayer.onUpdateAbilities(); + + entityPlayer.connection.teleport(bukkitPlayer.getLocation()); + + entityPlayer.resetSentInfo(); + + PlayerList playerList = entityPlayer.server.getPlayerList(); + playerList.sendPlayerPermissionLevel(entityPlayer); + playerList.sendLevelInfo(entityPlayer, world); + playerList.sendAllPlayerInfo(entityPlayer); + + for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { + ClientboundUpdateMobEffectPacket effect = new ClientboundUpdateMobEffectPacket(entityPlayer.getId(), mobEffect); + sendPacket(entityPlayer, effect); + } + return true; + } catch (Exception exception) { + throw new SkinException(exception); + } + }, runnable -> this.skinOverlay.getScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); + } + + private void sendPacket(@NotNull ServerPlayer player, Packet packet) { + player.connection.send(packet); + } +} diff --git a/bukkit/versions/mc1_19_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_R2.java b/bukkit/versions/mc1_19_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_R2.java new file mode 100644 index 00000000..f159f9ca --- /dev/null +++ b/bukkit/versions/mc1_19_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_R2.java @@ -0,0 +1,44 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.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 net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.v1_19_R2.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class GameProfileProvider_1_19_R2 extends GameProfileProvider { + @Override + public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + return entityPlayer.getGameProfile(); + } + + @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((s, property) -> propertyObjectMap.append(s, new SProperty(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((s, property) -> internalGameProfile.getProperties() + .put(s, new Property(s, property.value(), property.signature()))); + } +} diff --git a/bukkit/versions/mc1_19_R3/build.gradle.kts b/bukkit/versions/mc1_19_R3/build.gradle.kts new file mode 100644 index 00000000..97e4fc9b --- /dev/null +++ b/bukkit/versions/mc1_19_R3/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(libs.plugins.paperweight.userdev) +} + +dependencies { + paperweight.paperDevBundle("1.19.4-R0.1-SNAPSHOT") + compileOnly(project(":common")) +} + +tasks.reobfJar { + paperweight { + reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.REOBF_PRODUCTION + } +} + +tasks.assemble { + dependsOn(tasks.reobfJar) +} \ No newline at end of file diff --git a/bukkit/versions/mc1_19_R3/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_19_R3.java b/bukkit/versions/mc1_19_R3/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_19_R3.java new file mode 100644 index 00000000..b8aedc08 --- /dev/null +++ b/bukkit/versions/mc1_19_R3/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_19_R3.java @@ -0,0 +1,103 @@ +package com.georgev22.skinoverlay.appliers; + +import com.georgev22.skinoverlay.exceptions.SkinException; +import com.georgev22.skinoverlay.player.SPlayer; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import net.minecraft.network.protocol.game.ClientboundRespawnPacket; +import net.minecraft.network.protocol.game.ClientboundUpdateMobEffectPacket; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.ServerPlayerGameMode; +import net.minecraft.server.players.PlayerList; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.level.biome.BiomeManager; +import org.bukkit.craftbukkit.v1_19_R3.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; + +public class SkinApplier_1_19_R3 extends SkinApplier { + + @Override + public void applySkin(@NotNull SPlayer player) { + this.skinOverlay.getScheduler().getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { + Player bukkitPlayer = player.getPlayer(); + bukkitPlayer.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + bukkitPlayer.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + skinOverlay.getSkinApplier().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(skinOverlay.getPlugin(), () -> { + if (result) + skinOverlay.getPlayerProvider().getOnlinePlayers().stream() + .filter(onlinePlayer -> onlinePlayer != player).forEach(onlinePlayer -> { + Player p = onlinePlayer.getPlayer(); + p.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + p.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + }); + })); + }, 20L); + } + + @Override + protected @NotNull CompletableFuture sendPackets(@NotNull SPlayer player) { + return CompletableFuture.supplyAsync(() -> { + try { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + + ServerLevel world = entityPlayer.getLevel(); + ServerPlayerGameMode gameMode = entityPlayer.gameMode; + + ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( + world.dimensionTypeId(), + world.dimension(), + BiomeManager.obfuscateSeed(world.getSeed()), + gameMode.getGameModeForPlayer(), + gameMode.getPreviousGameModeForPlayer(), + world.isDebug(), + world.isFlat(), + ClientboundRespawnPacket.KEEP_ALL_DATA, + entityPlayer.getLastDeathLocation() + ); + + sendPacket(entityPlayer, new ClientboundPlayerInfoRemovePacket(List.of(bukkitPlayer.getUniqueId()))); + sendPacket(entityPlayer, ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityPlayer))); + + sendPacket(entityPlayer, respawn); + + entityPlayer.onUpdateAbilities(); + + entityPlayer.connection.teleport(bukkitPlayer.getLocation()); + + entityPlayer.resetSentInfo(); + + PlayerList playerList = entityPlayer.server.getPlayerList(); + playerList.sendPlayerPermissionLevel(entityPlayer); + playerList.sendLevelInfo(entityPlayer, world); + playerList.sendAllPlayerInfo(entityPlayer); + + for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { + ClientboundUpdateMobEffectPacket effect = new ClientboundUpdateMobEffectPacket(entityPlayer.getId(), mobEffect); + sendPacket(entityPlayer, effect); + } + return true; + } catch (Exception exception) { + throw new SkinException(exception); + } + }, runnable -> this.skinOverlay.getScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); + } + + private void sendPacket(@NotNull ServerPlayer player, Packet packet) { + player.connection.send(packet); + } +} diff --git a/bukkit/versions/mc1_19_R3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_R3.java b/bukkit/versions/mc1_19_R3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_R3.java new file mode 100644 index 00000000..8c1622b5 --- /dev/null +++ b/bukkit/versions/mc1_19_R3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_R3.java @@ -0,0 +1,44 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.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 net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.v1_19_R3.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class GameProfileProvider_1_19_R3 extends GameProfileProvider { + @Override + public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + return entityPlayer.getGameProfile(); + } + + @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((s, property) -> propertyObjectMap.append(s, new SProperty(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((s, property) -> internalGameProfile.getProperties() + .put(s, new Property(s, property.value(), property.signature()))); + } +} diff --git a/bukkit/versions/mc1_20_R1/build.gradle.kts b/bukkit/versions/mc1_20_R1/build.gradle.kts new file mode 100644 index 00000000..2808d35b --- /dev/null +++ b/bukkit/versions/mc1_20_R1/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(libs.plugins.paperweight.userdev) +} + +dependencies { + paperweight.paperDevBundle("1.20.1-R0.1-SNAPSHOT") + compileOnly(project(":common")) +} + +tasks.reobfJar { + paperweight { + reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.REOBF_PRODUCTION + } +} + +tasks.assemble { + dependsOn(tasks.reobfJar) +} \ No newline at end of file diff --git a/multiver/mc_1_20_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_20_R1.java b/bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R1.java similarity index 52% rename from multiver/mc_1_20_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_20_R1.java rename to bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R1.java index 1675f728..c6b8659a 100644 --- a/multiver/mc_1_20_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_20_R1.java +++ b/bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R1.java @@ -1,11 +1,7 @@ -package com.georgev22.skinoverlay.handler.handlers; +package com.georgev22.skinoverlay.appliers; import com.georgev22.skinoverlay.exceptions.SkinException; -import com.georgev22.skinoverlay.handler.SGameProfile; -import com.georgev22.skinoverlay.handler.SkinHandler; -import com.georgev22.skinoverlay.storage.data.Skin; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import com.mojang.authlib.GameProfile; +import com.georgev22.skinoverlay.player.SPlayer; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; @@ -25,27 +21,49 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -import static com.georgev22.skinoverlay.handler.handlers.SkinHandler_Unsupported.wrapper; +public class SkinApplier_1_20_R1 extends SkinApplier { -public final class SkinHandler_1_20_R1 extends SkinHandler { + @Override + public void applySkin(@NotNull SPlayer player) { + this.skinOverlay.getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { + Player bukkitPlayer = player.getPlayer(); + bukkitPlayer.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + bukkitPlayer.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + skinOverlay.getSkinApplier().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(skinOverlay.getPlugin(), () -> { + if (result) + skinOverlay.getPlayerProvider().getOnlinePlayers().stream() + .filter(onlinePlayer -> onlinePlayer != player).forEach(onlinePlayer -> { + Player p = onlinePlayer.getPlayer(); + p.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + p.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + }); + })); + }, 20L); + } @Override - public CompletableFuture updateSkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { + protected @NotNull CompletableFuture sendPackets(@NotNull SPlayer player) { return CompletableFuture.supplyAsync(() -> { try { - Player player = playerObject.player(); - final CraftPlayer craftPlayer = (CraftPlayer) player; + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; final ServerPlayer entityPlayer = craftPlayer.getHandle(); ServerLevel world = entityPlayer.serverLevel(); - ServerPlayerGameMode gamemode = entityPlayer.gameMode; + ServerPlayerGameMode gameMode = entityPlayer.gameMode; ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( world.dimensionTypeId(), world.dimension(), BiomeManager.obfuscateSeed(world.getSeed()), - gamemode.getGameModeForPlayer(), - gamemode.getPreviousGameModeForPlayer(), + gameMode.getGameModeForPlayer(), + gameMode.getPreviousGameModeForPlayer(), world.isDebug(), world.isFlat(), ClientboundRespawnPacket.KEEP_ALL_DATA, @@ -53,14 +71,14 @@ public CompletableFuture updateSkin(@NotNull PlayerObject playerObject, entityPlayer.getPortalCooldown() ); - sendPacket(entityPlayer, new ClientboundPlayerInfoRemovePacket(List.of(player.getUniqueId()))); + sendPacket(entityPlayer, new ClientboundPlayerInfoRemovePacket(List.of(bukkitPlayer.getUniqueId()))); sendPacket(entityPlayer, ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityPlayer))); sendPacket(entityPlayer, respawn); entityPlayer.onUpdateAbilities(); - entityPlayer.connection.teleport(player.getLocation()); + entityPlayer.connection.teleport(bukkitPlayer.getLocation()); entityPlayer.resetSentInfo(); @@ -77,50 +95,10 @@ public CompletableFuture updateSkin(@NotNull PlayerObject playerObject, } catch (Exception exception) { throw new SkinException(exception); } - }, runnable -> this.skinOverlay.getMinecraftScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); - } - - @Override - public void applySkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - this.skinOverlay.getMinecraftScheduler().getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { - Player player = playerObject.player(); - player.hidePlayer(skinOverlay.getSkinOverlay().plugin(), player); - player.showPlayer(skinOverlay.getSkinOverlay().plugin(), 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(skinOverlay.getPlugin(), () -> { - if (aBoolean) - skinOverlay.onlinePlayers().stream().filter(playerObjects -> playerObjects != playerObject).forEach(playerObjects -> { - Player p = playerObjects.player(); - p.hidePlayer(skinOverlay.getPlugin(), player); - p.showPlayer(skinOverlay.getPlugin(), player); - }); - })); - }, 20L); - } - - @Override - public GameProfile getInternalGameProfile(@NotNull PlayerObject playerObject) { - Player player = playerObject.player(); - final CraftPlayer craftPlayer = (CraftPlayer) player; - final ServerPlayer entityPlayer = craftPlayer.getHandle(); - return entityPlayer.getGameProfile(); - } - - @Override - public SGameProfile getGameProfile(@NotNull PlayerObject playerObject) { - if (sGameProfiles.containsKey(playerObject)) { - return sGameProfiles.get(playerObject); - } - return sGameProfiles.append(playerObject, wrapper(this.getInternalGameProfile(playerObject))).get(playerObject); + }, runnable -> this.skinOverlay.getScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); } private void sendPacket(@NotNull ServerPlayer player, Packet packet) { player.connection.send(packet); } } - diff --git a/bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R1.java b/bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R1.java new file mode 100644 index 00000000..23ac11f6 --- /dev/null +++ b/bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R1.java @@ -0,0 +1,44 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.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 net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.v1_20_R1.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class GameProfileProvider_1_20_R1 extends GameProfileProvider { + @Override + public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + return entityPlayer.getGameProfile(); + } + + @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((s, property) -> propertyObjectMap.append(s, new SProperty(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((s, property) -> internalGameProfile.getProperties() + .put(s, new Property(s, property.value(), property.signature()))); + } +} diff --git a/bukkit/versions/mc1_20_R2/build.gradle.kts b/bukkit/versions/mc1_20_R2/build.gradle.kts new file mode 100644 index 00000000..4278fd54 --- /dev/null +++ b/bukkit/versions/mc1_20_R2/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(libs.plugins.paperweight.userdev) +} + +dependencies { + paperweight.paperDevBundle("1.20.2-R0.1-SNAPSHOT") + compileOnly(project(":common")) +} + +tasks.reobfJar { + paperweight { + reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.REOBF_PRODUCTION + } +} + +tasks.assemble { + dependsOn(tasks.reobfJar) +} \ No newline at end of file diff --git a/bukkit/versions/mc1_20_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R2.java b/bukkit/versions/mc1_20_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R2.java new file mode 100644 index 00000000..047252aa --- /dev/null +++ b/bukkit/versions/mc1_20_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R2.java @@ -0,0 +1,90 @@ +package com.georgev22.skinoverlay.appliers; + +import com.georgev22.skinoverlay.exceptions.SkinException; +import com.georgev22.skinoverlay.player.SPlayer; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.*; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import net.minecraft.world.effect.MobEffectInstance; +import org.bukkit.craftbukkit.v1_20_R2.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; + +public class SkinApplier_1_20_R2 extends SkinApplier { + + @Override + public void applySkin(@NotNull SPlayer player) { + this.skinOverlay.getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { + Player bukkitPlayer = player.getPlayer(); + bukkitPlayer.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + bukkitPlayer.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + skinOverlay.getSkinApplier().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(skinOverlay.getPlugin(), () -> { + if (result) + skinOverlay.getPlayerProvider().getOnlinePlayers().stream() + .filter(onlinePlayer -> onlinePlayer != player).forEach(onlinePlayer -> { + Player p = onlinePlayer.getPlayer(); + p.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + p.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + }); + })); + }, 20L); + } + + @Override + protected @NotNull CompletableFuture sendPackets(@NotNull SPlayer player) { + return CompletableFuture.supplyAsync(() -> { + try { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + ServerLevel world = entityPlayer.serverLevel(); + + CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(world); + ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( + spawnInfo, + ClientboundRespawnPacket.KEEP_ALL_DATA + ); + + sendPacket(entityPlayer, new ClientboundPlayerInfoRemovePacket(List.of(bukkitPlayer.getUniqueId()))); + sendPacket(entityPlayer, ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityPlayer))); + + sendPacket(entityPlayer, respawn); + + entityPlayer.onUpdateAbilities(); + + entityPlayer.connection.teleport(bukkitPlayer.getLocation()); + + entityPlayer.resetSentInfo(); + + PlayerList playerList = entityPlayer.server.getPlayerList(); + playerList.sendPlayerPermissionLevel(entityPlayer); + playerList.sendLevelInfo(entityPlayer, world); + playerList.sendAllPlayerInfo(entityPlayer); + + for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { + ClientboundUpdateMobEffectPacket effect = new ClientboundUpdateMobEffectPacket(entityPlayer.getId(), mobEffect); + sendPacket(entityPlayer, effect); + } + return true; + } catch (Exception exception) { + throw new SkinException(exception); + } + }, runnable -> this.skinOverlay.getScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); + } + + private void sendPacket(@NotNull ServerPlayer player, Packet packet) { + player.connection.send(packet); + } +} diff --git a/bukkit/versions/mc1_20_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R2.java b/bukkit/versions/mc1_20_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R2.java new file mode 100644 index 00000000..3391d554 --- /dev/null +++ b/bukkit/versions/mc1_20_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R2.java @@ -0,0 +1,44 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.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 net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.v1_20_R2.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class GameProfileProvider_1_20_R2 extends GameProfileProvider { + @Override + public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + return entityPlayer.getGameProfile(); + } + + @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((s, property) -> propertyObjectMap.append(s, new SProperty(property.value(), property.signature()))); + 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((s, property) -> internalGameProfile.getProperties() + .put(s, new Property(s, property.value(), property.signature()))); + } +} diff --git a/bukkit/versions/mc1_20_R3/build.gradle.kts b/bukkit/versions/mc1_20_R3/build.gradle.kts new file mode 100644 index 00000000..79817993 --- /dev/null +++ b/bukkit/versions/mc1_20_R3/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(libs.plugins.paperweight.userdev) +} + +dependencies { + paperweight.paperDevBundle("1.20.4-R0.1-SNAPSHOT") + compileOnly(project(":common")) +} + +tasks.reobfJar { + paperweight { + reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.REOBF_PRODUCTION + } +} + +tasks.assemble { + dependsOn(tasks.reobfJar) +} \ No newline at end of file diff --git a/bukkit/versions/mc1_20_R3/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R3.java b/bukkit/versions/mc1_20_R3/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R3.java new file mode 100644 index 00000000..d164a225 --- /dev/null +++ b/bukkit/versions/mc1_20_R3/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R3.java @@ -0,0 +1,91 @@ +package com.georgev22.skinoverlay.appliers; + +import com.georgev22.skinoverlay.exceptions.SkinException; +import com.georgev22.skinoverlay.player.SPlayer; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.*; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import net.minecraft.world.effect.MobEffectInstance; +import org.bukkit.craftbukkit.v1_20_R3.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; + +public class SkinApplier_1_20_R3 extends SkinApplier { + + @Override + public void applySkin(@NotNull SPlayer player) { + this.skinOverlay.getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { + Player bukkitPlayer = player.getPlayer(); + bukkitPlayer.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + bukkitPlayer.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + skinOverlay.getSkinApplier().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(skinOverlay.getPlugin(), () -> { + if (result) + skinOverlay.getPlayerProvider().getOnlinePlayers().stream() + .filter(onlinePlayer -> onlinePlayer != player).forEach(onlinePlayer -> { + Player p = onlinePlayer.getPlayer(); + p.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + p.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + }); + })); + }, 20L); + } + + @Override + public @NotNull CompletableFuture sendPackets(@NotNull SPlayer player) { + return CompletableFuture.supplyAsync(() -> { + try { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + + ServerLevel world = entityPlayer.serverLevel(); + + CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(world); + ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( + spawnInfo, + ClientboundRespawnPacket.KEEP_ALL_DATA + ); + + sendPacket(entityPlayer, new ClientboundPlayerInfoRemovePacket(List.of(bukkitPlayer.getUniqueId()))); + sendPacket(entityPlayer, ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityPlayer))); + + sendPacket(entityPlayer, respawn); + + entityPlayer.onUpdateAbilities(); + + entityPlayer.connection.teleport(bukkitPlayer.getLocation()); + + entityPlayer.resetSentInfo(); + + PlayerList playerList = entityPlayer.server.getPlayerList(); + playerList.sendPlayerPermissionLevel(entityPlayer); + playerList.sendLevelInfo(entityPlayer, world); + playerList.sendAllPlayerInfo(entityPlayer); + + for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { + ClientboundUpdateMobEffectPacket effect = new ClientboundUpdateMobEffectPacket(entityPlayer.getId(), mobEffect); + sendPacket(entityPlayer, effect); + } + return true; + } catch (Exception exception) { + throw new SkinException(exception); + } + }, runnable -> this.skinOverlay.getScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); + } + + private void sendPacket(@NotNull ServerPlayer player, Packet packet) { + player.connection.send(packet); + } +} diff --git a/bukkit/versions/mc1_20_R3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R3.java b/bukkit/versions/mc1_20_R3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R3.java new file mode 100644 index 00000000..54482c5d --- /dev/null +++ b/bukkit/versions/mc1_20_R3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R3.java @@ -0,0 +1,44 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.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 net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.v1_20_R3.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class GameProfileProvider_1_20_R3 extends GameProfileProvider { + @Override + public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + return entityPlayer.getGameProfile(); + } + + @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((s, property) -> propertyObjectMap.append(s, new SProperty(property.value(), property.signature()))); + 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((s, property) -> internalGameProfile.getProperties() + .put(s, new Property(s, property.value(), property.signature()))); + } +} diff --git a/bukkit/versions/mc1_20_R4/build.gradle.kts b/bukkit/versions/mc1_20_R4/build.gradle.kts new file mode 100644 index 00000000..a709ae8c --- /dev/null +++ b/bukkit/versions/mc1_20_R4/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + alias(libs.plugins.paperweight.userdev) +} + +dependencies { + paperweight.paperDevBundle("1.20.6-R0.1-SNAPSHOT") + compileOnly(project(":common")) + compileOnly(libs.auth.lib.modern) +} + +tasks.reobfJar { + paperweight { + reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.REOBF_PRODUCTION + } +} + +tasks.assemble { + dependsOn(tasks.reobfJar) +} \ No newline at end of file diff --git a/bukkit/versions/mc1_20_R4/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R4.java b/bukkit/versions/mc1_20_R4/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R4.java new file mode 100644 index 00000000..abe1583f --- /dev/null +++ b/bukkit/versions/mc1_20_R4/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R4.java @@ -0,0 +1,91 @@ +package com.georgev22.skinoverlay.appliers; + +import com.georgev22.skinoverlay.exceptions.SkinException; +import com.georgev22.skinoverlay.player.SPlayer; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.*; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import net.minecraft.world.effect.MobEffectInstance; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; + +public class SkinApplier_1_20_R4 extends SkinApplier { + + @Override + public void applySkin(@NotNull SPlayer player) { + this.skinOverlay.getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { + Player bukkitPlayer = player.getPlayer(); + bukkitPlayer.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + bukkitPlayer.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + skinOverlay.getSkinApplier().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(skinOverlay.getPlugin(), () -> { + if (result) + skinOverlay.getPlayerProvider().getOnlinePlayers().stream() + .filter(onlinePlayer -> onlinePlayer != player).forEach(onlinePlayer -> { + Player p = onlinePlayer.getPlayer(); + p.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + p.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + }); + })); + }, 20L); + } + + @Override + protected @NotNull CompletableFuture sendPackets(@NotNull SPlayer player) { + return CompletableFuture.supplyAsync(() -> { + try { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + + ServerLevel world = entityPlayer.serverLevel(); + + CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(world); + ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( + spawnInfo, + ClientboundRespawnPacket.KEEP_ALL_DATA + ); + + sendPacket(entityPlayer, new ClientboundPlayerInfoRemovePacket(List.of(bukkitPlayer.getUniqueId()))); + sendPacket(entityPlayer, ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityPlayer))); + + sendPacket(entityPlayer, respawn); + + entityPlayer.onUpdateAbilities(); + + entityPlayer.connection.teleport(bukkitPlayer.getLocation()); + + entityPlayer.resetSentInfo(); + + PlayerList playerList = entityPlayer.server.getPlayerList(); + playerList.sendPlayerPermissionLevel(entityPlayer); + playerList.sendLevelInfo(entityPlayer, world); + playerList.sendAllPlayerInfo(entityPlayer); + + for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { + ClientboundUpdateMobEffectPacket effect = new ClientboundUpdateMobEffectPacket(entityPlayer.getId(), mobEffect, false); + sendPacket(entityPlayer, effect); + } + return true; + } catch (Exception exception) { + throw new SkinException(exception); + } + }, runnable -> this.skinOverlay.getScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); + } + + private void sendPacket(@NotNull ServerPlayer player, Packet packet) { + player.connection.send(packet); + } +} diff --git a/bukkit/versions/mc1_20_R4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R4.java b/bukkit/versions/mc1_20_R4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R4.java new file mode 100644 index 00000000..9d623e58 --- /dev/null +++ b/bukkit/versions/mc1_20_R4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R4.java @@ -0,0 +1,44 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.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 net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class GameProfileProvider_1_20_R4 extends GameProfileProvider { + @Override + public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + return entityPlayer.getGameProfile(); + } + + @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((s, property) -> propertyObjectMap.append(s, new SProperty(property.value(), property.signature()))); + 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((s, property) -> internalGameProfile.getProperties() + .put(s, new Property(s, property.value(), property.signature()))); + } +} diff --git a/bukkit/versions/mc1_21_R1/build.gradle.kts b/bukkit/versions/mc1_21_R1/build.gradle.kts new file mode 100644 index 00000000..eb72e006 --- /dev/null +++ b/bukkit/versions/mc1_21_R1/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + alias(libs.plugins.paperweight.userdev) +} + +dependencies { + paperweight.paperDevBundle("1.21-R0.1-SNAPSHOT") + compileOnly(project(":common")) + compileOnly(libs.auth.lib.modern) +} + +tasks.reobfJar { + paperweight { + reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.REOBF_PRODUCTION + } +} + +tasks.assemble { + dependsOn(tasks.reobfJar) +} \ No newline at end of file diff --git a/bukkit/versions/mc1_21_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R1.java b/bukkit/versions/mc1_21_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R1.java new file mode 100644 index 00000000..f908708a --- /dev/null +++ b/bukkit/versions/mc1_21_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R1.java @@ -0,0 +1,90 @@ +package com.georgev22.skinoverlay.appliers; + +import com.georgev22.skinoverlay.exceptions.SkinException; +import com.georgev22.skinoverlay.player.SPlayer; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.*; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import net.minecraft.world.effect.MobEffectInstance; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; + +public class SkinApplier_1_21_R1 extends SkinApplier { + + @Override + public void applySkin(@NotNull SPlayer player) { + this.skinOverlay.getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { + Player bukkitPlayer = player.getPlayer(); + bukkitPlayer.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + bukkitPlayer.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + skinOverlay.getSkinApplier().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(skinOverlay.getPlugin(), () -> { + if (result) + skinOverlay.getPlayerProvider().getOnlinePlayers().stream() + .filter(onlinePlayer -> onlinePlayer != player).forEach(onlinePlayer -> { + Player p = onlinePlayer.getPlayer(); + p.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + p.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + }); + })); + }, 20L); + } + + @Override + protected @NotNull CompletableFuture sendPackets(@NotNull SPlayer player) { + return CompletableFuture.supplyAsync(() -> { + try { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + + ServerLevel world = entityPlayer.serverLevel(); + + CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(world); + ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( + spawnInfo, + ClientboundRespawnPacket.KEEP_ALL_DATA + ); + + sendPacket(entityPlayer, new ClientboundPlayerInfoRemovePacket(List.of(bukkitPlayer.getUniqueId()))); + sendPacket(entityPlayer, ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityPlayer))); + + sendPacket(entityPlayer, respawn); + + entityPlayer.onUpdateAbilities(); + + entityPlayer.connection.teleport(bukkitPlayer.getLocation()); + + entityPlayer.resetSentInfo(); + + PlayerList playerList = entityPlayer.server.getPlayerList(); + playerList.sendPlayerPermissionLevel(entityPlayer); + playerList.sendLevelInfo(entityPlayer, world); + playerList.sendAllPlayerInfo(entityPlayer); + + for (MobEffectInstance effect : entityPlayer.getActiveEffects()) { + sendPacket(entityPlayer, new ClientboundUpdateMobEffectPacket(entityPlayer.getId(), effect, false)); + } + return true; + } catch (Exception exception) { + throw new SkinException(exception); + } + }, runnable -> this.skinOverlay.getScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); + } + + private void sendPacket(@NotNull ServerPlayer player, Packet packet) { + player.connection.send(packet); + } +} diff --git a/bukkit/versions/mc1_21_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R1.java b/bukkit/versions/mc1_21_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R1.java new file mode 100644 index 00000000..61bc64e8 --- /dev/null +++ b/bukkit/versions/mc1_21_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R1.java @@ -0,0 +1,44 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.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 net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class GameProfileProvider_1_21_R1 extends GameProfileProvider { + @Override + public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + return entityPlayer.getGameProfile(); + } + + @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((s, property) -> propertyObjectMap.append(s, new SProperty(property.value(), property.signature()))); + 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((s, property) -> internalGameProfile.getProperties() + .put(s, new Property(s, property.value(), property.signature()))); + } +} diff --git a/bukkit/versions/mc1_21_R2/build.gradle.kts b/bukkit/versions/mc1_21_R2/build.gradle.kts new file mode 100644 index 00000000..b4642ce5 --- /dev/null +++ b/bukkit/versions/mc1_21_R2/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + alias(libs.plugins.paperweight.userdev) +} + +dependencies { + paperweight.paperDevBundle("1.21.3-R0.1-SNAPSHOT") + compileOnly(project(":common")) + compileOnly(libs.auth.lib.modern) +} + +tasks.reobfJar { + paperweight { + reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.REOBF_PRODUCTION + } +} + +tasks.assemble { + dependsOn(tasks.reobfJar) +} \ No newline at end of file diff --git a/bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R2.java b/bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R2.java new file mode 100644 index 00000000..d97a372c --- /dev/null +++ b/bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R2.java @@ -0,0 +1,90 @@ +package com.georgev22.skinoverlay.appliers; + +import com.georgev22.skinoverlay.exceptions.SkinException; +import com.georgev22.skinoverlay.player.SPlayer; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.*; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import net.minecraft.world.effect.MobEffectInstance; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; + +public class SkinApplier_1_21_R2 extends SkinApplier { + + @Override + public void applySkin(@NotNull SPlayer player) { + this.skinOverlay.getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { + Player bukkitPlayer = player.getPlayer(); + bukkitPlayer.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + bukkitPlayer.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + skinOverlay.getSkinApplier().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(skinOverlay.getPlugin(), () -> { + if (result) + skinOverlay.getPlayerProvider().getOnlinePlayers().stream() + .filter(onlinePlayer -> onlinePlayer != player).forEach(onlinePlayer -> { + Player p = onlinePlayer.getPlayer(); + p.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + p.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + }); + })); + }, 20L); + } + + @Override + protected @NotNull CompletableFuture sendPackets(@NotNull SPlayer player) { + return CompletableFuture.supplyAsync(() -> { + try { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + + ServerLevel world = entityPlayer.serverLevel(); + + CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(world); + ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( + spawnInfo, + ClientboundRespawnPacket.KEEP_ALL_DATA + ); + + sendPacket(entityPlayer, new ClientboundPlayerInfoRemovePacket(List.of(bukkitPlayer.getUniqueId()))); + sendPacket(entityPlayer, ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityPlayer))); + + sendPacket(entityPlayer, respawn); + + entityPlayer.onUpdateAbilities(); + + entityPlayer.connection.teleport(bukkitPlayer.getLocation()); + + entityPlayer.resetSentInfo(); + + PlayerList playerList = entityPlayer.server.getPlayerList(); + playerList.sendPlayerPermissionLevel(entityPlayer); + playerList.sendLevelInfo(entityPlayer, world); + playerList.sendAllPlayerInfo(entityPlayer); + + for (MobEffectInstance effect : entityPlayer.getActiveEffects()) { + sendPacket(entityPlayer, new ClientboundUpdateMobEffectPacket(entityPlayer.getId(), effect, false)); + } + return true; + } catch (Exception exception) { + throw new SkinException(exception); + } + }, runnable -> this.skinOverlay.getScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); + } + + private void sendPacket(@NotNull ServerPlayer player, Packet packet) { + player.connection.send(packet); + } +} diff --git a/bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R2.java b/bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R2.java new file mode 100644 index 00000000..24a02256 --- /dev/null +++ b/bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R2.java @@ -0,0 +1,44 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.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 net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class GameProfileProvider_1_21_R2 extends GameProfileProvider { + @Override + public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + return entityPlayer.getGameProfile(); + } + + @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((s, property) -> propertyObjectMap.append(s, new SProperty(property.value(), property.signature()))); + 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((s, property) -> internalGameProfile.getProperties() + .put(s, new Property(s, property.value(), property.signature()))); + } +} diff --git a/bukkit/versions/mc1_21_R3/build.gradle.kts b/bukkit/versions/mc1_21_R3/build.gradle.kts new file mode 100644 index 00000000..aed856e1 --- /dev/null +++ b/bukkit/versions/mc1_21_R3/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + alias(libs.plugins.paperweight.userdev) +} + +dependencies { + paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT") + compileOnly(project(":common")) + compileOnly(libs.auth.lib.modern) +} + +tasks.reobfJar { + paperweight { + reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.REOBF_PRODUCTION + } +} + +tasks.assemble { + dependsOn(tasks.reobfJar) +} \ No newline at end of file diff --git a/bukkit/versions/mc1_21_R3/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R3.java b/bukkit/versions/mc1_21_R3/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R3.java new file mode 100644 index 00000000..6a200737 --- /dev/null +++ b/bukkit/versions/mc1_21_R3/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R3.java @@ -0,0 +1,90 @@ +package com.georgev22.skinoverlay.appliers; + +import com.georgev22.skinoverlay.exceptions.SkinException; +import com.georgev22.skinoverlay.player.SPlayer; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.*; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import net.minecraft.world.effect.MobEffectInstance; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; + +public class SkinApplier_1_21_R3 extends SkinApplier { + + @Override + public void applySkin(@NotNull SPlayer player) { + this.skinOverlay.getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { + Player bukkitPlayer = player.getPlayer(); + bukkitPlayer.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + bukkitPlayer.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + skinOverlay.getSkinApplier().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(skinOverlay.getPlugin(), () -> { + if (result) + skinOverlay.getPlayerProvider().getOnlinePlayers().stream() + .filter(onlinePlayer -> onlinePlayer != player).forEach(onlinePlayer -> { + Player p = onlinePlayer.getPlayer(); + p.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + p.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + }); + })); + }, 20L); + } + + @Override + protected @NotNull CompletableFuture sendPackets(@NotNull SPlayer player) { + return CompletableFuture.supplyAsync(() -> { + try { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + + ServerLevel world = entityPlayer.serverLevel(); + + CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(world); + ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( + spawnInfo, + ClientboundRespawnPacket.KEEP_ALL_DATA + ); + + sendPacket(entityPlayer, new ClientboundPlayerInfoRemovePacket(List.of(bukkitPlayer.getUniqueId()))); + sendPacket(entityPlayer, ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityPlayer))); + + sendPacket(entityPlayer, respawn); + + entityPlayer.onUpdateAbilities(); + + entityPlayer.connection.teleport(bukkitPlayer.getLocation()); + + entityPlayer.resetSentInfo(); + + PlayerList playerList = entityPlayer.server.getPlayerList(); + playerList.sendPlayerPermissionLevel(entityPlayer); + playerList.sendLevelInfo(entityPlayer, world); + playerList.sendAllPlayerInfo(entityPlayer); + + for (MobEffectInstance effect : entityPlayer.getActiveEffects()) { + sendPacket(entityPlayer, new ClientboundUpdateMobEffectPacket(entityPlayer.getId(), effect, false)); + } + return true; + } catch (Exception exception) { + throw new SkinException(exception); + } + }, runnable -> this.skinOverlay.getScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); + } + + private void sendPacket(@NotNull ServerPlayer player, Packet packet) { + player.connection.send(packet); + } +} diff --git a/bukkit/versions/mc1_21_R3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R3.java b/bukkit/versions/mc1_21_R3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R3.java new file mode 100644 index 00000000..6958b4e3 --- /dev/null +++ b/bukkit/versions/mc1_21_R3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R3.java @@ -0,0 +1,44 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.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 net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class GameProfileProvider_1_21_R3 extends GameProfileProvider { + @Override + public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + return entityPlayer.getGameProfile(); + } + + @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((s, property) -> propertyObjectMap.append(s, new SProperty(property.value(), property.signature()))); + 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((s, property) -> internalGameProfile.getProperties() + .put(s, new Property(s, property.value(), property.signature()))); + } +} diff --git a/bukkit/versions/mc1_21_R4/build.gradle.kts b/bukkit/versions/mc1_21_R4/build.gradle.kts new file mode 100644 index 00000000..a08d2aa5 --- /dev/null +++ b/bukkit/versions/mc1_21_R4/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + alias(libs.plugins.paperweight.userdev) +} + +dependencies { + paperweight.paperDevBundle("1.21.5-R0.1-SNAPSHOT") + compileOnly(project(":common")) + compileOnly(libs.auth.lib.modern) +} + +tasks.reobfJar { + paperweight { + reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.REOBF_PRODUCTION + } +} + +tasks.assemble { + dependsOn(tasks.reobfJar) +} \ No newline at end of file diff --git a/bukkit/versions/mc1_21_R4/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R4.java b/bukkit/versions/mc1_21_R4/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R4.java new file mode 100644 index 00000000..7e99d036 --- /dev/null +++ b/bukkit/versions/mc1_21_R4/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R4.java @@ -0,0 +1,90 @@ +package com.georgev22.skinoverlay.appliers; + +import com.georgev22.skinoverlay.exceptions.SkinException; +import com.georgev22.skinoverlay.player.SPlayer; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.*; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import net.minecraft.world.effect.MobEffectInstance; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; + +public class SkinApplier_1_21_R4 extends SkinApplier { + + @Override + public void applySkin(@NotNull SPlayer player) { + this.skinOverlay.getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { + Player bukkitPlayer = player.getPlayer(); + bukkitPlayer.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + bukkitPlayer.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + skinOverlay.getSkinApplier().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(skinOverlay.getPlugin(), () -> { + if (result) + skinOverlay.getPlayerProvider().getOnlinePlayers().stream() + .filter(onlinePlayer -> onlinePlayer != player).forEach(onlinePlayer -> { + Player p = onlinePlayer.getPlayer(); + p.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + p.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + }); + })); + }, 20L); + } + + @Override + protected @NotNull CompletableFuture sendPackets(@NotNull SPlayer player) { + return CompletableFuture.supplyAsync(() -> { + try { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + + ServerLevel world = entityPlayer.serverLevel(); + + CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(world); + ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( + spawnInfo, + ClientboundRespawnPacket.KEEP_ALL_DATA + ); + + sendPacket(entityPlayer, new ClientboundPlayerInfoRemovePacket(List.of(bukkitPlayer.getUniqueId()))); + sendPacket(entityPlayer, ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityPlayer))); + + sendPacket(entityPlayer, respawn); + + entityPlayer.onUpdateAbilities(); + + entityPlayer.connection.teleport(bukkitPlayer.getLocation()); + + entityPlayer.resetSentInfo(); + + PlayerList playerList = entityPlayer.server.getPlayerList(); + playerList.sendPlayerPermissionLevel(entityPlayer); + playerList.sendLevelInfo(entityPlayer, world); + playerList.sendAllPlayerInfo(entityPlayer); + + for (MobEffectInstance effect : entityPlayer.getActiveEffects()) { + sendPacket(entityPlayer, new ClientboundUpdateMobEffectPacket(entityPlayer.getId(), effect, false)); + } + return true; + } catch (Exception exception) { + throw new SkinException(exception); + } + }, runnable -> this.skinOverlay.getScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); + } + + private void sendPacket(@NotNull ServerPlayer player, Packet packet) { + player.connection.send(packet); + } +} diff --git a/bukkit/versions/mc1_21_R4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R4.java b/bukkit/versions/mc1_21_R4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R4.java new file mode 100644 index 00000000..f9006ab0 --- /dev/null +++ b/bukkit/versions/mc1_21_R4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R4.java @@ -0,0 +1,44 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.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 net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class GameProfileProvider_1_21_R4 extends GameProfileProvider { + @Override + public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + return entityPlayer.getGameProfile(); + } + + @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((s, property) -> propertyObjectMap.append(s, new SProperty(property.value(), property.signature()))); + 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((s, property) -> internalGameProfile.getProperties() + .put(s, new Property(s, property.value(), property.signature()))); + } +} diff --git a/bukkit/versions/mc1_21_R5/build.gradle.kts b/bukkit/versions/mc1_21_R5/build.gradle.kts new file mode 100644 index 00000000..79c45c20 --- /dev/null +++ b/bukkit/versions/mc1_21_R5/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + alias(libs.plugins.paperweight.userdev) +} + +dependencies { + paperweight.paperDevBundle("1.21.6-R0.1-SNAPSHOT") + compileOnly(project(":common")) + compileOnly(libs.auth.lib.modern) +} + +tasks.reobfJar { + paperweight { + reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.REOBF_PRODUCTION + } +} + +tasks.assemble { + dependsOn(tasks.reobfJar) +} \ No newline at end of file diff --git a/bukkit/versions/mc1_21_R5/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R5.java b/bukkit/versions/mc1_21_R5/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R5.java new file mode 100644 index 00000000..dc9a9934 --- /dev/null +++ b/bukkit/versions/mc1_21_R5/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R5.java @@ -0,0 +1,90 @@ +package com.georgev22.skinoverlay.appliers; + +import com.georgev22.skinoverlay.exceptions.SkinException; +import com.georgev22.skinoverlay.player.SPlayer; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.*; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import net.minecraft.world.effect.MobEffectInstance; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; + +public class SkinApplier_1_21_R5 extends SkinApplier { + + @Override + public void applySkin(@NotNull SPlayer player) { + this.skinOverlay.getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { + Player bukkitPlayer = player.getPlayer(); + bukkitPlayer.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + bukkitPlayer.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + skinOverlay.getSkinApplier().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(skinOverlay.getPlugin(), () -> { + if (result) + skinOverlay.getPlayerProvider().getOnlinePlayers().stream() + .filter(onlinePlayer -> onlinePlayer != player).forEach(onlinePlayer -> { + Player p = onlinePlayer.getPlayer(); + p.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + p.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + }); + })); + }, 20L); + } + + @Override + protected @NotNull CompletableFuture sendPackets(@NotNull SPlayer player) { + return CompletableFuture.supplyAsync(() -> { + try { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + + ServerLevel world = entityPlayer.gameMode.level; + + CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(world); + ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( + spawnInfo, + ClientboundRespawnPacket.KEEP_ALL_DATA + ); + + sendPacket(entityPlayer, new ClientboundPlayerInfoRemovePacket(List.of(bukkitPlayer.getUniqueId()))); + sendPacket(entityPlayer, ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityPlayer))); + + sendPacket(entityPlayer, respawn); + + entityPlayer.onUpdateAbilities(); + + entityPlayer.connection.teleport(bukkitPlayer.getLocation()); + + entityPlayer.resetSentInfo(); + + PlayerList playerList = entityPlayer.gameMode.level.getServer().getPlayerList(); + playerList.sendPlayerPermissionLevel(entityPlayer); + playerList.sendLevelInfo(entityPlayer, world); + playerList.sendAllPlayerInfo(entityPlayer); + + for (MobEffectInstance effect : entityPlayer.getActiveEffects()) { + sendPacket(entityPlayer, new ClientboundUpdateMobEffectPacket(entityPlayer.getId(), effect, false)); + } + return true; + } catch (Exception exception) { + throw new SkinException(exception); + } + }, runnable -> this.skinOverlay.getScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); + } + + private void sendPacket(@NotNull ServerPlayer player, Packet packet) { + player.connection.send(packet); + } +} diff --git a/bukkit/versions/mc1_21_R5/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R5.java b/bukkit/versions/mc1_21_R5/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R5.java new file mode 100644 index 00000000..d9340575 --- /dev/null +++ b/bukkit/versions/mc1_21_R5/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R5.java @@ -0,0 +1,44 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.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 net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class GameProfileProvider_1_21_R5 extends GameProfileProvider { + @Override + public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + return entityPlayer.getGameProfile(); + } + + @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((s, property) -> propertyObjectMap.append(s, new SProperty(property.value(), property.signature()))); + 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((s, property) -> internalGameProfile.getProperties() + .put(s, new Property(s, property.value(), property.signature()))); + } +} diff --git a/bukkit/versions/mc1_21_R6/build.gradle.kts b/bukkit/versions/mc1_21_R6/build.gradle.kts new file mode 100644 index 00000000..4cb1ce07 --- /dev/null +++ b/bukkit/versions/mc1_21_R6/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + alias(libs.plugins.paperweight.userdev) +} + +dependencies { + paperweight.paperDevBundle("1.21.10-R0.1-SNAPSHOT") + compileOnly(project(":common")) + compileOnly(libs.auth.lib.modern) + compileOnly(libs.reflect) +} + +tasks.reobfJar { + paperweight { + reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.REOBF_PRODUCTION + } +} + +tasks.assemble { + dependsOn(tasks.reobfJar) +} \ No newline at end of file diff --git a/bukkit/versions/mc1_21_R6/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R6.java b/bukkit/versions/mc1_21_R6/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R6.java new file mode 100644 index 00000000..84792a86 --- /dev/null +++ b/bukkit/versions/mc1_21_R6/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R6.java @@ -0,0 +1,97 @@ +package com.georgev22.skinoverlay.appliers; + +import com.georgev22.skinoverlay.exceptions.SkinException; +import com.georgev22.skinoverlay.player.SPlayer; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.*; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import net.minecraft.world.effect.MobEffectInstance; +import org.bukkit.Location; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; + +public class SkinApplier_1_21_R6 extends SkinApplier { + + @Override + public void applySkin(@NotNull SPlayer player) { + this.skinOverlay.getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { + Player bukkitPlayer = player.getPlayer(); + bukkitPlayer.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + bukkitPlayer.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + skinOverlay.getSkinApplier().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(skinOverlay.getPlugin(), () -> { + if (result) + skinOverlay.getPlayerProvider().getOnlinePlayers().stream() + .filter(onlinePlayer -> onlinePlayer != player).forEach(onlinePlayer -> { + Player p = onlinePlayer.getPlayer(); + p.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + p.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + }); + })); + }, 20L); + } + + @Override + protected @NotNull CompletableFuture sendPackets(@NotNull SPlayer player) { + return CompletableFuture.supplyAsync(() -> { + try { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + + ServerLevel world = entityPlayer.gameMode.level; + + CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(world); + ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( + spawnInfo, + ClientboundRespawnPacket.KEEP_ALL_DATA + ); + + sendPacket(entityPlayer, new ClientboundPlayerInfoRemovePacket(List.of(bukkitPlayer.getUniqueId()))); + sendPacket(entityPlayer, ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityPlayer))); + + sendPacket(entityPlayer, respawn); + + entityPlayer.onUpdateAbilities(); + + Location location = bukkitPlayer.getLocation(); + double x = location.getX(); + double y = location.getY(); + double z = location.getZ(); + float yaw = location.getYaw(); + float pitch = location.getPitch(); + entityPlayer.connection.teleport(x, y, z, yaw, pitch); + + entityPlayer.resetSentInfo(); + + PlayerList playerList = entityPlayer.gameMode.level.getServer().getPlayerList(); + playerList.sendPlayerPermissionLevel(entityPlayer); + playerList.sendLevelInfo(entityPlayer, world); + playerList.sendAllPlayerInfo(entityPlayer); + + for (MobEffectInstance effect : entityPlayer.getActiveEffects()) { + sendPacket(entityPlayer, new ClientboundUpdateMobEffectPacket(entityPlayer.getId(), effect, false)); + } + return true; + } catch (Exception exception) { + throw new SkinException(exception); + } + }, runnable -> this.skinOverlay.getScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); + } + + private void sendPacket(@NotNull ServerPlayer player, Packet packet) { + player.connection.send(packet); + } +} diff --git a/bukkit/versions/mc1_21_R6/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R6.java b/bukkit/versions/mc1_21_R6/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R6.java new file mode 100644 index 00000000..c7fb7153 --- /dev/null +++ b/bukkit/versions/mc1_21_R6/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R6.java @@ -0,0 +1,57 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.skin.SGameProfile; +import com.georgev22.skinoverlay.skin.SProperty; +import com.google.common.collect.ImmutableMultimap; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; +import com.mojang.authlib.properties.PropertyMap; +import net.lenni0451.reflect.stream.RStream; +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +public class GameProfileProvider_1_21_R6 extends GameProfileProvider { + @Override + public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { + Player bukkitPlayer = player.getPlayer(); + final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + final ServerPlayer entityPlayer = craftPlayer.getHandle(); + return entityPlayer.getGameProfile(); + } + + @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.properties().forEach((s, property) -> propertyObjectMap.append(s, new SProperty(property.value(), property.signature()))); + SGameProfile sGameProfile = new SGameProfile(gameProfile.name(), gameProfile.id(), propertyObjectMap); + return sGameProfiles.append(player, sGameProfile).get(player); + } + + @Override + public void applyUpdatedGameProfile(@NotNull SPlayer player) { + GameProfile internalGameProfile = this.getInternalGameProfile(player); + PropertyMap properties = internalGameProfile.properties(); + SGameProfile gameProfile = this.getGameProfile(player); + ImmutableMultimap.Builder newProperties = ImmutableMultimap.builder(); + for (Map.Entry entry : gameProfile.getProperties().entrySet()) { + newProperties.put(entry.getKey(), new Property(entry.getValue().value(), entry.getValue().signature())); + } + + RStream.of(properties) + .withSuper() + .fields() + .by("properties") + .set(newProperties.build()); + } +} diff --git a/bungee/build.gradle b/bungee/build.gradle deleted file mode 100644 index 1e3cb517..00000000 --- a/bungee/build.gradle +++ /dev/null @@ -1,10 +0,0 @@ -dependencies { - compileOnly project(path: ':core') - - compileOnly 'net.md-5:bungeecord-api:1.19-R0.1-SNAPSHOT' - compileOnly 'net.md-5:bungeecord-proxy:1.19-R0.1-SNAPSHOT' - - implementation "net.kyori:adventure-platform-bungeecord:4.3.0" -} - -defaultTasks 'build' \ No newline at end of file diff --git a/bungee/src/main/java/com/georgev22/skinoverlay/SkinOverlayBungee.java b/bungee/src/main/java/com/georgev22/skinoverlay/SkinOverlayBungee.java deleted file mode 100644 index ce219ed0..00000000 --- a/bungee/src/main/java/com/georgev22/skinoverlay/SkinOverlayBungee.java +++ /dev/null @@ -1,195 +0,0 @@ -package com.georgev22.skinoverlay; - -import co.aikar.commands.BungeeCommandManager; -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.BungeeMinecraftUtils; -import com.georgev22.library.minecraft.scheduler.MinecraftBungeeScheduler; -import com.georgev22.library.utilities.Utils; -import com.georgev22.skinoverlay.handler.handlers.SkinHandler_BungeeCord; -import com.georgev22.skinoverlay.listeners.bungee.DeveloperInformListener; -import com.georgev22.skinoverlay.listeners.bungee.PlayerListeners; -import com.georgev22.skinoverlay.utilities.BungeeCordPluginMessageUtils; -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.PlayerObjectBungee; -import net.kyori.adventure.platform.bungeecord.BungeeAudiences; -import net.md_5.bungee.api.ProxyServer; -import net.md_5.bungee.api.connection.ProxiedPlayer; -import net.md_5.bungee.api.plugin.Plugin; -import org.bstats.bungeecord.Metrics; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -import java.io.File; -import java.util.Collections; -import java.util.UUID; -import java.util.concurrent.TimeUnit; -import java.util.logging.Logger; - -@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("com.mojang:authlib:3.11.50:https://nexus.velocitypowered.com/repository/maven-public/") -@MavenLibrary("org.apache.commons:commons-lang3:3.12.0:https://repo1.maven.org/maven2/") -@ApiStatus.Internal -@ApiStatus.NonExtendable -public class SkinOverlayBungee extends Plugin implements SkinOverlayImpl { - - private boolean enabled = false; - private SkinOverlay skinOverlay; - - private BungeeAudiences adventure; - - private LibraryLoader libraryLoader; - - private static SkinOverlayBungee skinOverlayBungee; - - public static SkinOverlayBungee getInstance() { - return skinOverlayBungee; - } - - @Override - public void onLoad() { - this.skinOverlay = new SkinOverlay(this); - skinOverlayBungee = this; - try { - this.libraryLoader = new LibraryLoader(this.getClass().getClassLoader(), this.getDataFolder()); - this.libraryLoader.loadAll(this, true); - } catch (InvalidDependencyException | UnknownDependencyException e) { - throw new RuntimeException(e); - } - skinOverlay.onLoad(); - } - - @Override - public void onEnable() { - this.adventure = BungeeAudiences.create(this); - skinOverlay.setMinecraftScheduler(new MinecraftBungeeScheduler()); - skinOverlay.setSkinHandler(new SkinHandler_BungeeCord()); - skinOverlay.setCommandManager(new BungeeCommandManager(this)); - skinOverlay.onEnable(); - skinOverlay.setPluginMessageUtils(new BungeeCordPluginMessageUtils()); - BungeeMinecraftUtils.registerListeners(this, new PlayerListeners(), new DeveloperInformListener()); - getProxy().registerChannel("skinoverlay:bungee"); - getProxy().registerChannel("skinoverlay:message"); - if (OptionsUtil.METRICS.getBooleanValue()) - new Metrics(this, 17475); - enabled = true; - } - - @Override - public void onDisable() { - skinOverlay.onDisable(); - if (this.adventure != null) { - this.adventure.close(); - this.adventure = null; - } - enabled = false; - try { - this.libraryLoader.unloadAll(); - } catch (InvalidDependencyException e) { - throw new RuntimeException(e); - } - } - - @Override - public Type type() { - return Type.BUNGEE; - } - - @Override - public File dataFolder() { - return getDataFolder(); - } - - @Override - public Logger logger() { - return getLogger(); - } - - @Override - public Description description() { - return new Description(getDescription().getName(), getDescription().getVersion(), getDescription().getMain(), Collections.singletonList(getDescription().getAuthor())); - } - - @Override - public boolean enable(boolean enable) { - if (enable) { - onEnable(); - } else { - onDisable(); - } - return enabled(); - } - - @Override - public boolean enabled() { - return enabled; - } - - @Override - public void saveResource(@NotNull String resource, boolean replace) { - try { - Utils.saveResource(resource, replace, getDataFolder(), this.getClass()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public boolean onlineMode() { - return getProxy().getConfig().isOnlineMode(); - } - - private final ObservableObjectMap players = new ObservableObjectMap<>(); - - @Override - public ObservableObjectMap onlinePlayers() { - for (ProxiedPlayer player : getProxy().getPlayers()) { - if (players.containsKey(player.getUniqueId())) { - continue; - } - players.append(player.getUniqueId(), new PlayerObjectBungee(player)); - } - return players; - } - - @Override - public boolean isPluginEnabled(String pluginName) { - return getProxy().getPluginManager().getPlugin(pluginName) != null; - } - - @Override - public Plugin plugin() { - return this; - } - - @Override - public ProxyServer serverImpl() { - return getProxy(); - } - - @Override - public String serverVersion() { - return getProxy().getVersion(); - } - - @Override - public @NotNull BungeeAudiences adventure() { - if (this.adventure == null) { - throw new IllegalStateException("Cannot retrieve audience provider while plugin is not enabled"); - } - return this.adventure; - } -} diff --git a/bungee/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_BungeeCord.java b/bungee/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_BungeeCord.java deleted file mode 100644 index b660732a..00000000 --- a/bungee/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_BungeeCord.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.georgev22.skinoverlay.handler.handlers; - -import com.georgev22.library.maps.HashObjectMap; -import com.georgev22.library.maps.ObjectMap; -import com.georgev22.library.utilities.Utils; -import com.georgev22.skinoverlay.handler.SGameProfile; -import com.georgev22.skinoverlay.handler.SProperty; -import com.georgev22.skinoverlay.handler.SkinHandler; -import com.georgev22.skinoverlay.handler.profile.SGameProfileBungee; -import com.georgev22.skinoverlay.storage.data.Skin; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import net.md_5.bungee.api.connection.ProxiedPlayer; -import net.md_5.bungee.connection.InitialHandler; -import net.md_5.bungee.protocol.Property; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -import java.util.concurrent.CompletableFuture; -import java.util.logging.Level; - -@ApiStatus.Internal -@ApiStatus.NonExtendable -public class SkinHandler_BungeeCord extends SkinHandler { - - @Override - public CompletableFuture updateSkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - return CompletableFuture.supplyAsync(() -> { - try { - ProxiedPlayer proxiedPlayer = playerObject.player(); - skinOverlay.getPluginMessageUtils().setChannel("skinoverlay:bungee"); - skinOverlay.getPluginMessageUtils().setObject(proxiedPlayer.getServer().getInfo()); - if (skin.skinParts().getSkinName().equalsIgnoreCase("default")) { - skinOverlay.getPluginMessageUtils().sendDataToServer("reset", playerObject.playerUUID().toString(), Utils.serializeObjectToString(skin), "true"); - } else { - skinOverlay.getPluginMessageUtils().sendDataToServer("change", playerObject.playerUUID().toString(), Utils.serializeObjectToString(skin), "true"); - } - return true; - } catch (Exception exception) { - skinOverlay.getLogger().log(Level.SEVERE, exception.getMessage(), exception); - return false; - } - }); - } - - @Override - public void applySkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - skinOverlay.getSkinHandler().updateSkin(playerObject, skin).handleAsync((aBoolean, throwable) -> { - if (throwable != null) { - skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); - return false; - } - return aBoolean; - }); - } - - @Override - public SGameProfile getInternalGameProfile(@NotNull PlayerObject playerObject) { - ObjectMap properties = new HashObjectMap<>(); - for (Property property : ((InitialHandler) ((ProxiedPlayer) playerObject.player()).getPendingConnection()).getLoginProfile().getProperties()) { - properties.append(property.getName(), new SProperty(property.getName(), property.getValue(), property.getSignature())); - } - return new SGameProfileBungee(playerObject.playerName(), playerObject.playerUUID(), properties); - } - - @Override - public SGameProfile getGameProfile(@NotNull PlayerObject playerObject) { - if (sGameProfiles.containsKey(playerObject)) { - return sGameProfiles.get(playerObject); - } - return sGameProfiles.append(playerObject, this.getInternalGameProfile(playerObject)).get(playerObject); - } -} diff --git a/bungee/src/main/java/com/georgev22/skinoverlay/handler/profile/SGameProfileBungee.java b/bungee/src/main/java/com/georgev22/skinoverlay/handler/profile/SGameProfileBungee.java deleted file mode 100644 index 861c5c16..00000000 --- a/bungee/src/main/java/com/georgev22/skinoverlay/handler/profile/SGameProfileBungee.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.georgev22.skinoverlay.handler.profile; - -import com.georgev22.library.maps.ObjectMap; -import com.georgev22.skinoverlay.handler.SGameProfile; -import com.georgev22.skinoverlay.handler.SProperty; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import net.md_5.bungee.api.connection.ProxiedPlayer; -import net.md_5.bungee.connection.InitialHandler; -import net.md_5.bungee.connection.LoginResult; -import net.md_5.bungee.protocol.Property; -import org.jetbrains.annotations.ApiStatus; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -@ApiStatus.Internal -@ApiStatus.NonExtendable -public class SGameProfileBungee extends SGameProfile { - - - public SGameProfileBungee(String name, UUID uuid) { - super(name, uuid); - } - - public SGameProfileBungee(String name, UUID uuid, ObjectMap properties) { - super(name, uuid, properties); - } - - @Override - public void apply() { - PlayerObject playerObject = skinOverlay.getPlayer(getUUID()).orElseThrow(); - LoginResult initialHandler = ((InitialHandler) ((ProxiedPlayer) playerObject.player()).getPendingConnection()).getLoginProfile(); - List bungeeProperties = new ArrayList<>(); - this.getProperties().forEach((s, sProperty) -> bungeeProperties.add(new Property(sProperty.name(), sProperty.value(), sProperty.signature()))); - initialHandler.setProperties(bungeeProperties.toArray(Property[]::new)); - } -} diff --git a/bungee/src/main/java/com/georgev22/skinoverlay/listeners/bungee/DeveloperInformListener.java b/bungee/src/main/java/com/georgev22/skinoverlay/listeners/bungee/DeveloperInformListener.java deleted file mode 100644 index 8c8e3674..00000000 --- a/bungee/src/main/java/com/georgev22/skinoverlay/listeners/bungee/DeveloperInformListener.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.georgev22.skinoverlay.listeners.bungee; - -import com.georgev22.skinoverlay.SkinOverlay; -import net.md_5.bungee.api.event.PostLoginEvent; -import net.md_5.bungee.api.plugin.Listener; -import net.md_5.bungee.event.EventHandler; -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -@ApiStatus.NonExtendable -public class DeveloperInformListener implements Listener { - - private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); - - @EventHandler - public void onJoin(PostLoginEvent e) { - skinOverlay.getPlayer(e.getPlayer().getUniqueId()).orElseThrow().developerInform(); - } -} diff --git a/bungee/src/main/java/com/georgev22/skinoverlay/listeners/bungee/PlayerListeners.java b/bungee/src/main/java/com/georgev22/skinoverlay/listeners/bungee/PlayerListeners.java deleted file mode 100644 index d0060602..00000000 --- a/bungee/src/main/java/com/georgev22/skinoverlay/listeners/bungee/PlayerListeners.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.georgev22.skinoverlay.listeners.bungee; - -import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.event.events.player.PlayerObjectConnectionEvent; -import com.georgev22.skinoverlay.utilities.Utilities; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import com.google.common.io.ByteArrayDataInput; -import com.google.common.io.ByteStreams; -import net.md_5.bungee.api.connection.Server; -import net.md_5.bungee.api.event.PlayerDisconnectEvent; -import net.md_5.bungee.api.event.PluginMessageEvent; -import net.md_5.bungee.api.event.PostLoginEvent; -import net.md_5.bungee.api.plugin.Listener; -import net.md_5.bungee.event.EventHandler; -import org.jetbrains.annotations.ApiStatus; - -import java.util.Objects; -import java.util.UUID; - -@ApiStatus.Internal -@ApiStatus.NonExtendable -public class PlayerListeners implements Listener { - - private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); - - @EventHandler(priority = 1) - public void onLogin(PostLoginEvent postLoginEvent) { - if (!postLoginEvent.getPlayer().isConnected()) - return; - skinOverlay.getEventManager().callEvent( - new PlayerObjectConnectionEvent( - skinOverlay.getPlayer(postLoginEvent.getPlayer().getUniqueId()).orElseThrow(), - PlayerObjectConnectionEvent.ConnectionType.CONNECT, - true - ) - ); - } - - @EventHandler(priority = 1) - public void onQuit(PlayerDisconnectEvent playerDisconnectEvent) { - skinOverlay.getEventManager().callEvent( - new PlayerObjectConnectionEvent( - skinOverlay.getPlayer(playerDisconnectEvent.getPlayer().getUniqueId()).orElseThrow(), - PlayerObjectConnectionEvent.ConnectionType.DISCONNECT, - true - ) - ); - } - - @EventHandler - public void onPluginMessage(PluginMessageEvent pluginMessageEvent) { - if (!(pluginMessageEvent.getSender() instanceof Server)) { - return; - } - if (pluginMessageEvent.getTag().equalsIgnoreCase("skinoverlay:message")) { - ByteArrayDataInput in = ByteStreams.newDataInput(pluginMessageEvent.getData()); - String subChannel = in.readUTF(); - if (subChannel.equalsIgnoreCase("playerJoin")) { - UUID playerUUID = UUID.fromString(Objects.requireNonNull(Utilities.decrypt(in.readUTF()))); - this.skinOverlay.getMinecraftScheduler().runTask(skinOverlay.getPlugin(), () -> skinOverlay.getPlayer(playerUUID).ifPresent(PlayerObject::updateSkin)); - } - } - } -} - diff --git a/bungee/src/main/java/com/georgev22/skinoverlay/utilities/BungeeCordPluginMessageUtils.java b/bungee/src/main/java/com/georgev22/skinoverlay/utilities/BungeeCordPluginMessageUtils.java deleted file mode 100644 index 6a32a2cf..00000000 --- a/bungee/src/main/java/com/georgev22/skinoverlay/utilities/BungeeCordPluginMessageUtils.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.georgev22.skinoverlay.utilities; - -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import net.md_5.bungee.api.config.ServerInfo; -import net.md_5.bungee.api.connection.ProxiedPlayer; -import org.jetbrains.annotations.NotNull; - -public final class BungeeCordPluginMessageUtils extends PluginMessageUtils { - - @Override - public void sendDataToServer(@NotNull String subChannel, String... dataArray) { - if (getObject() == null) { - skinOverlay.getLogger().severe("ServerInfo is null!!"); - return; - } - ((ServerInfo) getObject()).sendData(getChannel(), this.toByteArray(subChannel, dataArray)); - } - - @Override - public void sendDataToPlayer(@NotNull String subChannel, @NotNull PlayerObject player, String... dataArray) { - ((ProxiedPlayer) player.player()).sendData(getChannel(), this.toByteArray(subChannel, dataArray)); - } -} diff --git a/bungee/src/main/java/com/georgev22/skinoverlay/utilities/player/PlayerObjectBungee.java b/bungee/src/main/java/com/georgev22/skinoverlay/utilities/player/PlayerObjectBungee.java deleted file mode 100644 index 49bf3035..00000000 --- a/bungee/src/main/java/com/georgev22/skinoverlay/utilities/player/PlayerObjectBungee.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.georgev22.skinoverlay.utilities.player; - -import com.georgev22.library.maps.ObjectMap; -import com.georgev22.library.minecraft.BungeeMinecraftUtils; -import com.georgev22.skinoverlay.SkinOverlay; -import net.kyori.adventure.audience.Audience; -import net.md_5.bungee.api.connection.ProxiedPlayer; - -import java.util.List; -import java.util.UUID; - -public class PlayerObjectBungee extends PlayerObject { - private final ProxiedPlayer proxiedPlayer; - - public PlayerObjectBungee(final ProxiedPlayer proxiedPlayer) { - this.proxiedPlayer = proxiedPlayer; - } - - @Override - public ProxiedPlayer player() { - return this.proxiedPlayer; - } - - @Override - public Audience audience() { - return SkinOverlay.getInstance().getSkinOverlay().adventure().player(this.proxiedPlayer.getUniqueId()); - } - - @Override - public UUID playerUUID() { - return this.proxiedPlayer.getUniqueId(); - } - - @Override - public String playerName() { - return this.proxiedPlayer.getName(); - } - - @Override - public void sendMessage(String input) { - BungeeMinecraftUtils.msg(proxiedPlayer, input); - } - - @Override - public void sendMessage(List input) { - BungeeMinecraftUtils.msg(proxiedPlayer, input); - } - - @Override - public void sendMessage(String... input) { - BungeeMinecraftUtils.msg(proxiedPlayer, input); - } - - @Override - public void sendMessage(String input, ObjectMap placeholders, boolean ignoreCase) { - BungeeMinecraftUtils.msg(proxiedPlayer, input, placeholders, ignoreCase); - } - - @Override - public void sendMessage(List input, ObjectMap placeholders, boolean ignoreCase) { - BungeeMinecraftUtils.msg(proxiedPlayer, input, placeholders, ignoreCase); - } - - @Override - public void sendMessage(String[] input, ObjectMap placeholders, boolean ignoreCase) { - BungeeMinecraftUtils.msg(proxiedPlayer, input, placeholders, ignoreCase); - } - - @Override - public boolean isOnline() { - return proxiedPlayer.isConnected(); - } - - @Override - public boolean permission(String permission) { - return isOnline() && player().hasPermission(permission); - } -} diff --git a/common/build.gradle.kts b/common/build.gradle.kts new file mode 100644 index 00000000..8e9e1d85 --- /dev/null +++ b/common/build.gradle.kts @@ -0,0 +1,83 @@ +plugins { + id("buildlogic.java-conventions") + alias(libs.plugins.gradleup.shadow) + alias(libs.plugins.blossom) +} + +apply(from = "$rootDir/gradle/publish.gradle") + +repositories { + mavenCentral() +} + +group = project.property("group") as String +description = project.property("description") as String +version = project.property("version") as String + +dependencies { + api(project(":build-info")) + compileOnly(libs.skinsrestorer.api) + // ADVENTURE + compileOnly(libs.adventure.api) + compileOnly(libs.adventure.platform.api) + compileOnly(libs.adventure.text.minimessage) + compileOnly(libs.adventure.text.serializer.legacy) + + // log4j + compileOnly(libs.log4j.api) + + + implementation(libs.hikari) { + exclude(group = "org.slf4j", module = "slf4j-api") + } + + implementation(libs.gson) + + // MINESKIN CLIENT + implementation(libs.mineskinclient.client) { + exclude(group = "com.google.code.gson", module = "gson") + exclude(group = "com.google.guava", module = "guava") + } + implementation(libs.mineskinclient.java11) { + exclude(group = "com.google.code.gson", module = "gson") + exclude(group = "com.google.guava", module = "guava") + } + + // YAML + implementation(libs.yamlconfiguration) { + exclude(group = "org.slf4j", module = "slf4j-api") + } + + implementation(libs.jedis) + + implementation(libs.jetbrains.annotations) +} + +configurations.configureEach { + resolutionStrategy { + force(libs.adventure.text.serializer.legacy) + force(libs.adventure.text.minimessage) + force(libs.jetbrains.annotations) + } +} + + +tasks.processResources { + filesMatching("**.yml") { + val props = mapOf( + "pluginName" to project.property("pluginName"), + "bungeeMain" to project.property("bungeeMain"), + "bukkitMain" to project.property("bukkitMain"), + "version" to version, + "author" to project.property("author"), + "packageName" to project.property("packageName"), + ) + expand(props) + filteringCharset = "UTF-8" + } +} + +tasks.shadowJar { + archiveBaseName.set("skinoverlay") + archiveClassifier.set("common") +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java new file mode 100644 index 00000000..bbdd3d43 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java @@ -0,0 +1,451 @@ +package com.georgev22.skinoverlay; + +import com.georgev22.skinoverlay.appliers.SkinApplier; +import com.georgev22.skinoverlay.command.CommandManager; +import com.georgev22.skinoverlay.command.CompletionEngine; +import com.georgev22.skinoverlay.command.commands.SkinOverlayMain; +import com.georgev22.skinoverlay.event.EventBus; +import com.georgev22.skinoverlay.event.HandlerPriority; +import com.georgev22.skinoverlay.event.events.player.SPlayerJoinEvent; +import com.georgev22.skinoverlay.event.events.player.SPlayerLeaveEvent; +import com.georgev22.skinoverlay.hooks.SkinHook; +import com.georgev22.skinoverlay.listeners.PlayerListeners; +import com.georgev22.skinoverlay.message.MessageEntry; +import com.georgev22.skinoverlay.message.MessagesRegistry; +import com.georgev22.skinoverlay.message.messages.CommandMessages; +import com.georgev22.skinoverlay.message.messages.CoreMessages; +import com.georgev22.skinoverlay.messaging.MessageManager; +import com.georgev22.skinoverlay.providers.GameProfileProvider; +import com.georgev22.skinoverlay.providers.PlayerProvider; +import com.georgev22.skinoverlay.providers.SkinProvider; +import com.georgev22.skinoverlay.registry.EntityManagerRegistry; +import com.georgev22.skinoverlay.scheduler.MinecraftScheduler; +import com.georgev22.skinoverlay.skin.SProperty; +import com.georgev22.skinoverlay.storage.EntityManager; +import com.georgev22.skinoverlay.storage.data.PlayerData; +import com.georgev22.skinoverlay.storage.data.Skin; +import com.georgev22.skinoverlay.storage.gson.*; +import com.georgev22.skinoverlay.storage.manager.gson.PlayerFileManager; +import com.georgev22.skinoverlay.storage.manager.gson.SkinFileManager; +import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; +import com.georgev22.skinoverlay.utilities.config.FileManager; +import com.georgev22.skinoverlay.utilities.config.SkinFileCache; +import com.georgev22.skinoverlay.utilities.skin.Part; +import com.georgev22.skinoverlay.utilities.skin.SkinParts; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import net.kyori.adventure.platform.AudienceProvider; +import org.bspfsystems.yamlconfiguration.file.FileConfiguration; + +import java.io.File; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Singleton class for managing the core components of SkinOverlay. + */ +public class SkinOverlay { + + private static SkinOverlay instance; + + /** + * Gets the singleton instance of SkinOverlay. + * + * @return the SkinOverlay instance + */ + public static SkinOverlay getInstance() { + return instance == null ? instance = new SkinOverlay() : instance; + } + + private EventBus eventBus; + private FileManager fileManager; + private SkinFileCache skinFileCache; + private Gson gson; + private CommandManager commandManager; + private Object plugin; + private MinecraftScheduler scheduler; + private AudienceProvider audienceProvider; + private Logger logger; + private PlayerProvider playerProvider; + private GameProfileProvider gameProfileProvider; + private SkinProvider skinProvider; + private SkinApplier skinApplier; + private SkinHook skinHook; + private boolean isOnlineMode; + private boolean isProxy; + private File dataFolder; + private MessageManager messageManager; + + /** + * Loads the plugin components. Should be called during plugin load phase. + */ + public void onLoad() { + this.eventBus = new EventBus(); + this.gson = new GsonBuilder() + .registerTypeAdapter(SerializableBufferedImage.class, new SerializableBufferedImageTypeAdapter()) + .registerTypeAdapter(Part.class, new PartTypeAdapter()) + .registerTypeAdapter(SkinParts.class, new SkinPartsTypeAdapter()) + .registerTypeAdapter(SProperty.class, new SPropertyTypeAdapter()) + .registerTypeAdapter(Skin.class, new SkinTypeAdapter()) + .registerTypeAdapter(PlayerData.class, new PlayerDataTypeAdapter()) + .setPrettyPrinting() + .create(); + this.fileManager = FileManager.getInstance(); + try { + this.fileManager.loadFiles(); + } catch (Exception e) { + throw new RuntimeException(e); + } + this.skinFileCache = new SkinFileCache(); + this.skinFileCache.cache(); + this.skinProvider = new SkinProvider(); + try { + MessagesRegistry.registerAll(new MessageEntry[][]{ + CommandMessages.values(), + CoreMessages.values() + }); + } catch (Exception e) { + getLogger().log(Level.SEVERE, "Error loading the language file: ", e); + } + PlayerFileManager playerFileManager = new PlayerFileManager(new File(getDataFolder(), "playerdata")); + EntityManagerRegistry.registerManager(PlayerData.class, playerFileManager); + + SkinFileManager skinFileManager = new SkinFileManager(new File(getDataFolder(), "skindata")); + EntityManagerRegistry.registerManager(Skin.class, skinFileManager); + + playerFileManager.loadAll(); + skinFileManager.loadAll(); + } + + /** + * Enables the plugin components. Should be called during plugin enable phase. + */ + public void onEnable() { + CompletionEngine.registerResolver("@overlays", (commandIssuer, args) -> + skinFileCache.getSkinConfigurationFiles().keySet()); + this.commandManager.registerCommand(new SkinOverlayMain()); + getLogger().info("Commands registered successfully!"); + + // Register event listeners + PlayerListeners playerListeners = new PlayerListeners(); + this.getEventBus().register(SPlayerJoinEvent.class, playerListeners::onPlayerJoin, HandlerPriority.LOWEST); + this.getEventBus().register(SPlayerLeaveEvent.class, playerListeners::onPlayerLeave, HandlerPriority.LOWEST); + } + + /** + * Disables the plugin components. Should be called during plugin disable phase. + */ + public void onDisable() { + if (this.scheduler != null) { + this.scheduler.cancelTasks(getPlugin()); + } + if (this.audienceProvider != null) { + this.audienceProvider.close(); + audienceProvider = null; + } + + EntityManagerRegistry.getManager(PlayerData.class).ifPresent(EntityManager::saveAll); + EntityManagerRegistry.getManager(Skin.class).ifPresent(EntityManager::saveAll); + } + + /** + * Gets the Gson instance used for JSON operations. + * + * @return the Gson instance + */ + public Gson getGson() { + return gson; + } + + /** + * Gets the FileManager instance. + * + * @return the FileManager + */ + public FileManager getFileManager() { + return fileManager; + } + + /** + * Gets the configuration FileConfiguration. + * + * @return the FileConfiguration + */ + public FileConfiguration getConfig() { + return fileManager.getConfig().getFileConfiguration(); + } + + /** + * Gets the plugin data folder. + * + * @return the data folder + */ + public File getDataFolder() { + return dataFolder; + } + + /** + * Sets the plugin data folder. + * + * @param dataFolder the data folder + */ + public void setDataFolder(File dataFolder) { + this.dataFolder = dataFolder; + } + + /** + * Gets the skins data folder. + * + * @return the skins data folder + */ + public File getSkinsDataFolder() { + return new File(getDataFolder(), "skins"); + } + + /** + * Gets the CommandManager instance. + * + * @return the CommandManager + */ + public CommandManager getCommandManager() { + return commandManager; + } + + /** + * Sets the CommandManager instance. + * + * @param commandManager the CommandManager + */ + public void setCommandManager(CommandManager commandManager) { + this.commandManager = commandManager; + } + + /** + * Gets the plugin instance. + * + * @param plugin type + * @return the plugin instance + */ + public T getPlugin() { + //noinspection unchecked + return (T) plugin; + } + + /** + * Sets the plugin instance. + * + * @param plugin the plugin instance + * @param plugin type + */ + public void setPlugin(T plugin) { + this.plugin = plugin; + } + + /** + * Gets the MinecraftScheduler instance. + * + * @return the MinecraftScheduler + */ + public MinecraftScheduler getScheduler() { + //noinspection unchecked + return (MinecraftScheduler) scheduler; + } + + /** + * Sets the MinecraftScheduler instance. + * + * @param scheduler the MinecraftScheduler + */ + public void setScheduler(MinecraftScheduler scheduler) { + this.scheduler = scheduler; + } + + /** + * Gets the AudienceProvider instance. + * + * @return the AudienceProvider + */ + public AudienceProvider getAudienceProvider() { + return audienceProvider; + } + + /** + * Sets the AudienceProvider instance. + * + * @param audienceProvider the AudienceProvider + */ + public void setAudienceProvider(AudienceProvider audienceProvider) { + this.audienceProvider = audienceProvider; + } + + /** + * Gets the logger instance. + * + * @return the logger + */ + public Logger getLogger() { + return logger; + } + + /** + * Sets the logger instance. + * + * @param logger the logger + */ + public void setLogger(Logger logger) { + this.logger = logger; + } + + /** + * Gets the PlayerProvider instance. + * + * @return the PlayerProvider + */ + public PlayerProvider getPlayerProvider() { + return playerProvider; + } + + /** + * Sets the PlayerProvider instance. + * + * @param playerProvider the PlayerProvider + */ + public void setPlayerProvider(PlayerProvider playerProvider) { + this.playerProvider = playerProvider; + } + + /** + * Gets the GameProfileProvider instance. + * + * @return the GameProfileProvider + */ + public GameProfileProvider getGameProfileProvider() { + return gameProfileProvider; + } + + /** + * Sets the GameProfileProvider instance. + * + * @param gameProfileProvider the GameProfileProvider + */ + public void setGameProfileProvider(GameProfileProvider gameProfileProvider) { + this.gameProfileProvider = gameProfileProvider; + } + + /** + * Gets the SkinProvider instance. + * + * @return the SkinProvider + */ + public SkinProvider getSkinProvider() { + return skinProvider; + } + + /** + * Gets the SkinApplier instance. + * + * @return the SkinApplier + */ + public SkinApplier getSkinApplier() { + return skinApplier; + } + + /** + * Sets the SkinApplier instance. + * + * @param skinApplier the SkinApplier + */ + public void setSkinApplier(SkinApplier skinApplier) { + this.skinApplier = skinApplier; + } + + /** + * Gets the SkinHook instance. + * + * @return the SkinHook + */ + public SkinHook getSkinHook() { + return skinHook; + } + + /** + * Sets the SkinHook instance. + * + * @param skinHook the SkinHook + */ + public void setSkinHook(SkinHook skinHook) { + this.skinHook = skinHook; + } + + /** + * 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; + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/appliers/NoopSkinApplier.java b/common/src/main/java/com/georgev22/skinoverlay/appliers/NoopSkinApplier.java new file mode 100644 index 00000000..0501492f --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/appliers/NoopSkinApplier.java @@ -0,0 +1,14 @@ +package com.georgev22.skinoverlay.appliers; + +import com.georgev22.skinoverlay.player.SPlayer; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + +public class NoopSkinApplier extends SkinApplier { + + @Override + protected @NotNull CompletableFuture sendPackets(@NotNull SPlayer player) { + return CompletableFuture.completedFuture(true); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier.java b/common/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier.java new file mode 100644 index 00000000..6698ce04 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier.java @@ -0,0 +1,66 @@ +package com.georgev22.skinoverlay.appliers; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.registry.EntityManagerRegistry; +import com.georgev22.skinoverlay.skin.SGameProfile; +import com.georgev22.skinoverlay.storage.EntityManager; +import com.georgev22.skinoverlay.storage.data.PlayerData; +import com.georgev22.skinoverlay.storage.data.Skin; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public abstract class SkinApplier { + + protected SkinOverlay skinOverlay = SkinOverlay.getInstance(); + + /** + * Apply the skin for the specified {@link SPlayer} + * + * @param player Player's {@link SPlayer} object. + * @param skin Skin + */ + public void setSkin(@NotNull SPlayer player, @NotNull Skin skin) { + SGameProfile gameProfile = skinOverlay.getGameProfileProvider().getGameProfile(player); + gameProfile.setProperty("textures", skin.getProperty()); + skinOverlay.getGameProfileProvider().applyUpdatedGameProfile(player); + applySkin(player, skin); + + Optional> optionalPlayerDataEntityManager = EntityManagerRegistry.getManager(PlayerData.class); + if (optionalPlayerDataEntityManager.isEmpty()) { + return; + } + EntityManager playerDataEntityManager = optionalPlayerDataEntityManager.get(); + Optional optionalPlayerData = playerDataEntityManager.findById(player.getUniqueId()); + if (optionalPlayerData.isEmpty()) { + return; + } + PlayerData playerData = optionalPlayerData.get(); + playerData.setCurrentSkin(skin); + + playerDataEntityManager.save(playerData); + } + + /** + * Apply the skin for the specified {@link SPlayer} + * + * @param player Player's {@link SPlayer} object. + */ + protected void applySkin(@NotNull final SPlayer player) { + } + + /** + * Apply the skin for the specified {@link SPlayer} + * + * @param player Player's {@link SPlayer} object. + * @param skin Skin + */ + protected void applySkin(@NotNull final SPlayer player, @NotNull final Skin skin) { + applySkin(player); + } + + protected abstract @NotNull CompletableFuture sendPackets(@NotNull SPlayer player); + +} 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..ccdce4dc --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/BaseCommand.java @@ -0,0 +1,372 @@ +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.maps.HashObjectMap; +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. + *

+ * Supports: + *

    + *
  • Subcommands (nested BaseCommand or method-based)
  • + *
  • Pre- and post-processing hooks
  • + *
  • Permission checks
  • + *
  • Player-only enforcement
  • + *
  • Automatic argument conversion (primitives, String, Bukkit types, custom resolvers)
  • + *
  • 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, + new HashObjectMap() + .append("%command%", context.getData().getOr("command", "")) + .append("%targets%", String.join(", ", targets)), + true); + return; + } + + // Permission check + if (permission != null && !permission.isEmpty() && !sender.hasPermission(permission)) { + CommandMessages.COMMAND_PERMISSION_DENIED.msg(sender, + new HashObjectMap() + .append("%command%", context.getData().getOr("command", "")), + true); + 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, + new HashObjectMap() + .append("%command%", context.getData().getOr("command", "")) + .append("%usage%", usage), + true); + } + } + + // Run postprocessors + runPostprocessors(sender, context); + } catch (Exception e) { + CommandMessages.COMMAND_ERROR.msg(sender, + new HashObjectMap() + .append("%command%", context.getData().getOr("command", "")), + true); + 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. + *

+ * + * @param commandIssuer the sender of the command, representing either a player or console + * @param args the arguments passed to the command + * @param context the {@link CommandContext} containing custom data or state for this execution + * @deprecated Annotate a method with {@link Default} instead. + */ + @Deprecated + protected boolean handle(@NotNull CommandIssuer commandIssuer, String @NotNull [] args, @NotNull CommandContext context) { + if (getUsage() != null) { + CommandMessages.COMMAND_USAGE.msg(commandIssuer, + new HashObjectMap() + .append("%command%", context.getData().getOr("command", "")) + .append("%usage%", usage), + true); + } + return false; + } + + static A getAnnotation(Class annotationClass, @Nullable Method method, Object parent) { + A ann = method == null ? null : method.getAnnotation(annotationClass); + if (ann == null) { + if (parent instanceof Class) { + ann = ((Class) parent).getAnnotation(annotationClass); + } else { + ann = parent.getClass().getAnnotation(annotationClass); + } + } + return ann; + } + +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/CommandContext.java b/common/src/main/java/com/georgev22/skinoverlay/command/CommandContext.java new file mode 100644 index 00000000..da9ec2e9 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/CommandContext.java @@ -0,0 +1,26 @@ +package com.georgev22.skinoverlay.command; + +import com.georgev22.skinoverlay.utilities.CustomData; + +/** + * Represents the context of a command execution. + *

+ * Provides a place to store custom data related to the current command execution, + * which can be used by preprocessors, postprocessors, or the command itself. + */ +public class CommandContext { + + /** + * Stores custom data associated with this command execution. + */ + private final CustomData customData = new CustomData(); + + /** + * Returns the custom data container for this command execution. + * + * @return the {@link CustomData} object + */ + public CustomData getData() { + return customData; + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/CommandIssuer.java b/common/src/main/java/com/georgev22/skinoverlay/command/CommandIssuer.java new file mode 100644 index 00000000..0a0114ef --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/CommandIssuer.java @@ -0,0 +1,96 @@ +package com.georgev22.skinoverlay.command; + +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * Represents an entity that can issue commands. + *

+ * Can be a player, console, or any custom command sender implementation. + */ +public interface CommandIssuer { + + /** + * Checks if the issuer is a player. + * + * @return true if the issuer is a player, false otherwise + */ + boolean isPlayer(); + + /** + * Sends a plain text message to the issuer. + * + * @param message the message to send + */ + void sendMessage(@NotNull String message); + + /** + * Sends multiple plain text messages to the issuer. + * + * @param messages the messages to send + */ + default void sendMessage(String @NotNull ... messages) { + for (String message : messages) { + sendMessage(message); + } + } + + /** + * Sends a rich text component to the issuer. + * + * @param component the component to send + */ + void sendMessage(@NotNull Component component); + + /** + * Sends multiple rich text components to the issuer. + * + * @param components the components to send + */ + default void sendMessage(Component @NotNull ... components) { + for (Component component : components) { + sendMessage(component); + } + } + + /** + * Returns the underlying issuer object. + *

+ * For example, a Bukkit {@code CommandSender} instance. + * + * @param the type of the underlying issuer + * @return the underlying issuer object + */ + @NotNull T getIssuer(); + + /** + * Checks if the issuer has a specific permission. + * + * @param permission the permission string + * @return true if the issuer has the permission, false otherwise + */ + boolean hasPermission(String permission); + + /** + * Checks if the issuer is an operator. + * + * @return true if the issuer is an op, false otherwise + */ + boolean isOp(); + + /** + * Gets the unique identifier (UUID) of the issuer. + * + * @return the UUID of the issuer + */ + UUID getUniqueId(); + + /** + * Gets the display name of the issuer. + * + * @return the name of the issuer + */ + String getName(); +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/CommandManager.java b/common/src/main/java/com/georgev22/skinoverlay/command/CommandManager.java new file mode 100644 index 00000000..e432ea0f --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/CommandManager.java @@ -0,0 +1,99 @@ +package com.georgev22.skinoverlay.command; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.command.annotation.CommandAlias; +import com.georgev22.skinoverlay.command.annotation.Subcommand; +import com.georgev22.skinoverlay.command.processors.PostProcessor; +import com.georgev22.skinoverlay.command.processors.PreProcessor; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; + +/** + * Central manager for registering and handling commands in the Core plugin. + *

+ * Supports registration of BaseCommand instances, nested subcommands, global preprocessors + * and postprocessors, and argument resolvers for tab completion and value parsing. + */ +public abstract class CommandManager { + + private final List globalPreprocessors = new ArrayList<>(); + private final List globalPostprocessors = new ArrayList<>(); + private final SkinOverlay plugin = SkinOverlay.getInstance(); + + protected CommandManager() { + // private constructor for singleton pattern + } + + /** + * Registers a {@link BaseCommand} and all its nested subcommands. + * + * @param command the command to register + */ + public void registerCommand(@NotNull BaseCommand command) { + try { + registerCommand0(command); + + for (Class innerClass : command.getClass().getDeclaredClasses()) { + if (!BaseCommand.class.isAssignableFrom(innerClass)) continue; + if (!innerClass.isAnnotationPresent(Subcommand.class)) continue; + + BaseCommand subcommand = (BaseCommand) innerClass.getDeclaredConstructor().newInstance(); + command.addSubcommand(subcommand); + + if (innerClass.isAnnotationPresent(CommandAlias.class)) { + registerCommand0(subcommand); + } + + registerCommand(subcommand); + } + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "Failed to register command: " + command.getClass().getName(), e); + } + } + + /** + * Registers a single command. + * + * @param command the command to register + */ + protected abstract void registerCommand0(@NotNull BaseCommand command); + + /** + * Adds a global preprocessor that runs before any command execution. + * + * @param preprocessor the preprocessor to add + */ + public void addGlobalPreprocessor(PreProcessor preprocessor) { + globalPreprocessors.add(preprocessor); + } + + /** + * Adds a global postprocessor that runs after any command execution. + * + * @param postprocessor the postprocessor to add + */ + public void addGlobalPostprocessor(PostProcessor postprocessor) { + globalPostprocessors.add(postprocessor); + } + + /** + * Returns a list of all global postprocessors. + * + * @return the global postprocessors + */ + public List getGlobalPostprocessors() { + return globalPostprocessors; + } + + /** + * Returns a list of all global preprocessors. + * + * @return the global preprocessors + */ + public List getGlobalPreprocessors() { + return globalPreprocessors; + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/CompletionEngine.java b/common/src/main/java/com/georgev22/skinoverlay/command/CompletionEngine.java new file mode 100644 index 00000000..99f99852 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/CompletionEngine.java @@ -0,0 +1,212 @@ +package com.georgev22.skinoverlay.command; + +import com.georgev22.skinoverlay.command.annotation.Argument; +import com.georgev22.skinoverlay.command.annotation.CommandCompletion; +import com.georgev22.skinoverlay.command.resolvers.ArgumentResolver; +import com.georgev22.skinoverlay.command.resolvers.PlayersResolver; +import com.georgev22.skinoverlay.command.resolvers.RangeResolver; +import com.georgev22.skinoverlay.maps.ConcurrentObjectMap; +import com.georgev22.skinoverlay.maps.ObjectMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.*; + +/** + * The {@code CompletionEngine} class provides a unified tab-completion system + * for the command framework. + *

+ * It supports both static completions (literal strings) and dynamic completions + * via registered {@link ArgumentResolver}s. Command classes can declare their + * completion pattern using the {@link CommandCompletion} annotation, while + * custom resolvers can be registered globally. + *

+ * Default resolvers include: + *

    + *
  • {@code @players} — provides online player names.
  • + *
  • {@code @range:min-max} — provides numeric ranges.
  • + *
+ * + * @see ArgumentResolver + * @see CommandCompletion + */ +public class CompletionEngine { + + /** + * Holds all registered argument resolvers, mapped by lowercase key. + */ + private static final ObjectMap resolvers = new ConcurrentObjectMap<>(); + + static { + registerDefaultResolvers(); + } + + /** + * Resolves possible tab completions for a command class and current user input. + * + * @param clazz the command class annotated with {@link CommandCompletion} + * @param commandIssuer the command issuer executing the command + * @param args the current arguments typed by the user + * @return a collection of completions filtered and sorted to match the current input; + * returns an empty collection if no matches are found or the class lacks {@link CommandCompletion} + */ + public static @NotNull Collection resolveCompletions( + @NotNull Class clazz, + CommandIssuer commandIssuer, + String[] args + ) { + if (!clazz.isAnnotationPresent(CommandCompletion.class)) { + return new ArrayList<>(); + } + + String pattern = clazz.getAnnotation(CommandCompletion.class).value(); + String[] parts = pattern.split(" "); + + if (args.length > parts.length) { + return new ArrayList<>(); + } + + String segment = parts[args.length - 1]; + return getStrings(commandIssuer, args, segment); + } + + /** + * Resolves possible tab completions for a method's arguments. + * + *

This method checks parameters annotated with {@link Argument} and uses + * the {@code completion} property to provide tab completions.

+ * + * @param method the method whose parameters are being completed + * @param commandIssuer the command issuer executing the command + * @param args the current arguments typed by the user + * @return a collection of completions filtered and sorted to match the current input; + * returns an empty collection if no matches are found or if no completion is defined + */ + public static @NotNull Collection resolveCompletions( + @NotNull Method method, + CommandIssuer commandIssuer, + String @NotNull [] args + ) { + Parameter[] parameters = Arrays.stream(method.getParameters()) + .filter(p -> p.isAnnotationPresent(Argument.class)) + .toArray(Parameter[]::new); + + if (args.length > parameters.length) { + return List.of(); + } + + Parameter param = parameters[args.length - 1]; + Argument argAnno = param.getAnnotation(Argument.class); + if (argAnno == null) { + return List.of(); + } + + String completion = argAnno.completion(); + if (completion.isEmpty()) { + return List.of(); + } + + return getStrings(commandIssuer, args, completion); + } + + /** + * Resolves an {@link ArgumentResolver} by its registered key. + * + * @param key the resolver key to look up — must start with {@code '@'} (case-insensitive) + * @return the corresponding {@link ArgumentResolver}, or {@code null} if none is found + */ + public static @Nullable ArgumentResolver resolve(@NotNull String key) { + if (!key.startsWith("@")) { + return null; + } + + String baseKey = key.toLowerCase(Locale.ROOT); + int colonIndex = baseKey.indexOf(':'); + if (colonIndex != -1) { + baseKey = baseKey.substring(0, colonIndex); + } + + return resolvers.get(baseKey); + } + + /** + * Registers a new {@link ArgumentResolver} for the given key. + * + * @param key the resolver key to register — must start with {@code '@'} + * @param resolver the {@link ArgumentResolver} instance to associate with the key + * @throws IllegalArgumentException if {@code key} does not start with {@code '@'} + * @throws NullPointerException if {@code resolver} is {@code null} + */ + public static void registerResolver(@NotNull String key, @NotNull ArgumentResolver resolver) { + if (!key.startsWith("@")) { + throw new IllegalArgumentException("Resolver key must start with '@' to avoid colliding with subcommands: " + key); + } + resolvers.put(key.toLowerCase(Locale.ROOT), Objects.requireNonNull(resolver, "resolver")); + } + + /** + * Unregisters an existing {@link ArgumentResolver} by key. + * + * @param key the resolver key to remove — must start with {@code '@'} + */ + public static void unregisterResolver(@NotNull String key) { + resolvers.remove(key.toLowerCase(Locale.ROOT)); + } + + /** + * Registers the built-in default argument resolvers. + * + *

Currently includes:

+ *
    + *
  • {@code @players} — resolves online player names.
  • + *
  • {@code @range} — resolves integer ranges (e.g., 1–10).
  • + *
+ */ + public static void registerDefaultResolvers() { + registerResolver("@players", new PlayersResolver()); + registerResolver("@range", new RangeResolver()); + } + + /** + * Processes a completion segment and returns matching suggestions. + * + *

The segment may contain literal options separated by {@code |} and/or + * dynamic resolver keys prefixed with {@code @}. If a resolver key contains + * a colon, the part after the colon is passed as an argument to the resolver.

+ * + *

Matches are filtered based on the current input (last element of {@code args}) + * and sorted with exact matches first, then by the index of the input in the option.

+ * + * @param commandIssuer the issuer of the command + * @param args the current arguments typed by the user + * @param segment the completion segment (literal options and/or resolver keys) + * @return an unmodifiable collection of matching completion strings + */ + private static @NotNull @Unmodifiable Collection getStrings(CommandIssuer commandIssuer, String[] args, @NotNull String segment) { + Collection completions = new ArrayList<>(); + + for (String option : segment.split("\\|")) { + if (option.startsWith("@")) { + String[] resolverParts = option.split(":", 2); + String resolverName = resolverParts[0]; + String[] resolverArgs = resolverParts.length > 1 ? new String[]{resolverParts[1]} : new String[0]; + + ArgumentResolver resolver = resolve(resolverName); + if (resolver != null) { + completions.addAll(resolver.resolve(commandIssuer, resolverArgs)); + } + } else { + completions.add(option); + } + } + + String currentInput = args[args.length - 1]; + return completions.stream() + .filter(s -> s.toLowerCase().startsWith(currentInput.toLowerCase())) + .sorted(Comparator.comparingInt(s -> s.equalsIgnoreCase(currentInput) ? 0 : s.toLowerCase().indexOf(currentInput.toLowerCase()))) + .toList(); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/ExecutionType.java b/common/src/main/java/com/georgev22/skinoverlay/command/ExecutionType.java new file mode 100644 index 00000000..0ce6b160 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/ExecutionType.java @@ -0,0 +1,20 @@ +package com.georgev22.skinoverlay.command; + +/** + * Represents how a command should be executed. + *
    + *
  • {@link #SYNC} — the command runs on the main server thread immediately.
  • + *
  • {@link #ASYNC} — the command runs asynchronously, off the main server thread.
  • + *
+ */ +public enum ExecutionType { + /** + * Execute the command on the main server thread (synchronously). + */ + SYNC, + + /** + * Execute the command asynchronously on a separate thread. + */ + ASYNC +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/MethodCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/MethodCommand.java new file mode 100644 index 00000000..a411e484 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/MethodCommand.java @@ -0,0 +1,164 @@ +package com.georgev22.skinoverlay.command; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.command.annotation.*; +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.message.messages.CommandMessages; +import com.georgev22.skinoverlay.player.SPlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.logging.Level; + +public class MethodCommand extends BaseCommand { + + private final BaseCommand parent; + protected final Method method; + private final boolean isDefault; + + public MethodCommand(@NotNull BaseCommand parent, @NotNull Method method) { + this.parent = parent; + this.method = method; + this.isDefault = method.isAnnotationPresent(Default.class); + + this.subCommandAliases = Optional.ofNullable(getAnnotation(Subcommand.class, method, parent)) + .map(Subcommand::value) + .orElse(new String[0]); + + this.permission = Optional.ofNullable(getAnnotation(Permission.class, method, parent)) + .map(Permission::value) + .orElse(""); + + this.description = Optional.ofNullable(getAnnotation(Description.class, method, parent)) + .map(Description::value) + .orElse(""); + + this.usage = Optional.ofNullable(getAnnotation(Usage.class, method, parent)) + .map(Usage::value) + .orElse(""); + + this.targets = Optional.ofNullable(getAnnotation(CommandTarget.class, method, parent)) + .map(CommandTarget::value) + .orElse(new String[]{"any"}); + } + + @Override + public void execute(CommandIssuer sender, String @NotNull [] args, CommandContext context) { + try { + method.setAccessible(true); + invokeHandler(sender, args, context); + } catch (Exception e) { + sender.sendMessage("§cError executing command."); + SkinOverlay.getInstance().getLogger() + .log(Level.SEVERE, "Error while executing command", e); + } + } + + public BaseCommand getParent() { + return parent; + } + + public boolean isDefault() { + return isDefault; + } + + @Override + public Collection tabComplete(CommandIssuer sender, String @NotNull [] args) { + Collection completions = CompletionEngine.resolveCompletions(method, sender, args); + if (!completions.isEmpty()) { + return completions; + } + + return CompletionEngine.resolveCompletions(method.getDeclaringClass(), sender, args); + } + + private void invokeHandler(@NotNull CommandIssuer sender, String[] args, CommandContext context) throws Exception { + List resolved = new ArrayList<>(); + int argIndex = 0; + + for (Parameter param : method.getParameters()) { + Class type = param.getType(); + + if (type.equals(CommandIssuer.class)) { + resolved.add(sender); + } else if (type.equals(CommandContext.class)) { + resolved.add(context); + } else if (type.equals(String[].class)) { + resolved.add(args); + } else if (param.isAnnotationPresent(Argument.class)) { + Argument argAnno = param.getAnnotation(Argument.class); + String argName = !argAnno.name().isEmpty() ? argAnno.name() : param.getName(); + String raw = argIndex < args.length ? args[argIndex] : null; + + if (raw == null || raw.isEmpty()) { + if (!argAnno.optional() && argAnno.defaultValue().isEmpty()) { + CommandMessages.COMMAND_MISSING_ARGUMENT.msg(sender, + new HashObjectMap() + .append("%command%", context.getData().getOr("command", "")) + .append("%arg%", argName), true); + return; + } + raw = argAnno.defaultValue(); + } + + Object value = convertArgument(raw, type); + + if (value == null) { + CommandMessages.COMMAND_INVALID_ARGUMENT.msg(sender, + new HashObjectMap() + .append("%command%", context.getData().getOr("command", "")) + .append("%arg%", argName), true); + return; + } + + // Resolve with completion key if present +// if (!argAnno.completion().isEmpty()) { +// var resolver = CompletionEngine.resolve(argAnno.completion()); +// if (resolver != null) value = resolver.resolveValue(sender, raw); +// } + + resolved.add(value); + argIndex++; + } else { + // fallback: raw string + resolved.add(argIndex < args.length ? args[argIndex++] : null); + } + } + + method.invoke(parent, resolved.toArray()); + } + + /** + * Converts a raw string argument into a typed object. + * + * @param raw the raw argument + * @param type the target type + * @return the converted object or null if conversion failed + */ + protected @Nullable Object convertArgument(@NotNull String raw, @NotNull Class type) { + try { + // primitives & String + if (type.equals(String.class)) return raw; + if (type.equals(int.class) || type.equals(Integer.class)) return Integer.parseInt(raw); + if (type.equals(double.class) || type.equals(Double.class)) return Double.parseDouble(raw); + if (type.equals(boolean.class) || type.equals(Boolean.class)) return Boolean.parseBoolean(raw); + if (type.equals(BigInteger.class)) return new BigInteger(raw); + if (type.equals(BigDecimal.class)) return new BigDecimal(raw); + + if (type.equals(SPlayer.class)) return SkinOverlay.getInstance().getPlayerProvider().getSPlayer(raw); + + // Fallback: raw string + return raw; + } catch (Exception e) { + return null; + } + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Argument.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Argument.java new file mode 100644 index 00000000..448c55a4 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Argument.java @@ -0,0 +1,67 @@ +package com.georgev22.skinoverlay.command.annotation; + +import java.lang.annotation.*; + +/** + * Marks a method parameter as a command argument with optional metadata. + *

+ * This annotation is placed on parameters of methods inside a class that extends + * {@link com.georgev22.skinoverlay.command.BaseCommand}. It allows specifying + * tab-completion hints, optional arguments, default values, and an explicit + * argument name. + *

+ * + *

Example usage:

+ *
{@code
+ * @Subcommand("teleport")
+ * public void onTeleport(
+ *         CommandIssuer sender,
+ *         @Argument(name = "target", completion = "@players") String target,
+ *         @Argument(name = "world", completion = "@worlds", optional = true, defaultValue = "world") String worldName
+ * ) {
+ *     sender.sendMessage("Teleporting " + target + " to " + worldName + "!");
+ * }
+ * }
+ * + *

Notes:

+ *
    + *
  • {@code name}: the display name for this argument (used in error messages, usage, etc).
  • + *
  • {@code completion}: the key used by the {@code CompletionEngine} to suggest values.
  • + *
  • {@code optional}: whether this argument can be omitted.
  • + *
  • {@code defaultValue}: the value to use if the argument is optional and not provided.
  • + *
+ */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Argument { + + /** + * The explicit name of the argument, used for display (e.g., in usage messages). + * If left empty, frameworks may fall back to reflection (param.getName()). + * + * @return the argument name + */ + String name() default ""; + + /** + * The completion key for this argument (e.g., "@players", "@worlds"). + * + * @return the completion key string + */ + String completion() default ""; + + /** + * Whether this argument is optional. + * + * @return true if the argument is optional, false otherwise + */ + boolean optional() default false; + + /** + * The default value to use if the argument is optional and not provided. + * + * @return the default value string + */ + String defaultValue() default ""; +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandAlias.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandAlias.java new file mode 100644 index 00000000..961abaec --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandAlias.java @@ -0,0 +1,36 @@ +package com.georgev22.skinoverlay.command.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares one or more aliases for a command class. + *

+ * This annotation is placed on a class that extends {@link com.georgev22.skinoverlay.command.BaseCommand} + * to define the main command label(s) that can be used to invoke it. + *

+ * + *

Example usage:

+ *
{@code
+ * @CommandAlias({"home", "homes"})
+ * public class HomeCommand extends BaseCommand {
+ *     // Command implementation here
+ * }
+ * }
+ * + *

In the above example, both {@code /home} and {@code /homes} would + * execute the {@code HomeCommand}.

+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface CommandAlias { + + /** + * One or more aliases that can be used to invoke the command. + * + * @return an array of command aliases + */ + String[] value(); +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandCompletion.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandCompletion.java new file mode 100644 index 00000000..13c5addc --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandCompletion.java @@ -0,0 +1,51 @@ +package com.georgev22.skinoverlay.command.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares the tab-completion pattern for a command or subcommand. + *

+ * This annotation is placed on a class that extends + * {@link com.georgev22.skinoverlay.command.BaseCommand} to define + * the default completion values shown when typing the command. + *

+ * + *

Example usage:

+ *
{@code
+ * @CommandAlias("teleport")
+ * @CommandCompletion("@players @worlds")
+ * public class TeleportCommand extends BaseCommand {
+ *     // Command implementation
+ * }
+ * }
+ * + *

In the above example:

+ *
    + *
  • The first argument will suggest online players.
  • + *
  • The second argument will suggest available worlds.
  • + *
+ * + *

+ * Completion values typically reference keys understood by your + * {@code CompletionEngine}, e.g. {@code @players}, {@code @worlds}, or + * custom registered completions. + *

+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface CommandCompletion { + + /** + * The completion pattern to apply to this command. + *

+ * Each argument is separated by a space, and may reference a completion key + * (e.g. {@code @players}) or a literal suggestion. + *

+ * + * @return the completion pattern string + */ + String value(); +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandTarget.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandTarget.java new file mode 100644 index 00000000..c6a0a99f --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandTarget.java @@ -0,0 +1,23 @@ +package com.georgev22.skinoverlay.command.annotation; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specifies which executor type(s) can execute this command. + *

+ * The value refers to a target name registered in the + * {@link com.georgev22.skinoverlay.registry.CommandTargetRegistry}. + *

+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface CommandTarget { + /** + * The name of the target (e.g. "player", "console", "any", etc.). + */ + String[] value() default {"any"}; +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Default.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Default.java new file mode 100644 index 00000000..f161ddb2 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Default.java @@ -0,0 +1,43 @@ +package com.georgev22.skinoverlay.command.annotation; + +import java.lang.annotation.*; + +/** + * Marks a command method as the default entry point for the command + * when no subcommand or arguments are specified. + *

+ * This annotation is placed on a method inside a class that extends + * {@link com.georgev22.skinoverlay.command.BaseCommand}. + * The annotated method will be executed if the command is run + * without any subcommand or extra arguments. + *

+ * + *

Example usage:

+ *
{@code
+ * @CommandAlias("home")
+ * public class HomeCommand extends BaseCommand {
+ *
+ *     @Default
+ *     public void onDefault(CommandIssuer sender) {
+ *         sender.sendMessage("Usage: /home ");
+ *     }
+ *
+ *     @Subcommand("set")
+ *     public void onSet(CommandIssuer sender, String name) {
+ *         // set home logic
+ *     }
+ * }
+ * }
+ * + *

Notes:

+ *
    + *
  • There should typically be only one method annotated with {@code @Default} per command class.
  • + *
  • The method can accept a {@link com.georgev22.skinoverlay.command.CommandIssuer} parameter + * and/or a {@code String[]} parameter for raw arguments.
  • + *
+ */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Default { +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Description.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Description.java new file mode 100644 index 00000000..e571695d --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Description.java @@ -0,0 +1,35 @@ +package com.georgev22.skinoverlay.command.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Provides a human-readable description for a command class. + *

+ * This annotation is placed on a class that extends + * {@link com.georgev22.skinoverlay.command.BaseCommand} to define + * a description that can be used in help menus, logs, or usage messages. + *

+ * + *

Example usage:

+ *
{@code
+ * @CommandAlias("home")
+ * @Description("Allows players to manage and teleport to their homes")
+ * public class HomeCommand extends BaseCommand {
+ *     // Command implementation
+ * }
+ * }
+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface Description { + + /** + * The description text for the command. + * + * @return the command description + */ + String value(); +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Execution.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Execution.java new file mode 100644 index 00000000..17a03aa8 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Execution.java @@ -0,0 +1,39 @@ +package com.georgev22.skinoverlay.command.annotation; + +import com.georgev22.skinoverlay.command.ExecutionType; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specifies the execution type of the command. + *

+ * This annotation is placed on a class that extends + * {@link com.georgev22.skinoverlay.command.BaseCommand} to indicate + * whether the command should run synchronously (SYNC) or asynchronously (ASYNC). + *

+ * + *

Example usage:

+ *
{@code
+ * @CommandAlias("backup")
+ * @Execution(ExecutionType.ASYNC)
+ * public class BackupCommand extends BaseCommand {
+ *     // Command implementation
+ * }
+ * }
+ * + *

By default, if this annotation is not present, the command runs synchronously.

+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Execution { + + /** + * The execution type of the command. + * + * @return {@link ExecutionType#SYNC} or {@link ExecutionType#ASYNC} + */ + ExecutionType value() default ExecutionType.SYNC; +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Permission.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Permission.java new file mode 100644 index 00000000..2e6e71c7 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Permission.java @@ -0,0 +1,38 @@ +package com.georgev22.skinoverlay.command.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Defines the permission node required to execute a command. + *

+ * This annotation is placed on a class that extends + * {@link com.georgev22.skinoverlay.command.BaseCommand} to specify + * which permission a user must have to run the command. + *

+ * + *

Example usage:

+ *
{@code
+ * @CommandAlias("fly")
+ * @Permission("core.fly")
+ * public class FlyCommand extends BaseCommand {
+ *     // Command implementation
+ * }
+ * }
+ * + *

If the permission is not set or is an empty string, the command + * will be available to all users.

+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface Permission { + + /** + * The permission node required to execute the command. + * + * @return the permission string + */ + String value(); +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Subcommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Subcommand.java new file mode 100644 index 00000000..335bf345 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Subcommand.java @@ -0,0 +1,43 @@ +package com.georgev22.skinoverlay.command.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a command class or method as a subcommand of a parent command. + *

+ * This annotation is placed on a class that extends {@link com.georgev22.skinoverlay.command.BaseCommand} + * or on a method inside a command class to indicate that it is a subcommand. + *

+ * + *

Example usage on a class:

+ *
{@code
+ * @Subcommand("list")
+ * public class ListSubCommand extends BaseCommand {
+ *     // Subcommand implementation
+ * }
+ * }
+ * + *

Example usage on a method:

+ *
{@code
+ * @Subcommand("set")
+ * public void onSet(CommandIssuer sender, String key, String value) {
+ *     // Subcommand logic
+ * }
+ * }
+ * + *

The {@code value()} array defines one or more aliases for the subcommand.

+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface Subcommand { + + /** + * One or more aliases for the subcommand. + * + * @return an array of subcommand names + */ + String[] value(); +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Usage.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Usage.java new file mode 100644 index 00000000..b40639b5 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Usage.java @@ -0,0 +1,38 @@ +package com.georgev22.skinoverlay.command.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Provides a usage string for a command, showing the correct syntax. + *

+ * This annotation is placed on a class that extends + * {@link com.georgev22.skinoverlay.command.BaseCommand} to indicate + * how the command should be executed. It is typically displayed + * when a user runs the command incorrectly or requests help. + *

+ * + *

Example usage:

+ *
{@code
+ * @CommandAlias("home")
+ * @Usage("/home  [name]")
+ * public class HomeCommand extends BaseCommand {
+ *     // Command implementation
+ * }
+ * }
+ * + *

The usage string is purely informational and does not enforce argument validation.

+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface Usage { + + /** + * The usage string describing the command syntax. + * + * @return the command usage + */ + String value(); +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayBaseCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayBaseCommand.java new file mode 100644 index 00000000..ff3c310b --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayBaseCommand.java @@ -0,0 +1,21 @@ +package com.georgev22.skinoverlay.command.commands; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.command.BaseCommand; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; +import java.util.logging.Level; + +public abstract class SkinOverlayBaseCommand extends BaseCommand { + protected final SkinOverlay mainPlugin = SkinOverlay.getInstance(); + + @Override + public void addSubcommand(@NotNull BaseCommand subcommand) { + try { + super.addSubcommand(subcommand); + } catch (Exception e) { + mainPlugin.getLogger().log(Level.SEVERE, "Failed to register subcommand " + subcommand.getClass().getName(), e); + } + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayMain.java b/common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayMain.java new file mode 100644 index 00000000..382e18bd --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayMain.java @@ -0,0 +1,29 @@ +package com.georgev22.skinoverlay.command.commands; + +import com.georgev22.skinoverlay.command.CommandContext; +import com.georgev22.skinoverlay.command.CommandIssuer; +import com.georgev22.skinoverlay.command.annotation.*; +import com.georgev22.skinoverlay.command.commands.sub.*; +import com.georgev22.skinoverlay.message.messages.CommandMessages; +import org.jetbrains.annotations.NotNull; + +@CommandAlias({"skinoverlay", "soverlay", "skino"}) +@Permission("skinoverlay.help") +@CommandCompletion("help|reload|wear|reset|url") +@Description("SkinOverlay commands") +@Usage("/skinoverlay [arguments]") +public class SkinOverlayMain extends SkinOverlayBaseCommand { + + public SkinOverlayMain() { + this.addSubcommand(new WearSubCommand()); + this.addSubcommand(new ClearSubCommand()); + this.addSubcommand(new WearUrlSubCommand()); + this.addSubcommand(new ReloadSubCommand()); + this.addSubcommand(new InfoSubCommand()); + } + + @Default + protected void handle(@NotNull CommandIssuer commandIssuer) { + CommandMessages.COMMAND_HELP.msg(commandIssuer); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/ClearSubCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/ClearSubCommand.java new file mode 100644 index 00000000..20556920 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/ClearSubCommand.java @@ -0,0 +1,74 @@ +package com.georgev22.skinoverlay.command.commands.sub; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.command.CommandIssuer; +import com.georgev22.skinoverlay.command.annotation.*; +import com.georgev22.skinoverlay.command.commands.SkinOverlayBaseCommand; +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.message.messages.CommandMessages; +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.storage.data.Skin; +import com.georgev22.skinoverlay.utilities.skin.SkinParts; +import org.jetbrains.annotations.NotNull; + +@Subcommand({"clear", "reset"}) +@CommandAlias("soclear") +@CommandCompletion("@players") +@Description("Removes the skin overlay") +@Permission("skinoverlay.wear.clear") +public class ClearSubCommand extends SkinOverlayBaseCommand { + + @Default + protected void handle(@NotNull CommandIssuer commandIssuer, @Argument(name = "target", completion = "@players", optional = true) SPlayer target) { + if (target == null) { + if (!commandIssuer.isPlayer()) { + CommandMessages.COMMAND_MISSING_ARGUMENT.msg(commandIssuer, + new HashObjectMap().append("%command%", "clear ").append("%arg%", "target"), true); + return; + } + SPlayer player = mainPlugin.getPlayerProvider().getSPlayer(commandIssuer.getUniqueId()); + if (player == null) { + return; + } + SkinParts skinParts = new SkinParts(null, "default"); + mainPlugin.getSkinProvider().retrieveOrGenerateSkin( + player, + skinParts) + .thenAcceptAsync(optionalSkin -> { + if (optionalSkin.isEmpty()) { + return; + } + Skin skin = optionalSkin.get(); + mainPlugin.getSkinApplier().setSkin(player, skin); + CommandMessages.COMMAND_OVERLAY_RESET.msg( + commandIssuer, + new HashObjectMap().append("%player%", player.getName()), + true + ); + + }, runnable -> SkinOverlay.getInstance().getScheduler().runTask(SkinOverlay.getInstance().getPlugin(), runnable)); + } else { + if (!target.isOnline()) { + CommandMessages.COMMAND_OFFLINE_PLAYER.msg(commandIssuer, new HashObjectMap().append("%player%", target.getName()), true); + return; + } + SkinParts skinParts = new SkinParts(null, "default"); + mainPlugin.getSkinProvider().retrieveOrGenerateSkin( + target, + skinParts) + .thenAcceptAsync(skinOptional -> { + if (skinOptional.isEmpty()) { + return; + } + Skin skin = skinOptional.get(); + mainPlugin.getSkinApplier().setSkin(target, skin); + CommandMessages.COMMAND_OVERLAY_RESET.msg( + commandIssuer, + new HashObjectMap().append("%player%", target.getName()), + true + ); + + }, runnable -> SkinOverlay.getInstance().getScheduler().runTask(SkinOverlay.getInstance().getPlugin(), runnable)); + } + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/InfoSubCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/InfoSubCommand.java new file mode 100644 index 00000000..59840c19 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/InfoSubCommand.java @@ -0,0 +1,41 @@ +package com.georgev22.skinoverlay.command.commands.sub; + +import com.georgev22.skinoverlay.BuildParameters; +import com.georgev22.skinoverlay.command.CommandIssuer; +import com.georgev22.skinoverlay.command.annotation.CommandAlias; +import com.georgev22.skinoverlay.command.annotation.Default; +import com.georgev22.skinoverlay.command.annotation.Permission; +import com.georgev22.skinoverlay.command.annotation.Subcommand; +import com.georgev22.skinoverlay.command.commands.SkinOverlayBaseCommand; +import org.jetbrains.annotations.NotNull; + +@Subcommand("info") +@CommandAlias("soinfo") +@Permission("skinoverlay.info") +public class InfoSubCommand extends SkinOverlayBaseCommand { + + @Default + protected void handle(@NotNull CommandIssuer commandIssuer) { + String pluginName = BuildParameters.PLUGIN_NAME; + String pluginVersion = BuildParameters.VERSION; + String pluginAuthor = BuildParameters.AUTHOR; + String pluginDescription = BuildParameters.DESCRIPTION; + String pluginWebsite = BuildParameters.URL; + String CIName = BuildParameters.CI_NAME; + String CIBuildNumber = BuildParameters.CI_BUILD_NUMBER; + String commit = BuildParameters.COMMIT; + String branch = BuildParameters.BRANCH; + String buildTime = BuildParameters.BUILD_TIME; + + commandIssuer.sendMessage("Name: " + pluginName); + commandIssuer.sendMessage("Version: " + pluginVersion); + commandIssuer.sendMessage("Author: " + pluginAuthor); + commandIssuer.sendMessage("Description: " + pluginDescription); + commandIssuer.sendMessage("Website: " + pluginWebsite); + commandIssuer.sendMessage("CI Name: " + CIName); + commandIssuer.sendMessage("CI Build Number: " + CIBuildNumber); + commandIssuer.sendMessage("Commit: " + commit); + commandIssuer.sendMessage("Branch: " + branch); + commandIssuer.sendMessage("Build Time: " + buildTime); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/ReloadSubCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/ReloadSubCommand.java new file mode 100644 index 00000000..26846995 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/ReloadSubCommand.java @@ -0,0 +1,35 @@ +package com.georgev22.skinoverlay.command.commands.sub; + +import com.georgev22.skinoverlay.command.CommandIssuer; +import com.georgev22.skinoverlay.command.annotation.CommandAlias; +import com.georgev22.skinoverlay.command.annotation.Default; +import com.georgev22.skinoverlay.command.annotation.Permission; +import com.georgev22.skinoverlay.command.annotation.Subcommand; +import com.georgev22.skinoverlay.command.commands.SkinOverlayBaseCommand; +import com.georgev22.skinoverlay.message.MessageEntry; +import com.georgev22.skinoverlay.message.MessagesRegistry; +import com.georgev22.skinoverlay.message.messages.CommandMessages; +import com.georgev22.skinoverlay.message.messages.CoreMessages; +import org.jetbrains.annotations.NotNull; + +import java.util.logging.Level; + +@Subcommand("reload") +@CommandAlias("soreload") +@Permission("skinoverlay.reload") +public class ReloadSubCommand extends SkinOverlayBaseCommand { + @Default + protected void handle(@NotNull CommandIssuer commandIssuer) { + //TODO RELOAD SKIN HANDLER + mainPlugin.getFileManager().getConfig().reloadFile(); + try { + MessagesRegistry.registerAll(new MessageEntry[][]{ + CommandMessages.values(), + CoreMessages.values(), + }); + } catch (Exception e) { + mainPlugin.getLogger().log(Level.SEVERE, "Error loading the language file: ", e); + } + CoreMessages.PLUGIN_RELOAD.msg(commandIssuer); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/WearSubCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/WearSubCommand.java new file mode 100644 index 00000000..9c636816 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/WearSubCommand.java @@ -0,0 +1,81 @@ +package com.georgev22.skinoverlay.command.commands.sub; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.command.CommandIssuer; +import com.georgev22.skinoverlay.command.annotation.*; +import com.georgev22.skinoverlay.command.commands.SkinOverlayBaseCommand; +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.message.messages.CommandMessages; +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.storage.data.Skin; +import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; +import com.georgev22.skinoverlay.utilities.skin.SkinParts; +import org.jetbrains.annotations.NotNull; + +import javax.imageio.ImageIO; +import java.io.File; +import java.io.IOException; +import java.util.logging.Level; + +@Subcommand({"wear", "overlay"}) +@CommandAlias("sowear") +@Permission("skinoverlay.wear.overlay") +@Description("Wear an overlay on a player's skin") +@CommandCompletion("@overlays @players") +public class WearSubCommand extends SkinOverlayBaseCommand { + + @Default + protected void handle(@NotNull CommandIssuer commandIssuer, + @Argument(name = "overlay", completion = "@overlays") String overlay, + @Argument(name = "target", completion = "@players", optional = true) SPlayer target) { + if (overlay == null || overlay.isEmpty()) { + CommandMessages.COMMAND_MISSING_ARGUMENT.msg(commandIssuer, new HashObjectMap() + .append("%command%", "wear ").append("%arg%", "overlay"), true); + return; + } + + if (target == null) { + if (!commandIssuer.isPlayer()) { + CommandMessages.COMMAND_MISSING_ARGUMENT.msg(commandIssuer, new HashObjectMap() + .append("%command%", "wear ").append("%arg%", "target"), true); + return; + } + target = mainPlugin.getPlayerProvider().getSPlayer(commandIssuer.getUniqueId()); + } + SkinParts skinParts; + try { + File overlayFile = new File(mainPlugin.getSkinsDataFolder(), overlay + ".png"); + if (!overlayFile.exists()) { + CommandMessages.COMMAND_OVERLAY_NOT_FOUND.msg(commandIssuer, new HashObjectMap().append("%overlay%", overlay), true); + return; + } + skinParts = new SkinParts(new SerializableBufferedImage(ImageIO.read(overlayFile)), overlay); + } catch (IOException e) { + mainPlugin.getLogger().log(Level.SEVERE, "Error while trying to load the skin: ", e); + return; + } + SPlayer finalTarget = target; + mainPlugin.getSkinProvider() + .retrieveOrGenerateSkin( + target, + skinParts + ).thenAcceptAsync(optionalSkin -> { + if (optionalSkin.isEmpty()) { + mainPlugin.getLogger().info("Skin is null"); + return; + } + Skin skin = optionalSkin.get(); + mainPlugin.getSkinApplier() + .setSkin(finalTarget, skin); + CommandMessages.COMMAND_OVERLAY_DONE.msg( + commandIssuer, + new HashObjectMap() + .append("%player%", finalTarget.getName()) + .append("%url%", skin.skinURL()) + .append("%name%", skin.getSkinParts().getSkinName()) + .append("%skinParts%", skin.getSkinParts().toString()), + true + ); + }, runnable -> SkinOverlay.getInstance().getScheduler().runTask(SkinOverlay.getInstance().getPlugin(), runnable)); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/WearUrlSubCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/WearUrlSubCommand.java new file mode 100644 index 00000000..a7784c09 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/WearUrlSubCommand.java @@ -0,0 +1,100 @@ +package com.georgev22.skinoverlay.command.commands.sub; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.command.CommandIssuer; +import com.georgev22.skinoverlay.command.annotation.*; +import com.georgev22.skinoverlay.command.commands.SkinOverlayBaseCommand; +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.message.messages.CommandMessages; +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.storage.data.Skin; +import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; +import com.georgev22.skinoverlay.utilities.skin.SkinParts; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.imageio.ImageIO; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.net.URL; +import java.util.logging.Level; + +@Subcommand("url") +@CommandAlias("sowearurl") +@Permission("skinoverlay.wear.url") +@Description("Wear an overlay on a player's skin") +@CommandCompletion(" @players") +public class WearUrlSubCommand extends SkinOverlayBaseCommand { + + @Default + protected void handle(@NotNull CommandIssuer commandIssuer, + @Argument(name = "url") String urlStr, + @Argument(name = "target", completion = "@players", optional = true) SPlayer target) { + + try { + URL url = new URL(urlStr); + + byte[] imageBytes = downloadImageBytes(url); + if (imageBytes == null) { + CommandMessages.COMMAND_INVALID_URL.msg(commandIssuer, new HashObjectMap().append("%url%", url.toString()), true); + return; + } + + if (target == null && !commandIssuer.isPlayer()) { + CommandMessages.COMMAND_MISSING_ARGUMENT.msg(commandIssuer, new HashObjectMap().append("%command%", "url ").append("%arg%", "target"), true); + return; + } + + if (target == null) + target = mainPlugin.getPlayerProvider().getSPlayer(commandIssuer.getUniqueId()); + + SkinParts skinParts = createSkinPartsFromBytes(imageBytes, url); + applySkinToPlayer(commandIssuer, target, skinParts); + + } catch (Exception e) { + mainPlugin.getLogger().log(Level.SEVERE, "Error executing WearUrlSubCommand:", e); + CommandMessages.COMMAND_ERROR.msg(commandIssuer, new HashObjectMap().append("%error%", e.getMessage()), true); + } + } + + private byte @Nullable [] downloadImageBytes(URL url) { + try (InputStream stream = url.openStream(); ByteArrayOutputStream output = new ByteArrayOutputStream()) { + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = stream.read(buffer)) != -1) { + output.write(buffer, 0, bytesRead); + } + return output.toByteArray(); + } catch (Exception e) { + mainPlugin.getLogger().log(Level.WARNING, "Failed to download image from URL: " + url, e); + return null; + } + } + + private @NotNull SkinParts createSkinPartsFromBytes(byte[] bytes, URL url) throws Exception { + SerializableBufferedImage bufferedImage = new SerializableBufferedImage(ImageIO.read(new ByteArrayInputStream(bytes))); + return new SkinParts(bufferedImage, "customURL-" + url); + } + + private void applySkinToPlayer(CommandIssuer issuer, SPlayer player, SkinParts skinParts) { + mainPlugin.getSkinProvider().retrieveOrGenerateSkin(player, skinParts).thenAcceptAsync(optionalSkin -> { + if (optionalSkin.isEmpty()) { + CommandMessages.COMMAND_ERROR.msg(issuer, new HashObjectMap().append("%error%", "Failed to retrieve or generate skin."), true); + return; + } + + Skin skin = optionalSkin.get(); + mainPlugin.getSkinApplier().setSkin(player, skin); + + CommandMessages.COMMAND_OVERLAY_DONE.msg( + issuer, + new HashObjectMap() + .append("%player%", player.getName()) + .append("%url%", skin.skinURL()) + .append("%overlay%", skin.getSkinParts().getSkinName()), + true + ); + }, runnable -> SkinOverlay.getInstance().getScheduler().runTask(SkinOverlay.getInstance().getPlugin(), runnable)); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/processors/PostProcessor.java b/common/src/main/java/com/georgev22/skinoverlay/command/processors/PostProcessor.java new file mode 100644 index 00000000..00fc1a91 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/processors/PostProcessor.java @@ -0,0 +1,14 @@ +package com.georgev22.skinoverlay.command.processors; + +import com.georgev22.skinoverlay.command.BaseCommand; +import com.georgev22.skinoverlay.command.CommandContext; +import com.georgev22.skinoverlay.command.CommandIssuer; + +import java.util.function.Supplier; + +@FunctionalInterface +public interface PostProcessor { + + T process(Supplier supplier, CommandIssuer commandIssuer, CommandContext context); + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/processors/PreProcessor.java b/common/src/main/java/com/georgev22/skinoverlay/command/processors/PreProcessor.java new file mode 100644 index 00000000..c3f977f1 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/processors/PreProcessor.java @@ -0,0 +1,14 @@ +package com.georgev22.skinoverlay.command.processors; + +import com.georgev22.skinoverlay.command.BaseCommand; +import com.georgev22.skinoverlay.command.CommandContext; +import com.georgev22.skinoverlay.command.CommandIssuer; + +import java.util.function.Supplier; + +@FunctionalInterface +public interface PreProcessor { + + T process(Supplier supplier, CommandIssuer commandIssuer, CommandContext context); + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/ArgumentResolver.java b/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/ArgumentResolver.java new file mode 100644 index 00000000..cec7e8fd --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/ArgumentResolver.java @@ -0,0 +1,24 @@ +package com.georgev22.skinoverlay.command.resolvers; + +import com.georgev22.skinoverlay.command.CommandIssuer; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +@FunctionalInterface +public interface ArgumentResolver { + + /** + * Returns a list of possible completions for tab-completion. + */ + Collection resolve(@NotNull CommandIssuer commandIssuer, @NotNull String... args); + + /** + * Resolves the actual argument value to pass to the method. + * By default, returns the raw string. + */ + default Object resolveValue(@NotNull CommandIssuer commandIssuer, @NotNull String arg) { + return arg; + } +} + diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/PipeSeparatedArgumentResolver.java b/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/PipeSeparatedArgumentResolver.java new file mode 100644 index 00000000..c9363a4f --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/PipeSeparatedArgumentResolver.java @@ -0,0 +1,64 @@ +package com.georgev22.skinoverlay.command.resolvers; + +import com.georgev22.skinoverlay.command.CommandIssuer; +import com.georgev22.skinoverlay.command.CompletionEngine; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class PipeSeparatedArgumentResolver implements ArgumentResolver { + + private record ResolverWrapper(ArgumentResolver resolver, String[] args) { + } + + private final List resolvers = new ArrayList<>(); + + public PipeSeparatedArgumentResolver(@NotNull String pipeSeparated) { + String[] tokens = pipeSeparated.split("\\|"); + + for (String token : tokens) { + if (token.startsWith("@")) { + String stripped = token.substring(1); + String[] parts = stripped.split(":", 2); + String resolverKey = parts[0]; + String[] resolverArgs = parts.length > 1 ? new String[]{parts[1]} : new String[0]; + + ArgumentResolver dynamicResolver = CompletionEngine.resolve(resolverKey); + if (dynamicResolver != null) { + resolvers.add(new ResolverWrapper(dynamicResolver, resolverArgs)); + } + } else { + resolvers.add(new ResolverWrapper((sender, args) -> Collections.singletonList(token), new String[0])); + } + } + } + + @Override + public Collection resolve(@NotNull CommandIssuer commandIssuer, String... ignored) { + List results = new ArrayList<>(); + for (ResolverWrapper wrapper : resolvers) { + results.addAll(wrapper.resolver.resolve(commandIssuer, wrapper.args)); + } + return results; + } + + @Override + public Object resolveValue(@NotNull CommandIssuer commandIssuer, @NotNull String arg) { + for (ResolverWrapper wrapper : resolvers) { + if (wrapper.resolver == null) continue; + + Collection completions = wrapper.resolver.resolve(commandIssuer, wrapper.args); + if (completions.contains(arg)) { + return arg; + } + + Object value = wrapper.resolver.resolveValue(commandIssuer, arg); + if (value != null) return value; + } + + return arg; + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/PlayersResolver.java b/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/PlayersResolver.java new file mode 100644 index 00000000..75661e11 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/PlayersResolver.java @@ -0,0 +1,26 @@ +package com.georgev22.skinoverlay.command.resolvers; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.command.CommandIssuer; +import com.georgev22.skinoverlay.player.SPlayer; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.stream.Collectors; + +public class PlayersResolver implements ArgumentResolver { + + @Override + public List resolve(@NotNull CommandIssuer commandIssuer, String @NotNull ... args) { + String prefix = args.length > 0 ? args[0].toLowerCase() : ""; + return SkinOverlay.getInstance().getPlayerProvider().getOnlinePlayers().stream() + .map(SPlayer::getName) + .filter(name -> name.toLowerCase().startsWith(prefix)) + .collect(Collectors.toList()); + } + + @Override + public Object resolveValue(@NotNull CommandIssuer commandIssuer, @NotNull String arg) { + return SkinOverlay.getInstance().getPlayerProvider().getSPlayer(arg); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/RangeResolver.java b/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/RangeResolver.java new file mode 100644 index 00000000..2918b8bb --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/RangeResolver.java @@ -0,0 +1,108 @@ +package com.georgev22.skinoverlay.command.resolvers; + +import com.georgev22.skinoverlay.command.CommandIssuer; +import org.jetbrains.annotations.NotNull; + +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Resolves numeric ranges (integers or decimals) into lists of string values. + *

+ * Supported formats: + *

    + *
  • Integer range: {@code 1-5} → {@code ["1", "2", "3", "4", "5"]}
  • + *
  • Decimal range: {@code 1.1-2.0} → {@code ["1.1", "1.2", ..., "2.0"]} + *
    (step auto-detected based on decimal places)
  • + *
  • Range with explicit step: {@code 1-5:0.5} → {@code ["1.0", "1.5", "2.0", ..., "5.0"]}
  • + *
  • Negative numbers: {@code -3--1} → {@code ["-3", "-2", "-1"]}
  • + *
  • Descending ranges: {@code 5-1} → {@code ["5", "4", "3", "2", "1"]}
  • + *
+ *

+ * Notes: + *

    + *
  • Decimals are handled with {@link BigDecimal} to avoid floating-point errors.
  • + *
  • Output is formatted to match the maximum decimal places of the inputs or the step.
  • + *
  • If no explicit step is provided, it defaults to {@code 1} for integers or + * {@code 0.1}, {@code 0.01}, etc. for decimals (depending on precision).
  • + *
+ */ +public class RangeResolver implements ArgumentResolver { + + /** + * Regex supporting: + *
    + *
  • {@code start-end}
  • + *
  • {@code start-end:step}
  • + *
  • Optional +/- signs
  • + *
+ */ + private static final Pattern RANGE_PATTERN = + Pattern.compile("\\s*([+-]?\\d+(?:\\.\\d+)?)\\s*-\\s*([+-]?\\d+(?:\\.\\d+)?)(?::([+-]?\\d+(?:\\.\\d+)?))?\\s*"); + + /** + * Resolves a range string (like "1-5", "1.1-2.0", "1-5:0.5") into a list of values. + * + * @param commandIssuer the command issuer (unused here, but required by interface) + * @param args the arguments, where the first element is expected to be a range expression + * @return list of numbers as strings, or empty list if input is invalid + */ + @Override + public List resolve(@NotNull CommandIssuer commandIssuer, String @NotNull ... args) { + if (args.length == 0) return List.of(); + + Matcher m = RANGE_PATTERN.matcher(args[0]); + if (!m.matches()) return List.of(); + + String left = m.group(1); + String right = m.group(2); + String stepStr = m.group(3); + + BigDecimal start = new BigDecimal(left); + BigDecimal end = new BigDecimal(right); + + int decimals = Math.max(getDecimalPlaces(left), getDecimalPlaces(right)); + + BigDecimal step; + if (stepStr != null) { + step = new BigDecimal(stepStr); + decimals = Math.max(decimals, getDecimalPlaces(stepStr)); + } else { + step = BigDecimal.ONE.movePointLeft(decimals); + } + + if (step.compareTo(BigDecimal.ZERO) <= 0) return List.of(); + + DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(Locale.ROOT); + DecimalFormat df = new DecimalFormat(); + df.setDecimalFormatSymbols(symbols); + df.setGroupingUsed(false); + df.setMinimumFractionDigits(decimals); + df.setMaximumFractionDigits(decimals); + + List out = new ArrayList<>(); + + if (start.compareTo(end) <= 0) { + for (BigDecimal cur = start; cur.compareTo(end) <= 0; cur = cur.add(step)) { + out.add(df.format(cur)); + } + } else { + for (BigDecimal cur = start; cur.compareTo(end) >= 0; cur = cur.subtract(step)) { + out.add(df.format(cur)); + } + } + + return out; + } + + private int getDecimalPlaces(@NotNull String s) { + int idx = s.indexOf('.'); + return idx < 0 ? 0 : s.length() - idx - 1; + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/event/Cancellable.java b/common/src/main/java/com/georgev22/skinoverlay/event/Cancellable.java new file mode 100644 index 00000000..e6a644c1 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/event/Cancellable.java @@ -0,0 +1,21 @@ +package com.georgev22.skinoverlay.event; + +/** + * Interface for events that can be cancelled to stop further propagation. + */ +public interface Cancellable { + + /** + * Checks if this event is cancelled. + * + * @return true if cancelled, false otherwise + */ + boolean isCancelled(); + + /** + * Sets the cancelled state of this event. + * + * @param cancelled true to cancel, false to allow propagation + */ + void setCancelled(boolean cancelled); +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/event/Event.java b/common/src/main/java/com/georgev22/skinoverlay/event/Event.java new file mode 100644 index 00000000..240b63af --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/event/Event.java @@ -0,0 +1,7 @@ +package com.georgev22.skinoverlay.event; + +/** + * Marker interface for all events dispatched through the EventBus. + */ +public interface Event { +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/event/EventBus.java b/common/src/main/java/com/georgev22/skinoverlay/event/EventBus.java new file mode 100644 index 00000000..72aac295 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/event/EventBus.java @@ -0,0 +1,126 @@ +package com.georgev22.skinoverlay.event; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * The EventBus manages event handlers and dispatches events to them + * in priority order, supporting cancellation and monitor handlers. + */ +public class EventBus { + + /** + * Internal wrapper to store handler and its priority. + * + * @param the event type + */ + private record HandlerWrapper(EventHandler handler, HandlerPriority priority) { + } + + /** + * Map storing handlers registered for each event class. + */ + private final Map, List>> handlers = new ConcurrentHashMap<>(); + + /** + * Registers an event handler for a specific event class with a given priority. + * + * @param eventClass the event class + * @param handler the event handler + * @param priority the handler priority + * @param the event type + */ + public void register(Class eventClass, EventHandler handler, HandlerPriority priority) { + handlers.computeIfAbsent(eventClass, k -> new ArrayList<>()).add(new HandlerWrapper<>(handler, priority)); + handlers.get(eventClass).sort(Comparator.comparing(h -> h.priority.ordinal())); + } + + /** + * Registers an event handler and returns a Registration for later unregistration. + * + * @param eventClass the event class + * @param handler the handler + * @param priority the priority + * @param the event type + * @return a Registration object to unregister this handler + */ + public Registration registerWithReturn(Class eventClass, EventHandler handler, HandlerPriority priority) { + register(eventClass, handler, priority); + return new Registration<>(this, eventClass, handler); + } + + /** + * Unregisters an event handler from a specific event class. + * + * @param eventClass the event class + * @param handler the handler to remove + * @param the event type + */ + public void unregister(Class eventClass, EventHandler handler) { + List> list = handlers.get(eventClass); + if (list != null) { + list.removeIf(wrapper -> wrapper.handler.equals(handler)); + } + } + + /** + * Posts an event to all registered handlers. + * Handlers are called in priority order from LOWEST to HIGHEST. + * If the event implements {@link Cancellable} and is cancelled during propagation, + * further handlers are skipped except MONITOR handlers. + * + * @param event the event instance to post + * @param the event type + */ + public void post(@NotNull T event) { + List> list = handlers.get(event.getClass()); + if (list != null) { + for (HandlerWrapper wrapper : list) { + if (wrapper.priority == HandlerPriority.MONITOR) continue; + + @SuppressWarnings("unchecked") + HandlerWrapper w = (HandlerWrapper) wrapper; + w.handler.handle(event); + + if (event instanceof Cancellable && ((Cancellable) event).isCancelled()) { + break; + } + } + for (HandlerWrapper wrapper : list) { + if (wrapper.priority != HandlerPriority.MONITOR) continue; + + @SuppressWarnings("unchecked") + HandlerWrapper w = (HandlerWrapper) wrapper; + w.handler.handle(event); + } + } + } + + + /** + * Represents a registration that can be unregistered. + */ + public static class Registration { + private final EventBus bus; + private final Class eventClass; + private final EventHandler handler; + + private Registration(EventBus bus, Class eventClass, EventHandler handler) { + this.bus = bus; + this.eventClass = eventClass; + this.handler = handler; + } + + /** + * Unregisters this handler from its event class. + */ + public void unregister() { + bus.unregister(eventClass, handler); + } + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/event/EventHandler.java b/common/src/main/java/com/georgev22/skinoverlay/event/EventHandler.java new file mode 100644 index 00000000..e44b4150 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/event/EventHandler.java @@ -0,0 +1,17 @@ +package com.georgev22.skinoverlay.event; + +/** + * Functional interface for handling events of type T. + * + * @param the event type + */ +@FunctionalInterface +public interface EventHandler { + + /** + * Handles the given event. + * + * @param event the event to handle + */ + void handle(T event); +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/event/HandlerPriority.java b/common/src/main/java/com/georgev22/skinoverlay/event/HandlerPriority.java new file mode 100644 index 00000000..7528b941 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/event/HandlerPriority.java @@ -0,0 +1,38 @@ +package com.georgev22.skinoverlay.event; + +/** + * Defines the priority of event handlers. + * Handlers are executed from LOWEST to HIGHEST, then MONITOR runs last regardless of cancellation. + */ +public enum HandlerPriority { + /** + * Lowest priority, runs first. + */ + LOWEST, + + /** + * Low priority, runs after LOWEST. + */ + LOW, + + /** + * Normal priority, runs after LOW. + */ + NORMAL, + + /** + * High priority, runs after NORMAL. + */ + HIGH, + + /** + * Highest priority, runs after HIGH. + */ + HIGHEST, + + /** + * Monitor priority, always runs last regardless of cancellation. + * Use for logging or observing without modifying the event flow. + */ + MONITOR +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerDataLoadEvent.java b/common/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerDataLoadEvent.java new file mode 100644 index 00000000..002e23d1 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerDataLoadEvent.java @@ -0,0 +1,18 @@ +package com.georgev22.skinoverlay.event.events.player; + +import com.georgev22.skinoverlay.event.Event; +import com.georgev22.skinoverlay.storage.data.PlayerData; + +@SuppressWarnings("ClassCanBeRecord") +public class PlayerDataLoadEvent implements Event { + private final PlayerData playerData; + + public PlayerDataLoadEvent(PlayerData playerData) { + this.playerData = playerData; + } + + + public PlayerData getPlayerData() { + return playerData; + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerDataSaveEvent.java b/common/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerDataSaveEvent.java new file mode 100644 index 00000000..36079d18 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerDataSaveEvent.java @@ -0,0 +1,19 @@ +package com.georgev22.skinoverlay.event.events.player; + +import com.georgev22.skinoverlay.event.Event; +import com.georgev22.skinoverlay.storage.data.PlayerData; + +@SuppressWarnings("ClassCanBeRecord") +public class PlayerDataSaveEvent implements Event { + private final PlayerData playerData; + + public PlayerDataSaveEvent(PlayerData playerData) { + this.playerData = playerData; + } + + + public PlayerData getPlayerData() { + return playerData; + } + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/event/events/player/SPlayerJoinEvent.java b/common/src/main/java/com/georgev22/skinoverlay/event/events/player/SPlayerJoinEvent.java new file mode 100644 index 00000000..901837a5 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/event/events/player/SPlayerJoinEvent.java @@ -0,0 +1,19 @@ +package com.georgev22.skinoverlay.event.events.player; + +import com.georgev22.skinoverlay.event.Event; +import com.georgev22.skinoverlay.player.SPlayer; + +@SuppressWarnings("ClassCanBeRecord") +public class SPlayerJoinEvent implements Event { + + private final SPlayer player; + + public SPlayerJoinEvent(SPlayer player) { + this.player = player; + } + + public SPlayer getPlayer() { + return player; + } + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/event/events/player/SPlayerLeaveEvent.java b/common/src/main/java/com/georgev22/skinoverlay/event/events/player/SPlayerLeaveEvent.java new file mode 100644 index 00000000..bfaa7c6c --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/event/events/player/SPlayerLeaveEvent.java @@ -0,0 +1,19 @@ +package com.georgev22.skinoverlay.event.events.player; + +import com.georgev22.skinoverlay.event.Event; +import com.georgev22.skinoverlay.player.SPlayer; + +@SuppressWarnings("ClassCanBeRecord") +public class SPlayerLeaveEvent implements Event { + + private final SPlayer player; + + public SPlayerLeaveEvent(SPlayer player) { + this.player = player; + } + + public SPlayer getPlayer() { + return player; + } + +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/exceptions/MessageEncryptionException.java b/common/src/main/java/com/georgev22/skinoverlay/exceptions/MessageEncryptionException.java new file mode 100644 index 00000000..599b2687 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/exceptions/MessageEncryptionException.java @@ -0,0 +1,10 @@ +package com.georgev22.skinoverlay.exceptions; + +/** + * Custom runtime exception indicating encryption or decryption failure during message processing. + */ +public class MessageEncryptionException extends RuntimeException { + public MessageEncryptionException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/exceptions/NotFoundException.java b/common/src/main/java/com/georgev22/skinoverlay/exceptions/NotFoundException.java new file mode 100644 index 00000000..79d5e40e --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/exceptions/NotFoundException.java @@ -0,0 +1,34 @@ +package com.georgev22.skinoverlay.exceptions; + +public class NotFoundException extends Exception { + + /** + * Constructs a new NotFoundException based on the given + * Exception + * + * @param throwable Exception that triggered this Exception + */ + public NotFoundException(final Throwable throwable) { + super(throwable); + } + + /** + * Constructs a new NotFoundException with the given message + * + * @param message Brief message explaining the cause of the exception + */ + public NotFoundException(String message) { + super(message); + } + + /** + * Constructs a new NotFoundException based on the given + * Exception + * + * @param message Brief message explaining the cause of the exception + * @param throwable Exception that triggered this Exception + */ + public NotFoundException(String message, Throwable throwable) { + super(message, throwable); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/exceptions/PairDocumentException.java b/common/src/main/java/com/georgev22/skinoverlay/exceptions/PairDocumentException.java new file mode 100644 index 00000000..6218e6dc --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/exceptions/PairDocumentException.java @@ -0,0 +1,36 @@ +package com.georgev22.skinoverlay.exceptions; + +public class PairDocumentException extends RuntimeException { + + /** + * Constructs a new PairDocumentException based on the given + * Exception + * + * @param throwable Exception that triggered this Exception + */ + public PairDocumentException(final Throwable throwable) { + super(throwable); + } + + /** + * Constructs a new PairDocumentException with the given message + * + * @param message Brief message explaining the cause of the exception + */ + public PairDocumentException(String message) { + super(message); + } + + /** + * Constructs a new PairDocumentException based on the given + * Exception + * + * @param message Brief message explaining the cause of the exception + * @param throwable Exception that triggered this Exception + */ + public PairDocumentException(String message, Throwable throwable) { + super(message, throwable); + } + + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/exceptions/ReflectionException.java b/common/src/main/java/com/georgev22/skinoverlay/exceptions/ReflectionException.java new file mode 100644 index 00000000..6be52edd --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/exceptions/ReflectionException.java @@ -0,0 +1,35 @@ +package com.georgev22.skinoverlay.exceptions; + +public class ReflectionException extends RuntimeException { + + /** + * Constructs a new ReflectionException based on the given + * Exception + * + * @param throwable Exception that triggered this Exception + */ + public ReflectionException(final Throwable throwable) { + super(throwable); + } + + /** + * Constructs a new ReflectionException with the given message + * + * @param message Brief message explaining the cause of the exception + */ + public ReflectionException(String message) { + super(message); + } + + /** + * Constructs a new ReflectionException based on the given + * Exception + * + * @param message Brief message explaining the cause of the exception + * @param throwable Exception that triggered this Exception + */ + public ReflectionException(String message, Throwable throwable) { + super(message, throwable); + } + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/exceptions/SkinException.java b/common/src/main/java/com/georgev22/skinoverlay/exceptions/SkinException.java new file mode 100644 index 00000000..dd20f779 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/exceptions/SkinException.java @@ -0,0 +1,11 @@ +package com.georgev22.skinoverlay.exceptions; + +public class SkinException extends RuntimeException { + public SkinException(String message) { + super(message); + } + + public SkinException(Throwable throwable) { + super(throwable); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/hooks/SkinHook.java b/common/src/main/java/com/georgev22/skinoverlay/hooks/SkinHook.java new file mode 100644 index 00000000..e3b6eec5 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/hooks/SkinHook.java @@ -0,0 +1,25 @@ +package com.georgev22.skinoverlay.hooks; + +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.skin.SProperty; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +/** + * The SkinHook interface defines the methods required for a skin hook. + * A skin hook is responsible for retrieving the skin property for a given player object. + */ +public interface SkinHook { + + /** + * Retrieves the SProperty for the given {@link SPlayer}. + * + * @param player The player object to retrieve the SProperty for. + * @return The SProperty for the given {@link SPlayer}, or null if the property cannot be retrieved. + */ + @Nullable SProperty getProperty(@NotNull SPlayer player); + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/hooks/SkinHookNoop.java b/common/src/main/java/com/georgev22/skinoverlay/hooks/SkinHookNoop.java new file mode 100644 index 00000000..b4c11e41 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/hooks/SkinHookNoop.java @@ -0,0 +1,16 @@ +package com.georgev22.skinoverlay.hooks; + +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.skin.SProperty; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * The SkinHookNoop class implements the SkinHook interface and returns null for all property retrievals. + */ +public class SkinHookNoop implements SkinHook { + @Override + public @Nullable SProperty getProperty(@NotNull SPlayer player) { + return null; + } +} diff --git a/core/src/main/java/com/georgev22/skinoverlay/hook/hooks/SkinsRestorerHook.java b/common/src/main/java/com/georgev22/skinoverlay/hooks/SkinsRestorerHook.java similarity index 58% rename from core/src/main/java/com/georgev22/skinoverlay/hook/hooks/SkinsRestorerHook.java rename to common/src/main/java/com/georgev22/skinoverlay/hooks/SkinsRestorerHook.java index b9472041..d0229b6b 100644 --- a/core/src/main/java/com/georgev22/skinoverlay/hook/hooks/SkinsRestorerHook.java +++ b/common/src/main/java/com/georgev22/skinoverlay/hooks/SkinsRestorerHook.java @@ -1,19 +1,17 @@ -package com.georgev22.skinoverlay.hook.hooks; +package com.georgev22.skinoverlay.hooks; import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.handler.SProperty; -import com.georgev22.skinoverlay.hook.SkinHook; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.skin.SProperty; import net.skinsrestorer.api.SkinsRestorer; import net.skinsrestorer.api.SkinsRestorerProvider; +import net.skinsrestorer.api.exception.DataRequestException; import net.skinsrestorer.api.property.SkinProperty; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.IOException; import java.util.Optional; -import java.util.concurrent.ExecutionException; /** * SkinsRestorerHook class implements the SkinHook interface for handling skin property @@ -45,21 +43,19 @@ public SkinsRestorerHook() { * If the player does not have a skin set or the skin data cannot be retrieved, * the method returns null and the skin data is retrieved using SkinOverlay. * - * @param playerObject the PlayerObject to retrieve skin property data for + * @param player the {@link SPlayer} to retrieve skin property data for * @return the SProperty object containing the skin property data, or null if the skin data could not be retrieved - * @throws IOException if there is an IO error - * @throws ExecutionException if there is an Execution error - * @throws InterruptedException if the thread is interrupted */ @Override @Nullable - public SProperty getProperty(@NotNull PlayerObject playerObject) throws IOException, ExecutionException, InterruptedException { + public SProperty getProperty(@NotNull SPlayer player) { + Optional property; try { - Optional property = skinsRestorerAPI.getPlayerStorage().getSkinForPlayer(playerObject.playerUUID(), playerObject.playerName()); - return property.map(skinProperty -> new SProperty(SkinProperty.TEXTURES_NAME, skinProperty.getValue(), skinProperty.getSignature())).orElse(null); - } catch (Exception exception) { - return skinOverlay.getDefaultSkinHook().getProperty(playerObject); + property = skinsRestorerAPI.getPlayerStorage().getSkinForPlayer(player.getUniqueId(), player.getName()); + } catch (DataRequestException e) { + return null; } + return property.map(skinProperty -> new SProperty(skinProperty.getValue(), skinProperty.getSignature())).orElse(null); } } diff --git a/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java b/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java new file mode 100644 index 00000000..8e6e5fb0 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java @@ -0,0 +1,91 @@ +package com.georgev22.skinoverlay.listeners; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.event.events.player.SPlayerJoinEvent; +import com.georgev22.skinoverlay.event.events.player.SPlayerLeaveEvent; +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.registry.EntityManagerRegistry; +import com.georgev22.skinoverlay.skin.SGameProfile; +import com.georgev22.skinoverlay.skin.SProperty; +import com.georgev22.skinoverlay.storage.EntityManager; +import com.georgev22.skinoverlay.storage.data.PlayerData; +import com.georgev22.skinoverlay.storage.data.Skin; +import com.georgev22.skinoverlay.utilities.Utils; +import com.georgev22.skinoverlay.utilities.config.OptionsUtil; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.Optional; +import java.util.UUID; +import java.util.logging.Level; + +public class PlayerListeners { + + private final SkinOverlay mainPlugin = SkinOverlay.getInstance(); + + public void onPlayerJoin(@NotNull SPlayerJoinEvent event) { + SPlayer player = event.getPlayer(); + + if (!mainPlugin.isProxy() && OptionsUtil.PROXY.getBooleanValue()) { + // If we don't delay the publishPlayerJoin, the proxy won't receive the plugin message (idk why) + this.mainPlugin.getScheduler().createDelayedForEntity( + this.mainPlugin.getPlugin(), + () -> mainPlugin.getMessageManager().publishPlayerJoin(player.getUniqueId()), + () -> { + }, + player.getPlayer(), + 20L + ); + return; + } + + Optional> playerDataManagerOpt = EntityManagerRegistry.getManager(PlayerData.class); + Optional> skinManagerOpt = EntityManagerRegistry.getManager(Skin.class); + + if (playerDataManagerOpt.isEmpty() || skinManagerOpt.isEmpty()) { + return; + } + + EntityManager playerDataManager = playerDataManagerOpt.get(); + EntityManager skinManager = skinManagerOpt.get(); + + PlayerData playerData = playerDataManager.findById(player.getUniqueId()).orElse(null); + + if (playerData == null) { + return; + } + + SGameProfile gameProfile = player.getGameProfile(); + SProperty property = gameProfile.getProperty("textures"); + + if (property == null) { + try { + property = mainPlugin.getSkinProvider().getJavaSkin(player); + } catch (IOException e) { + mainPlugin.getLogger().log(Level.SEVERE, "Error loading Skin:", e); + return; + } + } + + UUID uuid = Utils.generateUUID("default" + player.getUniqueId()); + Skin defaultSkin = skinManager.findById(uuid).orElseGet(() -> new Skin(uuid)); + defaultSkin.setProperty(property); + + playerData.setDefaultSkin(defaultSkin); + + if (playerData.getCurrentSkin() != null && !playerData.getCurrentSkin().equals(defaultSkin)) { + mainPlugin.getSkinApplier().setSkin(player, playerData.getCurrentSkin()); + } + + skinManager.save(defaultSkin); + playerDataManager.save(playerData); + } + + public void onPlayerLeave(@NotNull SPlayerLeaveEvent event) { + SPlayer player = event.getPlayer(); + EntityManagerRegistry.getManager(PlayerData.class) + .ifPresent(entityManager -> entityManager.findById(player.getUniqueId()) + .ifPresent(entityManager::save)); + } + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/maps/ConcurrentObjectMap.java b/common/src/main/java/com/georgev22/skinoverlay/maps/ConcurrentObjectMap.java new file mode 100644 index 00000000..b6f734b0 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/maps/ConcurrentObjectMap.java @@ -0,0 +1,504 @@ +package com.georgev22.skinoverlay.maps; + +import org.jetbrains.annotations.NotNull; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static java.lang.String.format; + +public class ConcurrentObjectMap extends ConcurrentHashMap implements ObjectMap { + + /** + * Creates an ConcurrentObjectMap instance. + */ + public ConcurrentObjectMap() { + } + + /** + * Creates a ConcurrentObjectMap instance initialized with the given map. + * + * @param map initial map + */ + public ConcurrentObjectMap(final ObjectMap map) { + putAll(map); + } + + /** + * Creates a ConcurrentObjectMap instance initialized with the given map. + * + * @param map initial map + */ + public ConcurrentObjectMap(final Map map) { + putAll(map); + } + + /** + * Constructs a new ConcurrentObjectMap with the specified initial capacity. + * + * @param initialCapacity The initial capacity of the ConcurrentObjectMap. + */ + public ConcurrentObjectMap(final int initialCapacity) { + super(initialCapacity); + } + + /** + * Constructs a new ConcurrentObjectMap with the specified initial capacity and load factor. + * + * @param initialCapacity The initial capacity of the ConcurrentObjectMap. + * @param loadFactor The load factor of the ConcurrentObjectMap. + */ + public ConcurrentObjectMap(final int initialCapacity, final float loadFactor) { + super(initialCapacity, loadFactor); + } + + /** + * Put/replace the given key/value pair into this User and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.append("a", 1).append("b", 2)}
+     * 
+ * + * @param key key + * @param value value + * @return this + */ + public ConcurrentObjectMap append(final K key, final V value) { + if (containsKey(key)) { + replace(key, value); + } else { + put(key, value); + } + return this; + } + + @Override + public ConcurrentObjectMap append(@NotNull Map map) { + for (Entry entry : map.entrySet()) { + append(entry.getKey(), entry.getValue()); + } + return this; + } + + @Override + public ConcurrentObjectMap append(@NotNull ObjectMap map) { + for (Entry entry : map.entrySet()) { + append(entry.getKey(), entry.getValue()); + } + return this; + } + + /** + * Put/replace the given key/value pair into ConcurrentObjectMap if boolean is true and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.append("a", 1, check1).append("b", 2, check2)}
+     * 
+ * + * @param key key + * @param value value + * @param ifTrue ifTrue + * @return this + */ + public ConcurrentObjectMap appendIfTrue(final K key, final V value, boolean ifTrue) { + if (ifTrue) + append(key, value); + return this; + } + + /** + * Put/replace the given key/value pair into ObjectMap if boolean is true or not and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, 2, check1).appendIfTrue("b", 3, 4, check2)}
+     * 
+ * + * @param key key + * @param valueIfTrue the value if the ifTrue is true + * @param valueIfFalse the value if the ifTrue is false + * @param ifTrue ifTrue + * @return this + */ + public ConcurrentObjectMap appendIfTrue(final K key, final V valueIfTrue, final V valueIfFalse, boolean ifTrue) { + if (ifTrue) { + append(key, valueIfTrue); + } else { + append(key, valueIfFalse); + } + return this; + } + + /** + * Put/replace a given map into this ObjectMap if boolean is true and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, check1).appendIfTrue(map, check2)}
+     * 
+ * + * @param map key + * @param ifTrue ifTrue + * @return this + */ + @Override + public ConcurrentObjectMap appendIfTrue(@NotNull Map map, boolean ifTrue) { + for (Entry entry : map.entrySet()) { + appendIfTrue(entry.getKey(), entry.getValue(), ifTrue); + } + return this; + } + + /** + * Put/replace the given key/value pair into ObjectMap if boolean is true or not and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, 2, check1).appendIfTrue(map1, map2, check2)}
+     * 
+ * + * @param mapIfTrue the map if the ifTrue is true + * @param mapIfFalse the map if the ifTrue is false + * @param ifTrue ifTrue + * @return this + */ + @Override + public ConcurrentObjectMap appendIfTrue(Map mapIfTrue, Map mapIfFalse, boolean ifTrue) { + if (ifTrue) { + append(mapIfTrue); + } else { + append(mapIfFalse); + } + return this; + } + + /** + * Put/replace a given map into this ObjectMap if boolean is true and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, check1).appendIfTrue(map, check2)}
+     * 
+ * + * @param map key + * @param ifTrue ifTrue + * @return this + */ + @Override + public ConcurrentObjectMap appendIfTrue(@NotNull ObjectMap map, boolean ifTrue) { + for (Entry entry : map.entrySet()) { + appendIfTrue(entry.getKey(), entry.getValue(), ifTrue); + } + return this; + } + + /** + * Put/replace the given key/value pair into ObjectMap if boolean is true or not and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, 2, check1).appendIfTrue(map1, map2, check2)}
+     * 
+ * + * @param mapIfTrue the map if the ifTrue is true + * @param mapIfFalse the map if the ifTrue is false + * @param ifTrue ifTrue + * @return this + */ + @Override + public ConcurrentObjectMap appendIfTrue(ObjectMap mapIfTrue, Map mapIfFalse, boolean ifTrue) { + if (ifTrue) { + append(mapIfTrue); + } else { + append(mapIfFalse); + } + return this; + } + + /** + * Removes the entry with the specified key from the ObjectMap. + * + * @param key the key of the entry to be removed + * @return the modified ObjectMap with the specified entry removed, or the original ObjectMap if the key was not found + */ + @Override + public ConcurrentObjectMap removeEntry(K key) { + remove(key); + return this; + } + + /** + * Removes all entries with keys present in the specified map from the ObjectMap. + * + * @param map the map containing the keys to be removed + * @return the modified ObjectMap with the entries corresponding to the specified keys removed + */ + @Override + public ConcurrentObjectMap removeEntries(Map map) { + for (Entry entry : map.entrySet()) { + remove(entry.getKey()); + } + return this; + } + + /** + * Removes all entries with keys present in the specified ObjectMap from the ObjectMap. + * + * @param map the ObjectMap containing the keys to be removed + * @return the modified ObjectMap with the entries corresponding to the keys in the specified ObjectMap removed + */ + @Override + public ConcurrentObjectMap removeEntries(ObjectMap map) { + for (ObjectMap.Entry entry : map.entrySet()) { + remove(entry.getKey()); + } + return this; + } + + /** + * Removes the entry with the specified key from the ObjectMap if the condition is true. + * + * @param key the key of the entry to be removed + * @param ifTrue the condition to check before removing the entry + * @return the modified ObjectMap with the specified entry removed if the condition is true, or the original ObjectMap otherwise + */ + @Override + public ConcurrentObjectMap removeEntryIfTrue(K key, boolean ifTrue) { + if (ifTrue) { + remove(key); + } + return this; + } + + /** + * Removes all entries with keys present in the specified map from the ObjectMap if the condition is true. + * + * @param map the map containing the keys to be removed + * @param ifTrue the condition to check before removing the entries + * @return the modified ObjectMap with the entries corresponding to the keys in the specified map removed if the condition is true, or the original ObjectMap otherwise + */ + @Override + public ConcurrentObjectMap removeEntriesIfTrue(Map map, boolean ifTrue) { + if (ifTrue) { + for (Entry entry : map.entrySet()) { + remove(entry.getKey()); + } + } + return this; + } + + /** + * Removes all entries with keys present in the specified ObjectMap from the ObjectMap if the condition is true. + * + * @param map the ObjectMap containing the keys to be removed + * @param ifTrue the condition to check before removing the entries + * @return the modified ObjectMap with the entries corresponding to the keys in the specified ObjectMap removed if the condition is true, or the original ObjectMap otherwise + */ + @Override + public ConcurrentObjectMap removeEntriesIfTrue(ObjectMap map, boolean ifTrue) { + if (ifTrue) { + for (ObjectMap.Entry entry : map.entrySet()) { + remove(entry.getKey()); + } + } + return this; + } + + /** + * Gets the value of the given key as an Integer. + * + * @param key the key + * @return the value as an integer, which may be null + * @throws ClassCastException if the value is not an integer + */ + public Integer getInteger(final Object key) { + return getInteger(key, 0); + } + + /** + * Gets the value of the given key as a primitive int. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as an integer, which may be null + * @throws ClassCastException if the value is not an integer + */ + public int getInteger(final Object key, final int defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value of the given key as a Long. + * + * @param key the key + * @return the value as a long, which may be null + * @throws ClassCastException if the value is not an long + */ + public Long getLong(final Object key) { + return getLong(key, 0L); + } + + /** + * Gets the value of the given key as a Long. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a long, which may be null + * @throws ClassCastException if the value is not an long + */ + public Long getLong(final Object key, final long defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value of the given key as a Double. + * + * @param key the key + * @return the value as a double, which may be null + * @throws ClassCastException if the value is not an double + */ + public Double getDouble(final Object key) { + return getDouble(key, 0D); + } + + /** + * Gets the value of the given key as a Double. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a double, which may be null + * @throws ClassCastException if the value is not an double + */ + public Double getDouble(final Object key, final double defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value of the given key as a String. + * + * @param key the key + * @return the value as a String, which may be null + * @throws ClassCastException if the value is not a String + */ + public String getString(final Object key) { + return getString(key, ""); + } + + /** + * Gets the value of the given key as a String. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a String, which may be null + * @throws ClassCastException if the value is not a String + */ + public String getString(final Object key, final String defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value of the given key as a Boolean. + * + * @param key the key + * @return the value as a Boolean, which may be null + * @throws ClassCastException if the value is not an boolean + */ + public Boolean getBoolean(final Object key) { + return getBoolean(key, false); + } + + /** + * Gets the value of the given key as a primitive boolean. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a primitive boolean + * @throws ClassCastException if the value is not a boolean + */ + public boolean getBoolean(final Object key, final boolean defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value of the given key as a Date. + * + * @param key the key + * @return the value as a Date, which may be null + * @throws ClassCastException if the value is not a Date + */ + public Date getDate(final Object key) { + return getDate(key, new Date()); + } + + /** + * Gets the value of the given key as a Date. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a Date, which may be null + * @throws ClassCastException if the value is not a Date + */ + public Date getDate(final Object key, final Date defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the list value of the given key, casting the list elements to the given {@code Class}. This is useful to avoid having + * casts in client code, though the effect is the same. + * + * @param key the key + * @param clazz the non-null class to cast the list value to + * @param the type of the class + * @return the list value of the given key, or null if the instance does not contain this key. + * @throws ClassCastException if the elements in the list value of the given key is not of type T or the value is not a list + */ + public List getList(Object key, Class clazz) { + return getList(key, clazz, null); + } + + /** + * Gets the list value of the given key, casting the list elements to {@code Class} or returning the default list value if null. + * This is useful to avoid having casts in client code, though the effect is the same. + * + * @param key the key + * @param clazz the non-null class to cast the list value to + * @param defaultValue what to return if the value is null + * @param the type of the class + * @return the list value of the given key, or the default list value if the instance does not contain this key. + * @throws ClassCastException if the value of the given key is not of type T + */ + public List getList(final Object key, final Class clazz, final List defaultValue) { + List value = get(key, List.class); + if (value == null) { + return defaultValue; + } + + for (Object item : value) { + if (!clazz.isAssignableFrom(item.getClass())) { + throw new ClassCastException(format("List element cannot be cast to %s", clazz.getName())); + } + } + return value; + } + + /** + * Gets the value of the given key, casting it to the given {@code Class}. This is useful to avoid having casts in client code, + * though the effect is the same. So to get the value of a key that is of type String, you would write {@code String name = + * doc.get("name", String.class)} instead of {@code String name = (String) doc.get("x") }. + * + * @param key the key + * @param clazz the non-null class to cast the value to + * @param the type of the class + * @return the value of the given key, or null if the instance does not contain this key. + * @throws ClassCastException if the value of the given key is not of type T + */ + public T get(final Object key, final Class clazz) { + return clazz.cast(get(key)); + } + + /** + * Gets the value of the given key, casting it to {@code Class} or returning the default value if null. + * This is useful to avoid having casts in client code, though the effect is the same. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @param the type of the class + * @return the value of the given key, or null if the instance does not contain this key. + * @throws ClassCastException if the value of the given key is not of type T + */ + public T get(final Object key, final T defaultValue) { + Object value = get(key); + return value == null ? defaultValue : (T) value; + } + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/maps/HashObjectMap.java b/common/src/main/java/com/georgev22/skinoverlay/maps/HashObjectMap.java new file mode 100644 index 00000000..b956f6bc --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/maps/HashObjectMap.java @@ -0,0 +1,504 @@ +package com.georgev22.skinoverlay.maps; + +import org.jetbrains.annotations.NotNull; + +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.lang.String.format; + +public class HashObjectMap extends HashMap implements ObjectMap { + + /** + * Creates an HashObjectMap instance. + */ + public HashObjectMap() { + } + + /** + * Creates a HashObjectMap instance initialized with the given map. + * + * @param map initial map + */ + public HashObjectMap(final ObjectMap map) { + putAll(map); + } + + /** + * Creates a HashObjectMap instance initialized with the given map. + * + * @param map initial map + */ + public HashObjectMap(final Map map) { + putAll(map); + } + + /** + * Constructs a new HashObjectMap with the specified initial capacity. + * + * @param initialCapacity The initial capacity of the HashObjectMap. + */ + public HashObjectMap(final int initialCapacity) { + super(initialCapacity); + } + + /** + * Constructs a new HashObjectMap with the specified initial capacity and load factor. + * + * @param initialCapacity The initial capacity of the HashObjectMap. + * @param loadFactor The load factor of the HashObjectMap. + */ + public HashObjectMap(final int initialCapacity, final float loadFactor) { + super(initialCapacity, loadFactor); + } + + /** + * Put/replace the given key/value pair into this User and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.append("a", 1).append("b", 2)}
+     * 
+ * + * @param key key + * @param value value + * @return this + */ + public HashObjectMap append(final K key, final V value) { + if (containsKey(key)) { + replace(key, value); + } else { + put(key, value); + } + return this; + } + + @Override + public HashObjectMap append(@NotNull Map map) { + for (Entry entry : map.entrySet()) { + append(entry.getKey(), entry.getValue()); + } + return this; + } + + @Override + public HashObjectMap append(@NotNull ObjectMap map) { + for (Entry entry : map.entrySet()) { + append(entry.getKey(), entry.getValue()); + } + return this; + } + + /** + * Put/replace the given key/value pair into HashObjectMap if boolean is true and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.append("a", 1, check1).append("b", 2, check2)}
+     * 
+ * + * @param key key + * @param value value + * @param ifTrue ifTrue + * @return this + */ + public HashObjectMap appendIfTrue(final K key, final V value, boolean ifTrue) { + if (ifTrue) + append(key, value); + return this; + } + + /** + * Put/replace the given key/value pair into ObjectMap if boolean is true or not and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, 2, check1).appendIfTrue("b", 3, 4, check2)}
+     * 
+ * + * @param key key + * @param valueIfTrue the value if the ifTrue is true + * @param valueIfFalse the value if the ifTrue is false + * @param ifTrue ifTrue + * @return this + */ + public HashObjectMap appendIfTrue(final K key, final V valueIfTrue, final V valueIfFalse, boolean ifTrue) { + if (ifTrue) { + append(key, valueIfTrue); + } else { + append(key, valueIfFalse); + } + return this; + } + + /** + * Put/replace a given map into this ObjectMap if boolean is true and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, check1).appendIfTrue(map, check2)}
+     * 
+ * + * @param map key + * @param ifTrue ifTrue + * @return this + */ + @Override + public HashObjectMap appendIfTrue(@NotNull Map map, boolean ifTrue) { + for (Entry entry : map.entrySet()) { + appendIfTrue(entry.getKey(), entry.getValue(), ifTrue); + } + return this; + } + + /** + * Put/replace the given key/value pair into ObjectMap if boolean is true or not and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, 2, check1).appendIfTrue(map1, map2, check2)}
+     * 
+ * + * @param mapIfTrue the map if the ifTrue is true + * @param mapIfFalse the map if the ifTrue is false + * @param ifTrue ifTrue + * @return this + */ + @Override + public HashObjectMap appendIfTrue(Map mapIfTrue, Map mapIfFalse, boolean ifTrue) { + if (ifTrue) { + append(mapIfTrue); + } else { + append(mapIfFalse); + } + return this; + } + + /** + * Put/replace a given map into this ObjectMap if boolean is true and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, check1).appendIfTrue(map, check2)}
+     * 
+ * + * @param map key + * @param ifTrue ifTrue + * @return this + */ + @Override + public HashObjectMap appendIfTrue(@NotNull ObjectMap map, boolean ifTrue) { + for (Entry entry : map.entrySet()) { + appendIfTrue(entry.getKey(), entry.getValue(), ifTrue); + } + return this; + } + + /** + * Put/replace the given key/value pair into ObjectMap if boolean is true or not and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, 2, check1).appendIfTrue(map1, map2, check2)}
+     * 
+ * + * @param mapIfTrue the map if the ifTrue is true + * @param mapIfFalse the map if the ifTrue is false + * @param ifTrue ifTrue + * @return this + */ + @Override + public HashObjectMap appendIfTrue(ObjectMap mapIfTrue, Map mapIfFalse, boolean ifTrue) { + if (ifTrue) { + append(mapIfTrue); + } else { + append(mapIfFalse); + } + return this; + } + + /** + * Removes the entry with the specified key from the ObjectMap. + * + * @param key the key of the entry to be removed + * @return the modified ObjectMap with the specified entry removed, or the original ObjectMap if the key was not found + */ + @Override + public HashObjectMap removeEntry(K key) { + remove(key); + return this; + } + + /** + * Removes all entries with keys present in the specified map from the ObjectMap. + * + * @param map the map containing the keys to be removed + * @return the modified ObjectMap with the entries corresponding to the specified keys removed + */ + @Override + public HashObjectMap removeEntries(Map map) { + for (Entry entry : map.entrySet()) { + remove(entry.getKey()); + } + return this; + } + + /** + * Removes all entries with keys present in the specified ObjectMap from the ObjectMap. + * + * @param map the ObjectMap containing the keys to be removed + * @return the modified ObjectMap with the entries corresponding to the keys in the specified ObjectMap removed + */ + @Override + public HashObjectMap removeEntries(ObjectMap map) { + for (ObjectMap.Entry entry : map.entrySet()) { + remove(entry.getKey()); + } + return this; + } + + /** + * Removes the entry with the specified key from the ObjectMap if the condition is true. + * + * @param key the key of the entry to be removed + * @param ifTrue the condition to check before removing the entry + * @return the modified ObjectMap with the specified entry removed if the condition is true, or the original ObjectMap otherwise + */ + @Override + public HashObjectMap removeEntryIfTrue(K key, boolean ifTrue) { + if (ifTrue) { + remove(key); + } + return this; + } + + /** + * Removes all entries with keys present in the specified map from the ObjectMap if the condition is true. + * + * @param map the map containing the keys to be removed + * @param ifTrue the condition to check before removing the entries + * @return the modified ObjectMap with the entries corresponding to the keys in the specified map removed if the condition is true, or the original ObjectMap otherwise + */ + @Override + public HashObjectMap removeEntriesIfTrue(Map map, boolean ifTrue) { + if (ifTrue) { + for (Entry entry : map.entrySet()) { + remove(entry.getKey()); + } + } + return this; + } + + /** + * Removes all entries with keys present in the specified ObjectMap from the ObjectMap if the condition is true. + * + * @param map the ObjectMap containing the keys to be removed + * @param ifTrue the condition to check before removing the entries + * @return the modified ObjectMap with the entries corresponding to the keys in the specified ObjectMap removed if the condition is true, or the original ObjectMap otherwise + */ + @Override + public HashObjectMap removeEntriesIfTrue(ObjectMap map, boolean ifTrue) { + if (ifTrue) { + for (ObjectMap.Entry entry : map.entrySet()) { + remove(entry.getKey()); + } + } + return this; + } + + /** + * Gets the value of the given key as an Integer. + * + * @param key the key + * @return the value as an integer, which may be null + * @throws ClassCastException if the value is not an integer + */ + public Integer getInteger(final Object key) { + return getInteger(key, 0); + } + + /** + * Gets the value of the given key as a primitive int. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as an integer, which may be null + * @throws ClassCastException if the value is not an integer + */ + public int getInteger(final Object key, final int defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value of the given key as a Long. + * + * @param key the key + * @return the value as a long, which may be null + * @throws ClassCastException if the value is not an long + */ + public Long getLong(final Object key) { + return getLong(key, 0L); + } + + /** + * Gets the value of the given key as a Long. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a long, which may be null + * @throws ClassCastException if the value is not an long + */ + public Long getLong(final Object key, final long defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value of the given key as a Double. + * + * @param key the key + * @return the value as a double, which may be null + * @throws ClassCastException if the value is not an double + */ + public Double getDouble(final Object key) { + return getDouble(key, 0D); + } + + /** + * Gets the value of the given key as a Double. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a double, which may be null + * @throws ClassCastException if the value is not an double + */ + public Double getDouble(final Object key, final double defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value of the given key as a String. + * + * @param key the key + * @return the value as a String, which may be null + * @throws ClassCastException if the value is not a String + */ + public String getString(final Object key) { + return getString(key, ""); + } + + /** + * Gets the value of the given key as a String. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a String, which may be null + * @throws ClassCastException if the value is not a String + */ + public String getString(final Object key, final String defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value of the given key as a Boolean. + * + * @param key the key + * @return the value as a Boolean, which may be null + * @throws ClassCastException if the value is not an boolean + */ + public Boolean getBoolean(final Object key) { + return getBoolean(key, false); + } + + /** + * Gets the value of the given key as a primitive boolean. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a primitive boolean + * @throws ClassCastException if the value is not a boolean + */ + public boolean getBoolean(final Object key, final boolean defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value of the given key as a Date. + * + * @param key the key + * @return the value as a Date, which may be null + * @throws ClassCastException if the value is not a Date + */ + public Date getDate(final Object key) { + return getDate(key, new Date()); + } + + /** + * Gets the value of the given key as a Date. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a Date, which may be null + * @throws ClassCastException if the value is not a Date + */ + public Date getDate(final Object key, final Date defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the list value of the given key, casting the list elements to the given {@code Class}. This is useful to avoid having + * casts in client code, though the effect is the same. + * + * @param key the key + * @param clazz the non-null class to cast the list value to + * @param the type of the class + * @return the list value of the given key, or null if the instance does not contain this key. + * @throws ClassCastException if the elements in the list value of the given key is not of type T or the value is not a list + */ + public List getList(Object key, Class clazz) { + return getList(key, clazz, null); + } + + /** + * Gets the list value of the given key, casting the list elements to {@code Class} or returning the default list value if null. + * This is useful to avoid having casts in client code, though the effect is the same. + * + * @param key the key + * @param clazz the non-null class to cast the list value to + * @param defaultValue what to return if the value is null + * @param the type of the class + * @return the list value of the given key, or the default list value if the instance does not contain this key. + * @throws ClassCastException if the value of the given key is not of type T + */ + public List getList(final Object key, final Class clazz, final List defaultValue) { + List value = get(key, List.class); + if (value == null) { + return defaultValue; + } + + for (Object item : value) { + if (!clazz.isAssignableFrom(item.getClass())) { + throw new ClassCastException(format("List element cannot be cast to %s", clazz.getName())); + } + } + return value; + } + + /** + * Gets the value of the given key, casting it to the given {@code Class}. This is useful to avoid having casts in client code, + * though the effect is the same. So to get the value of a key that is of type String, you would write {@code String name = + * doc.get("name", String.class)} instead of {@code String name = (String) doc.get("x") }. + * + * @param key the key + * @param clazz the non-null class to cast the value to + * @param the type of the class + * @return the value of the given key, or null if the instance does not contain this key. + * @throws ClassCastException if the value of the given key is not of type T + */ + public T get(final Object key, final Class clazz) { + return clazz.cast(get(key)); + } + + /** + * Gets the value of the given key, casting it to {@code Class} or returning the default value if null. + * This is useful to avoid having casts in client code, though the effect is the same. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @param the type of the class + * @return the value of the given key, or null if the instance does not contain this key. + * @throws ClassCastException if the value of the given key is not of type T + */ + public T get(final Object key, final T defaultValue) { + Object value = get(key); + return value == null ? defaultValue : (T) value; + } + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/maps/LinkedObjectMap.java b/common/src/main/java/com/georgev22/skinoverlay/maps/LinkedObjectMap.java new file mode 100644 index 00000000..2b575295 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/maps/LinkedObjectMap.java @@ -0,0 +1,504 @@ +package com.georgev22.skinoverlay.maps; + +import org.jetbrains.annotations.NotNull; + +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static java.lang.String.format; + +public class LinkedObjectMap extends LinkedHashMap implements ObjectMap { + + /** + * Creates an LinkedObjectMap instance. + */ + public LinkedObjectMap() { + } + + /** + * Creates a LinkedObjectMap instance initialized with the given map. + * + * @param map initial map + */ + public LinkedObjectMap(final ObjectMap map) { + putAll(map); + } + + /** + * Creates a LinkedObjectMap instance initialized with the given map. + * + * @param map initial map + */ + public LinkedObjectMap(final Map map) { + putAll(map); + } + + /** + * Constructs a new LinkedObjectMap with the specified initial capacity. + * + * @param initialCapacity The initial capacity of the LinkedObjectMap. + */ + public LinkedObjectMap(final int initialCapacity) { + super(initialCapacity); + } + + /** + * Constructs a new LinkedObjectMap with the specified initial capacity and load factor. + * + * @param initialCapacity The initial capacity of the LinkedObjectMap. + * @param loadFactor The load factor of the LinkedObjectMap. + */ + public LinkedObjectMap(final int initialCapacity, final float loadFactor) { + super(initialCapacity, loadFactor); + } + + /** + * Put/replace the given key/value pair into this User and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.append("a", 1).append("b", 2)}
+     * 
+ * + * @param key key + * @param value value + * @return this + */ + public LinkedObjectMap append(final K key, final V value) { + if (containsKey(key)) { + replace(key, value); + } else { + put(key, value); + } + return this; + } + + @Override + public LinkedObjectMap append(@NotNull Map map) { + for (Map.Entry entry : map.entrySet()) { + append(entry.getKey(), entry.getValue()); + } + return this; + } + + @Override + public LinkedObjectMap append(@NotNull ObjectMap map) { + for (Map.Entry entry : map.entrySet()) { + append(entry.getKey(), entry.getValue()); + } + return this; + } + + /** + * Put/replace the given key/value pair into LinkedObjectMap if boolean is true and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.append("a", 1, check1).append("b", 2, check2)}
+     * 
+ * + * @param key key + * @param value value + * @param ifTrue ifTrue + * @return this + */ + public LinkedObjectMap appendIfTrue(final K key, final V value, boolean ifTrue) { + if (ifTrue) + append(key, value); + return this; + } + + /** + * Put/replace the given key/value pair into ObjectMap if boolean is true or not and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, 2, check1).appendIfTrue("b", 3, 4, check2)}
+     * 
+ * + * @param key key + * @param valueIfTrue the value if the ifTrue is true + * @param valueIfFalse the value if the ifTrue is false + * @param ifTrue ifTrue + * @return this + */ + public LinkedObjectMap appendIfTrue(final K key, final V valueIfTrue, final V valueIfFalse, boolean ifTrue) { + if (ifTrue) { + append(key, valueIfTrue); + } else { + append(key, valueIfFalse); + } + return this; + } + + /** + * Put/replace a given map into this ObjectMap if boolean is true and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, check1).appendIfTrue(map, check2)}
+     * 
+ * + * @param map key + * @param ifTrue ifTrue + * @return this + */ + @Override + public LinkedObjectMap appendIfTrue(@NotNull Map map, boolean ifTrue) { + for (Map.Entry entry : map.entrySet()) { + appendIfTrue(entry.getKey(), entry.getValue(), ifTrue); + } + return this; + } + + /** + * Put/replace the given key/value pair into ObjectMap if boolean is true or not and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, 2, check1).appendIfTrue(map1, map2, check2)}
+     * 
+ * + * @param mapIfTrue the map if the ifTrue is true + * @param mapIfFalse the map if the ifTrue is false + * @param ifTrue ifTrue + * @return this + */ + @Override + public LinkedObjectMap appendIfTrue(Map mapIfTrue, Map mapIfFalse, boolean ifTrue) { + if (ifTrue) { + append(mapIfTrue); + } else { + append(mapIfFalse); + } + return this; + } + + /** + * Put/replace a given map into this ObjectMap if boolean is true and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, check1).appendIfTrue(map, check2)}
+     * 
+ * + * @param map key + * @param ifTrue ifTrue + * @return this + */ + @Override + public LinkedObjectMap appendIfTrue(@NotNull ObjectMap map, boolean ifTrue) { + for (Map.Entry entry : map.entrySet()) { + appendIfTrue(entry.getKey(), entry.getValue(), ifTrue); + } + return this; + } + + /** + * Put/replace the given key/value pair into ObjectMap if boolean is true or not and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, 2, check1).appendIfTrue(map1, map2, check2)}
+     * 
+ * + * @param mapIfTrue the map if the ifTrue is true + * @param mapIfFalse the map if the ifTrue is false + * @param ifTrue ifTrue + * @return this + */ + @Override + public LinkedObjectMap appendIfTrue(ObjectMap mapIfTrue, Map mapIfFalse, boolean ifTrue) { + if (ifTrue) { + append(mapIfTrue); + } else { + append(mapIfFalse); + } + return this; + } + + /** + * Removes the entry with the specified key from the ObjectMap. + * + * @param key the key of the entry to be removed + * @return the modified ObjectMap with the specified entry removed, or the original ObjectMap if the key was not found + */ + @Override + public LinkedObjectMap removeEntry(K key) { + remove(key); + return this; + } + + /** + * Removes all entries with keys present in the specified map from the ObjectMap. + * + * @param map the map containing the keys to be removed + * @return the modified ObjectMap with the entries corresponding to the specified keys removed + */ + @Override + public LinkedObjectMap removeEntries(Map map) { + for (Map.Entry entry : map.entrySet()) { + remove(entry.getKey()); + } + return this; + } + + /** + * Removes all entries with keys present in the specified ObjectMap from the ObjectMap. + * + * @param map the ObjectMap containing the keys to be removed + * @return the modified ObjectMap with the entries corresponding to the keys in the specified ObjectMap removed + */ + @Override + public LinkedObjectMap removeEntries(ObjectMap map) { + for (ObjectMap.Entry entry : map.entrySet()) { + remove(entry.getKey()); + } + return this; + } + + /** + * Removes the entry with the specified key from the ObjectMap if the condition is true. + * + * @param key the key of the entry to be removed + * @param ifTrue the condition to check before removing the entry + * @return the modified ObjectMap with the specified entry removed if the condition is true, or the original ObjectMap otherwise + */ + @Override + public LinkedObjectMap removeEntryIfTrue(K key, boolean ifTrue) { + if (ifTrue) { + remove(key); + } + return this; + } + + /** + * Removes all entries with keys present in the specified map from the ObjectMap if the condition is true. + * + * @param map the map containing the keys to be removed + * @param ifTrue the condition to check before removing the entries + * @return the modified ObjectMap with the entries corresponding to the keys in the specified map removed if the condition is true, or the original ObjectMap otherwise + */ + @Override + public LinkedObjectMap removeEntriesIfTrue(Map map, boolean ifTrue) { + if (ifTrue) { + for (Map.Entry entry : map.entrySet()) { + remove(entry.getKey()); + } + } + return this; + } + + /** + * Removes all entries with keys present in the specified ObjectMap from the ObjectMap if the condition is true. + * + * @param map the ObjectMap containing the keys to be removed + * @param ifTrue the condition to check before removing the entries + * @return the modified ObjectMap with the entries corresponding to the keys in the specified ObjectMap removed if the condition is true, or the original ObjectMap otherwise + */ + @Override + public LinkedObjectMap removeEntriesIfTrue(ObjectMap map, boolean ifTrue) { + if (ifTrue) { + for (ObjectMap.Entry entry : map.entrySet()) { + remove(entry.getKey()); + } + } + return this; + } + + /** + * Gets the value of the given key as an Integer. + * + * @param key the key + * @return the value as an integer, which may be null + * @throws ClassCastException if the value is not an integer + */ + public Integer getInteger(final Object key) { + return getInteger(key, 0); + } + + /** + * Gets the value of the given key as a primitive int. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as an integer, which may be null + * @throws ClassCastException if the value is not an integer + */ + public int getInteger(final Object key, final int defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value of the given key as a Long. + * + * @param key the key + * @return the value as a long, which may be null + * @throws ClassCastException if the value is not an long + */ + public Long getLong(final Object key) { + return getLong(key, 0L); + } + + /** + * Gets the value of the given key as a Long. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a long, which may be null + * @throws ClassCastException if the value is not an long + */ + public Long getLong(final Object key, final long defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value of the given key as a Double. + * + * @param key the key + * @return the value as a double, which may be null + * @throws ClassCastException if the value is not an double + */ + public Double getDouble(final Object key) { + return getDouble(key, 0D); + } + + /** + * Gets the value of the given key as a Double. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a double, which may be null + * @throws ClassCastException if the value is not an double + */ + public Double getDouble(final Object key, final double defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value of the given key as a String. + * + * @param key the key + * @return the value as a String, which may be null + * @throws ClassCastException if the value is not a String + */ + public String getString(final Object key) { + return getString(key, ""); + } + + /** + * Gets the value of the given key as a String. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a String, which may be null + * @throws ClassCastException if the value is not a String + */ + public String getString(final Object key, final String defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value of the given key as a Boolean. + * + * @param key the key + * @return the value as a Boolean, which may be null + * @throws ClassCastException if the value is not an boolean + */ + public Boolean getBoolean(final Object key) { + return getBoolean(key, false); + } + + /** + * Gets the value of the given key as a primitive boolean. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a primitive boolean + * @throws ClassCastException if the value is not a boolean + */ + public boolean getBoolean(final Object key, final boolean defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value of the given key as a Date. + * + * @param key the key + * @return the value as a Date, which may be null + * @throws ClassCastException if the value is not a Date + */ + public Date getDate(final Object key) { + return getDate(key, new Date()); + } + + /** + * Gets the value of the given key as a Date. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a Date, which may be null + * @throws ClassCastException if the value is not a Date + */ + public Date getDate(final Object key, final Date defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the list value of the given key, casting the list elements to the given {@code Class}. This is useful to avoid having + * casts in client code, though the effect is the same. + * + * @param key the key + * @param clazz the non-null class to cast the list value to + * @param the type of the class + * @return the list value of the given key, or null if the instance does not contain this key. + * @throws ClassCastException if the elements in the list value of the given key is not of type T or the value is not a list + */ + public List getList(Object key, Class clazz) { + return getList(key, clazz, null); + } + + /** + * Gets the list value of the given key, casting the list elements to {@code Class} or returning the default list value if null. + * This is useful to avoid having casts in client code, though the effect is the same. + * + * @param key the key + * @param clazz the non-null class to cast the list value to + * @param defaultValue what to return if the value is null + * @param the type of the class + * @return the list value of the given key, or the default list value if the instance does not contain this key. + * @throws ClassCastException if the value of the given key is not of type T + */ + public List getList(final Object key, final Class clazz, final List defaultValue) { + List value = get(key, List.class); + if (value == null) { + return defaultValue; + } + + for (Object item : value) { + if (!clazz.isAssignableFrom(item.getClass())) { + throw new ClassCastException(format("List element cannot be cast to %s", clazz.getName())); + } + } + return value; + } + + /** + * Gets the value of the given key, casting it to the given {@code Class}. This is useful to avoid having casts in client code, + * though the effect is the same. So to get the value of a key that is of type String, you would write {@code String name = + * doc.get("name", String.class)} instead of {@code String name = (String) doc.get("x") }. + * + * @param key the key + * @param clazz the non-null class to cast the value to + * @param the type of the class + * @return the value of the given key, or null if the instance does not contain this key. + * @throws ClassCastException if the value of the given key is not of type T + */ + public T get(final Object key, final Class clazz) { + return clazz.cast(get(key)); + } + + /** + * Gets the value of the given key, casting it to {@code Class} or returning the default value if null. + * This is useful to avoid having casts in client code, though the effect is the same. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @param the type of the class + * @return the value of the given key, or null if the instance does not contain this key. + * @throws ClassCastException if the value of the given key is not of type T + */ + public T get(final Object key, final T defaultValue) { + Object value = get(key); + return value == null ? defaultValue : (T) value; + } + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/maps/ObjectMap.java b/common/src/main/java/com/georgev22/skinoverlay/maps/ObjectMap.java new file mode 100644 index 00000000..fea02e40 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/maps/ObjectMap.java @@ -0,0 +1,466 @@ +package com.georgev22.skinoverlay.maps; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +public interface ObjectMap extends Map { + + /** + * Creates a new empty {@link LinkedObjectMap} instance. + * + * @return a new empty {@link LinkedObjectMap} instance. + */ + @Contract(" -> new") + static @NotNull LinkedObjectMap newLinkedObjectMap() { + return new LinkedObjectMap<>(); + } + + /** + * Creates a new empty {@link ConcurrentObjectMap} instance. + * + * @return a new empty {@link ConcurrentObjectMap} instance. + */ + @Contract(" -> new") + static @NotNull ConcurrentObjectMap newConcurrentObjectMap() { + return new ConcurrentObjectMap<>(); + } + + /** + * Creates a new empty {@link HashObjectMap} instance. + * + * @return a new empty {@link HashObjectMap} instance. + */ + @Contract(" -> new") + static @NotNull HashObjectMap newHashObjectMap() { + return new HashObjectMap<>(); + } + + /** + * Creates a new empty {@link TreeObjectMap} instance. + * + * @return a new empty {@link TreeObjectMap} instance. + */ + @Contract(" -> new") + static @NotNull TreeObjectMap newTreeObjectMap() { + return new TreeObjectMap<>(); + } + + /** + * Creates a {@link LinkedObjectMap} instance with the same mappings as the specified map. + * + * @param map the mappings to be placed in the new map + * @return a new {@link LinkedObjectMap#LinkedObjectMap(ObjectMap)} initialized with the mappings from {@code map} + */ + @Contract("_ -> new") + static @NotNull LinkedObjectMap newLinkedObjectMap(ObjectMap map) { + return new LinkedObjectMap<>(map); + } + + /** + * Creates a {@link LinkedObjectMap} instance with the same mappings as the specified map. + * + * @param map the mappings to be placed in the new map + * @return a new {@link LinkedObjectMap#LinkedObjectMap(Map)} initialized with the mappings from {@code map} + */ + @Contract("_ -> new") + static @NotNull LinkedObjectMap newLinkedObjectMap(Map map) { + return new LinkedObjectMap<>(map); + } + + /** + * Creates a {@link ConcurrentObjectMap} instance with the same mappings as the specified map. + * + * @param map the mappings to be placed in the new map + * @return a new {@link ConcurrentObjectMap#ConcurrentObjectMap(ObjectMap)} initialized with the mappings from {@code map} + */ + @Contract("_ -> new") + static @NotNull ConcurrentObjectMap newConcurrentObjectMap(ObjectMap map) { + return new ConcurrentObjectMap<>(map); + } + + /** + * Creates a {@link ConcurrentObjectMap} instance with the same mappings as the specified map. + * + * @param map the mappings to be placed in the new map + * @return a new {@link ConcurrentObjectMap#ConcurrentObjectMap(Map)} initialized with the mappings from {@code map} + */ + @Contract("_ -> new") + static @NotNull ConcurrentObjectMap newConcurrentObjectMap(Map map) { + return new ConcurrentObjectMap<>(map); + } + + /** + * Creates a {@link HashObjectMap} instance with the same mappings as the specified map. + * + * @param map the mappings to be placed in the new map + * @return a new {@link HashObjectMap#HashObjectMap(ObjectMap)} initialized with the mappings from {@code map} + */ + @Contract("_ -> new") + static @NotNull HashObjectMap newHashObjectMap(ObjectMap map) { + return new HashObjectMap<>(map); + } + + /** + * Creates a {@link HashObjectMap} instance with the same mappings as the specified map. + * + * @param map the mappings to be placed in the new map + * @return a new {@link HashObjectMap#HashObjectMap(Map)} initialized with the mappings from {@code map} + */ + @Contract("_ -> new") + static @NotNull HashObjectMap newHashObjectMap(Map map) { + return new HashObjectMap<>(map); + } + + /** + * Creates a {@link TreeObjectMap} instance with the same mappings as the specified map. + * + * @param map the mappings to be placed in the new map + * @return a new {@link TreeObjectMap#TreeObjectMap(ObjectMap)} initialized with the mappings from {@code map} + */ + @Contract("_ -> new") + static @NotNull TreeObjectMap newTreeObjectMap(ObjectMap map) { + return new TreeObjectMap<>(map); + } + + /** + * Creates a {@link TreeObjectMap} instance with the same mappings as the specified map. + * + * @param map the mappings to be placed in the new map + * @return a new {@link TreeObjectMap#TreeObjectMap(Map)} initialized with the mappings from {@code map} + */ + @Contract("_ -> new") + static @NotNull TreeObjectMap newTreeObjectMap(Map map) { + return new TreeObjectMap<>(map); + } + + + /** + * Put/replace the given key/value pair into this ObjectMap and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.append("a", 1).append("b", 2)}
+     * 
+ * + * @param key key + * @param value value + * @return this + */ + ObjectMap append(final K key, final V value); + + /** + * Put/replace a given map into this ObjectMap and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.append("a", 1).append(map)}
+     * 
+ * + * @param map the map to append to the current one + * @return this + */ + ObjectMap append(final Map map); + + /** + * Put/replace a given map into this ObjectMap and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.append("a", 1).append(map)}
+     * 
+ * + * @param map the map to append to the current one + * @return this + */ + ObjectMap append(final ObjectMap map); + + /** + * Put/replace the given key/value pair into ObjectMap if boolean is true and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, check1).appendIfTrue("b", 2, check2)}
+     * 
+ * + * @param key key + * @param value value + * @param ifTrue ifTrue + * @return this + */ + ObjectMap appendIfTrue(final K key, final V value, boolean ifTrue); + + /** + * Put/replace the given key/value pair into ObjectMap if boolean is true or not and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, 2, check1).appendIfTrue("b", 3, 4, check2)}
+     * 
+ * + * @param key key + * @param valueIfTrue the value if the ifTrue is true + * @param valueIfFalse the value if the ifTrue is false + * @param ifTrue ifTrue + * @return this + */ + ObjectMap appendIfTrue(final K key, final V valueIfTrue, final V valueIfFalse, boolean ifTrue); + + /** + * Put/replace a given map into this ObjectMap if boolean is true and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, check1).appendIfTrue(map, check2)}
+     * 
+ * + * @param map key + * @param ifTrue ifTrue + * @return this + */ + ObjectMap appendIfTrue(final Map map, boolean ifTrue); + + /** + * Put/replace the given key/value pair into ObjectMap if boolean is true or not and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, 2, check1).appendIfTrue(map1, map2, check2)}
+     * 
+ * + * @param mapIfTrue the map if the ifTrue is true + * @param mapIfFalse the map if the ifTrue is false + * @param ifTrue ifTrue + * @return this + */ + ObjectMap appendIfTrue(final Map mapIfTrue, final Map mapIfFalse, boolean ifTrue); + + /** + * Put/replace a given map into this ObjectMap if boolean is true and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, check1).appendIfTrue(map, check2)}
+     * 
+ * + * @param map key + * @param ifTrue ifTrue + * @return this + */ + ObjectMap appendIfTrue(final ObjectMap map, boolean ifTrue); + + /** + * Put/replace the given key/value pair into ObjectMap if boolean is true or not and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, 2, check1).appendIfTrue(map1, map2, check2)}
+     * 
+ * + * @param mapIfTrue the map if the ifTrue is true + * @param mapIfFalse the map if the ifTrue is false + * @param ifTrue ifTrue + * @return this + */ + ObjectMap appendIfTrue(final ObjectMap mapIfTrue, final Map mapIfFalse, boolean ifTrue); + + /** + * Removes the entry with the specified key from the ObjectMap. + * + * @param key the key of the entry to be removed + * @return the modified ObjectMap with the specified entry removed, or the original ObjectMap if the key was not found + */ + ObjectMap removeEntry(final K key); + + /** + * Removes all entries with keys present in the specified map from the ObjectMap. + * + * @param map the map containing the keys to be removed + * @return the modified ObjectMap with the entries corresponding to the specified keys removed + */ + ObjectMap removeEntries(final Map map); + + /** + * Removes all entries with keys present in the specified ObjectMap from the ObjectMap. + * + * @param map the ObjectMap containing the keys to be removed + * @return the modified ObjectMap with the entries corresponding to the keys in the specified ObjectMap removed + */ + ObjectMap removeEntries(final ObjectMap map); + + /** + * Removes the entry with the specified key from the ObjectMap if the condition is true. + * + * @param key the key of the entry to be removed + * @param ifTrue the condition to check before removing the entry + * @return the modified ObjectMap with the specified entry removed if the condition is true, or the original ObjectMap otherwise + */ + ObjectMap removeEntryIfTrue(final K key, boolean ifTrue); + + /** + * Removes all entries with keys present in the specified map from the ObjectMap if the condition is true. + * + * @param map the map containing the keys to be removed + * @param ifTrue the condition to check before removing the entries + * @return the modified ObjectMap with the entries corresponding to the keys in the specified map removed if the condition is true, or the original ObjectMap otherwise + */ + ObjectMap removeEntriesIfTrue(final Map map, boolean ifTrue); + + /** + * Removes all entries with keys present in the specified ObjectMap from the ObjectMap if the condition is true. + * + * @param map the ObjectMap containing the keys to be removed + * @param ifTrue the condition to check before removing the entries + * @return the modified ObjectMap with the entries corresponding to the keys in the specified ObjectMap removed if the condition is true, or the original ObjectMap otherwise + */ + ObjectMap removeEntriesIfTrue(final ObjectMap map, boolean ifTrue); + + /** + * Gets the value of the given key as an Integer. + * + * @param key the key + * @return the value as an integer, which may be null + * @throws ClassCastException if the value is not an integer + */ + Integer getInteger(final Object key); + + /** + * Gets the value of the given key as a primitive int. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as an integer, which may be null + * @throws ClassCastException if the value is not an integer + */ + int getInteger(final Object key, final int defaultValue); + + /** + * Gets the value of the given key as a Long. + * + * @param key the key + * @return the value as a long, which may be null + * @throws ClassCastException if the value is not an long + */ + Long getLong(final Object key); + + /** + * Gets the value of the given key as a Long. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a long, which may be null + * @throws ClassCastException if the value is not an long + */ + Long getLong(final Object key, final long defaultValue); + + /** + * Gets the value of the given key as a Double. + * + * @param key the key + * @return the value as a double, which may be null + * @throws ClassCastException if the value is not an double + */ + Double getDouble(final Object key); + + /** + * Gets the value of the given key as a Double. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a double, which may be null + * @throws ClassCastException if the value is not an double + */ + Double getDouble(final Object key, final double defaultValue); + + /** + * Gets the value of the given key as a String. + * + * @param key the key + * @return the value as a String, which may be null + * @throws ClassCastException if the value is not a String + */ + String getString(final Object key); + + /** + * Gets the value of the given key as a String. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a String, which may be null + * @throws ClassCastException if the value is not a String + */ + String getString(final Object key, final String defaultValue); + + /** + * Gets the value of the given key as a Boolean. + * + * @param key the key + * @return the value as a Boolean, which may be null + * @throws ClassCastException if the value is not an boolean + */ + Boolean getBoolean(final Object key); + + /** + * Gets the value of the given key as a primitive boolean. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a primitive boolean + * @throws ClassCastException if the value is not a boolean + */ + boolean getBoolean(final Object key, final boolean defaultValue); + + /** + * Gets the value of the given key as a Date. + * + * @param key the key + * @return the value as a Date, which may be null + * @throws ClassCastException if the value is not a Date + */ + Date getDate(final Object key); + + /** + * Gets the value of the given key as a Date. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a Date, which may be null + * @throws ClassCastException if the value is not a Date + */ + Date getDate(final Object key, final Date defaultValue); + + /** + * Gets the list value of the given key, casting the list elements to the given {@code Class}. This is useful to avoid having + * casts in client code, though the effect is the same. + * + * @param key the key + * @param clazz the non-null class to cast the list value to + * @param the type of the class + * @return the list value of the given key, or null if the instance does not contain this key. + * @throws ClassCastException if the elements in the list value of the given key is not of type T or the value is not a list + */ + List getList(Object key, Class clazz); + + /** + * Gets the list value of the given key, casting the list elements to {@code Class} or returning the default list value if null. + * This is useful to avoid having casts in client code, though the effect is the same. + * + * @param key the key + * @param clazz the non-null class to cast the list value to + * @param defaultValue what to return if the value is null + * @param the type of the class + * @return the list value of the given key, or the default list value if the instance does not contain this key. + * @throws ClassCastException if the value of the given key is not of type T + */ + List getList(final Object key, final Class clazz, final List defaultValue); + + /** + * Gets the value of the given key, casting it to the given {@code Class}. This is useful to avoid having casts in client code, + * though the effect is the same. So to get the value of a key that is of type String, you would write {@code String name = + * doc.get("name", String.class)} instead of {@code String name = (String) doc.get("x") }. + * + * @param key the key + * @param clazz the non-null class to cast the value to + * @param the type of the class + * @return the value of the given key, or null if the instance does not contain this key. + * @throws ClassCastException if the value of the given key is not of type T + */ + T get(final Object key, final Class clazz); + + /** + * Gets the value of the given key, casting it to {@code Class} or returning the default value if null. + * This is useful to avoid having casts in client code, though the effect is the same. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @param the type of the class + * @return the value of the given key, or null if the instance does not contain this key. + * @throws ClassCastException if the value of the given key is not of type T + */ + T get(final Object key, final T defaultValue); +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/maps/ObservableObjectMap.java b/common/src/main/java/com/georgev22/skinoverlay/maps/ObservableObjectMap.java new file mode 100644 index 00000000..515188ad --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/maps/ObservableObjectMap.java @@ -0,0 +1,217 @@ +package com.georgev22.skinoverlay.maps; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.BiFunction; + +/** + * An implementation of the {@link ObjectMap} interface that provides an easy way to add listeners + * that get notified when a new entry is added to the map. + * + * @param the type of keys maintained by this map + * @param the type of mapped values + */ +public class ObservableObjectMap extends ConcurrentObjectMap { + + private final List> listeners = new ArrayList<>(); + + /** + * Adds a {@link MapChangeListener} to this map. + * + * @param listener the listener to be added + */ + public void addListener(MapChangeListener listener) { + listeners.add(listener); + } + + /** + * Removes a {@link MapChangeListener} to this map. + * + * @param listener the listener to be removed + */ + public void removeListener(MapChangeListener listener) { + listeners.remove(listener); + } + + /** + * Returns an unmodifiable List of the registered MapChangeListeners. + * + * @return An unmodifiable List of the registered MapChangeListeners. + */ + public List> getListeners() { + return Collections.unmodifiableList(listeners); + } + + /** + * Associates the specified value with the specified key in this map. If the map previously contained a mapping + * for the key, the old value is replaced by the specified value. Notifies all registered listeners with the + * added key-value pair. + * + * @param key key with which the specified value is to be associated + * @param value value to be associated with the specified key + * @return the previous value associated with the specified key, or {@code null} if there was no mapping for the key + */ + @Override + public V put(@NotNull K key, @NotNull V value) { + V oldValue = super.put(key, value); + fireEntryAddedEvent(key, value); + return oldValue; + } + + /** + * Associates the specified value with the specified key in this map if it is not already associated with a value. + * If the specified key is already associated with a value, the existing value is returned and no change is made. + * + * @param key the key with which the specified value is to be associated + * @param value the value to be associated with the specified key + * @return the previous value associated with the specified key, or {@code null} if there was no mapping for the key + */ + @Override + public V putIfAbsent(K key, V value) { + V oldValue = super.putIfAbsent(key, value); + fireEntryAddedEvent(key, value); + return oldValue; + } + + /** + * Copies all the mappings from the specified map to this map. + * The effect of this call is equivalent to that of calling + * {@code put(k, v)} on this map once for each mapping from key {@code k} to value {@code v} in the specified map. + * The behavior of this operation is undefined if the specified map is modified while the operation is in progress. + * + * @param m the map whose mappings are to be added to this map + */ + @Override + public void putAll(@NotNull Map m) { + m.forEach(this::fireEntryAddedEvent); + super.putAll(m); + } + + /** + * Removes the mapping for a key from this map if it is present. + * The value previously associated with the key is returned. + * + * @param key the key whose mapping is to be removed from the map + * @return the previous value associated with the key, or {@code null} if there was no mapping for the key + */ + @Override + public V remove(@NotNull Object key) { + fireEntryRemovedEvent(key, null); + return super.remove(key); + } + + /** + * Removes the entry for a key only if currently mapped to a given value. + * + * @param key key with which the specified value is associated + * @param value value expected to be associated with the specified key + * @return {@code true} if the value was removed + */ + @Override + public boolean remove(Object key, Object value) { + fireEntryRemovedEvent(key, value); + return super.remove(key, value); + } + + /** + * Replaces the entry for a key only if currently mapped to some value. + * + * @param key key with which the specified value is associated + * @param oldValue value expected to be associated with the specified key + * @param newValue value to be associated with the specified key + * @return {@code true} if the value was replaced + */ + @Override + public boolean replace(K key, V oldValue, V newValue) { + fireEntryRemovedEvent(key, oldValue); + fireEntryAddedEvent(key, newValue); + return super.replace(key, oldValue, newValue); + } + + /** + * Replaces the entry for a key only if currently mapped to some value. + * + * @param key key with which the specified value is associated + * @param value value to be associated with the specified key + * @return the previous value associated with the specified key, or {@code null} if there was no mapping for the key + */ + @Override + public V replace(K key, V value) { + fireEntryRemovedEvent(key, null); + fireEntryAddedEvent(key, value); + return super.replace(key, value); + } + + /** + * Replaces each entry's value with the result of invoking the given function on that entry. + * + * @param function the function to apply to each entry + */ + @Override + public void replaceAll(BiFunction function) { + Map oldMap = new ConcurrentObjectMap<>(this); + + super.replaceAll(function); + + for (K key : oldMap.keySet()) { + V oldValue = oldMap.get(key); + V newValue = get(key); + if (!Objects.equals(oldValue, newValue)) { + fireEntryRemovedEvent(key, oldValue); + fireEntryAddedEvent(key, newValue); + } + } + } + + + /** + * Notifies all registered listeners that a new entry has been added to the map. + * + * @param key the key of the added entry + * @param value the value of the added entry + */ + private void fireEntryAddedEvent(K key, V value) { + for (MapChangeListener listener : listeners) { + listener.entryAdded(key, value); + } + } + + /** + * Notifies all registered listeners that an entry has been removed from the map. + * + * @param key the key of the removed entry + * @param value the value of the removed entry, or {@code null} if the key was not previously mapped + */ + private void fireEntryRemovedEvent(Object key, @Nullable Object value) { + for (MapChangeListener listener : listeners) { + listener.entryRemoved(key, value); + } + } + + /** + * A listener interface for receiving notifications when a new entry is added to an {@link ObservableObjectMap}. + * + * @param the type of keys maintained by the map + * @param the type of mapped values + */ + public interface MapChangeListener { + /** + * Called when a new entry is added to the map. + * + * @param key the key of the added entry + * @param value the value of the added entry + */ + void entryAdded(K key, V value); + + /** + * Called when an entry is removed from the map. + * + * @param key the key of the removed entry + * @param value the value of the removed entry, or {@code null} if there was no value associated with the key + */ + void entryRemoved(Object key, @Nullable Object value); + } + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/maps/Pair.java b/common/src/main/java/com/georgev22/skinoverlay/maps/Pair.java new file mode 100644 index 00000000..d52f0614 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/maps/Pair.java @@ -0,0 +1,113 @@ +package com.georgev22.skinoverlay.maps; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Objects; + +/** + * A simple generic class representing a pair of elements. + * + * @param the type of the first element (key) + * @param the type of the second element (value) + */ +public final class Pair implements Serializable { + @Serial + private static final long serialVersionUID = 0L; + private final K key; + private V value; + + /** + * Constructs a new pair with the specified key and value. + * + * @param key the first element (key) of the pair + * @param value the second element (value) of the pair + */ + public Pair(K key, V value) { + this.key = key; + this.value = value; + } + + /** + * Indicates whether some other object is "equal to" this pair. + * + * @param o the reference object with which to compare + * @return {@code true} if this pair is the same as the o argument; {@code false} otherwise + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof Pair)) { + return false; + } + + Pair p = (Pair) o; + + return Objects.equals(p.key, key) && Objects.equals(p.value, value); + } + + /** + * Returns the hash code value for this pair. + * + * @return the hash code value for this pair + */ + @Override + public int hashCode() { + return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode()); + } + + /** + * Returns a string representation of this pair. + * + * @return a string representation of this pair + */ + @Contract(pure = true) + @Override + public @NotNull String toString() { + return "Pair{" + + "key=" + key + ", " + + "value=" + value + "}"; + } + + /** + * Creates a new pair with the specified key and value. + * + * @param key the first element (key) of the pair + * @param value the second element (value) of the pair + * @param the type of the first element (key) + * @param the type of the second element (value) + * @return a new pair with the specified key and value + */ + @Contract(value = "_, _ -> new", pure = true) + public static @NotNull Pair create(K key, V value) { + return new Pair<>(key, value); + } + + /** + * Returns the key of this pair. + * + * @return the key of this pair + */ + public K key() { + return key; + } + + /** + * Returns the value of this pair. + * + * @return the value of this pair + */ + public V value() { + return value; + } + + /** + * Sets the value of this pair. + * + * @param value the new value to set + */ + public void setValue(V value) { + this.value = value; + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/maps/PairDocument.java b/common/src/main/java/com/georgev22/skinoverlay/maps/PairDocument.java new file mode 100644 index 00000000..9ffb4088 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/maps/PairDocument.java @@ -0,0 +1,291 @@ +package com.georgev22.skinoverlay.maps; + +import com.georgev22.skinoverlay.exceptions.PairDocumentException; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.Serial; +import java.io.Serializable; +import java.util.*; + +import static java.lang.String.format; + +/** + * A record representing a document composed of key-value pairs. + */ +public final class PairDocument implements Serializable { + @Serial + private static final long serialVersionUID = 0L; + private final List> objectPairs; + + /** + * Constructs a PairDocument with the specified key-value pairs. + * + * @param objectPairs the key-value pairs + * @throws PairDocumentException if the given array of pairs is empty + */ + public PairDocument(Pair[] objectPairs) { + this(objectPairs != null ? Arrays.asList(objectPairs) : Collections.emptyList()); + } + + /** + * Constructs a PairDocument with the specified key-value pairs. + * + * @param objectPairs the key-value pairs + * @throws PairDocumentException if the given list of pairs is empty + */ + public PairDocument(List> objectPairs) { + if (objectPairs.isEmpty()) { + throw new PairDocumentException("PairDocument is empty"); + } + this.objectPairs = objectPairs; + } + + /** + * Gets the value associated with the specified key as an Integer. + * + * @param key the key whose associated value is to be returned + * @return the value associated with the specified key as an Integer, or null if no mapping exists for the key + * @throws ClassCastException if the value is not an Integer + */ + public Integer getInteger(final Object key) { + return getInteger(key, 0); + } + + /** + * Gets the value associated with the specified key as an Integer, or a default value if the key is not present or the value is null. + * + * @param key the key whose associated value is to be returned + * @param defaultValue the default value to return if the key is not present or the value is null + * @return the value associated with the specified key as an Integer, or the specified default value if no mapping exists for the key + * @throws ClassCastException if the value is not an Integer + */ + public int getInteger(final Object key, final int defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value associated with the specified key as a Long. + * + * @param key the key whose associated value is to be returned + * @return the value associated with the specified key as a Long, or null if no mapping exists for the key + * @throws ClassCastException if the value is not a Long + */ + public Long getLong(final Object key) { + return getLong(key, 0L); + } + + /** + * Gets the value associated with the specified key as a Long, or a default value if the key is not present or the value is null. + * + * @param key the key whose associated value is to be returned + * @param defaultValue the default value to return if the key is not present or the value is null + * @return the value associated with the specified key as a Long, or the specified default value if no mapping exists for the key + * @throws ClassCastException if the value is not a Long + */ + public Long getLong(final Object key, final long defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value associated with the specified key as a Double. + * + * @param key the key whose associated value is to be returned + * @return the value associated with the specified key as a Double, or null if no mapping exists for the key + * @throws ClassCastException if the value is not a Double + */ + public Double getDouble(final Object key) { + return getDouble(key, 0D); + } + + /** + * Gets the value associated with the specified key as a Double, or a default value if the key is not present or the value is null. + * + * @param key the key whose associated value is to be returned + * @param defaultValue the default value to return if the key is not present or the value is null + * @return the value associated with the specified key as a Double, or the specified default value if no mapping exists for the key + * @throws ClassCastException if the value is not a Double + */ + public Double getDouble(final Object key, final double defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value associated with the specified key as a String. + * + * @param key the key whose associated value is to be returned + * @return the value associated with the specified key as a String, or null if no mapping exists for the key + * @throws ClassCastException if the value is not a String + */ + public String getString(final Object key) { + return getString(key, ""); + } + + /** + * Gets the value associated with the specified key as a String, or a default value if the key is not present or the value is null. + * + * @param key the key whose associated value is to be returned + * @param defaultValue the default value to return if the key is not present or the value is null + * @return the value associated with the specified key as a String, or the specified default value if no mapping exists for the key + * @throws ClassCastException if the value is not a String + */ + public String getString(final Object key, final String defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value associated with the specified key as a Boolean. + * + * @param key the key whose associated value is to be returned + * @return the value associated with the specified key as a Boolean, or null if no mapping exists for the key + * @throws ClassCastException if the value is not a Boolean + */ + public Boolean getBoolean(final Object key) { + return getBoolean(key, false); + } + + /** + * Gets the value associated with the specified key as a Boolean, or a default value if the key is not present or the value is null. + * + * @param key the key whose associated value is to be returned + * @param defaultValue the default value to return if the key is not present or the value is null + * @return the value associated with the specified key as a Boolean, or the specified default value if no mapping exists for the key + * @throws ClassCastException if the value is not a Boolean + */ + public boolean getBoolean(final Object key, final boolean defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value associated with the specified key as a Date. + * + * @param key the key whose associated value is to be returned + * @return the value associated with the specified key as a Date, or null if no mapping exists for the key + * @throws ClassCastException if the value is not a Date + */ + public Date getDate(final Object key) { + return getDate(key, new Date()); + } + + /** + * Gets the value associated with the specified key as a Date, or a default value if the key is not present or the value is null. + * + * @param key the key whose associated value is to be returned + * @param defaultValue the default value to return if the key is not present or the value is null + * @return the value associated with the specified key as a Date, or the specified default value if no mapping exists for the key + * @throws ClassCastException if the value is not a Date + */ + public Date getDate(final Object key, final Date defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the list value associated with the specified key, casting the list elements to the given class. + * + * @param key the key whose associated list value is to be returned + * @param clazz the non-null class to cast the list value to + * @param the type of the class + * @return the list value associated with the specified key, or null if no mapping exists for the key + * @throws ClassCastException if the elements in the list value associated with the specified key are not of type T, or the value is not a list + */ + public List getList(Object key, Class clazz) { + return getList(key, clazz, null); + } + + /** + * Gets the list value associated with the specified key, casting the list elements to the given class, or a default list value if null. + * + * @param key the key whose associated list value is to be returned + * @param clazz the non-null class to cast the list value to + * @param defaultValue the default list value to return if the key is not present or the value is null + * @param the type of the class + * @return the list value associated with the specified key, or the default list value if no mapping exists for the key + * @throws ClassCastException if the elements in the list value associated with the specified key are not of type T, or the value is not a list + */ + public List getList(final Object key, final Class clazz, final List defaultValue) { + List value = get(key, List.class); + if (value == null) { + return defaultValue; + } + + for (Object item : value) { + if (!clazz.isAssignableFrom(item.getClass())) { + throw new ClassCastException(format("List element cannot be cast to %s", clazz.getName())); + } + } + return value; + } + + /** + * Gets the value associated with the specified key, casting it to the given class. + * + * @param key the key whose associated value is to be returned + * @param clazz the non-null class to cast the value to + * @param the type of the class + * @return the value associated with the specified key, cast to the specified class, or null if no mapping exists for the key + */ + public T get(final Object key, final Class clazz) { + return clazz.cast(get(key)); + } + + /** + * Gets the value associated with the specified key, or a default value if the key is not present or the value is null. + * + * @param key the key whose associated value is to be returned + * @param defaultValue the default value to return if the key is not present or the value is null + * @param the type of the value + * @return the value associated with the specified key, or the specified default value if no mapping exists for the key + */ + public T get(final Object key, final T defaultValue) { + Object value = get(key); + return value == null ? defaultValue : (T) value; + } + + /** + * Gets the value associated with the specified key. + * + * @param key the key whose associated value is to be returned + * @param the type of the value + * @return the value associated with the specified key, or null if no mapping exists for the key + */ + public @Nullable T get(final Object key) { + for (Pair pair : objectPairs) { + if (pair.key().equals(key)) { + return (T) pair.value(); + } + } + return null; + } + + /** + * Returns a string representation of this PairDocument. + * + * @return a string representation of this PairDocument + */ + @Contract(pure = true) + @Override + public @NotNull String toString() { + return "PairDocument{" + + "pairs=" + objectPairs + + "}"; + } + + public List> objectPairs() { + return objectPairs; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (PairDocument) obj; + return Objects.equals(this.objectPairs, that.objectPairs); + } + + @Override + public int hashCode() { + return Objects.hash(objectPairs); + } + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/maps/TreeObjectMap.java b/common/src/main/java/com/georgev22/skinoverlay/maps/TreeObjectMap.java new file mode 100644 index 00000000..c7c578af --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/maps/TreeObjectMap.java @@ -0,0 +1,513 @@ +package com.georgev22.skinoverlay.maps; + +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +import static java.lang.String.format; + +public class TreeObjectMap extends TreeMap implements ObjectMap { + + /** + * Creates a TreeObjectMap instance. + */ + public TreeObjectMap() { + } + + /** + * Creates a TreeObjectMap instance with the specified comparator. + * + * @param comparator the comparator to order the keys + */ + public TreeObjectMap(Comparator comparator) { + super(comparator); + } + + /** + * Creates a TreeObjectMap instance initialized with the given ObjectMap. + * + * @param map the initial ObjectMap + */ + public TreeObjectMap(final ObjectMap map) { + putAll(map); + } + + /** + * Creates a TreeObjectMap instance initialized with the given Map. + * + * @param map the initial Map + */ + public TreeObjectMap(final Map map) { + putAll(map); + } + + /** + * Creates a TreeObjectMap instance with the specified comparator and initialized with the given ObjectMap. + * + * @param comparator the comparator to order the keys + * @param map the initial ObjectMap + */ + public TreeObjectMap(Comparator comparator, final ObjectMap map) { + super(comparator); + putAll(map); + } + + /** + * Creates a TreeObjectMap instance with the specified comparator and initialized with the given Map. + * + * @param comparator the comparator to order the keys + * @param map the initial Map + */ + public TreeObjectMap(Comparator comparator, final Map map) { + super(comparator); + putAll(map); + } + + /** + * Put/replace the given key/value pair into this User and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.append("a", 1).append("b", 2)}
+     * 
+ * + * @param key key + * @param value value + * @return this + */ + public TreeObjectMap append(final K key, final V value) { + if (containsKey(key)) { + replace(key, value); + } else { + put(key, value); + } + return this; + } + + @Override + public TreeObjectMap append(@NotNull Map map) { + for (Map.Entry entry : map.entrySet()) { + append(entry.getKey(), entry.getValue()); + } + return this; + } + + @Override + public TreeObjectMap append(@NotNull ObjectMap map) { + for (Map.Entry entry : map.entrySet()) { + append(entry.getKey(), entry.getValue()); + } + return this; + } + + /** + * Put/replace the given key/value pair into TreeObjectMap if boolean is true and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.append("a", 1, check1).append("b", 2, check2)}
+     * 
+ * + * @param key key + * @param value value + * @param ifTrue ifTrue + * @return this + */ + public TreeObjectMap appendIfTrue(final K key, final V value, boolean ifTrue) { + if (ifTrue) + append(key, value); + return this; + } + + /** + * Put/replace the given key/value pair into ObjectMap if boolean is true or not and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, 2, check1).appendIfTrue("b", 3, 4, check2)}
+     * 
+ * + * @param key key + * @param valueIfTrue the value if the ifTrue is true + * @param valueIfFalse the value if the ifTrue is false + * @param ifTrue ifTrue + * @return this + */ + public TreeObjectMap appendIfTrue(final K key, final V valueIfTrue, final V valueIfFalse, boolean ifTrue) { + if (ifTrue) { + append(key, valueIfTrue); + } else { + append(key, valueIfFalse); + } + return this; + } + + /** + * Put/replace a given map into this ObjectMap if boolean is true and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, check1).appendIfTrue(map, check2)}
+     * 
+ * + * @param map key + * @param ifTrue ifTrue + * @return this + */ + @Override + public TreeObjectMap appendIfTrue(@NotNull Map map, boolean ifTrue) { + for (Map.Entry entry : map.entrySet()) { + appendIfTrue(entry.getKey(), entry.getValue(), ifTrue); + } + return this; + } + + /** + * Put/replace the given key/value pair into ObjectMap if boolean is true or not and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, 2, check1).appendIfTrue(map1, map2, check2)}
+     * 
+ * + * @param mapIfTrue the map if the ifTrue is true + * @param mapIfFalse the map if the ifTrue is false + * @param ifTrue ifTrue + * @return this + */ + @Override + public TreeObjectMap appendIfTrue(Map mapIfTrue, Map mapIfFalse, boolean ifTrue) { + if (ifTrue) { + append(mapIfTrue); + } else { + append(mapIfFalse); + } + return this; + } + + /** + * Put/replace a given map into this ObjectMap if boolean is true and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, check1).appendIfTrue(map, check2)}
+     * 
+ * + * @param map key + * @param ifTrue ifTrue + * @return this + */ + @Override + public TreeObjectMap appendIfTrue(@NotNull ObjectMap map, boolean ifTrue) { + for (Map.Entry entry : map.entrySet()) { + appendIfTrue(entry.getKey(), entry.getValue(), ifTrue); + } + return this; + } + + /** + * Put/replace the given key/value pair into ObjectMap if boolean is true or not and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, 2, check1).appendIfTrue(map1, map2, check2)}
+     * 
+ * + * @param mapIfTrue the map if the ifTrue is true + * @param mapIfFalse the map if the ifTrue is false + * @param ifTrue ifTrue + * @return this + */ + @Override + public TreeObjectMap appendIfTrue(ObjectMap mapIfTrue, Map mapIfFalse, boolean ifTrue) { + if (ifTrue) { + append(mapIfTrue); + } else { + append(mapIfFalse); + } + return this; + } + + /** + * Removes the entry with the specified key from the ObjectMap. + * + * @param key the key of the entry to be removed + * @return the modified ObjectMap with the specified entry removed, or the original ObjectMap if the key was not found + */ + @Override + public TreeObjectMap removeEntry(K key) { + remove(key); + return this; + } + + /** + * Removes all entries with keys present in the specified map from the ObjectMap. + * + * @param map the map containing the keys to be removed + * @return the modified ObjectMap with the entries corresponding to the specified keys removed + */ + @Override + public TreeObjectMap removeEntries(Map map) { + for (Map.Entry entry : map.entrySet()) { + remove(entry.getKey()); + } + return this; + } + + /** + * Removes all entries with keys present in the specified ObjectMap from the ObjectMap. + * + * @param map the ObjectMap containing the keys to be removed + * @return the modified ObjectMap with the entries corresponding to the keys in the specified ObjectMap removed + */ + @Override + public TreeObjectMap removeEntries(ObjectMap map) { + for (ObjectMap.Entry entry : map.entrySet()) { + remove(entry.getKey()); + } + return this; + } + + /** + * Removes the entry with the specified key from the ObjectMap if the condition is true. + * + * @param key the key of the entry to be removed + * @param ifTrue the condition to check before removing the entry + * @return the modified ObjectMap with the specified entry removed if the condition is true, or the original ObjectMap otherwise + */ + @Override + public TreeObjectMap removeEntryIfTrue(K key, boolean ifTrue) { + if (ifTrue) { + remove(key); + } + return this; + } + + /** + * Removes all entries with keys present in the specified map from the ObjectMap if the condition is true. + * + * @param map the map containing the keys to be removed + * @param ifTrue the condition to check before removing the entries + * @return the modified ObjectMap with the entries corresponding to the keys in the specified map removed if the condition is true, or the original ObjectMap otherwise + */ + @Override + public TreeObjectMap removeEntriesIfTrue(Map map, boolean ifTrue) { + if (ifTrue) { + for (Map.Entry entry : map.entrySet()) { + remove(entry.getKey()); + } + } + return this; + } + + /** + * Removes all entries with keys present in the specified ObjectMap from the ObjectMap if the condition is true. + * + * @param map the ObjectMap containing the keys to be removed + * @param ifTrue the condition to check before removing the entries + * @return the modified ObjectMap with the entries corresponding to the keys in the specified ObjectMap removed if the condition is true, or the original ObjectMap otherwise + */ + @Override + public TreeObjectMap removeEntriesIfTrue(ObjectMap map, boolean ifTrue) { + if (ifTrue) { + for (ObjectMap.Entry entry : map.entrySet()) { + remove(entry.getKey()); + } + } + return this; + } + + /** + * Gets the value of the given key as an Integer. + * + * @param key the key + * @return the value as an integer, which may be null + * @throws ClassCastException if the value is not an integer + */ + public Integer getInteger(final Object key) { + return getInteger(key, 0); + } + + /** + * Gets the value of the given key as a primitive int. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as an integer, which may be null + * @throws ClassCastException if the value is not an integer + */ + public int getInteger(final Object key, final int defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value of the given key as a Long. + * + * @param key the key + * @return the value as a long, which may be null + * @throws ClassCastException if the value is not an long + */ + public Long getLong(final Object key) { + return getLong(key, 0L); + } + + /** + * Gets the value of the given key as a Long. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a long, which may be null + * @throws ClassCastException if the value is not an long + */ + public Long getLong(final Object key, final long defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value of the given key as a Double. + * + * @param key the key + * @return the value as a double, which may be null + * @throws ClassCastException if the value is not an double + */ + public Double getDouble(final Object key) { + return getDouble(key, 0D); + } + + /** + * Gets the value of the given key as a Double. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a double, which may be null + * @throws ClassCastException if the value is not an double + */ + public Double getDouble(final Object key, final double defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value of the given key as a String. + * + * @param key the key + * @return the value as a String, which may be null + * @throws ClassCastException if the value is not a String + */ + public String getString(final Object key) { + return getString(key, ""); + } + + /** + * Gets the value of the given key as a String. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a String, which may be null + * @throws ClassCastException if the value is not a String + */ + public String getString(final Object key, final String defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value of the given key as a Boolean. + * + * @param key the key + * @return the value as a Boolean, which may be null + * @throws ClassCastException if the value is not an boolean + */ + public Boolean getBoolean(final Object key) { + return getBoolean(key, false); + } + + /** + * Gets the value of the given key as a primitive boolean. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a primitive boolean + * @throws ClassCastException if the value is not a boolean + */ + public boolean getBoolean(final Object key, final boolean defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the value of the given key as a Date. + * + * @param key the key + * @return the value as a Date, which may be null + * @throws ClassCastException if the value is not a Date + */ + public Date getDate(final Object key) { + return getDate(key, new Date()); + } + + /** + * Gets the value of the given key as a Date. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a Date, which may be null + * @throws ClassCastException if the value is not a Date + */ + public Date getDate(final Object key, final Date defaultValue) { + return get(key, defaultValue); + } + + /** + * Gets the list value of the given key, casting the list elements to the given {@code Class}. This is useful to avoid having + * casts in client code, though the effect is the same. + * + * @param key the key + * @param clazz the non-null class to cast the list value to + * @param the type of the class + * @return the list value of the given key, or null if the instance does not contain this key. + * @throws ClassCastException if the elements in the list value of the given key is not of type T or the value is not a list + */ + public List getList(Object key, Class clazz) { + return getList(key, clazz, null); + } + + /** + * Gets the list value of the given key, casting the list elements to {@code Class} or returning the default list value if null. + * This is useful to avoid having casts in client code, though the effect is the same. + * + * @param key the key + * @param clazz the non-null class to cast the list value to + * @param defaultValue what to return if the value is null + * @param the type of the class + * @return the list value of the given key, or the default list value if the instance does not contain this key. + * @throws ClassCastException if the value of the given key is not of type T + */ + public List getList(final Object key, final Class clazz, final List defaultValue) { + List value = get(key, List.class); + if (value == null) { + return defaultValue; + } + + for (Object item : value) { + if (!clazz.isAssignableFrom(item.getClass())) { + throw new ClassCastException(format("List element cannot be cast to %s", clazz.getName())); + } + } + return value; + } + + /** + * Gets the value of the given key, casting it to the given {@code Class}. This is useful to avoid having casts in client code, + * though the effect is the same. So to get the value of a key that is of type String, you would write {@code String name = + * doc.get("name", String.class)} instead of {@code String name = (String) doc.get("x") }. + * + * @param key the key + * @param clazz the non-null class to cast the value to + * @param the type of the class + * @return the value of the given key, or null if the instance does not contain this key. + * @throws ClassCastException if the value of the given key is not of type T + */ + public T get(final Object key, final Class clazz) { + return clazz.cast(get(key)); + } + + /** + * Gets the value of the given key, casting it to {@code Class} or returning the default value if null. + * This is useful to avoid having casts in client code, though the effect is the same. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @param the type of the class + * @return the value of the given key, or null if the instance does not contain this key. + * @throws ClassCastException if the value of the given key is not of type T + */ + public T get(final Object key, final T defaultValue) { + Object value = get(key); + return value == null ? defaultValue : (T) value; + } + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/maps/UnmodifiableObjectMap.java b/common/src/main/java/com/georgev22/skinoverlay/maps/UnmodifiableObjectMap.java new file mode 100644 index 00000000..bd93b23e --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/maps/UnmodifiableObjectMap.java @@ -0,0 +1,705 @@ +package com.georgev22.skinoverlay.maps; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.Serializable; +import java.util.*; + +/** + * Returns an unmodifiable view of the + * specified map. Query operations on the returned map "read through" + * to the specified map, and attempts to modify the returned + * map, whether direct or via its collection views, result in an + * {@code UnsupportedOperationException}.

+ */ +public class UnmodifiableObjectMap implements ObjectMap, Serializable { + + private final HashObjectMap hashObjectMap = new HashObjectMap<>(); + + public UnmodifiableObjectMap(ObjectMap map) { + hashObjectMap.append(map); + } + + public UnmodifiableObjectMap(Map map) { + hashObjectMap.append(map); + } + + /** + * Put/replace the given key/value pair into this ObjectMap and return this. Useful for chaining puts in a single expression, e.g. + *

+     * user.append("a", 1).append("b", 2)}
+     * 
+ * + * @param key key + * @param value value + * @return this + */ + @Override + public UnmodifiableObjectMap append(K key, V value) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * Put/replace a given map into this ObjectMap and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.append("a", 1).append(map)}
+     * 
+ * + * @param map the map to append to the current one + * @return this + */ + @Override + public UnmodifiableObjectMap append(Map map) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * Put/replace a given map into this ObjectMap and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.append("a", 1).append(map)}
+     * 
+ * + * @param map the map to append to the current one + * @return this + */ + @Override + public UnmodifiableObjectMap append(ObjectMap map) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * Put/replace the given key/value pair into ObjectMap if boolean is true and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, check1).appendIfTrue("b", 2, check2)}
+     * 
+ * + * @param key key + * @param value value + * @param ifTrue ifTrue + * @return this + */ + @Override + public UnmodifiableObjectMap appendIfTrue(K key, V value, boolean ifTrue) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * Put/replace the given key/value pair into ObjectMap if boolean is true or not and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, 2, check1).appendIfTrue("b", 3, 4, check2)}
+     * 
+ * + * @param key key + * @param valueIfTrue the value if the ifTrue is true + * @param valueIfFalse the value if the ifTrue is false + * @param ifTrue ifTrue + * @return this + */ + @Override + public UnmodifiableObjectMap appendIfTrue(K key, V valueIfTrue, V valueIfFalse, boolean ifTrue) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * Put/replace a given map into this ObjectMap if boolean is true and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, check1).appendIfTrue(map, check2)}
+     * 
+ * + * @param map key + * @param ifTrue ifTrue + * @return this + */ + @Override + public UnmodifiableObjectMap appendIfTrue(Map map, boolean ifTrue) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * Put/replace the given key/value pair into ObjectMap if boolean is true or not and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, 2, check1).appendIfTrue(map1, map2, check2)}
+     * 
+ * + * @param mapIfTrue the map if the ifTrue is true + * @param mapIfFalse the map if the ifTrue is false + * @param ifTrue ifTrue + * @return this + */ + @Override + public UnmodifiableObjectMap appendIfTrue(Map mapIfTrue, Map mapIfFalse, boolean ifTrue) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * Put/replace a given map into this ObjectMap if boolean is true and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, check1).appendIfTrue(map, check2)}
+     * 
+ * + * @param map key + * @param ifTrue ifTrue + * @return this + */ + @Override + public UnmodifiableObjectMap appendIfTrue(ObjectMap map, boolean ifTrue) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * Put/replace the given key/value pair into ObjectMap if boolean is true or not and return this. Useful for chaining puts in a single expression, e.g. + *
+     * user.appendIfTrue("a", 1, 2, check1).appendIfTrue(map1, map2, check2)}
+     * 
+ * + * @param mapIfTrue the map if the ifTrue is true + * @param mapIfFalse the map if the ifTrue is false + * @param ifTrue ifTrue + * @return this + */ + @Override + public UnmodifiableObjectMap appendIfTrue(ObjectMap mapIfTrue, Map mapIfFalse, boolean ifTrue) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * Removes the entry with the specified key from the ObjectMap. + * + * @param key the key of the entry to be removed + * @return the modified ObjectMap with the specified entry removed, or the original ObjectMap if the key was not found + */ + @Override + public UnmodifiableObjectMap removeEntry(K key) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * Removes all entries with keys present in the specified map from the ObjectMap. + * + * @param map the map containing the keys to be removed + * @return the modified ObjectMap with the entries corresponding to the specified keys removed + */ + @Override + public UnmodifiableObjectMap removeEntries(Map map) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * Removes all entries with keys present in the specified ObjectMap from the ObjectMap. + * + * @param map the ObjectMap containing the keys to be removed + * @return the modified ObjectMap with the entries corresponding to the keys in the specified ObjectMap removed + */ + @Override + public UnmodifiableObjectMap removeEntries(ObjectMap map) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * Removes the entry with the specified key from the ObjectMap if the condition is true. + * + * @param key the key of the entry to be removed + * @param ifTrue the condition to check before removing the entry + * @return the modified ObjectMap with the specified entry removed if the condition is true, or the original ObjectMap otherwise + */ + @Override + public UnmodifiableObjectMap removeEntryIfTrue(K key, boolean ifTrue) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * Removes all entries with keys present in the specified map from the ObjectMap if the condition is true. + * + * @param map the map containing the keys to be removed + * @param ifTrue the condition to check before removing the entries + * @return the modified ObjectMap with the entries corresponding to the keys in the specified map removed if the condition is true, or the original ObjectMap otherwise + */ + @Override + public UnmodifiableObjectMap removeEntriesIfTrue(Map map, boolean ifTrue) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * Removes all entries with keys present in the specified ObjectMap from the ObjectMap if the condition is true. + * + * @param map the ObjectMap containing the keys to be removed + * @param ifTrue the condition to check before removing the entries + * @return the modified ObjectMap with the entries corresponding to the keys in the specified ObjectMap removed if the condition is true, or the original ObjectMap otherwise + */ + @Override + public UnmodifiableObjectMap removeEntriesIfTrue(ObjectMap map, boolean ifTrue) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * Gets the value of the given key as an Integer. + * + * @param key the key + * @return the value as an integer, which may be null + * @throws ClassCastException if the value is not an integer + */ + @Override + public Integer getInteger(Object key) { + return hashObjectMap.getInteger(key); + } + + /** + * Gets the value of the given key as a primitive int. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as an integer, which may be null + * @throws ClassCastException if the value is not an integer + */ + @Override + public int getInteger(Object key, int defaultValue) { + return hashObjectMap.getInteger(key, defaultValue); + } + + /** + * Gets the value of the given key as a Long. + * + * @param key the key + * @return the value as a long, which may be null + * @throws ClassCastException if the value is not an long + */ + @Override + public Long getLong(Object key) { + return hashObjectMap.getLong(key); + } + + /** + * Gets the value of the given key as a Long. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a long, which may be null + * @throws ClassCastException if the value is not an long + */ + @Override + public Long getLong(Object key, long defaultValue) { + return hashObjectMap.getLong(key, defaultValue); + } + + /** + * Gets the value of the given key as a Double. + * + * @param key the key + * @return the value as a double, which may be null + * @throws ClassCastException if the value is not an double + */ + @Override + public Double getDouble(Object key) { + return hashObjectMap.getDouble(key); + } + + /** + * Gets the value of the given key as a Double. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a double, which may be null + * @throws ClassCastException if the value is not an double + */ + @Override + public Double getDouble(Object key, double defaultValue) { + return hashObjectMap.getDouble(key, defaultValue); + } + + /** + * Gets the value of the given key as a String. + * + * @param key the key + * @return the value as a String, which may be null + * @throws ClassCastException if the value is not a String + */ + @Override + public String getString(Object key) { + return hashObjectMap.getString(key); + } + + /** + * Gets the value of the given key as a String. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a String, which may be null + * @throws ClassCastException if the value is not a String + */ + @Override + public String getString(Object key, String defaultValue) { + return hashObjectMap.getString(key, defaultValue); + } + + /** + * Gets the value of the given key as a Boolean. + * + * @param key the key + * @return the value as a Boolean, which may be null + * @throws ClassCastException if the value is not an boolean + */ + @Override + public Boolean getBoolean(Object key) { + return hashObjectMap.getBoolean(key); + } + + /** + * Gets the value of the given key as a primitive boolean. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a primitive boolean + * @throws ClassCastException if the value is not a boolean + */ + @Override + public boolean getBoolean(Object key, boolean defaultValue) { + return hashObjectMap.getBoolean(key, defaultValue); + } + + /** + * Gets the value of the given key as a Date. + * + * @param key the key + * @return the value as a Date, which may be null + * @throws ClassCastException if the value is not a Date + */ + @Override + public Date getDate(Object key) { + return hashObjectMap.getDate(key); + } + + /** + * Gets the value of the given key as a Date. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value as a Date, which may be null + * @throws ClassCastException if the value is not a Date + */ + @Override + public Date getDate(Object key, Date defaultValue) { + return hashObjectMap.getDate(key, defaultValue); + } + + /** + * Gets the list value of the given key, casting the list elements to the given {@code Class}. This is useful to avoid having + * casts in client code, though the effect is the same. + * + * @param key the key + * @param clazz the non-null class to cast the list value to + * @return the list value of the given key, or null if the instance does not contain this key. + * @throws ClassCastException if the elements in the list value of the given key is not of type T or the value is not a list + */ + @Override + public List getList(Object key, Class clazz) { + return hashObjectMap.getList(key, clazz); + } + + /** + * Gets the list value of the given key, casting the list elements to {@code Class} or returning the default list value if null. + * This is useful to avoid having casts in client code, though the effect is the same. + * + * @param key the key + * @param clazz the non-null class to cast the list value to + * @param defaultValue what to return if the value is null + * @return the list value of the given key, or the default list value if the instance does not contain this key. + * @throws ClassCastException if the value of the given key is not of type T + */ + @Override + public List getList(Object key, Class clazz, List defaultValue) { + return hashObjectMap.getList(key, clazz, defaultValue); + } + + /** + * Gets the value of the given key, casting it to the given {@code Class}. This is useful to avoid having casts in client code, + * though the effect is the same. So to get the value of a key that is of type String, you would write {@code String name = + * doc.get("name", String.class)} instead of {@code String name = (String) doc.get("x") }. + * + * @param key the key + * @param clazz the non-null class to cast the value to + * @return the value of the given key, or null if the instance does not contain this key. + * @throws ClassCastException if the value of the given key is not of type T + */ + @Override + public T get(Object key, Class clazz) { + return hashObjectMap.get(key, clazz); + } + + /** + * Gets the value of the given key, casting it to {@code Class} or returning the default value if null. + * This is useful to avoid having casts in client code, though the effect is the same. + * + * @param key the key + * @param defaultValue what to return if the value is null + * @return the value of the given key, or null if the instance does not contain this key. + * @throws ClassCastException if the value of the given key is not of type T + */ + @Override + public T get(Object key, T defaultValue) { + return hashObjectMap.get(key, defaultValue); + } + + /** + * Returns the number of key-value mappings in this map. If the + * map contains more than {@code Integer.MAX_VALUE} elements, returns + * {@code Integer.MAX_VALUE}. + * + * @return the number of key-value mappings in this map + */ + @Override + public int size() { + return hashObjectMap.size(); + } + + /** + * Returns {@code true} if this map contains no key-value mappings. + * + * @return {@code true} if this map contains no key-value mappings + */ + @Override + public boolean isEmpty() { + return hashObjectMap.isEmpty(); + } + + /** + * Returns {@code true} if this map contains a mapping for the specified + * key. More formally, returns {@code true} if and only if + * this map contains a mapping for a key {@code k} such that + * {@code Objects.equals(key, k)}. (There can be + * at most one such mapping.) + * + * @param key key whose presence in this map is to be tested + * @return {@code true} if this map contains a mapping for the specified + * key + * @throws ClassCastException if the key is of an inappropriate type for + * this map + * (optional) + * @throws NullPointerException if the specified key is null and this map + * does not permit null keys + * (optional) + */ + @Override + public boolean containsKey(Object key) { + return hashObjectMap.containsKey(key); + } + + /** + * Returns {@code true} if this map maps one or more keys to the + * specified value. More formally, returns {@code true} if and only if + * this map contains at least one mapping to a value {@code v} such that + * {@code Objects.equals(value, v)}. This operation + * will probably require time linear in the map size for most + * implementations of the {@code Map} interface. + * + * @param value value whose presence in this map is to be tested + * @return {@code true} if this map maps one or more keys to the + * specified value + * @throws ClassCastException if the value is of an inappropriate type for + * this map + * (optional) + * @throws NullPointerException if the specified value is null and this + * map does not permit null values + * (optional) + */ + @Override + public boolean containsValue(Object value) { + return hashObjectMap.containsValue(value); + } + + /** + * Returns the value to which the specified key is mapped, + * or {@code null} if this map contains no mapping for the key. + * + *

More formally, if this map contains a mapping from a key + * {@code k} to a value {@code v} such that + * {@code Objects.equals(key, k)}, + * then this method returns {@code v}; otherwise + * it returns {@code null}. (There can be at most one such mapping.) + * + *

If this map permits null values, then a return value of + * {@code null} does not necessarily indicate that the map + * contains no mapping for the key; it's also possible that the map + * explicitly maps the key to {@code null}. The {@link #containsKey + * containsKey} operation may be used to distinguish these two cases. + * + * @param key the key whose associated value is to be returned + * @return the value to which the specified key is mapped, or + * {@code null} if this map contains no mapping for the key + * @throws ClassCastException if the key is of an inappropriate type for + * this map + * (optional) + * @throws NullPointerException if the specified key is null and this map + * does not permit null keys + * (optional) + */ + @Override + public V get(Object key) { + return hashObjectMap.get(key); + } + + /** + * Associates the specified value with the specified key in this map + * (optional operation). If the map previously contained a mapping for + * the key, the old value is replaced by the specified value. (A map + * {@code m} is said to contain a mapping for a key {@code k} if and only + * if {@link #containsKey(Object) m.containsKey(k)} would return + * {@code true}.) + * + * @param key key with which the specified value is to be associated + * @param value value to be associated with the specified key + * @return the previous value associated with {@code key}, or + * {@code null} if there was no mapping for {@code key}. + * (A {@code null} return can also indicate that the map + * previously associated {@code null} with {@code key}, + * if the implementation supports {@code null} values.) + * @throws UnsupportedOperationException if the {@code put} operation + * is not supported by this map + * @throws ClassCastException if the class of the specified key or value + * prevents it from being stored in this map + * @throws NullPointerException if the specified key or value is null + * and this map does not permit null keys or values + * @throws IllegalArgumentException if some property of the specified key + * or value prevents it from being stored in this map + */ + @Nullable + @Override + public V put(K key, V value) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * Removes the mapping for a key from this map if it is present + * (optional operation). More formally, if this map contains a mapping + * from key {@code k} to value {@code v} such that + * {@code Objects.equals(key, k)}, that mapping + * is removed. (The map can contain at most one such mapping.) + * + *

Returns the value to which this map previously associated the key, + * or {@code null} if the map contained no mapping for the key. + * + *

If this map permits null values, then a return value of + * {@code null} does not necessarily indicate that the map + * contained no mapping for the key; it's also possible that the map + * explicitly mapped the key to {@code null}. + * + *

The map will not contain a mapping for the specified key once the + * call returns. + * + * @param key key whose mapping is to be removed from the map + * @return the previous value associated with {@code key}, or + * {@code null} if there was no mapping for {@code key}. + * @throws UnsupportedOperationException if the {@code remove} operation + * is not supported by this map + * @throws ClassCastException if the key is of an inappropriate type for + * this map + * (optional) + * @throws NullPointerException if the specified key is null and this + * map does not permit null keys + * (optional) + */ + @Override + public V remove(Object key) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * Copies all of the mappings from the specified map to this map + * (optional operation). The effect of this call is equivalent to that + * of calling {@link #put(Object, Object) put(k, v)} on this map once + * for each mapping from key {@code k} to value {@code v} in the + * specified map. The behavior of this operation is undefined if the + * specified map is modified while the operation is in progress. + * + * @param m mappings to be stored in this map + * @throws UnsupportedOperationException if the {@code putAll} operation + * is not supported by this map + * @throws ClassCastException if the class of a key or value in the + * specified map prevents it from being stored in this map + * @throws NullPointerException if the specified map is null, or if + * this map does not permit null keys or values, and the + * specified map contains null keys or values + * @throws IllegalArgumentException if some property of a key or value in + * the specified map prevents it from being stored in this map + */ + @Override + public void putAll(@NotNull Map m) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * Removes all of the mappings from this map (optional operation). + * The map will be empty after this call returns. + * + * @throws UnsupportedOperationException if the {@code clear} operation + * is not supported by this map + */ + @Override + public void clear() { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * Returns a {@link Set} view of the keys contained in this map. + * The set is backed by the map, so changes to the map are + * reflected in the set, and vice-versa. If the map is modified + * while an iteration over the set is in progress (except through + * the iterator's own {@code remove} operation), the results of + * the iteration are undefined. The set supports element removal, + * which removes the corresponding mapping from the map, via the + * {@code Iterator.remove}, {@code Set.remove}, + * {@code removeAll}, {@code retainAll}, and {@code clear} + * operations. It does not support the {@code add} or {@code addAll} + * operations. + * + * @return a set view of the keys contained in this map + */ + @NotNull + @Override + public Set keySet() { + return Collections.unmodifiableSet(hashObjectMap.keySet()); + } + + /** + * Returns a {@link Collection} view of the values contained in this map. + * The collection is backed by the map, so changes to the map are + * reflected in the collection, and vice-versa. If the map is + * modified while an iteration over the collection is in progress + * (except through the iterator's own {@code remove} operation), + * the results of the iteration are undefined. The collection + * supports element removal, which removes the corresponding + * mapping from the map, via the {@code Iterator.remove}, + * {@code Collection.remove}, {@code removeAll}, + * {@code retainAll} and {@code clear} operations. It does not + * support the {@code add} or {@code addAll} operations. + * + * @return a collection view of the values contained in this map + */ + @NotNull + @Override + public Collection values() { + return Collections.unmodifiableCollection(hashObjectMap.values()); + } + + /** + * Returns a {@link Set} view of the mappings contained in this map. + * The set is backed by the map, so changes to the map are + * reflected in the set, and vice-versa. If the map is modified + * while an iteration over the set is in progress (except through + * the iterator's own {@code remove} operation, or through the + * {@code setValue} operation on a map entry returned by the + * iterator) the results of the iteration are undefined. The set + * supports element removal, which removes the corresponding + * mapping from the map, via the {@code Iterator.remove}, + * {@code Set.remove}, {@code removeAll}, {@code retainAll} and + * {@code clear} operations. It does not support the + * {@code add} or {@code addAll} operations. + * + * @return a set view of the mappings contained in this map + */ + @NotNull + @Override + public Set> entrySet() { + return Collections.unmodifiableSet(hashObjectMap.entrySet()); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/message/MessageBuilder.java b/common/src/main/java/com/georgev22/skinoverlay/message/MessageBuilder.java new file mode 100644 index 00000000..004d8ded --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/message/MessageBuilder.java @@ -0,0 +1,504 @@ +package com.georgev22.skinoverlay.message; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.utilities.Utils; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.logging.Level; + +/** + * A utility class for building and sending styled chat messages to players in a Minecraft server environment + * using the Adventure API. This class supports text colors, decorations, click events, hover events, and MiniMessage + * formatting. + * + *

Example usage:

+ *
{@code
+ * // Create an instance of the MessageBuilder
+ * MessageBuilder messageBuilder = new MessageBuilder();
+ *
+ * // Build a message using plain text and styles
+ * messageBuilder
+ *     .append("Welcome, ")
+ *     .color(NamedTextColor.AQUA)
+ *     .decorate(TextDecoration.BOLD)
+ *     .append(player.getName())
+ *     .color(NamedTextColor.GREEN)
+ *     .append("! Click ")
+ *     .color(NamedTextColor.YELLOW)
+ *     .append("here")
+ *     .color(NamedTextColor.RED)
+ *     .decorate(TextDecoration.UNDERLINED)
+ *     .clickEvent(ClickEvent.Action.RUN_COMMAND, "/help")
+ *     .hoverEvent("Click to run /help")
+ *     .append(" for more info.")
+ *     .send(player);
+ *
+ * // Using MiniMessage for more complex formatting
+ * messageBuilder
+ *     .appendMiniMessage("Welcome, " + player.getName() + "")
+ *     .appendMiniMessage(" Click here for more info.")
+ *     .clickEvent(ClickEvent.Action.RUN_COMMAND, "/help")
+ *     .hoverEvent("Click to run /help")
+ *     .send(player);
+ * }
+ */ +@SuppressWarnings({"UnusedReturnValue", "unused"}) +public class MessageBuilder { + private final ArrayList currentDecorations; + private TextComponent.Builder componentBuilder; + private TextColor currentColor = NamedTextColor.WHITE; + private ClickEvent currentClickEvent; + private HoverEvent currentHoverEvent; + private ObjectMap placeholders = new HashObjectMap<>(); + + /** + * Constructs a new MessageBuilder instance. + */ + public MessageBuilder() { + this.componentBuilder = Component.text(); + this.currentDecorations = new ArrayList<>(); + } + + /** + * Creates a new MessageBuilder instance. + * + * @return A new MessageBuilder instance. + */ + @Contract(" -> new") + public static @NotNull MessageBuilder builder() { + return new MessageBuilder(); + } + + /** + * Appends plain text to the message and applies the current styles. + * + *

Example usage:

+ *
{@code
+     * messageBuilder
+     *     .append("Hello ")
+     *     .color(NamedTextColor.RED)
+     *     .decorate(TextDecoration.BOLD)
+     *     .append("World!");
+     * }
+ * + * @param text The text to append. + * @return The MessageBuilder instance for method chaining. + */ + public MessageBuilder append(String text) { + text = replacePlaceholders(text); + TextComponent.Builder textComponent = Component.text() + .content(text) + .color(currentColor); + + for (TextDecoration decoration : currentDecorations) { + textComponent.decoration(decoration, true); + } + + if (currentClickEvent != null) { + textComponent.clickEvent(currentClickEvent); + } + + if (currentHoverEvent != null) { + textComponent.hoverEvent(currentHoverEvent); + } + + componentBuilder.append(textComponent.build()); + return this; + } + + /** + * Appends a component to the message. + * + * @param component The component to append. + * @return The MessageBuilder instance for method chaining. + */ + public MessageBuilder append(Component component) { + componentBuilder.append(component); + return this; + } + + /** + * Appends MiniMessage-formatted text, allowing complex formatting using MiniMessage syntax. + *

Example usage:

+ *
{@code
+     * messageBuilder
+     *     .appendMiniMessage("Hello, World!")
+     *     .clickEvent(ClickEvent.Action.RUN_COMMAND, "/example")
+     *     .hoverEvent("Execute command /example");
+     * }
+ * + * @param miniMessage The MiniMessage string to append. + * @return The MessageBuilder instance for method chaining. + */ + public MessageBuilder appendMiniMessage(String miniMessage) { + return appendMiniMessage(miniMessage, null); + } + + /** + * Appends MiniMessage-formatted text, allowing complex formatting using MiniMessage syntax. + *

Example usage:

+ *
{@code
+     * messageBuilder
+     *     .appendMiniMessage("Hello, World!")
+     *     .clickEvent(ClickEvent.Action.RUN_COMMAND, "/example")
+     *     .hoverEvent("Execute command /example");
+     * }
+ * + * @param miniMessage The MiniMessage string to append. + * @param tagResolver A tag resolver for parsing the MiniMessage, can be null. + * @return The MessageBuilder instance for method chaining. + */ + public MessageBuilder appendMiniMessage(String miniMessage, TagResolver tagResolver) { + componentBuilder.append(miniMessage(miniMessage, tagResolver)).decoration(TextDecoration.ITALIC, false); + return this; + } + + /** + * Converts a MiniMessage string into a Component using an optional TagResolver. + * + * @param message The MiniMessage string to convert. + * @param tagResolver The TagResolver to use, can be null if not needed. + * @return The converted Component. + */ + private @NotNull Component miniMessage(String message, TagResolver tagResolver) { + message = replacePlaceholders(message); + return MessageParser.miniMessage(message, tagResolver); + } + + /** + * Sets the color of the subsequent text. + * + *

Example usage:

+ *
{@code
+     * messageBuilder
+     *     .color(NamedTextColor.BLUE)
+     *     .append("This text is blue");
+     * }
+ * + * @param color The TextColor to set. + * @return The MessageBuilder instance for method chaining. + */ + public MessageBuilder color(TextColor color) { + this.currentColor = color; + return this; + } + + /** + * Adds text decorations like bold, italic, etc. + * + *

Example usage:

+ *
{@code
+     * messageBuilder
+     *     .decorate(TextDecoration.BOLD, TextDecoration.ITALIC)
+     *     .append("Bold and Italic Text");
+     * }
+ * + * @param decorations The TextDecorations to apply. + * @return The MessageBuilder instance for method chaining. + */ + public MessageBuilder decorate(TextDecoration... decorations) { + Collections.addAll(this.currentDecorations, decorations); + return this; + } + + /** + * Removes text decorations. + * + *

Example usage:

+ *
{@code
+     * messageBuilder
+     *     .decorate(TextDecoration.BOLD)
+     *     .append("Bold text")
+     *     .removeDecorate(TextDecoration.BOLD)
+     *     .append("Normal text");
+     * }
+ * + * @param decorations The TextDecorations to remove. + * @return The MessageBuilder instance for method chaining. + */ + public MessageBuilder removeDecorate(TextDecoration... decorations) { + this.currentDecorations.removeAll(Arrays.asList(decorations)); + return this; + } + + /** + * Sets a click event for the subsequent text. + * + *

Example usage:

+ *
{@code
+     * messageBuilder
+     *     .append("Click me")
+     *     .clickEvent(ClickEvent.Action.RUN_COMMAND, "/example");
+     * }
+ * + * @param action The ClickEvent.Action to set. + * @param value The value associated with the action. + * @return The MessageBuilder instance for method chaining. + */ + public MessageBuilder clickEvent(ClickEvent.Action action, String value) { + value = replacePlaceholders(value); + this.currentClickEvent = ClickEvent.clickEvent(action, value); + return this; + } + + /** + * Sets a hover event for the subsequent text. + * + *

Example usage:

+ *
{@code
+     * messageBuilder
+     *     .append("Hover over me")
+     *     .hoverEvent("This is hover text");
+     * }
+ * + * @param hoverText The text to show on hover. + * @return The MessageBuilder instance for method chaining. + */ + public MessageBuilder hoverEvent(String hoverText) { + hoverText = replacePlaceholders(hoverText); + this.currentHoverEvent = HoverEvent.showText(miniMessage(hoverText, null)); + return this; + } + + /** + * Sends the constructed message to the specified player. + * + *

Example usage:

+ *
{@code
+     * messageBuilder
+     *     .append("Hello, ")
+     *     .append(player.getName())
+     *     .append("!")
+     *     .send(player);
+     * }
+ * + * @param audience The player to send the message to. + */ + public void send(@NotNull Audience audience) { + audience.sendMessage(componentBuilder.build()); + } + + /** + * Builds the component for further use. + * + *

Example usage:

+ *
{@code
+     * Component component = messageBuilder
+     *     .append("Hello, World!")
+     *     .build();
+     * }
+ * + * @return The constructed Component. + */ + public Component build() { + return componentBuilder.build(); + } + + /** + * Serializes the built component into a string using Legacy format. + * + *

Example usage:

+ *
{@code
+     * String messageString = messageBuilder
+     *     .append("Hello, World!")
+     *     .buildLegacyString();
+     * }
+ * + * @return The serialized Legacy string. + */ + public String buildLegacyString() { + return LegacyComponentSerializer.builder() + .hexColors() + .useUnusualXRepeatedCharacterHexFormat() + .build() + .serialize(build()); + } + + /** + * Builds the component and resets the current MessageBuilder. + * + *

Example usage:

+ *
{@code
+     * Component component = messageBuilder
+     *     .append("Hello, World!")
+     *     .buildAndReset();
+     * }
+ * + * @return The constructed Component. + */ + public Component buildAndReset() { + Component component = this.build(); + this.reset(); + return component; + } + + /** + * Builds the Legacy string and resets the current MessageBuilder. + * + *

Example usage:

+ *
{@code
+     * String legacyString = messageBuilder
+     *     .append("Hello, World!")
+     *     .buildAndResetLegacyString();
+     * }
+ * + * @return The serialized Legacy string. + */ + public String buildAndResetLegacyString() { + String legacyString = this.buildLegacyString(); + this.reset(); + return legacyString; + } + + /** + * Serializes the built component into a string using MiniMessage format. + * + *

Example usage:

+ *
{@code
+     * String messageString = messageBuilder
+     *     .append("Hello, World!")
+     *     .buildString();
+     * }
+ * + * @return The serialized MiniMessage string. + */ + public String buildString() { + return MiniMessage.miniMessage().serialize(build()); + } + + /** + * Resets the current styles and events, restoring them to default values. + *

Example usage:

+ *
{@code
+     * messageBuilder
+     *     .resetStyles()
+     *     .append("This text has default styling");
+     * }
+ * + * @return The MessageBuilder instance for method chaining. + */ + public MessageBuilder resetStyles() { + this.currentColor = NamedTextColor.WHITE; + this.currentDecorations.clear(); + this.currentClickEvent = null; + this.currentHoverEvent = null; + return this; + } + + public MessageBuilder reset() { + this.componentBuilder = Component.text(); + this.placeholders.clear(); + return this.resetStyles(); + } + + /** + * Sets the placeholders for the message. + * + * @param placeholders The placeholders to set. + * @return The MessageBuilder instance for method chaining. + */ + public MessageBuilder placeholders(ObjectMap placeholders) { + this.placeholders = new HashObjectMap<>(placeholders); + return this; + } + + /** + * Clears all placeholders from the message. + * + * @return The MessageBuilder instance for method chaining. + */ + public MessageBuilder clearPlaceholders() { + this.placeholders.clear(); + return this; + } + + /** + * Adds a placeholder to the message. + * + * @param key The placeholder key. + * @param value The placeholder value. + * @return The MessageBuilder instance for method chaining. + */ + public MessageBuilder addPlaceholder(String key, String value) { + this.placeholders.put(key, value); + return this; + } + + /** + * Attempts to convert this message (built as a MiniMessage string) into a Paper Adventure + * {@code net.kyori..adventure.text.Component} instance at runtime, without directly referencing + * Adventure API classes in bytecode. + * + *

This method is designed for environments where Adventure is shaded and relocated (e.g. Spigot), + * but Paper provides its own Adventure API. Direct casts or static imports would be relocated by the + * shadow plugin, causing runtime type mismatches. To avoid relocation, class names are constructed + * dynamically and reflection is used to invoke the MiniMessage deserializer on Paper.

+ * + *

If running on a non-Paper server (or if the Paper MiniMessage API is not available), this + * method returns {@code null}. The caller must handle the {@code null} return and fall back to the + * relocated Adventure API.

+ * + * @return a Paper {@code net.kyori.adventure.text.Component} instance if Paper Adventure is present, + * otherwise {@code null} + */ + public @Nullable Object toPaperComponent() { + try { + Class mmClass = Class.forName( + n("net", "kyori", "adventure", "text", "minimessage", "MiniMessage") + ); + Class componentClass = Class.forName( + n("net", "kyori", "adventure", "text", "Component") + ); + + Class tagResolverClass = Class.forName( + n("net", "kyori", "adventure", "text", "minimessage", "tag", "resolver", "TagResolver") + ); + + Object mm = mmClass.getMethod("miniMessage").invoke(null); + + Object emptyResolvers = java.lang.reflect.Array.newInstance(tagResolverClass, 0); + + return mmClass + .getMethod("deserialize", String.class, java.lang.reflect.Array.newInstance(tagResolverClass, 0).getClass()) + .invoke(mm, buildString(), emptyResolvers); + + } catch (Throwable throwable) { + SkinOverlay.getInstance().getLogger().log( + Level.WARNING, "Could not deserialize MiniMessage", throwable + ); + return null; + } + } + + /** + * Replaces placeholders in the given string with their corresponding values. + * + * @param input The input string containing placeholders. + * @return The input string with placeholders replaced. + */ + private String replacePlaceholders(String input) { + return Utils.placeHolder(input, placeholders, true); + } + + private String n(String... p) { + return String.join(".", p); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/message/MessageEntry.java b/common/src/main/java/com/georgev22/skinoverlay/message/MessageEntry.java new file mode 100644 index 00000000..941057ad --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/message/MessageEntry.java @@ -0,0 +1,219 @@ +package com.georgev22.skinoverlay.message; + + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.command.CommandIssuer; +import com.georgev22.skinoverlay.maps.HashObjectMap; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.kyori.adventure.text.minimessage.tag.standard.StandardTags; +import net.kyori.adventure.title.Title; + +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.Map; + +import static com.georgev22.skinoverlay.utilities.Utils.placeHolder; + + +/** + * Represents a single configurable message entry in a plugin. + *

+ * Implementations are usually enums, where each constant represents + * a message key with its default values. + */ +public interface MessageEntry { + + MessageEntry NONE = null; + + /** + * @return the path of this message in the configuration file + */ + String getPath(); + + /** + * @return the default messages for this entry + */ + String[] getDefaultMessages(); + + /** + * Updates the messages for this entry (loaded from config). + * + * @param messages array of messages (multi-line support) + */ + void setMessages(String[] messages); + + /** + * @return the current messages (from config, or defaults if not overridden) + */ + String[] getMessages(); + + /** + * Name of the YAML file, WITHOUT .yml extension + * Example: "core", "player", "menus" + */ + @NotNull String getFile(); + + /** + * Represents how a message should be displayed to a player. + */ + enum MessageType { + CHAT, + ACTIONBAR, + TITLE + } + + /** + * Sends a message to a sender as chat by default. + * + * @param issuer the receiver + */ + default void msg(CommandIssuer issuer) { + msg(issuer, new HashObjectMap<>(), false, MessageType.CHAT); + } + + /** + * Sends a message to a sender with a specific type. + * + * @param issuer the receiver + */ + default void msg(CommandIssuer issuer, MessageType type) { + msg(issuer, new HashObjectMap<>(), false, type); + } + + /** + * Sends a message with placeholder replacement. + * + * @param issuer the receiver + * @param map placeholder replacements + * @param ignoreCase whether to ignore case when replacing + */ + default void msg(CommandIssuer issuer, Map map, boolean ignoreCase) { + msg(issuer, map, ignoreCase, MessageType.CHAT); + } + + /** + * Sends a message with full customization. + * + * @param issuer the receiver + * @param map placeholder replacements + * @param ignoreCase whether to ignore case when replacing + * @param type how the message should be shown + */ + default void msg(CommandIssuer issuer, + Map map, boolean ignoreCase, + @NotNull MessageType type) { + + String[] messages = this.getMessages(); + MessageBuilder builder = new MessageBuilder(); + TagResolver resolver = StandardTags.defaults(); + + Audience audience = SkinOverlay.getInstance().getAudienceProvider().player(issuer.getUniqueId()); + + switch (type) { + case ACTIONBAR -> { + audience.sendActionBar( + builder.appendMiniMessage( + placeHolder(messages[0], map, ignoreCase), + resolver + ) + .buildAndReset() + ); + } + case TITLE -> { + Component title = builder + .appendMiniMessage(placeHolder(messages[0], map, ignoreCase), resolver) + .buildAndReset(); + + Component subtitle = messages.length > 1 + ? builder.appendMiniMessage(placeHolder(messages[1], map, ignoreCase), resolver).buildAndReset() + : Component.text(""); + + audience.showTitle(Title.title(title, subtitle, Title.DEFAULT_TIMES)); + } + case CHAT -> { + if (messages.length > 1) { + Arrays.stream(messages).forEach(msg -> + audience.sendMessage(builder + .appendMiniMessage(placeHolder(msg, map, ignoreCase), resolver) + .buildAndReset()) + ); + } else { + audience.sendMessage(builder + .appendMiniMessage(placeHolder(messages[0], map, ignoreCase), resolver) + .buildAndReset()); + } + } + } + } + + /** + * Sends a message to the console. + * + */ + default void msgConsole() { + msgConsole(Map.of(), false); + } + + /** + * Sends a message to the console with placeholders. + * + * @param map placeholder replacements + * @param ignoreCase whether to ignore case + */ + default void msgConsole(Map map, boolean ignoreCase) { + String[] messages = this.getMessages(); + Audience console = SkinOverlay.getInstance().getAudienceProvider().console(); + MessageBuilder messageBuilder = new MessageBuilder(); + TagResolver resolver = StandardTags.defaults(); + if (messages.length > 1) { + for (String message : messages) { + messageBuilder.appendMiniMessage(placeHolder(message, map, ignoreCase), resolver); + } + messageBuilder.send(console); + } else { + messageBuilder.appendMiniMessage(placeHolder(messages[0], map, ignoreCase), resolver); + messageBuilder.send(console); + } + } + + /** + * Sends a message to all online players. + * + */ + default void msgAll() { + msgAll(Map.of(), false); + } + + /** + * Sends a message to all online players with placeholders. + * + * @param map placeholder replacements + * @param ignoreCase whether to ignore case + */ + default void msgAll(Map map, boolean ignoreCase) { + SkinOverlay.getInstance().getPlayerProvider().getOnlinePlayers().forEach(p -> msg(p, map, ignoreCase)); + } + + /** + * Sends a message to all online players. + * + * @param type the message type (e.g. ACTIONBAR or CHAT) + */ + default void msgAll(MessageType type) { + SkinOverlay.getInstance().getPlayerProvider().getOnlinePlayers().forEach(p -> msg(p, type)); + } + + /** + * Sends a message to all online players with placeholders. + * + * @param map placeholder replacements + * @param ignoreCase whether to ignore case + * @param type the message type (e.g. ACTIONBAR or CHAT) + */ + default void msgAll(Map map, boolean ignoreCase, MessageType type) { + SkinOverlay.getInstance().getPlayerProvider().getOnlinePlayers().forEach(p -> msg(p, map, ignoreCase, type)); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/message/MessageParser.java b/common/src/main/java/com/georgev22/skinoverlay/message/MessageParser.java new file mode 100644 index 00000000..736d5bf4 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/message/MessageParser.java @@ -0,0 +1,90 @@ +package com.georgev22.skinoverlay.message; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class MessageParser { + + private static final Pattern MINI_TAG_PATTERN = Pattern.compile("<[^>]+>"); + private static final Pattern QUOTED_TAG_PATTERN = Pattern.compile( + "<([a-zA-Z0-9:_-]+):\"((?:[^\"\\\\]|\\\\.)*)\">" + ); + + /** + * Parses a string containing legacy Minecraft color codes and MiniMessage tags, + * converting legacy codes outside tags to MiniMessage format and fixing legacy codes inside quoted tag arguments. + * + * @param input the input string containing legacy codes and MiniMessage tags + * @param tagResolver optional resolver for custom MiniMessage tags, can be null + * @return the parsed Component + */ + public static @NotNull Component miniMessage(@NotNull String input, @Nullable TagResolver tagResolver) { + //noinspection UnnecessaryUnicodeEscape + input = input.replace('\u00A7', '&'); + input = fixLegacyInQuotedText(input); + + Matcher matcher = MINI_TAG_PATTERN.matcher(input); + int lastEnd = 0; + StringBuilder miniMessageBuilder = new StringBuilder(); + + while (matcher.find()) { + String textPart = input.substring(lastEnd, matcher.start()); + miniMessageBuilder.append(legacyToMiniMessage(textPart)); + miniMessageBuilder.append(matcher.group()); + lastEnd = matcher.end(); + } + + miniMessageBuilder.append(legacyToMiniMessage(input.substring(lastEnd))); + + String combinedMiniMessage = miniMessageBuilder.toString(); + + if (tagResolver != null) { + return MiniMessage.miniMessage().deserialize(combinedMiniMessage, tagResolver); + } + return MiniMessage.miniMessage().deserialize(combinedMiniMessage); + } + + /** + * Converts legacy color codes within quoted text arguments of MiniMessage tags to MiniMessage format. + * + * @param input the raw input string + * @return input with legacy codes inside quoted tag arguments converted to MiniMessage + */ + private static @NotNull String fixLegacyInQuotedText(@NotNull String input) { + Matcher matcher = QUOTED_TAG_PATTERN.matcher(input); + StringBuilder sb = new StringBuilder(); + + while (matcher.find()) { + String tagName = matcher.group(1); + String quotedText = matcher.group(2); + + String unescapedQuoted = quotedText.replace("\\\"", "\"").replace("\\\\", "\\"); + String fixedQuoted = legacyToMiniMessage(unescapedQuoted); + String escapedFixedQuoted = fixedQuoted.replace("\"", "\\\""); + + String replacement = "<" + tagName + ":\"" + escapedFixedQuoted + "\">"; + matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); + } + matcher.appendTail(sb); + return sb.toString(); + } + + /** + * Converts legacy Minecraft color codes in a string to MiniMessage format. + * + * @param legacyText text containing legacy color codes + * @return MiniMessage-formatted string + */ + private static @NotNull String legacyToMiniMessage(@NotNull String legacyText) { + if (legacyText.isEmpty()) return ""; + Component comp = LegacyComponentSerializer.legacyAmpersand().deserialize(legacyText); + return MiniMessage.miniMessage().serialize(comp); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/message/MessagesRegistry.java b/common/src/main/java/com/georgev22/skinoverlay/message/MessagesRegistry.java new file mode 100644 index 00000000..91c375d6 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/message/MessagesRegistry.java @@ -0,0 +1,82 @@ +package com.georgev22.skinoverlay.message; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.utilities.Utils; +import com.georgev22.skinoverlay.utilities.config.CFG; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +public final class MessagesRegistry { + + private static final Map cachedFiles = new HashMap<>(); + + private MessagesRegistry() { + } + + /** + * Registers all message entries inside the appropriate YML file. + * + * @param entries The message entries to register. + */ + public static void registerAll(MessageEntry @NotNull [] @NotNull [] entries) { + for (MessageEntry[] entry : entries) { + for (MessageEntry messageEntry : entry) { + register(messageEntry); + } + } + } + + /** + * Registers a message entry inside the appropriate YML file. + * + * @param entry The message entry to register. + */ + public static void register(@NotNull MessageEntry entry) { + String fileName = entry.getFile(); + CFG cfg = cachedFiles.computeIfAbsent(fileName, f -> + new CFG(f, new File(SkinOverlay.getInstance().getDataFolder(), "messages"), false, false, + SkinOverlay.getInstance().getLogger(), SkinOverlay.getInstance().getClass())); + + cfg.reloadFile(); + + boolean changed = false; + + if (cfg.getFileConfiguration().contains(entry.getPath())) { + + if (Utils.isList(cfg.getFileConfiguration(), entry.getPath())) { + entry.setMessages( + cfg.getFileConfiguration().getStringList(entry.getPath()) + .toArray(new String[0]) + ); + } else { + entry.setMessages(new String[]{ + cfg.getFileConfiguration().getString(entry.getPath()) + }); + } + + } else { + // Write default values + if (entry.getDefaultMessages().length > 1) { + cfg.getFileConfiguration().set(entry.getPath(), entry.getDefaultMessages()); + } else { + cfg.getFileConfiguration().set(entry.getPath(), entry.getDefaultMessages()[0]); + } + changed = true; + } + + if (changed) { + cfg.saveFile(); + } + } + + /** + * Gets the CFG instance for a specific file of a plugin. + */ + public static @Nullable CFG getMessagesCFG(String fileName) { + return cachedFiles.get(fileName); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/message/messages/CommandMessages.java b/common/src/main/java/com/georgev22/skinoverlay/message/messages/CommandMessages.java new file mode 100644 index 00000000..13069972 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/message/messages/CommandMessages.java @@ -0,0 +1,67 @@ +package com.georgev22.skinoverlay.message.messages; + +import com.georgev22.skinoverlay.message.MessageEntry; +import org.jetbrains.annotations.NotNull; + +public enum CommandMessages implements MessageEntry { + COMMAND_INVALID_ARGUMENT("Messages.command.invalid-argument", "&c&l(!) &cInvalid argument (%arg%) for command %command%"), + COMMAND_MISSING_ARGUMENT("Messages.command.missing-argument", "&c&l(!) &cMissing argument (%arg%) for command %command%"), + COMMAND_USAGE("Messages.command.usage", "&c&l(!) &cUsage: %usage%"), + COMMAND_TARGET_DENIED("Messages.command.target-denied", "&c&l(!) &cThis command can only be executed by: %targets%"), + COMMAND_PERMISSION_DENIED("Messages.command.permission-denied", "&c&l(!) &cYou do not have the correct permissions to do this!"), + COMMAND_ERROR("Messages.command.error", "&c&l(!) &cAn error occurred while executing command %command%"), + COMMAND_OFFLINE_PLAYER("Messages.command.offline-player", "&c&l(!) &cPlayer %player% is offline!"), + + COMMAND_HELP("Messages.command.help", + "", + "&c&l(!) &cSkinOverlay Commands:", + "&8» &7/skinoverlay &ehelp &7- &oShows this help.", + "&8» &7/skinoverlay &eoverlay &o &7- &oWear a specific overlay from the plugin files.", + "&8» &7/skinoverlay &eurl &o &7- &oWear a specific overlay from a URL.", + "&8» &7/skinoverlay &ereset &7- &oReset the player's skin.", + "&8» &7/skinoverlay &ereload &7- &oReload the plugin configuration files (some settings need server restart).", + ""), + + COMMAND_OVERLAY_NOT_FOUND("Messages.command.overlay.not-found", "&c&l(!)&c Overlay %overlay% not found!"), + + COMMAND_OVERLAY_DONE("Messages.command.overlay.done", "&a&l(!)&a Overlay %url% applied!"), + + COMMAND_OVERLAY_RESET("Messages.command.overlay.reset", "&a&l(!)&a Default skin applied (%player%)!"), + + COMMAND_FAILED_TO_RETRIEVE_OR_GENERATE_SKIN("Messages.command.failed-to-retrieve-or-generate-skin", "&c&l(!)&c Failed to retrieve or generate skin for %player%!"), + + COMMAND_INVALID_URL("Messages.command.invalid-url", "&c&l(!)&c Invalid URL or failed to download image: %url%"); + + private final String path; + private String[] messages; + + CommandMessages(String path, String... defaultMessages) { + this.path = path; + this.messages = defaultMessages; + } + + @Override + public String getPath() { + return path; + } + + @Override + public String[] getDefaultMessages() { + return messages; + } + + @Override + public void setMessages(String[] messages) { + this.messages = messages; + } + + @Override + public String[] getMessages() { + return messages; + } + + @Override + public @NotNull String getFile() { + return "commands"; + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/message/messages/CoreMessages.java b/common/src/main/java/com/georgev22/skinoverlay/message/messages/CoreMessages.java new file mode 100644 index 00000000..ad1faf22 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/message/messages/CoreMessages.java @@ -0,0 +1,51 @@ +package com.georgev22.skinoverlay.message.messages; + +import com.georgev22.skinoverlay.message.MessageEntry; +import org.jetbrains.annotations.NotNull; + +/** + * Default core messages provided by the Core plugin. + *

+ * Other plugins can define their own enums implementing {@link MessageEntry}. + */ +public enum CoreMessages implements MessageEntry { + + PLUGIN_RELOAD("Messages.plugin-reload", "&a&l(!) &aAll data have been reloaded successfully."), + PLUGIN_LOADING_DATA("Messages.plugin-loading-data", "&aLoading plugin data."), + NUMBER_INVALID("Messages.number.invalid", "&c&l(!) &c%number%&c is not a valid number."), + + ; + + private final String path; + private String[] messages; + + CoreMessages(String path, String... defaultMessages) { + this.path = path; + this.messages = defaultMessages; + } + + @Override + public String getPath() { + return path; + } + + @Override + public String[] getDefaultMessages() { + return messages; + } + + @Override + public void setMessages(String[] messages) { + this.messages = messages; + } + + @Override + public String[] getMessages() { + return messages; + } + + @Override + public @NotNull String getFile() { + return "core"; + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/messaging/MessageData.java b/common/src/main/java/com/georgev22/skinoverlay/messaging/MessageData.java new file mode 100644 index 00000000..c878d93d --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/messaging/MessageData.java @@ -0,0 +1,10 @@ +package com.georgev22.skinoverlay.messaging; + +/** + * Represents parsed message data with sub-channel and data entries. + * + * @param subChannel the sub-channel string + * @param dataEntries the decrypted data entries + */ +public record MessageData(String subChannel, String[] dataEntries) { +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/messaging/MessageManager.java b/common/src/main/java/com/georgev22/skinoverlay/messaging/MessageManager.java new file mode 100644 index 00000000..08164f11 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/messaging/MessageManager.java @@ -0,0 +1,160 @@ +package com.georgev22.skinoverlay.messaging; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.exceptions.MessageEncryptionException; +import com.georgev22.skinoverlay.storage.data.Skin; +import com.georgev22.skinoverlay.utilities.EncryptUtils; +import com.georgev22.skinoverlay.utilities.config.OptionsUtil; +import org.jetbrains.annotations.NotNull; + +import java.io.*; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Manages plugin message communication for SkinOverlay. + *

+ * Provides publishing and subscription for skin data and player join events, + * and utility methods for constructing and parsing plugin message packets with encryption and compression. + *

+ * All messages are encrypted using AES-GCM with compression to ensure confidentiality + * and reduce message size to fit plugin messaging limits (e.g. {@link Short#MAX_VALUE} bytes). + */ +public abstract class MessageManager { + + /** + * Reference to the main SkinOverlay plugin instance. + */ + protected final SkinOverlay mainPlugin = SkinOverlay.getInstance(); + + /** + * Channel name used to send messages to backend systems. + */ + protected static final String CHANNEL_TO_BACKEND = "skinoverlay:tobackend"; + + /** + * Channel name used to receive messages from backend systems. + */ + protected static final String CHANNEL_FROM_BACKEND = "skinoverlay:frombackend"; + + /** + * Publishes skin properties for the given player UUID. + * + * @param playerUUID UUID of the player + * @param skin Skin data to publish + */ + public abstract void publishSkinProperty(@NotNull UUID playerUUID, @NotNull Skin skin); + + /** + * Subscribes to skin property updates. + * + * @param handler BiConsumer to handle incoming skin updates with player UUID and Skin + */ + public abstract void subscribeSkinProperty(@NotNull BiConsumer handler); + + /** + * Publishes a player join event. + * + * @param playerUUID UUID of the joined player + */ + public abstract void publishPlayerJoin(@NotNull UUID playerUUID); + + /** + * Subscribes to player join events. + * + * @param handler Consumer to handle player join events with player UUID + */ + public abstract void subscribePlayerJoin(Consumer handler); + + /** + * Closes the message manager, cleaning up any resources such as channels or listeners. + */ + public abstract void close(); + + /** + * Creates a {@link ByteArrayOutputStream} containing the plugin message payload with sub-channel and data. + *

+ * Each data entry is compressed and encrypted before writing to ensure small payload size + * and confidentiality during transmission. + * + * @param subChannel Sub-channel identifier within the plugin message channel + * @param dataArray Array of data entries to write (each will be encrypted) + * @return A {@link ByteArrayOutputStream} containing the constructed payload + * @throws MessageEncryptionException if encryption fails for any data entry + */ + @NotNull + public ByteArrayOutputStream byteArrayDataOutput(@NotNull String subChannel, String @NotNull ... dataArray) { + ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + DataOutputStream dataOut = new DataOutputStream(byteOut); + try { + dataOut.writeUTF(subChannel); + for (String data : dataArray) { + String encryptedData = EncryptUtils.encrypt(data, OptionsUtil.SECRET.getStringValue()); + dataOut.writeUTF(Objects.requireNonNull(encryptedData)); + } + dataOut.flush(); + } catch (IOException e) { + throw new UncheckedIOException("Failed to write data output", e); + } catch (GeneralSecurityException e) { + throw new MessageEncryptionException("Failed to encrypt data", e); + } + return byteOut; + } + + /** + * Converts the given channel and data entries to a byte array suitable for plugin message sending. + * + * @param channel The sub-channel to use + * @param dataArray Data entries to include in the message + * @return Byte array containing the message payload + * @throws MessageEncryptionException if encryption fails for any data entry + */ + public byte @NotNull [] toByteArray(@NotNull String channel, String... dataArray) { + return this.byteArrayDataOutput(channel, dataArray).toByteArray(); + } + + /** + * Reads and parses a plugin message byte array into its sub-channel and decrypted data entries. + * + * @param byteArray The byte array received from a plugin message channel + * @return A {@link MessageData} object containing the sub-channel and data entries + * @throws IOException if an I/O error occurs while reading + * @throws MessageEncryptionException if decryption fails for any data entry + */ + public @NotNull MessageData readByteArray(byte @NotNull [] byteArray) throws IOException { + try (ByteArrayInputStream byteIn = new ByteArrayInputStream(byteArray); + DataInputStream dataIn = new DataInputStream(byteIn)) { + return readDataInput(dataIn); + } + } + + /** + * Reads and parses a {@link DataInputStream} into its sub-channel and decrypted data entries. + * + * @param dataIn The {@link DataInputStream} to read from + * @return A {@link MessageData} object containing the sub-channel and data entries + * @throws IOException if an I/O error occurs while reading + * @throws MessageEncryptionException if decryption fails for any data entry + */ + public @NotNull MessageData readDataInput(@NotNull DataInputStream dataIn) throws IOException { + String subChannel = dataIn.readUTF(); + List dataList = new ArrayList<>(); + + while (dataIn.available() > 0) { + String encryptedData = dataIn.readUTF(); + try { + String decryptedData = EncryptUtils.decrypt(encryptedData, OptionsUtil.SECRET.getStringValue()); + dataList.add(decryptedData); + } catch (GeneralSecurityException e) { + throw new MessageEncryptionException("Failed to decrypt data", e); + } + } + + return new MessageData(subChannel, dataList.toArray(new String[0])); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/messaging/MessageManagerNoop.java b/common/src/main/java/com/georgev22/skinoverlay/messaging/MessageManagerNoop.java new file mode 100644 index 00000000..33d7d92c --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/messaging/MessageManagerNoop.java @@ -0,0 +1,35 @@ +package com.georgev22.skinoverlay.messaging; + +import com.georgev22.skinoverlay.storage.data.Skin; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +public class MessageManagerNoop extends MessageManager { + @Override + public void publishSkinProperty(@NotNull UUID playerUUID, @NotNull Skin skin) { + + } + + @Override + public void subscribeSkinProperty(@NotNull BiConsumer handler) { + + } + + @Override + public void publishPlayerJoin(@NotNull UUID playerUUID) { + + } + + @Override + public void subscribePlayerJoin(Consumer handler) { + + } + + @Override + public void close() { + + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/messaging/RedisManager.java b/common/src/main/java/com/georgev22/skinoverlay/messaging/RedisManager.java new file mode 100644 index 00000000..df134d29 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/messaging/RedisManager.java @@ -0,0 +1,158 @@ +package com.georgev22.skinoverlay.messaging; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.storage.data.Skin; +import org.jetbrains.annotations.NotNull; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPubSub; + +import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.logging.Level; + +public class RedisManager extends MessageManager { + + private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); + private final String host; + private final int port; + private final String password; + private final Jedis jedis; + private Thread subscriberThread; + private Thread publisherThread; + private volatile boolean running = true; + + private BiConsumer skinPropertyHandler = (uuid, skin) -> { + }; + private Consumer playerJoinHandler = uuid -> { + }; + + public RedisManager(String host, int port, String password) { + this.host = host; + this.port = port; + this.password = password; + this.jedis = new Jedis(host, port); + if (password != null && !password.isEmpty()) { + jedis.auth(password); + } + } + + @Override + public void publishSkinProperty(@NotNull UUID playerUUID, @NotNull Skin skin) { + String message = playerUUID + "|" + skin.toBase64(); + jedis.publish(CHANNEL_TO_BACKEND, message); + } + + @Override + public void subscribeSkinProperty(@NotNull BiConsumer handler) { + this.skinPropertyHandler = handler; + if (subscriberThread != null) { + subscriberThread.interrupt(); + subscriberThread = null; + } + subscriberThread = new Thread(() -> { + while (running && !Thread.currentThread().isInterrupted()) { + try (Jedis subJedis = new Jedis(host, port)) { + if (password != null && !password.isEmpty()) { + subJedis.auth(password); + } + + JedisPubSub jedisPubSub = new JedisPubSub() { + @Override + public void onMessage(String ch, String message) { + try { + String[] parts = message.split("\\|", 2); + if (parts.length != 2) { + System.err.println("Invalid skin property message: " + message); + return; + } + UUID uuid = UUID.fromString(parts[0]); + String base64Skin = parts[1]; + Skin skin = Skin.fromBase64(base64Skin); + + skinOverlay.getScheduler().runTask(skinOverlay.getPlugin(), () -> { + skinPropertyHandler.accept(uuid, skin); + }); + + } catch (Exception e) { + skinOverlay.getLogger().log(Level.SEVERE, "Invalid skin property message: " + message, e); + } + } + }; + + subJedis.subscribe(jedisPubSub, CHANNEL_TO_BACKEND); + } catch (Exception e) { + skinOverlay.getLogger().log(Level.SEVERE, "Redis subscribe connection lost, retrying in 5 seconds...", e); + try { + //noinspection BusyWait + Thread.sleep(5000); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + break; + } + } + } + }, "SkinOverlayRedisSkinUpdateThread"); + + subscriberThread.setDaemon(true); + subscriberThread.start(); + } + + @Override + public void subscribePlayerJoin(Consumer handler) { + this.playerJoinHandler = handler; + if (publisherThread != null) { + publisherThread.interrupt(); + publisherThread = null; + } + publisherThread = new Thread(() -> { + while (running && !Thread.currentThread().isInterrupted()) { + try (Jedis subJedis = new Jedis(host, port)) { + if (password != null && !password.isEmpty()) { + subJedis.auth(password); + } + subJedis.subscribe(new JedisPubSub() { + @Override + public void onMessage(String ch, String message) { + UUID uuid; + try { + uuid = UUID.fromString(message); + } catch (Exception e) { + skinOverlay.getLogger().log(Level.SEVERE, "Error parsing UUID from redis message: " + message, e); + return; + } + playerJoinHandler.accept(uuid); + } + }, CHANNEL_FROM_BACKEND); + } catch (Exception e) { + skinOverlay.getLogger().log(Level.SEVERE, "Redis subscribe connection lost, retrying in 5 seconds...", e); + try { + //noinspection BusyWait + Thread.sleep(5000); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + break; + } + } + } + }, "SkinOverlayRedisPlayerJoinThread"); + publisherThread.setDaemon(true); + publisherThread.start(); + } + + public void publishPlayerJoin(@NotNull UUID playerUUID) { + jedis.publish(CHANNEL_FROM_BACKEND, playerUUID.toString()); + } + + @Override + public void close() { + running = false; + if (subscriberThread != null) { + subscriberThread.interrupt(); + } + if (publisherThread != null) { + publisherThread.interrupt(); + } + jedis.close(); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/player/SPlayer.java b/common/src/main/java/com/georgev22/skinoverlay/player/SPlayer.java new file mode 100644 index 00000000..f352ec8c --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/player/SPlayer.java @@ -0,0 +1,25 @@ +package com.georgev22.skinoverlay.player; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.command.CommandIssuer; +import com.georgev22.skinoverlay.skin.SGameProfile; + +public interface SPlayer extends CommandIssuer { + + boolean isOnline(); + + T getPlayer(); + + /** + * Checks if the player is using Bedrock Edition. + * + * @return true if the player is using Bedrock Edition, false otherwise. + */ + default boolean isBedrock() { + return this.getUniqueId().toString().replace("-", "").startsWith("000000"); + } + + default SGameProfile getGameProfile() { + return SkinOverlay.getInstance().getGameProfileProvider().getCachedGameProfile(this); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider.java b/common/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider.java new file mode 100644 index 00000000..ebeb96e1 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider.java @@ -0,0 +1,28 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.skin.SGameProfile; +import org.jetbrains.annotations.NotNull; + +public abstract class GameProfileProvider { + + protected final SkinOverlay skinOverlay = SkinOverlay.getInstance(); + protected final ObjectMap sGameProfiles = new HashObjectMap<>(); + + public abstract Object getInternalGameProfile(@NotNull SPlayer player); + + public abstract SGameProfile getGameProfile(@NotNull SPlayer player); + + public SGameProfile getCachedGameProfile(@NotNull SPlayer player) { + if (sGameProfiles.containsKey(player)) { + return sGameProfiles.get(player); + } + return getGameProfile(player); + } + + public abstract void applyUpdatedGameProfile(@NotNull SPlayer player); + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProviderNoop.java b/common/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProviderNoop.java new file mode 100644 index 00000000..6de8b6ce --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProviderNoop.java @@ -0,0 +1,23 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.skin.SGameProfile; +import org.jetbrains.annotations.NotNull; + +public class GameProfileProviderNoop extends GameProfileProvider { + + @Override + public Object getInternalGameProfile(@NotNull SPlayer player) { + return new SGameProfile(player.getName(), player.getUniqueId()); + } + + @Override + public SGameProfile getGameProfile(@NotNull SPlayer player) { + return new SGameProfile(player.getName(), player.getUniqueId()); + } + + @Override + public void applyUpdatedGameProfile(@NotNull SPlayer player) { + + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_SkinsRestorer.java b/common/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_SkinsRestorer.java new file mode 100644 index 00000000..48c99bb0 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_SkinsRestorer.java @@ -0,0 +1,54 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.skin.SGameProfile; +import com.georgev22.skinoverlay.skin.SProperty; +import net.skinsrestorer.api.SkinsRestorer; +import net.skinsrestorer.api.SkinsRestorerProvider; +import net.skinsrestorer.api.exception.DataRequestException; +import net.skinsrestorer.api.property.SkinProperty; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +public class GameProfileProvider_SkinsRestorer extends GameProfileProvider { + + private final SkinsRestorer skinsRestorerAPI; + + public GameProfileProvider_SkinsRestorer() { + skinsRestorerAPI = SkinsRestorerProvider.get(); + } + + @Override + public SGameProfile getInternalGameProfile(@NotNull SPlayer player) { + Optional property; + try { + property = skinsRestorerAPI.getPlayerStorage().getSkinForPlayer(player.getUniqueId(), player.getName()); + } catch (DataRequestException e) { + property = Optional.empty(); + } + SGameProfile gameProfile = new SGameProfile(player.getName(), player.getUniqueId()); + property.map(skinProperty -> new SProperty(skinProperty.getValue(), skinProperty.getSignature())).ifPresent(sProperty -> gameProfile.setProperty("textures", sProperty)); + return gameProfile; + } + + @Override + public SGameProfile getGameProfile(@NotNull SPlayer player) { + if (sGameProfiles.containsKey(player)) { + return sGameProfiles.get(player); + } + return sGameProfiles.append(player, getInternalGameProfile(player)).get(player); + } + + @Override + public void applyUpdatedGameProfile(@NotNull SPlayer player) { + SGameProfile gameProfile = getGameProfile(player); + SProperty property = gameProfile.getProperty("textures"); + if (property == null) { + this.skinOverlay.getLogger().warning("Skin Property is null for player: " + player.getName()); + return; + } + SkinProperty skinProperty = SkinProperty.of(property.value(), property.signature()); + skinsRestorerAPI.getSkinApplier(player.getPlayer().getClass()).applySkin(player.getPlayer(), skinProperty); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/providers/PlayerProvider.java b/common/src/main/java/com/georgev22/skinoverlay/providers/PlayerProvider.java new file mode 100644 index 00000000..792246c7 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/providers/PlayerProvider.java @@ -0,0 +1,46 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.command.CommandIssuer; +import com.georgev22.skinoverlay.player.SPlayer; + +import java.util.List; +import java.util.UUID; + +public abstract class PlayerProvider { + + public abstract SPlayer getSPlayer(Object player); + + public abstract SPlayer getSPlayer(CommandIssuer commandIssuer); + + public abstract SPlayer getSPlayer(String name); + + public abstract SPlayer getSPlayer(UUID uuid); + + public abstract List getOnlinePlayers(); + + public boolean isOnline(SPlayer player) { + try { + return player.isOnline() && player.getPlayer() != null; + } catch (Exception e) { + return false; + } + } + + public boolean isOnline(String name) { + try { + SPlayer player = getSPlayer(name); + return isOnline(player); + } catch (Exception e) { + return false; + } + } + + public boolean isOnline(UUID uuid) { + try { + SPlayer player = getSPlayer(uuid); + return isOnline(player); + } catch (Exception e) { + return false; + } + } +} diff --git a/core/src/main/java/com/georgev22/skinoverlay/handler/SkinHandler.java b/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java similarity index 55% rename from core/src/main/java/com/georgev22/skinoverlay/handler/SkinHandler.java rename to common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java index eb254fd9..90a2bf8c 100644 --- a/core/src/main/java/com/georgev22/skinoverlay/handler/SkinHandler.java +++ b/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java @@ -1,26 +1,31 @@ -package com.georgev22.skinoverlay.handler; +package com.georgev22.skinoverlay.providers; -import com.georgev22.library.maps.HashObjectMap; -import com.georgev22.library.maps.ObjectMap; -import com.georgev22.library.yaml.file.FileConfiguration; import com.georgev22.skinoverlay.SkinOverlay; import com.georgev22.skinoverlay.exceptions.SkinException; -import com.georgev22.skinoverlay.handler.skin.MinecraftSkinRenderer; -import com.georgev22.skinoverlay.handler.skin.Part; -import com.georgev22.skinoverlay.handler.skin.SkinParts; +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.registry.EntityManagerRegistry; +import com.georgev22.skinoverlay.skin.SGameProfile; +import com.georgev22.skinoverlay.skin.SProperty; +import com.georgev22.skinoverlay.storage.EntityManager; import com.georgev22.skinoverlay.storage.data.Skin; import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; -import com.georgev22.skinoverlay.utilities.Utilities; +import com.georgev22.skinoverlay.utilities.Utils; +import com.georgev22.skinoverlay.utilities.Utils.Request; import com.georgev22.skinoverlay.utilities.config.OptionsUtil; import com.georgev22.skinoverlay.utilities.config.OverlayOptionsUtil; import com.georgev22.skinoverlay.utilities.config.SkinConfigurationFile; -import com.georgev22.skinoverlay.utilities.interfaces.ImageSupplier; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; +import com.georgev22.skinoverlay.utilities.skin.MinecraftSkinRenderer; +import com.georgev22.skinoverlay.utilities.skin.Part; +import com.georgev22.skinoverlay.utilities.skin.SkinParts; import com.google.gson.*; +import org.bspfsystems.yamlconfiguration.file.FileConfiguration; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.mineskin.MineskinClient; -import org.mineskin.data.Texture; +import org.mineskin.Java11RequestHandler; +import org.mineskin.MineSkinClient; +import org.mineskin.data.JobInfo; +import org.mineskin.data.Visibility; +import org.mineskin.request.GenerateRequest; import javax.imageio.ImageIO; import java.awt.*; @@ -31,103 +36,75 @@ import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; -import java.util.ArrayList; -import java.util.Base64; +import java.util.*; import java.util.List; -import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.logging.Level; -public abstract class SkinHandler { +public class SkinProvider { protected final SkinOverlay skinOverlay = SkinOverlay.getInstance(); + protected final MineSkinClient mineskinClient; - protected final ObjectMap sGameProfiles = new HashObjectMap<>(); - - protected final MineskinClient mineskinClient; - - public SkinHandler() { - mineskinClient = (OptionsUtil.MINESKIN_API_KEY.getStringValue().equalsIgnoreCase("none") || - OptionsUtil.MINESKIN_API_KEY.getStringValue().isBlank()) ? - new MineskinClient("SkinOverlay") : - new MineskinClient("SkinOverlay", OptionsUtil.MINESKIN_API_KEY.getStringValue()); + public SkinProvider() { + String mineSkinAPIKey = OptionsUtil.MINESKIN_API_KEY.getStringValue(); + if (mineSkinAPIKey.equalsIgnoreCase("none") || mineSkinAPIKey.isBlank()) { + mineSkinAPIKey = null; + } + this.mineskinClient = + MineSkinClient.builder() + .requestHandler(Java11RequestHandler::new) + .userAgent("SkinOverlay/v1.0") + .apiKey(mineSkinAPIKey) + .build(); } - /** - * Update the skin for the specified {@link PlayerObject} - * - * @param playerObject Player's {@link PlayerObject} object. - * @param skin Skin - */ - public abstract CompletableFuture updateSkin( - @NotNull final PlayerObject playerObject, - @NotNull final Skin skin); - - public abstract void applySkin(@NotNull final PlayerObject playerObject, @NotNull final Skin skin); - - /** - * Retrieves {@link PlayerObject}'s internal game profile - * - * @param playerObject {@link PlayerObject} object - * @return {@link PlayerObject}'s internal game profile - * @throws IOException When an I/O exception to some sort has occurred. - * @throws ExecutionException When attempting to retrieve the result of a task that aborted by throwing an exception. - * @throws InterruptedException When a thread is waiting, sleeping, or otherwise occupied, and the thread is interrupted - */ - public abstract Object getInternalGameProfile(@NotNull final PlayerObject playerObject) throws IOException, ExecutionException, InterruptedException; - - /** - * Retrieves {@link PlayerObject}'s {@link SGameProfile} - * - * @param playerObject {@link PlayerObject} object - * @return {@link PlayerObject}'s {@link SGameProfile} - * @throws IOException When an I/O exception to some sort has occurred. - * @throws ExecutionException When attempting to retrieve the result of a task that aborted by throwing an exception. - * @throws InterruptedException When a thread is waiting, sleeping, or otherwise occupied, and the thread is interrupted - */ - public abstract SGameProfile getGameProfile(@NotNull final PlayerObject playerObject) throws IOException, ExecutionException, InterruptedException; - - public CompletableFuture retrieveOrGenerateSkin(@NotNull PlayerObject playerObject, @Nullable ImageSupplier image, @NotNull SkinParts skinParts) { - UUID skinUUID = Utilities.generateUUID(skinParts.getSkinName() + playerObject.playerUUID().toString()); - return skinOverlay.getSkinManager().exists(skinUUID).thenApplyAsync(result -> { - if (result) { - skinOverlay.getLogger().info("Skin: " + skinUUID + " found for player: " + playerObject.playerName()); - return skinOverlay.getSkinManager().getLoadedEntities().get(skinUUID); - } else { + public CompletableFuture> retrieveOrGenerateSkin(@NotNull SPlayer player, @NotNull SkinParts skinParts) { + UUID skinUUID = Utils.generateUUID(skinParts.getSkinName() + player.getUniqueId().toString()); + @NotNull Optional> skinEntityManager = EntityManagerRegistry.getManager(Skin.class); + if (skinEntityManager.isEmpty()) { + skinOverlay.getLogger().log(Level.SEVERE, "SkinEntityManager cannot be null", new SkinException("SkinEntityManager cannot be null")); + return CompletableFuture.completedFuture(Optional.empty()); + } + EntityManager skinManager = skinEntityManager.get(); + return CompletableFuture.supplyAsync(() -> { + boolean exists = skinManager.exists(skinUUID); + if (exists) { + skinOverlay.getLogger().info("Skin: " + skinUUID + " found for player: " + player.getName()); + return skinManager.findById(skinUUID); + } else if (skinParts.getSkinName().equalsIgnoreCase("default")) { + this.skinOverlay.getLogger().warning("Default skin not found for player: " + player.getName()); try { - ImageSupplier imageSupplier = image; - if (imageSupplier == null) { - imageSupplier = () -> { - try { - return this.getSkinImage(playerObject); - } catch (ExecutionException | InterruptedException e) { - this.skinOverlay.getLogger().log(Level.SEVERE, "Failed to get image", e); - return null; - } - }; - if (imageSupplier.get() == null) { - skinOverlay.getLogger().log(Level.SEVERE, "ImageSupplier cannot be null", new SkinException("ImageSupplier cannot be null")); - return null; - } + SProperty property = this.getSkin(player); + Skin skin = new Skin(skinUUID); + skin.setProperty(property); + skin.setSkinParts(skinParts); + if (!this.skinOverlay.isProxy() && OptionsUtil.PROXY.getBooleanValue()) { + return Optional.of(skin); } - + skinManager.save(skin); + return Optional.of(skin); + } catch (IOException | ExecutionException | InterruptedException e) { + this.skinOverlay.getLogger().log(Level.SEVERE, "Failed to get skin for player: " + player.getName(), e); + return Optional.empty(); + } + } else { + try { SkinConfigurationFile skinConfigurationFile = this.skinOverlay.getSkinFileCache().getCacheSkinConfig(skinParts.getSkinName()); if (skinConfigurationFile == null) { skinOverlay.getLogger().log(Level.SEVERE, "SkinConfigurationFile cannot be null", new SkinException("SkinConfigurationFile cannot be null")); - return null; + return Optional.empty(); } FileConfiguration fileConfiguration = skinConfigurationFile.getFileConfiguration(); - BufferedImage currentSkin = this.getSkinImage(this.getProfileBytes(playerObject, this.skinOverlay.getSkinHook().getProperty(playerObject))); + BufferedImage currentSkin = this.getSkinImage(this.getProfileBytes(player, this.skinOverlay.getSkinHook().getProperty(player))); SkinParts currentSkinParts = new SkinParts(new SerializableBufferedImage(currentSkin), "currentSkin"); currentSkinParts.createParts(); - BufferedImage overlay = imageSupplier.get(); - List overlaySkinParts = new ArrayList<>(); for (Part part : skinParts.getParts().values()) { if (part.name().startsWith("Jacket")) { @@ -211,244 +188,125 @@ public CompletableFuture retrieveOrGenerateSkin(@NotNull PlayerObject play canvas.drawImage(overlaySkinRenderer.getFullSkinImage().getBufferedImage(), 0, 0, null); canvas.dispose(); - Texture texture = mineskinClient.generateUpload(skinToBeGenerated).get().data.texture; - if (texture == null) { - throw new SkinException("Texture cannot be null"); - } - SProperty property = new SProperty("textures", texture.value, texture.signature); - Skin skin = new Skin(skinUUID, property, skinParts); - if (!skinOverlay.getSkinOverlay().type().isProxy() && OptionsUtil.PROXY.getBooleanValue()) { - return skin; + GenerateRequest generateRequest = GenerateRequest.upload(skinToBeGenerated) + .name("SkinOverlay-" + player.getName() + "-" + skinParts.getSkinName()) + .visibility(Visibility.UNLISTED); + SProperty property = mineskinClient + .queue() + .submit(generateRequest) + .thenCompose(queueResponse -> { + JobInfo job = queueResponse.getJob(); + return job.waitForCompletion(mineskinClient); + }) + .thenCompose(jobReference -> jobReference.getOrLoadSkin(mineskinClient)) + .thenApply(skinInfo -> { + String value = skinInfo.texture().data().value(); + String signature = skinInfo.texture().data().signature(); + return new SProperty(value, signature); + }).join(); + + Skin skin = new Skin(skinUUID); + skin.setSkinParts(skinParts); + skin.setProperty(property); + if (!skinOverlay.isProxy() && OptionsUtil.PROXY.getBooleanValue()) { + return Optional.of(skin); } - skinOverlay.getSkinManager().save(skin); - return skin; - } catch (IOException | ExecutionException | InterruptedException | RuntimeException exception) { + skinManager.save(skin); + return Optional.of(skin); + } catch (IOException | RuntimeException exception) { skinOverlay.getLogger().log(Level.SEVERE, "Error generating or retrieving the skin:", exception); - return null; + return Optional.empty(); } } }); } - public void setSkin(@NotNull PlayerObject playerObject, Skin skin) { - skinOverlay.getUserManager().getEntity(playerObject.playerUUID()). - handle((user, throwable) -> { - if (throwable != null) { - skinOverlay.getLogger().log(Level.SEVERE, "Error while getting the user: ", throwable); - return null; - } - return user; - }).thenAccept(user -> { - if (user != null) { - try { - SGameProfile gameProfile = getGameProfile(playerObject); - gameProfile.removeProperty("textures").addProperty("textures", skin.skinProperty()); - } catch (IOException | ExecutionException | InterruptedException e) { - skinOverlay.getLogger().log(Level.SEVERE, "Error while trying to apply new texture: ", e); - return; - } - user.addCustomData("skin", skin); - applySkin(playerObject, skin); - if (!skinOverlay.getSkinOverlay().type().isProxy() && OptionsUtil.PROXY.getBooleanValue()) { - return; - } - skinOverlay.getUserManager().save(user); - } - }); + /** + * Retrieves {@link SPlayer}'s {@link SGameProfile} bytes + * + * @param player {@link SPlayer} object + * @param property If you want to use a {@link SProperty} instead of {@link SGameProfile} ones + * @return {@link SPlayer}'s {@link SGameProfile} bytes + * @throws IOException When an I/O exception to some sort has occurred. + */ + public byte[] getProfileBytes(@NotNull final SPlayer player, @Nullable SProperty property) throws IOException { + return player.isBedrock() ? this.getBedrockProfileBytes(player, property) : this.getJavaProfileBytes(player, property); } /** - * Retrieves {@link PlayerObject}'s {@link SGameProfile} bytes + * Retrieves Bedrock {@link SPlayer}'s {@link SGameProfile} bytes * - * @param playerObject {@link PlayerObject} object - * @param property If you want to use a {@link SProperty} instead of {@link SGameProfile} ones - * @return {@link PlayerObject}'s {@link SGameProfile} bytes - * @throws IOException When an I/O exception to some sort has occurred. - * @throws ExecutionException When attempting to retrieve the result of a task that aborted by throwing an exception. - * @throws InterruptedException When a thread is waiting, sleeping, or otherwise occupied, and the thread is interrupted + * @param player {@link SPlayer} object + * @param property If you want to use a {@link SProperty} instead of {@link SGameProfile} ones + * @return {@link SPlayer}'s {@link SGameProfile} bytes + * @throws IOException When an I/O exception to some sort has occurred. */ - public byte[] getProfileBytes(@NotNull final PlayerObject playerObject, @Nullable SProperty property) throws IOException, ExecutionException, InterruptedException { - return playerObject.isBedrock() ? this.getBedrockProfileBytes(playerObject, property) : this.getJavaProfileBytes(playerObject, property); + public byte[] getBedrockProfileBytes(@NotNull final SPlayer player, final SProperty property) throws IOException { + return property != null ? + new ByteArrayInputStream(this.createJsonFromProperty(player, property).getAsJsonObject().toString().getBytes()).readAllBytes() : + new ByteArrayInputStream(this.createJsonForBedrock(player).getAsJsonObject().toString().getBytes()).readAllBytes(); } /** - * Retrieves Java {@link PlayerObject}'s {@link SGameProfile} bytes + * Retrieves Java {@link SPlayer}'s {@link SGameProfile} bytes * - * @param playerObject {@link PlayerObject} object - * @param property If you want to use a {@link SProperty} instead of {@link SGameProfile} ones - * @return {@link PlayerObject}'s {@link SGameProfile} bytes - * @throws IOException When an I/O exception to some sort has occurred. - * @throws ExecutionException When attempting to retrieve the result of a task that aborted by throwing an exception. - * @throws InterruptedException When a thread is waiting, sleeping, or otherwise occupied, and the thread is interrupted + * @param player {@link SPlayer} object + * @param property If you want to use a {@link SProperty} instead of {@link SGameProfile} ones + * @return {@link SPlayer}'s {@link SGameProfile} bytes + * @throws IOException When an I/O exception to some sort has occurred. */ - public byte[] getJavaProfileBytes(@NotNull final PlayerObject playerObject, @Nullable SProperty property) throws IOException, ExecutionException, InterruptedException { + public byte[] getJavaProfileBytes(@NotNull final SPlayer player, @Nullable SProperty property) throws IOException { return property != null ? - new ByteArrayInputStream(this.createJsonFromProperty(playerObject, property) + new ByteArrayInputStream(this.createJsonFromProperty(player, property) .getAsJsonObject().toString().getBytes()).readAllBytes() : - new Utilities.Request() + new Request() .openConnection( String.format( "https://sessionserver.mojang.com/session/minecraft/profile/%s?unsigned=false", (skinOverlay.isOnlineMode() ? - playerObject.playerUUID() : - getUUID(playerObject.playerName())) - .toString().replaceAll("-", ""))) + player.getUniqueId() : + getUUID(player.getName())) + .toString().replaceAll("-", "")) + ) .getRequest() .finalizeRequest() .getBytes(); } /** - * Retrieves Bedrock {@link PlayerObject}'s {@link SGameProfile} bytes - * - * @param playerObject {@link PlayerObject} object - * @param property If you want to use a {@link SProperty} instead of {@link SGameProfile} ones - * @return {@link PlayerObject}'s {@link SGameProfile} bytes - * @throws IOException When an I/O exception to some sort has occurred. - * @throws ExecutionException When attempting to retrieve the result of a task that aborted by throwing an exception. - * @throws InterruptedException When a thread is waiting, sleeping, or otherwise occupied, and the thread is interrupted - */ - public byte[] getBedrockProfileBytes(@NotNull final PlayerObject playerObject, final SProperty property) throws IOException, ExecutionException, InterruptedException { - return property != null ? - new ByteArrayInputStream(this.createJsonFromProperty(playerObject, property).getAsJsonObject().toString().getBytes()).readAllBytes() : - new ByteArrayInputStream(this.createJsonForBedrock(playerObject).getAsJsonObject().toString().getBytes()).readAllBytes(); - } - - /** - * Creates a similar JSON as the Java one for the specified Bedrock Player - * - * @param playerObject {@link PlayerObject}'s object - * @return a JSON for the specified Bedrock Player - * @throws IOException When an I/O exception to some sort has occurred. - */ - public JsonObject createJsonForBedrock(@NotNull final PlayerObject playerObject) throws IOException { - final byte[] profileBytes = new Utilities.Request().openConnection(String.format("https://api.geysermc.org/v2/skin/%s", this.getXUID(playerObject))).getRequest().finalizeRequest().getBytes(); - final JsonElement json = JsonParser.parseString(new String(profileBytes)); - final JsonElement value = json.getAsJsonObject().get("value"); - final JsonElement signature = json.getAsJsonObject().get("signature"); - final JsonArray properties = new JsonArray(); - final JsonObject innerProperties = new JsonObject(); - innerProperties.add("name", new JsonPrimitive("textures")); - innerProperties.add("value", value); - innerProperties.add("signature", signature); - properties.add(innerProperties); - final JsonObject jsonObject = new JsonObject(); - jsonObject.add("properties", properties); - return jsonObject; - } - - /** - * Creates a JSON from a {@link SGameProfile} or {@link SProperty} - * - * @param playerObject {@link PlayerObject}'s object - * @param property If you want to use a {@link SProperty} instead of {@link SGameProfile} ones - * @return a JSON from a {@link SGameProfile} or {@link SProperty} - * @throws IOException When an I/O exception to some sort has occurred. - * @throws ExecutionException When attempting to retrieve the result of a task that aborted by throwing an exception. - * @throws InterruptedException When a thread is waiting, sleeping, or otherwise occupied, and the thread is interrupted - */ - public JsonObject createJsonFromProperty(@NotNull final PlayerObject playerObject, @Nullable SProperty property) throws IOException, ExecutionException, InterruptedException { - if (property == null) - property = this.getGameProfile(playerObject).getProperties().get("textures"); - final JsonArray properties = new JsonArray(); - final JsonObject innerProperties = new JsonObject(); - innerProperties.add("name", new JsonPrimitive("textures")); - innerProperties.add("value", new JsonPrimitive(property.value())); - innerProperties.add("signature", new JsonPrimitive(property.signature())); - properties.add(innerProperties); - final JsonObject jsonObject = new JsonObject(); - jsonObject.add("properties", properties); - return jsonObject; - } - - /** - * Retrieves the XUID for the specified Bedrock Player - * - * @param playerObject {@link PlayerObject}'s object - * @return the XUID for the specified Bedrock Player - * @throws IOException When an I/O exception to some sort has occurred. - */ - public String getXUID(@NotNull final PlayerObject playerObject) throws IOException { - Utilities.Request request = new Utilities.Request().openConnection(String.format("https://api.geysermc.org/v2/xbox/xuid/%s", playerObject.playerName().replace(".", ""))).getRequest().finalizeRequest(); - final int httpCode = request.getHttpCode(); - if (httpCode != 200) { - request = new Utilities.Request() - .openConnection(String.format("https://api.geysermc.org/v2/xbox/xuid/%s", playerObject.playerName().replace(".", "").replace("_", "%20"))) - .getRequest() - .finalizeRequest(); - } - final byte[] profileBytes = request.getBytes(); - final JsonElement json = JsonParser.parseString(new String(profileBytes)); - return json.getAsJsonObject().get("xuid").getAsString(); - } - - /** - * Retrieves the UUID for the specified Java Player. - *

- * If the specified player is not a premium account, returns a default UUID (Steve). - * - * @param playerName The player's Minecraft username. - * @return The UUID for the specified Java Player. - * @throws IOException If an I/O exception to some sort has occurred. - */ - public UUID getUUID(final String playerName) throws IOException { - if (!isUsernamePremium(playerName)) { - return UUID.fromString(OptionsUtil.DEFAULT_SKIN_UUID.getStringValue()); - } - Utilities.Request request; - try { - request = new Utilities.Request().openConnection(String.format("https://api.minetools.eu/uuid/%s", playerName)).getRequest().finalizeRequest(); - } catch (IOException ioException) { - request = new Utilities.Request().openConnection(String.format("https://api.mojang.com/users/profiles/minecraft/%s", playerName)).getRequest().finalizeRequest(); - } - - final byte[] jsonBytes = request.getBytes(); - final JsonElement json = JsonParser.parseString(new String(jsonBytes)); - return UUID.fromString(json.getAsJsonObject().get("id").getAsString().replaceAll( - "(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", - "$1-$2-$3-$4-$5")); - } - - /** - * Retrieves the Skin {@link SProperty} for the specified Bedrock Player - * - * @param xuid Player's XUID (check {@link #getXUID(PlayerObject)}) - * @return the Skin {@link SProperty} for the specified Bedrock Player - * @throws IOException When an I/O exception to some sort has occurred. - */ - public SProperty getXUIDSkin(final String xuid) throws IOException { - final Utilities.Request profileBytes = new Utilities.Request().openConnection(String.format("https://api.geysermc.org/v2/skin/%s", xuid)).getRequest().finalizeRequest(); - final JsonElement json = JsonParser.parseString(new String(profileBytes.getBytes())); - return new SProperty("textures", json.getAsJsonObject().get("value").getAsString(), json.getAsJsonObject().get("signature").getAsString()); - } - - /** - * Retrieves the Skin {@link SProperty} for the specified Java Player + * Retrieves a skin image from the provided SPlayer. * - * @param playerObject {@link PlayerObject}'s object - * @return the Skin {@link SProperty} for the specified Java Player - * @throws IOException When an I/O exception to some sort has occurred. - * @throws ExecutionException When attempting to retrieve the result of a task that aborted by throwing an exception. - * @throws InterruptedException When a thread is waiting, sleeping, or otherwise occupied, and the thread is interrupted + * @param player The SPlayer for which to fetch the skin. + * @return The BufferedImage representing the player's skin. + * @throws IOException If an I/O error occurs while fetching the image. + * @throws ExecutionException If an exception occurs during execution. + * @throws InterruptedException If the execution is interrupted. */ - public SProperty getJavaSkin(final PlayerObject playerObject) throws IOException, ExecutionException, InterruptedException { - if (skinOverlay.getSkinHook().getProperty(playerObject) != null) { - return skinOverlay.getSkinHook().getProperty(playerObject); - } - return skinOverlay.getDefaultSkinHook().getProperty(playerObject); + public BufferedImage getSkinImage(final @NotNull SPlayer player) throws IOException, ExecutionException, InterruptedException { + SGameProfile gameProfile = skinOverlay.getGameProfileProvider().getGameProfile(player); + SProperty sProperty = gameProfile.getProperties().get("textures") != null + ? gameProfile.getProperties().get("textures") + : getSkin(player); + String url = JsonParser.parseString(new String(Base64.getDecoder().decode(sProperty.value()))) + .getAsJsonObject() + .getAsJsonObject("textures") + .getAsJsonObject("SKIN") + .get("url") + .getAsString(); + return ImageIO.read(new URL(url)); } /** * Retrieves the Skin {@link SProperty} for the specified Player * - * @param playerObject {@link PlayerObject}'s object - * @return the Skin {@link SProperty} for the specified {@link PlayerObject} + * @param player {@link SPlayer}'s object + * @return the Skin {@link SProperty} for the specified {@link SPlayer} * @throws IOException When an I/O exception to some sort has occurred. * @throws ExecutionException When attempting to retrieve the result of a task that aborted by throwing an exception. * @throws InterruptedException When a thread is waiting, sleeping, or otherwise occupied, and the thread is interrupted */ - public SProperty getSkin(@NotNull final PlayerObject playerObject) throws IOException, ExecutionException, InterruptedException { - return playerObject.isBedrock() ? this.getXUIDSkin(this.getXUID(playerObject)) : this.getJavaSkin(playerObject); + public SProperty getSkin(@NotNull final SPlayer player) throws IOException, ExecutionException, InterruptedException { + return player.isBedrock() ? this.getXUIDSkin(this.getXUID(player)) : this.getJavaSkin(player); } /** @@ -468,28 +326,6 @@ public BufferedImage getSkinImage(final @NotNull SProperty sProperty) throws IOE return ImageIO.read(new URL(url)); } - /** - * Retrieves a skin image from the provided PlayerObject. - * - * @param playerObject The PlayerObject for which to fetch the skin. - * @return The BufferedImage representing the player's skin. - * @throws IOException If an I/O error occurs while fetching the image. - * @throws ExecutionException If an exception occurs during execution. - * @throws InterruptedException If the execution is interrupted. - */ - public BufferedImage getSkinImage(final @NotNull PlayerObject playerObject) throws IOException, ExecutionException, InterruptedException { - SProperty sProperty = playerObject.gameProfile().getProperties().get("textures") != null - ? playerObject.gameProfile().getProperties().get("textures") - : skinOverlay.getSkinHandler().getSkin(playerObject); - String url = JsonParser.parseString(new String(Base64.getDecoder().decode(sProperty.value()))) - .getAsJsonObject() - .getAsJsonObject("textures") - .getAsJsonObject("SKIN") - .get("url") - .getAsString(); - return ImageIO.read(new URL(url)); - } - /** * Retrieves a skin image from the provided profile bytes. * @@ -572,4 +408,134 @@ public boolean isUsernamePremium(String username) { return false; } } + + /** + * Retrieves the Skin {@link SProperty} for the specified Java Player + * + * @param player {@link SPlayer}'s object + * @return the Skin {@link SProperty} for the specified Java Player + * @throws IOException When an I/O exception to some sort has occurred. + */ + public SProperty getJavaSkin(final SPlayer player) throws IOException { + if (skinOverlay.getSkinHook().getProperty(player) != null) { + return skinOverlay.getSkinHook().getProperty(player); + } + final JsonElement json = JsonParser.parseString(new String(this.getProfileBytes(player, null))); + final JsonArray properties = json.getAsJsonObject().get("properties").getAsJsonArray(); + SProperty property = null; + for (final JsonElement object : properties) { + if (object.getAsJsonObject().get("name").getAsString().equals("textures")) { + property = new SProperty(object.getAsJsonObject().get("value").getAsString(), object.getAsJsonObject().get("signature").getAsString()); + } + } + return property; + } + + /** + * Retrieves the Skin {@link SProperty} for the specified Bedrock Player + * + * @param xuid Player's XUID (check {@link #getXUID(SPlayer)}) + * @return the Skin {@link SProperty} for the specified Bedrock Player + * @throws IOException When an I/O exception to some sort has occurred. + */ + public SProperty getXUIDSkin(final String xuid) throws IOException { + final Request profileBytes = new Request().openConnection(String.format("https://api.geysermc.org/v2/skin/%s", xuid)).getRequest().finalizeRequest(); + final JsonElement json = JsonParser.parseString(new String(profileBytes.getBytes())); + return new SProperty(json.getAsJsonObject().get("value").getAsString(), json.getAsJsonObject().get("signature").getAsString()); + } + + /** + * Retrieves the XUID for the specified Bedrock Player + * + * @param player {@link SPlayer}'s object + * @return the XUID for the specified Bedrock Player + * @throws IOException When the request fails + */ + public String getXUID(@NotNull final SPlayer player) throws IOException { + Request request = new Request().openConnection(String.format("https://api.geysermc.org/v2/xbox/xuid/%s", player.getName().replace(".", ""))).getRequest().finalizeRequest(); + final int httpCode = request.getHttpCode(); + if (httpCode != 200) { + request = new Request() + .openConnection(String.format("https://api.geysermc.org/v2/xbox/xuid/%s", player.getName().replace(".", "").replace("_", "%20"))) + .getRequest() + .finalizeRequest(); + } + final byte[] profileBytes = request.getBytes(); + final JsonElement json = JsonParser.parseString(new String(profileBytes)); + return json.getAsJsonObject().get("xuid").getAsString(); + } + + + /** + * Creates a similar JSON as the Java one for the specified Bedrock Player + * + * @param player {@link SPlayer}'s object + * @return a JSON for the specified Bedrock Player + * @throws IOException When the request fails + */ + public JsonObject createJsonForBedrock(@NotNull final SPlayer player) throws IOException { + final byte[] profileBytes = new Request().openConnection(String.format("https://api.geysermc.org/v2/skin/%s", + this.getXUID(player))).getRequest().finalizeRequest().getBytes(); + final JsonElement json = JsonParser.parseString(new String(profileBytes)); + final JsonElement value = json.getAsJsonObject().get("value"); + final JsonElement signature = json.getAsJsonObject().get("signature"); + final JsonArray properties = new JsonArray(); + final JsonObject innerProperties = new JsonObject(); + innerProperties.add("name", new JsonPrimitive("textures")); + innerProperties.add("value", value); + innerProperties.add("signature", signature); + properties.add(innerProperties); + final JsonObject jsonObject = new JsonObject(); + jsonObject.add("properties", properties); + return jsonObject; + } + + /** + * Creates a JSON from a {@link SGameProfile} or {@link SProperty} + * + * @param player {@link SPlayer}'s object + * @param property If you want to use a {@link SProperty} instead of {@link SGameProfile} ones + * @return a JSON from a {@link SGameProfile} or {@link SProperty} + */ + public JsonObject createJsonFromProperty(@NotNull final SPlayer player, @Nullable SProperty property) { + if (property == null) + property = skinOverlay.getGameProfileProvider().getGameProfile(player).getProperties().get("textures"); + final JsonArray properties = new JsonArray(); + final JsonObject innerProperties = new JsonObject(); + innerProperties.add("name", new JsonPrimitive("textures")); + innerProperties.add("value", new JsonPrimitive(property.value())); + innerProperties.add("signature", new JsonPrimitive(property.signature())); + properties.add(innerProperties); + final JsonObject jsonObject = new JsonObject(); + jsonObject.add("properties", properties); + return jsonObject; + } + + /** + * Retrieves the UUID for the specified Java Player. + *

+ * If the specified player is not a premium account, returns a default UUID (Steve). + * + * @param playerName The player's Minecraft username. + * @return The UUID for the specified Java Player. + * @throws IOException If an I/O exception to some sort has occurred. + */ + public UUID getUUID(final String playerName) throws IOException { + if (!isUsernamePremium(playerName)) { + return UUID.fromString(OptionsUtil.DEFAULT_SKIN_UUID.getStringValue()); + } + Request request; + try { + request = new Request().openConnection(String.format("https://api.minetools.eu/uuid/%s", playerName)).getRequest().finalizeRequest(); + } catch (IOException ioException) { + request = new Request().openConnection(String.format("https://api.mojang.com/users/profiles/minecraft/%s", playerName)).getRequest().finalizeRequest(); + } + + final byte[] jsonBytes = request.getBytes(); + final JsonElement json = JsonParser.parseString(new String(jsonBytes)); + return UUID.fromString(json.getAsJsonObject().get("id").getAsString().replaceAll( + "(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", + "$1-$2-$3-$4-$5")); + } + } diff --git a/common/src/main/java/com/georgev22/skinoverlay/registry/AbstractRegistry.java b/common/src/main/java/com/georgev22/skinoverlay/registry/AbstractRegistry.java new file mode 100644 index 00000000..3017f48c --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/registry/AbstractRegistry.java @@ -0,0 +1,69 @@ +package com.georgev22.skinoverlay.registry; + +import com.georgev22.skinoverlay.maps.ConcurrentObjectMap; +import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.maps.UnmodifiableObjectMap; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +public abstract class AbstractRegistry implements Registry { + + protected final ObjectMap registry = new ConcurrentObjectMap<>(); + + /** + * {@inheritDoc} + */ + @Override + public void register(@NotNull K key, @NotNull V value) { + if (registry.containsKey(key)) { + throw new IllegalArgumentException("Already registered for key: " + key); + } + registry.put(key, value); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean replaceOrRegister(@NotNull K key, @NotNull V value) { + boolean exists = registry.containsKey(key); + registry.put(key, value); + return exists; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean unregister(@NotNull K key) { + return registry.remove(key) != null; + } + + /** + * {@inheritDoc} + */ + @Override + public @NotNull Optional get(K key) { + if (!registry.containsKey(key)) { + return Optional.empty(); + } + return Optional.ofNullable(registry.get(key)); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean contains(@NotNull K key) { + return registry.containsKey(key); + } + + /** + * {@inheritDoc} + */ + @Override + public @NotNull ObjectMap entries() { + return new UnmodifiableObjectMap<>(registry); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/registry/CommandTargetRegistry.java b/common/src/main/java/com/georgev22/skinoverlay/registry/CommandTargetRegistry.java new file mode 100644 index 00000000..12ca09c9 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/registry/CommandTargetRegistry.java @@ -0,0 +1,86 @@ +package com.georgev22.skinoverlay.registry; + +import com.georgev22.skinoverlay.command.CommandIssuer; +import com.georgev22.skinoverlay.maps.ObjectMap; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Predicate; + +/** + * Registry for command executor targets. + *

+ * This integrates with the core {@link Registry} system, + * allowing plugins or modules to register custom sender types + * (e.g., "discord", "webpanel", etc.). + *

+ * + *

Default targets:

+ *
    + *
  • {@code any} – allows all senders
  • + *
  • {@code player} – allows only players
  • + *
  • {@code console} – allows only console senders
  • + *
  • {@code block} – allows only block command senders
  • + *
+ */ +public final class CommandTargetRegistry extends AbstractRegistry> { + + private static final CommandTargetRegistry INSTANCE = new CommandTargetRegistry(); + + /** + * Returns the singleton instance of the CommandTargetRegistry. + */ + public static @NotNull CommandTargetRegistry getInstance() { + return INSTANCE; + } + + private CommandTargetRegistry() { + register(DefaultTargets.ANY, sender -> true); + register(DefaultTargets.PLAYER, CommandIssuer::isPlayer); + register(DefaultTargets.CONSOLE, sender -> !sender.isPlayer()); + } + + /** + * Checks if a sender matches the predicate associated with the given target name. + * + * @param targetNames the names of the target (case-insensitive) + * @param sender the command sender + * @return true if the sender matches, false otherwise + */ + public boolean matches(@NotNull String @NotNull [] targetNames, @NotNull CommandIssuer sender) { + for (String targetName : targetNames) { + if (get(targetName.toLowerCase()) + .map(predicate -> predicate.test(sender)) + .orElse(false)) { + return true; + } + } + return false; + } + + @Override + public void register(@NotNull Predicate value) throws IllegalArgumentException { + throw new UnsupportedOperationException("Cannot register a value without a key"); + } + + @Override + public boolean replaceOrRegister(@NotNull Predicate value) { + throw new UnsupportedOperationException("Cannot register a value without a key"); + } + + /** + * @return an unmodifiable view of all registered command targets. + */ + @Override + public @NotNull ObjectMap> entries() { + return super.entries(); + } + + /** + * Default command target constants. + */ + public interface DefaultTargets { + String ANY = "any"; + String PLAYER = "player"; + String CONSOLE = "console"; + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/registry/EntityManagerRegistry.java b/common/src/main/java/com/georgev22/skinoverlay/registry/EntityManagerRegistry.java new file mode 100644 index 00000000..34e45347 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/registry/EntityManagerRegistry.java @@ -0,0 +1,81 @@ +package com.georgev22.skinoverlay.registry; + +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.storage.EntityManager; +import com.georgev22.skinoverlay.storage.data.Entity; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; + +/** + * Singleton registry for managing EntityManager instances by entity class. + */ +public class EntityManagerRegistry { + private static final ObjectMap, EntityManager> managers = new HashObjectMap<>(); + + /** + * Registers an EntityManager for a specific entity class. + * + * @param entityClass the class of the entity + * @param manager the manager to register + * @param the type of entity + * @throws IllegalArgumentException if a manager is already registered for the given entity class + */ + public static void registerManager(Class entityClass, EntityManager manager) throws IllegalArgumentException { + if (managers.containsKey(entityClass)) { + throw new IllegalArgumentException("An EntityManager is already registered for class " + entityClass.getName()); + } + managers.put(entityClass, manager); + } + + /** + * Registers a new EntityManager for a specific entity class, replacing any existing manager. + *

+ * **Warning**: Replacing an existing manager may lead to data loss or inconsistencies if + * the new manager does not properly handle existing entities. Use this method with caution. + *

+ * + * @param entityClass the class of the entity + * @param manager the manager to register or replace + * @param the type of entity + * @return {@code true} if an existing manager was replaced, {@code false} if this is a new registration + */ + public static boolean replaceOrRegisterManager(Class entityClass, EntityManager manager) { + boolean isReplacing = managers.containsKey(entityClass); + managers.put(entityClass, manager); + return isReplacing; + } + + /** + * Retrieves the EntityManager for a specific entity class. + * + * @param entityClass the class of the entity + * @param the type of entity + * @return the EntityManager instance, or {@code Optional.empty()} if not registered + */ + @SuppressWarnings("unchecked") + public static @NotNull Optional> getManager(Class entityClass) { + return Optional.ofNullable((EntityManager) managers.get(entityClass)); + } + + /** + * Checks if an EntityManager is registered for a specific entity class. + * + * @param entityClass the class of the entity + * @param the type of entity + * @return {@code true} if an EntityManager is registered, {@code false} otherwise + */ + public static boolean containsManager(Class entityClass) { + return managers.containsKey(entityClass); + } + + @Contract(pure = true) + public static @NotNull @UnmodifiableView Map, EntityManager> getManagers() { + return Collections.unmodifiableMap(managers); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/registry/Registry.java b/common/src/main/java/com/georgev22/skinoverlay/registry/Registry.java new file mode 100644 index 00000000..1c60f9c2 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/registry/Registry.java @@ -0,0 +1,84 @@ +package com.georgev22.skinoverlay.registry; + +import com.georgev22.skinoverlay.maps.ObjectMap; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +public interface Registry { + + /** + * Registers a value with the given key. + * + * @param key the key used to identify the value + * @param value the value to register + * @throws IllegalArgumentException if a value is already registered with the given key + */ + void register(@NotNull K key, @NotNull V value) throws IllegalArgumentException; + + /** + * Registers a value that can provide its own key. + *

+ * The value type {@code V} must have a way of exposing its key (for example, + * via a {@code getKey()} method). The registry will extract the key from + * the value and use it for registration. + *

+ * + * @param value the value to register, which must provide its own key + * @throws IllegalArgumentException if a value is already registered with the same key + */ + void register(@NotNull V value) throws IllegalArgumentException; + + /** + * Registers or replaces an existing value with the given key. + * + * @param key the key used to identify the value + * @param value the value to register or replace + * @return {@code true} if an existing value was replaced, {@code false} if this is a new registration + */ + boolean replaceOrRegister(@NotNull K key, @NotNull V value); + + /** + * Registers or replaces a value that can provide its own key. + *

+ * The value type {@code V} must have a way of exposing its key (for example, + * via a {@code getKey()} method). The registry will extract the key from + * the value and use it for registration. + *

+ * + * @param value the value to register or replace, which must provide its own key + * @return {@code true} if an existing value was replaced, {@code false} if this is a new registration + */ + boolean replaceOrRegister(@NotNull V value); + + /** + * Unregisters a value by its key. + * + * @param key the key used to identify the value + * @return {@code true} if the value was unregistered, {@code false} if not registered + */ + boolean unregister(@NotNull K key); + + /** + * Retrieves a registered value by its key. + * + * @param key the key used to look up the value + * @return an {@link Optional} containing the value if found, or {@link Optional#empty()} if not registered + */ + @NotNull Optional get(K key); + + /** + * Checks whether a value is registered under the given key. + * + * @param key the key to check + * @return {@code true} if a value is registered under the key, {@code false} otherwise + */ + boolean contains(@NotNull K key); + + /** + * Returns an unmodifiable view of the registry entries. + * + * @return an {@link ObjectMap} containing all key–value pairs currently registered + */ + @NotNull ObjectMap entries(); +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/scheduler/MinecraftScheduler.java b/common/src/main/java/com/georgev22/skinoverlay/scheduler/MinecraftScheduler.java new file mode 100644 index 00000000..d74fa12f --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/scheduler/MinecraftScheduler.java @@ -0,0 +1,327 @@ +package com.georgev22.skinoverlay.scheduler; + +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +/** + * A non-extendable interface representing a scheduler for task scheduling and cancellation. + * Tasks can be scheduled synchronously (on the main server thread) or asynchronously, + * with optional delays, repetition, and context-specific execution (world, chunk, location, entity). + */ +public interface MinecraftScheduler { + + /** + * Schedules a synchronous task to run on the main server thread. + * + * @param plugin the plugin scheduling the task + * @param task the task to run + * @return a {@link SchedulerTask} instance representing the scheduled task + */ + SchedulerTask runTask(Plugin plugin, Runnable task); + + /** + * Schedules a synchronous task that returns a result via a {@link CompletableFuture}. + * The supplier runs on the main server thread. + * + * @param plugin the plugin scheduling the task + * @param task the supplier providing the result + * @param the type of the result + * @return a {@link CompletableFuture} that will be completed with the supplier's result + */ + CompletableFuture runTask(Plugin plugin, Supplier task); + + /** + * Schedules an asynchronous task to run off the main server thread. + * + * @param plugin the plugin scheduling the task + * @param task the task to run + * @return a {@link SchedulerTask} instance representing the scheduled task + */ + SchedulerTask runAsyncTask(Plugin plugin, Runnable task); + + /** + * Schedules an asynchronous task that returns a result via a {@link CompletableFuture}. + * The supplier runs off the main server thread. + * + * @param plugin the plugin scheduling the task + * @param task the supplier providing the result + * @param the type of the result + * @return a {@link CompletableFuture} that will be completed with the supplier's result + */ + CompletableFuture runAsyncTask(Plugin plugin, Supplier task); + + /** + * Schedules a synchronous delayed task to run after the specified delay. + * + * @param plugin the plugin scheduling the task + * @param task the task to run + * @param delay the delay in ticks before the task executes + * @return a {@link SchedulerTask} instance representing the scheduled task + */ + SchedulerTask createDelayedTask(Plugin plugin, Runnable task, long delay); + + /** + * Schedules a synchronous delayed task that returns a result via a {@link CompletableFuture}. + * The supplier runs after the delay on the main server thread. + * + * @param plugin the plugin scheduling the task + * @param task the supplier providing the result + * @param delay the delay in ticks before the supplier executes + * @param the type of the result + * @return a {@link CompletableFuture} that will be completed with the supplier's result + */ + CompletableFuture createDelayedTask(Plugin plugin, Supplier task, long delay); + + /** + * Schedules a synchronous repeating task with an initial delay and subsequent periods. + * + * @param plugin the plugin scheduling the task + * @param task the task to run + * @param delay the delay in ticks before the first execution + * @param period the period in ticks between subsequent executions + * @return a {@link SchedulerTask} instance representing the scheduled task + */ + SchedulerTask createRepeatingTask(Plugin plugin, Runnable task, long delay, long period); + + /** + * Schedules an asynchronous delayed task to run after the specified delay. + * + * @param plugin the plugin scheduling the task + * @param task the task to run + * @param delay the delay in ticks before the task executes + * @return a {@link SchedulerTask} instance representing the scheduled task + */ + SchedulerTask createAsyncDelayedTask(Plugin plugin, Runnable task, long delay); + + /** + * Schedules an asynchronous delayed task that returns a result via a {@link CompletableFuture}. + * The supplier runs after the delay off the main server thread. + * + * @param plugin the plugin scheduling the task + * @param task the supplier providing the result + * @param delay the delay in ticks before the supplier executes + * @param the type of the result + * @return a {@link CompletableFuture} that will be completed with the supplier's result + */ + CompletableFuture createAsyncDelayedTask(Plugin plugin, Supplier task, long delay); + + /** + * Schedules an asynchronous repeating task with an initial delay and subsequent periods. + * + * @param plugin the plugin scheduling the task + * @param task the task to run + * @param delay the delay in ticks before the first execution + * @param period the period in ticks between subsequent executions + * @return a {@link SchedulerTask} instance representing the scheduled task + */ + SchedulerTask createAsyncRepeatingTask(Plugin plugin, Runnable task, long delay, long period); + + /** + * Schedules a synchronous delayed task to run in the specified world and chunk context. + * The task will only execute if the chunk is loaded. + * + * @param plugin the plugin scheduling the task + * @param task the task to run + * @param world the world associated with the task + * @param chunk the chunk associated with the task (must not be null) + * @param delay the delay in ticks before the task executes + * @return a {@link SchedulerTask} instance representing the scheduled task + */ + SchedulerTask createDelayedTaskForWorld(Plugin plugin, Runnable task, World world, @NotNull Chunk chunk, long delay); + + /** + * Schedules a synchronous delayed task in the specified world and chunk context that returns a result. + * The supplier runs on the main server thread if the chunk is loaded. + * + * @param plugin the plugin scheduling the task + * @param task the supplier providing the result + * @param world the world associated with the task + * @param chunk the chunk associated with the task (must not be null) + * @param delay the delay in ticks before the supplier executes + * @param the type of the result + * @return a {@link CompletableFuture} that will be completed with the supplier's result + */ + CompletableFuture createDelayedTaskForWorld(Plugin plugin, Supplier task, World world, @NotNull Chunk chunk, long delay); + + /** + * Schedules a synchronous delayed task associated with a specific location. + * The task runs on the main thread when the location's chunk is loaded. + * + * @param plugin the plugin scheduling the task + * @param task the task to run + * @param location the location associated with the task + * @param delay the delay in ticks before the task executes + * @return a {@link SchedulerTask} instance representing the scheduled task + */ + SchedulerTask createDelayedForLocation(Plugin plugin, Runnable task, Location location, long delay); + + /** + * Schedules a synchronous delayed task associated with a location that returns a result. + * The supplier runs on the main server thread when the location's chunk is loaded. + * + * @param plugin the plugin scheduling the task + * @param task the supplier providing the result + * @param location the location associated with the task + * @param delay the delay in ticks before the supplier executes + * @param the type of the result + * @return a {@link CompletableFuture} that will be completed with the supplier's result + */ + CompletableFuture createDelayedForLocation(Plugin plugin, Supplier task, Location location, long delay); + + /** + * Schedules a synchronous delayed task associated with an entity. If the entity is retired (removed), + * the retired runnable is executed. + * + * @param plugin the plugin scheduling the task + * @param task the task to run + * @param retired the runnable to execute if the entity is retired before the task runs + * @param entity the entity associated with the task + * @param delay the delay in ticks before the task executes + * @return a {@link SchedulerTask} instance representing the scheduled task + */ + SchedulerTask createDelayedForEntity(Plugin plugin, Runnable task, Runnable retired, Entity entity, long delay); + + /** + * Schedules a synchronous delayed task associated with an entity that returns a result. + * If the entity is retired, the retired runnable is executed. + * + * @param plugin the plugin scheduling the task + * @param task the supplier providing the result + * @param retired the runnable to execute if the entity is retired + * @param entity the entity associated with the task + * @param delay the delay in ticks before the supplier executes + * @param the type of the result + * @return a {@link CompletableFuture} that will be completed with the supplier's result + */ + CompletableFuture createDelayedForEntity(Plugin plugin, Supplier task, Runnable retired, Entity entity, long delay); + + /** + * Schedules a synchronous task to run in the specified world and chunk context. + * The task executes immediately if the chunk is loaded. + * + * @param plugin the plugin scheduling the task + * @param task the task to run + * @param world the world associated with the task + * @param chunk the chunk associated with the task (must not be null) + * @return a {@link SchedulerTask} instance representing the scheduled task + */ + SchedulerTask createTaskForWorld(Plugin plugin, Runnable task, World world, @NotNull Chunk chunk); + + /** + * Schedules a synchronous task in the specified world and chunk context that returns a result. + * The supplier runs on the main server thread if the chunk is loaded. + * + * @param plugin the plugin scheduling the task + * @param task the supplier providing the result + * @param world the world associated with the task + * @param chunk the chunk associated with the task (must not be null) + * @param the type of the result + * @return a {@link CompletableFuture} that will be completed with the supplier's result + */ + CompletableFuture createTaskForWorld(Plugin plugin, Supplier task, World world, @NotNull Chunk chunk); + + /** + * Schedules a synchronous task associated with a specific location. + * The task runs on the main thread when the location's chunk is loaded. + * + * @param plugin the plugin scheduling the task + * @param task the task to run + * @param location the location associated with the task + * @return a {@link SchedulerTask} instance representing the scheduled task + */ + SchedulerTask createTaskForLocation(Plugin plugin, Runnable task, Location location); + + /** + * Schedules a synchronous task associated with a location that returns a result. + * The supplier runs on the main server thread when the location's chunk is loaded. + * + * @param plugin the plugin scheduling the task + * @param task the supplier providing the result + * @param location the location associated with the task + * @param the type of the result + * @return a {@link CompletableFuture} that will be completed with the supplier's result + */ + CompletableFuture createTaskForLocation(Plugin plugin, Supplier task, Location location); + + /** + * Schedules a synchronous task associated with an entity. If the entity is retired, + * the retired runnable is executed. + * + * @param plugin the plugin scheduling the task + * @param task the task to run + * @param retired the runnable to execute if the entity is retired + * @param entity the entity associated with the task + * @return a {@link SchedulerTask} instance representing the scheduled task + */ + SchedulerTask createTaskForEntity(Plugin plugin, Runnable task, Runnable retired, Entity entity); + + /** + * Schedules a synchronous task associated with an entity that returns a result. + * If the entity is retired, the retired runnable is executed. + * + * @param plugin the plugin scheduling the task + * @param task the supplier providing the result + * @param retired the runnable to execute if the entity is retired + * @param entity the entity associated with the task + * @param the type of the result + * @return a {@link CompletableFuture} that will be completed with the supplier's result + */ + CompletableFuture createTaskForEntity(Plugin plugin, Supplier task, Runnable retired, Entity entity); + + /** + * Schedules a synchronous repeating task in the specified world and chunk context. + * The task repeats only if the chunk is loaded. + * + * @param plugin the plugin scheduling the task + * @param task the task to run + * @param world the world associated with the task + * @param chunk the chunk associated with the task (must not be null) + * @param delay the delay in ticks before the first execution + * @param period the period in ticks between subsequent executions + * @return a {@link SchedulerTask} instance representing the scheduled task + */ + SchedulerTask createRepeatingTaskForWorld(Plugin plugin, Runnable task, World world, @NotNull Chunk chunk, long delay, long period); + + /** + * Schedules a synchronous repeating task associated with a specific location. + * The task runs on the main thread when the location's chunk is loaded. + * + * @param plugin the plugin scheduling the task + * @param task the task to run + * @param location the location associated with the task + * @param delay the delay in ticks before the first execution + * @param period the period in ticks between subsequent executions + * @return a {@link SchedulerTask} instance representing the scheduled task + */ + SchedulerTask createRepeatingTaskForLocation(Plugin plugin, Runnable task, Location location, long delay, long period); + + /** + * Schedules a synchronous repeating task associated with an entity. The task runs with an initial delay + * and repeats with the specified period. If the entity is retired, the retired runnable is called. + * + * @param plugin the plugin scheduling the task + * @param task the task to run + * @param retired the runnable to execute if the entity is retired + * @param entity the entity associated with the task + * @param delay the delay in ticks before the first execution + * @param period the period in ticks between subsequent executions + */ + SchedulerTask createRepeatingTaskForEntity(Plugin plugin, Runnable task, Runnable retired, Entity entity, long delay, long period); + + /** + * Cancels all tasks associated with the given `plugin`. + * + * @param plugin The plugin whose tasks should be canceled. + */ + void cancelTasks(Plugin plugin); + + /** + * Gets the scheduler + * + * @return The scheduler + */ + MinecraftScheduler getScheduler(); + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/scheduler/SchedulerRunnable.java b/common/src/main/java/com/georgev22/skinoverlay/scheduler/SchedulerRunnable.java new file mode 100644 index 00000000..0c2c417c --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/scheduler/SchedulerRunnable.java @@ -0,0 +1,167 @@ +package com.georgev22.skinoverlay.scheduler; + +import org.jetbrains.annotations.NotNull; + +public abstract class SchedulerRunnable implements Runnable { + + private SchedulerTask task; + + private final MinecraftScheduler minecraftScheduler; + + public SchedulerRunnable(MinecraftScheduler minecraftScheduler) { + this.minecraftScheduler = minecraftScheduler; + } + + /** + * Returns true if this task has been cancelled. + * + * @return true if the task has been cancelled + * @throws IllegalStateException if task was not scheduled yet + */ + public synchronized boolean isCancelled() throws IllegalStateException { + checkScheduled(); + return task.isCancelled(); + } + + /** + * Attempts to cancel this task. + * + * @throws IllegalStateException if task was not scheduled yet + */ + public synchronized void cancel() throws IllegalStateException { + task.cancel(); + } + + /** + * Schedules this in the Bukkit scheduler to run on next tick. + * + * @param plugin The Plugin associated with this task. + * @return a Task that contains the id number + * @throws IllegalArgumentException if clazz, is null + * @throws IllegalStateException if this was already scheduled + */ + @NotNull + public synchronized SchedulerTask runTask(Plugin plugin) throws IllegalArgumentException, IllegalStateException { + checkNotYetScheduled(); + return setupTask(this.minecraftScheduler.runTask(plugin, this)); + } + + /** + * Asynchronous tasks should never access any API in Bukkit. Great care + * should be taken to assure the thread-safety of asynchronous tasks. + *

+ * Schedules this in the Bukkit scheduler to run asynchronously. + * + * @param plugin The Plugin associated with this task. + * @return a Task that contains the id number + * @throws IllegalArgumentException if clazz, is null + * @throws IllegalStateException if this was already scheduled + */ + @NotNull + public synchronized SchedulerTask runTaskAsynchronously(@NotNull Plugin plugin) throws IllegalArgumentException, IllegalStateException { + checkNotYetScheduled(); + return setupTask(this.minecraftScheduler.runAsyncTask(plugin, this)); + } + + /** + * Schedules this to run after the specified number of server ticks. + * + * @param plugin The Plugin associated with this task. + * @param delay the ticks to wait before running the task + * @return a Task that contains the id number + * @throws IllegalArgumentException if clazz, is null + * @throws IllegalStateException if this was already scheduled + */ + @NotNull + public synchronized SchedulerTask runTaskLater(@NotNull Plugin plugin, long delay) throws IllegalArgumentException, IllegalStateException { + checkNotYetScheduled(); + return setupTask(this.minecraftScheduler.createDelayedTask(plugin, this, delay)); + } + + /** + * Asynchronous tasks should never access any API in Bukkit. Great care + * should be taken to assure the thread-safety of asynchronous tasks. + *

+ * Schedules this to run asynchronously after the specified number of + * server ticks. + * + * @param plugin The Plugin associated with this task. + * @param delay the ticks to wait before running the task + * @return a Task that contains the id number + * @throws IllegalArgumentException if clazz, is null + * @throws IllegalStateException if this was already scheduled + */ + @NotNull + public synchronized SchedulerTask runTaskLaterAsynchronously(@NotNull Plugin plugin, long delay) throws IllegalArgumentException, IllegalStateException { + checkNotYetScheduled(); + return setupTask(this.minecraftScheduler.createAsyncDelayedTask(plugin, this, delay)); + } + + /** + * Schedules this to repeatedly run until cancelled, starting after the + * specified number of server ticks. + * + * @param plugin The Plugin associated with this task. + * @param delay the ticks to wait before running the task + * @param period the ticks to wait between runs + * @return a Task that contains the id number + * @throws IllegalArgumentException if clazz, is null + * @throws IllegalStateException if this was already scheduled + */ + @NotNull + public synchronized SchedulerTask runTaskTimer(@NotNull Plugin plugin, long delay, long period) throws IllegalArgumentException, IllegalStateException { + checkNotYetScheduled(); + return setupTask(this.minecraftScheduler.createRepeatingTask(plugin, this, delay, period)); + } + + /** + * Asynchronous tasks should never access any API in Bukkit. Great care + * should be taken to assure the thread-safety of asynchronous tasks. + *

+ * Schedules this to repeatedly run asynchronously until cancelled, + * starting after the specified number of server ticks. + * + * @param plugin The Plugin associated with this task. + * @param delay the ticks to wait before running the task for the first + * time + * @param period the ticks to wait between runs + * @return a Task that contains the id number + * @throws IllegalArgumentException if clazz, is null + * @throws IllegalStateException if this was already scheduled + */ + @NotNull + public synchronized SchedulerTask runTaskTimerAsynchronously(@NotNull Plugin plugin, long delay, long period) throws IllegalArgumentException, IllegalStateException { + checkNotYetScheduled(); + return setupTask(this.minecraftScheduler.createAsyncRepeatingTask(plugin, this, delay, period)); + } + + /** + * Gets the task id for this runnable. + * + * @return the task id that this runnable was scheduled as + * @throws IllegalStateException if task was not scheduled yet + */ + public synchronized int getTaskId() throws IllegalStateException { + checkScheduled(); + return task.getTaskId(); + } + + private void checkScheduled() { + if (task == null) { + throw new IllegalStateException("Not scheduled yet"); + } + } + + private void checkNotYetScheduled() { + if (task != null) { + throw new IllegalStateException("Already scheduled as " + task.getTaskId()); + } + } + + @NotNull + private SchedulerTask setupTask(@NotNull final SchedulerTask task) { + this.task = task; + return task; + } + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/scheduler/SchedulerTask.java b/common/src/main/java/com/georgev22/skinoverlay/scheduler/SchedulerTask.java new file mode 100644 index 00000000..372b969b --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/scheduler/SchedulerTask.java @@ -0,0 +1,13 @@ +package com.georgev22.skinoverlay.scheduler; + +public interface SchedulerTask { + + void cancel(); + + boolean isCancelled(); + + int getTaskId(); + + boolean isRunning(); + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/skin/SGameProfile.java b/common/src/main/java/com/georgev22/skinoverlay/skin/SGameProfile.java new file mode 100644 index 00000000..4ef65f56 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/skin/SGameProfile.java @@ -0,0 +1,55 @@ +package com.georgev22.skinoverlay.skin; + +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.maps.ObjectMap; + +import java.util.UUID; + +public class SGameProfile { + + private final String name; + private final UUID uuid; + private final ObjectMap properties = new HashObjectMap<>(); + + public SGameProfile(String name, UUID uuid) { + this.name = name; + this.uuid = uuid; + } + + public SGameProfile(String name, UUID uuid, ObjectMap properties) { + this.name = name; + this.uuid = uuid; + this.properties.putAll(properties); + } + + public String getName() { + return name; + } + + public UUID getUniqueId() { + return uuid; + } + + public ObjectMap getProperties() { + return properties; + } + + public SProperty getProperty(String property) { + return this.properties.get(property); + } + + public SGameProfile addProperty(String property, SProperty value) { + this.properties.append(property, value); + return this; + } + + public SGameProfile removeProperty(String property) { + this.properties.remove(property); + return this; + } + + public SGameProfile setProperty(String property, SProperty value) { + this.properties.append(property, value); + return this; + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/skin/SProperty.java b/common/src/main/java/com/georgev22/skinoverlay/skin/SProperty.java new file mode 100644 index 00000000..edb5de40 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/skin/SProperty.java @@ -0,0 +1,5 @@ +package com.georgev22.skinoverlay.skin; + + +public record SProperty(String value, String signature) { +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/EntityManager.java b/common/src/main/java/com/georgev22/skinoverlay/storage/EntityManager.java new file mode 100644 index 00000000..e4f82dec --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/EntityManager.java @@ -0,0 +1,187 @@ +package com.georgev22.skinoverlay.storage; + +import com.georgev22.skinoverlay.storage.data.Entity; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Consumer; + +/** + * Interface for managing entities with CRUD operations and additional helper methods. + * + * @param the type of entity to manage + */ +public interface EntityManager { + + /** + * Saves the specified entity. + * + * @param entity the entity to save + */ + void save(@NotNull E entity); + + /** + * Finds an entity by its unique identifier. + * + * @param id the unique identifier + * @return an {@link Optional} containing the entity if found, or an empty Optional if not found + */ + Optional findById(@NotNull String id); + + /** + * Finds an entity by its unique identifier. + * + * @param uuid the unique identifier + * @return an {@link Optional} containing the entity if found, or an empty Optional if not found + */ + default Optional findById(@NotNull UUID uuid) { + return findById(uuid.toString()); + } + + /** + * Deletes the specified entity. + * + * @param entity the entity to delete + */ + void delete(@NotNull E entity); + + /** + * Loads an entity by its unique identifier. + * + * @param id the unique identifier + * @return an {@link Optional} containing the entity if found, or an empty Optional if not found + */ + Optional load(@NotNull String id); + + /** + * Loads an entity by its unique identifier. + * + * @param uuid the unique identifier + * @return an {@link Optional} containing the entity if found, or an empty Optional if not found + */ + default Optional load(@NotNull UUID uuid) { + return load(uuid.toString()); + } + + /** + * Loads all entities. + */ + void loadAll(); + + /** + * Saves all entities. + */ + default void saveAll() { + saveAll(entity -> { + }); + } + + /** + * Saves all entities and executes the provided consumer on each one. + * + * @param consumer the consumer to apply to each saved entity + */ + void saveAll(Consumer consumer); + + /** + * Returns all entities managed by this manager. + * + * @return a list of all entities + */ + List getAll(); + + /** + * Checks if an entity with the specified identifier exists. + * + * @param id the unique identifier + * @return {@code true} if the entity exists, {@code false} otherwise + */ + boolean exists(@NotNull String id); + + /** + * Checks if an entity with the specified identifier exists. + * + * @param uuid the unique identifier + * @return {@code true} if the entity exists, {@code false} otherwise + */ + default boolean exists(@NotNull UUID uuid) { + return exists(uuid.toString()); + } + + /** + * Retrieves an entity by its unique identifier. Optionally, loads the entity + * if it exists. + * + * @param id the unique identifier + * @param loadIfExists whether to load the entity if it exists + * @return an {@link Optional} containing the entity if found, or empty if not found + */ + Optional getEntity(@NotNull String id, boolean loadIfExists); + + /** + * Retrieves an entity by its unique identifier. Optionally, loads the entity + * if it exists. + * + * @param uuid the unique identifier + * @param loadIfExists whether to load the entity if it exists + * @return an {@link Optional} containing the entity if found, or empty if not found + */ + default Optional getEntity(@NotNull UUID uuid, boolean loadIfExists) { + return getEntity(uuid.toString(), loadIfExists); + } + + /** + * Creates a new entity with the specified identifier and executes the provided consumer. + * + * @param id the unique identifier + * @param consumer the consumer to apply to the new entity + * @return an {@link Optional} containing the new entity if created, or empty if not created + */ + Optional create(@NotNull String id, @NotNull Consumer consumer); + + /** + * Creates a new entity with the specified identifier and executes the provided consumer. + * + * @param uuid the unique identifier + * @param consumer the consumer to apply to the new entity + * @return an {@link Optional} containing the new entity if created, or empty if not created + */ + default Optional create(@NotNull UUID uuid, @NotNull Consumer consumer) { + return create(uuid.toString(), consumer); + } + + /** + * Gets the name of this entity manager. + * + * @return the name + */ + String getName(); + + /** + * Gets the simple name of this entity manager. + * + * @return the simple name + */ + String getSimpleName(); + + /** + * Shuts down this manager. + */ + @ApiStatus.Internal + default void shutdown() { + shutdown(entity -> { + }); + } + + /** + * Shuts down this manager and applies the given consumer to each entity. + * + * @param consumer the consumer to apply + */ + @ApiStatus.Internal + void shutdown(Consumer consumer); + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/data/Entity.java b/common/src/main/java/com/georgev22/skinoverlay/storage/data/Entity.java new file mode 100644 index 00000000..8de7dcc5 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/data/Entity.java @@ -0,0 +1,70 @@ +package com.georgev22.skinoverlay.storage.data; + +import com.georgev22.skinoverlay.utilities.CustomData; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * Represents a generic entity with a unique identifier. + *

+ * This interface provides a base for all identifiable entities and includes + * methods for comparison and equality checks. + */ +public interface Entity extends Comparable { + + /** + * Returns the universally unique identifier (UUID) of this entity. + * + * @return the UUID representing the identity of this entity + */ + UUID getId(); + + + /** + * Retrieves the custom data associated with this entity. + *

+ * Note: Custom data are not saved, persisted or loaded from storage. + * + * @return The custom data associated with this entity. + */ + CustomData customData(); + + /** + * Compares this entity with the specified entity for order. + *

+ * The comparison is based on the natural ordering of their UUIDs. + * + * @param o the entity to be compared + * @return a negative integer, zero, or a positive integer as this entity's UUID + * is less than, equal to, or greater than the specified entity's UUID + */ + @Override + default int compareTo(@NotNull Entity o) { + return getId().compareTo(o.getId()); + } + + /** + * Indicates whether some other object is "equal to" this one. + *

+ * Two entities are considered equal if their UUIDs are equal. + * Implementations should override this method to enforce identity-based equality. + * + * @param o the reference object with which to compare + * @return {@code true} if this entity is the same as the {@code o} argument; + * {@code false} otherwise + */ + @Override + boolean equals(Object o); + + /** + * Performs a deep equality check between this entity and another object. + *

+ * This method compares all relevant fields of the entities, not just the UUID. + * + * @param o the object to compare with + * @return {@code true} if all fields of this entity are equal to those of the given object; + * {@code false} otherwise + */ + boolean equalsExact(Object o); +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/data/PlayerData.java b/common/src/main/java/com/georgev22/skinoverlay/storage/data/PlayerData.java new file mode 100644 index 00000000..c3fb6f12 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/data/PlayerData.java @@ -0,0 +1,145 @@ +package com.georgev22.skinoverlay.storage.data; + +import com.georgev22.skinoverlay.utilities.CustomData; + +import java.util.Objects; +import java.util.UUID; + +/** + * Represents player-specific data within the SkinOverlay system. + * This includes the player's UUID, default skin, current skin, and associated custom data. + * Implements {@link Entity} for unified entity identification and data access. + *

+ * The {@code equals} method compares only the UUID for identity checks, + * while {@code equalsExact} compares all fields for strict equality. + *

+ * + * @author George + */ +public class PlayerData implements Entity { + + /** + * The UUID of the player. + */ + private final UUID uuid; + + /** + * The custom data associated with this player. + */ + private final CustomData customData = new CustomData(); + + /** + * The default skin assigned to this player. + */ + private Skin defaultSkin; + + /** + * The current skin being used by this player. + */ + private Skin currentSkin; + + /** + * Constructs a new {@code PlayerData} instance for the specified player UUID. + * + * @param uuid the UUID of the player + */ + public PlayerData(UUID uuid) { + this.uuid = uuid; + } + + /** + * Returns the UUID of this player. + * + * @return the player's UUID + */ + @Override + public UUID getId() { + return this.uuid; + } + + /** + * Returns the default skin of this player. + * + * @return the default {@link Skin}, or {@code null} if not set + */ + public Skin getDefaultSkin() { + return defaultSkin; + } + + /** + * Returns the current skin of this player. + * + * @return the current {@link Skin}, or {@code null} if not set + */ + public Skin getCurrentSkin() { + return currentSkin; + } + + /** + * Sets the current skin of this player. + * + * @param skin the new current {@link Skin} + */ + public void setCurrentSkin(Skin skin) { + this.currentSkin = skin; + } + + /** + * Sets the default skin of this player. + * + * @param skin the new default {@link Skin} + */ + public void setDefaultSkin(Skin skin) { + this.defaultSkin = skin; + } + + /** + * Returns the custom data container for this player. + * + * @return the {@link CustomData} instance + */ + @Override + public CustomData customData() { + return this.customData; + } + + /** + * Checks whether this {@code PlayerData} is exactly equal to another object. + * This strict comparison checks UUID, default skin, current skin, and custom data. + * + * @param o the object to compare with + * @return {@code true} if all fields are exactly equal, otherwise {@code false} + */ + @Override + public boolean equalsExact(Object o) { + if (!(o instanceof PlayerData other)) return false; + return Objects.equals(this.uuid, other.uuid) && + Objects.equals(this.defaultSkin, other.defaultSkin) && + Objects.equals(this.currentSkin, other.currentSkin) && + Objects.equals(this.customData, other.customData); + } + + /** + * Checks whether this {@code PlayerData} is equal to another object. + * Two {@code PlayerData} instances are considered equal if their UUIDs are equal. + * + * @param obj the object to compare with + * @return {@code true} if the UUIDs are equal, otherwise {@code false} + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof PlayerData other)) return false; + return Objects.equals(this.uuid, other.uuid); + } + + /** + * Returns a hash code value for this {@code PlayerData}. + * The hash code is based solely on the player's UUID. + * + * @return the hash code value + */ + @Override + public int hashCode() { + return this.uuid.hashCode(); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/data/Skin.java b/common/src/main/java/com/georgev22/skinoverlay/storage/data/Skin.java new file mode 100644 index 00000000..fb4ef972 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/data/Skin.java @@ -0,0 +1,174 @@ +package com.georgev22.skinoverlay.storage.data; + +import com.georgev22.skinoverlay.skin.SProperty; +import com.georgev22.skinoverlay.storage.gson.SkinTypeAdapter; +import com.georgev22.skinoverlay.utilities.CustomData; +import com.georgev22.skinoverlay.utilities.skin.SkinParts; +import com.google.gson.JsonParser; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Objects; +import java.util.UUID; + +/** + * Represents a skin within the SkinOverlay system. + * A skin is uniquely identified by its UUID and contains a {@link SProperty} + * defining its texture and signature data, as well as associated {@link CustomData}. + *

+ * The {@code equals} method compares only the skin's UUID for identity checks, + * while {@code equalsExact} compares all fields for strict equality. + *

+ */ +public class Skin implements Entity { + + /** + * The custom data associated with this skin. + */ + private final CustomData customData = new CustomData(); + + /** + * The unique identifier for this skin. + */ + private final UUID skinUniqueId; + + /** + * The property containing texture and signature information for this skin. + */ + private SProperty property; + + /** + * The skin parts associated with this skin. + */ + private SkinParts skinParts; + + /** + * Constructs a new {@code Skin} instance with the specified UUID. + * + * @param id the unique identifier for the skin + */ + public Skin(UUID id) { + this.skinUniqueId = id; + } + + /** + * Returns the unique identifier of this skin. + * + * @return the skin's UUID + */ + @Override + public UUID getId() { + return this.skinUniqueId; + } + + /** + * Returns the {@link SProperty} associated with this skin. + * + * @return the skin property, or {@code null} if not set + */ + public SProperty getProperty() { + return property; + } + + /** + * Sets the {@link SProperty} associated with this skin. + * + * @param property the skin property to set + */ + public void setProperty(SProperty property) { + this.property = property; + } + + /** + * Returns the {@link SkinParts} associated with this skin. + * + * @return the skin parts, or {@code null} if not set + */ + public SkinParts getSkinParts() { + return skinParts; + } + + /** + * Sets the {@link SkinParts} associated with this skin. + * + * @param skinParts the skin parts to set + */ + public void setSkinParts(SkinParts skinParts) { + this.skinParts = skinParts; + } + + /** + * Gets the URL of the skin based on the SProperty. + * + * @return The URL of the skin. + */ + public @Nullable String skinURL() { + return JsonParser.parseString(new String(Base64.getDecoder().decode(property.value()))) + .getAsJsonObject() + .getAsJsonObject("textures") + .getAsJsonObject("SKIN") + .get("url") + .getAsString(); + } + + /** + * Returns the custom data container for this skin. + * + * @return the {@link CustomData} instance + */ + @Override + public CustomData customData() { + return this.customData; + } + + /** + * Checks whether this {@code Skin} is exactly equal to another object. + * This strict comparison checks UUID, property, and custom data. + * + * @param o the object to compare with + * @return {@code true} if all fields are exactly equal, otherwise {@code false} + */ + @Override + public boolean equalsExact(Object o) { + if (!(o instanceof Skin other)) return false; + return Objects.equals(this.skinUniqueId, other.skinUniqueId) && + Objects.equals(this.property, other.property) && + Objects.equals(this.customData, other.customData); + } + + /** + * Checks whether this {@code Skin} is equal to another object. + * Two {@code Skin} instances are considered equal if their UUIDs are equal. + * + * @param obj the object to compare with + * @return {@code true} if the UUIDs are equal, otherwise {@code false} + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Skin other)) return false; + return Objects.equals(this.skinUniqueId, other.skinUniqueId); + } + + /** + * Returns a hash code value for this {@code Skin}. + * The hash code is based solely on the skin's UUID. + * + * @return the hash code value + */ + @Override + public int hashCode() { + return this.skinUniqueId.hashCode(); + } + + @ApiStatus.Internal + public String toBase64() { + return Base64.getEncoder().encodeToString(SkinTypeAdapter.toJson(this).getBytes(StandardCharsets.UTF_8)); + } + + @ApiStatus.Internal + public static Skin fromBase64(String base64) { + return SkinTypeAdapter.fromJson("", new String(Base64.getDecoder().decode(base64), StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/PartTypeAdapter.java b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/PartTypeAdapter.java similarity index 97% rename from core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/PartTypeAdapter.java rename to common/src/main/java/com/georgev22/skinoverlay/storage/gson/PartTypeAdapter.java index 2ca4071b..3a157b55 100644 --- a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/PartTypeAdapter.java +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/PartTypeAdapter.java @@ -1,7 +1,7 @@ -package com.georgev22.skinoverlay.storage.gsonAdapters; +package com.georgev22.skinoverlay.storage.gson; -import com.georgev22.skinoverlay.handler.skin.Part; import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; +import com.georgev22.skinoverlay.utilities.skin.Part; import com.google.gson.*; import org.jetbrains.annotations.NotNull; @@ -84,4 +84,4 @@ public static Part fromJson(String json) { if (json.isEmpty()) throw new IllegalArgumentException("Json cannot be empty"); return Part.fromJson(json); } -} +} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/UserTypeAdapter.java b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/PlayerDataTypeAdapter.java similarity index 50% rename from core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/UserTypeAdapter.java rename to common/src/main/java/com/georgev22/skinoverlay/storage/gson/PlayerDataTypeAdapter.java index 94e7b2c5..b2ce191b 100644 --- a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/UserTypeAdapter.java +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/PlayerDataTypeAdapter.java @@ -1,7 +1,8 @@ -package com.georgev22.skinoverlay.storage.gsonAdapters; +package com.georgev22.skinoverlay.storage.gson; +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.storage.data.PlayerData; import com.georgev22.skinoverlay.storage.data.Skin; -import com.georgev22.skinoverlay.storage.data.User; import com.google.gson.*; import org.jetbrains.annotations.NotNull; @@ -9,85 +10,85 @@ import java.util.UUID; /** - * Gson TypeAdapter for serializing and deserializing {@link User} objects. + * Gson TypeAdapter for serializing and deserializing {@link PlayerData} objects. *

- * This adapter is responsible for converting {@link User} objects to and from JSON format. + * This adapter is responsible for converting {@link PlayerData} objects to and from JSON format. * It handles the serialization and deserialization of the user's ID, default skin, and current skin properties. */ -public class UserTypeAdapter implements JsonSerializer, JsonDeserializer { +public class PlayerDataTypeAdapter implements JsonSerializer, JsonDeserializer { /** - * Serializes a {@link User} object to a JSON representation. + * Serializes a {@link PlayerData} object to a JSON representation. * - * @param user The {@link User} object to be serialized. + * @param user The {@link PlayerData} object to be serialized. * @param type The type of the source object. * @param jsonSerializationContext The serialization context. - * @return A {@link JsonElement} representing the serialized {@link User} object. + * @return A {@link JsonElement} representing the serialized {@link PlayerData} object. */ @Override - public JsonElement serialize(@NotNull User user, Type type, @NotNull JsonSerializationContext jsonSerializationContext) { + public JsonElement serialize(@NotNull PlayerData user, Type type, @NotNull JsonSerializationContext jsonSerializationContext) { JsonObject jsonObject = new JsonObject(); jsonObject.addProperty("entity_id", user.getId().toString()); - jsonObject.add("defaultSkin", jsonSerializationContext.serialize(user.defaultSkin())); - jsonObject.add("skin", jsonSerializationContext.serialize(user.skin())); + jsonObject.add("defaultSkin", jsonSerializationContext.serialize(user.getDefaultSkin())); + jsonObject.add("skin", jsonSerializationContext.serialize(user.getCurrentSkin())); return jsonObject; } /** - * Deserializes a JSON representation into a {@link User} object. + * Deserializes a JSON representation into a {@link PlayerData} object. * * @param jsonElement The {@link JsonElement} containing the JSON representation. * @param type The target type to deserialize into. * @param jsonDeserializationContext The deserialization context. - * @return A deserialized {@link User} object. + * @return A deserialized {@link PlayerData} object. */ @Override - public User deserialize(@NotNull JsonElement jsonElement, Type type, @NotNull JsonDeserializationContext jsonDeserializationContext) { + public PlayerData deserialize(@NotNull JsonElement jsonElement, Type type, @NotNull JsonDeserializationContext jsonDeserializationContext) { JsonObject jsonObject = jsonElement.getAsJsonObject(); UUID entityId = UUID.fromString(jsonObject.get("entity_id").getAsString()); Skin defaultSkin = jsonDeserializationContext.deserialize(jsonObject.get("defaultSkin"), Skin.class); Skin skin = jsonDeserializationContext.deserialize(jsonObject.get("skin"), Skin.class); - User user = new User(entityId); + PlayerData user = new PlayerData(entityId); user.setDefaultSkin(defaultSkin); - user.setSkin(skin); + user.setCurrentSkin(skin); return user; } /** - * Converts a {@link User} object to its JSON representation. + * Converts a {@link PlayerData} object to its JSON representation. * - * @param user The {@link User} object to be converted. - * @return A JSON string representing the serialized {@link User} object. - * @throws IllegalArgumentException If the provided {@link User} object is null. + * @param user The {@link PlayerData} object to be converted. + * @return A JSON string representing the serialized {@link PlayerData} object. + * @throws IllegalArgumentException If the provided {@link PlayerData} object is null. */ - public static String toJson(@NotNull User user) throws IllegalArgumentException { + public static String toJson(@NotNull PlayerData user) throws IllegalArgumentException { //noinspection ConstantValue if (user == null) { - throw new IllegalArgumentException("User cannot be null"); + throw new IllegalArgumentException("PlayerData cannot be null"); } - return user.toJson(); + return SkinOverlay.getInstance().getGson().toJson(user); } /** - * Converts a JSON string to a {@link User} object. + * Converts a JSON string to a {@link PlayerData} object. * - * @param json The JSON string representing the serialized {@link User} object. - * @return A {@link User} object deserialized from the provided JSON string. + * @param json The JSON string representing the serialized {@link PlayerData} object. + * @return A {@link PlayerData} object deserialized from the provided JSON string. * @throws IllegalArgumentException If the provided JSON string is null or empty. */ - public static User fromJson(@NotNull String json) { + public static PlayerData fromJson(@NotNull String entityId, @NotNull String json) { //noinspection ConstantValue if (json == null) { - throw new IllegalArgumentException("Json cannot be null"); + throw new IllegalArgumentException("Json cannot be null (entityId: " + entityId + ")"); } if (json.isEmpty()) { - throw new IllegalArgumentException("Json cannot be empty"); + throw new IllegalArgumentException("Json cannot be empty (entityId: " + entityId + ")"); } - return User.fromJson(json); + return SkinOverlay.getInstance().getGson().fromJson(json, PlayerData.class); } -} +} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SPropertyTypeAdapter.java b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SPropertyTypeAdapter.java similarity index 89% rename from core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SPropertyTypeAdapter.java rename to common/src/main/java/com/georgev22/skinoverlay/storage/gson/SPropertyTypeAdapter.java index 33fba3c3..a9b87de9 100644 --- a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SPropertyTypeAdapter.java +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SPropertyTypeAdapter.java @@ -1,6 +1,7 @@ -package com.georgev22.skinoverlay.storage.gsonAdapters; +package com.georgev22.skinoverlay.storage.gson; -import com.georgev22.skinoverlay.handler.SProperty; +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.skin.SProperty; import com.google.gson.*; import org.jetbrains.annotations.NotNull; @@ -28,11 +29,10 @@ public class SPropertyTypeAdapter implements public @NotNull SProperty deserialize(@NotNull JsonElement jsonElement, Type typeOfT, JsonDeserializationContext jsonDeserializationContext) { JsonObject jsonObject = jsonElement.getAsJsonObject(); - String name = jsonObject.get("name").getAsString(); String value = jsonObject.get("value").getAsString(); String signature = jsonObject.get("signature").getAsString(); - return new SProperty(name, value, signature); + return new SProperty(value, signature); } /** @@ -46,7 +46,6 @@ public class SPropertyTypeAdapter implements @Override public @NotNull JsonElement serialize(@NotNull SProperty src, Type typeOfSrc, JsonSerializationContext jsonSerializationContext) { JsonObject jsonObject = new JsonObject(); - jsonObject.addProperty("name", src.name()); jsonObject.addProperty("value", src.value()); jsonObject.addProperty("signature", src.signature()); return jsonObject; @@ -62,7 +61,7 @@ public class SPropertyTypeAdapter implements public static SProperty fromJson(String json) { if (json == null) throw new IllegalArgumentException("Json cannot be null"); if (json.isEmpty()) throw new IllegalArgumentException("Json cannot be empty"); - return SProperty.fromJson(json); + return SkinOverlay.getInstance().getGson().fromJson(json, SProperty.class); } /** @@ -74,6 +73,6 @@ public static SProperty fromJson(String json) { */ public static String toJson(SProperty sProperty) { if (sProperty == null) throw new IllegalArgumentException("SProperty cannot be null"); - return sProperty.toJson(); + return SkinOverlay.getInstance().getGson().toJson(sProperty); } -} +} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SerializableBufferedImageTypeAdapter.java b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SerializableBufferedImageTypeAdapter.java similarity index 98% rename from core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SerializableBufferedImageTypeAdapter.java rename to common/src/main/java/com/georgev22/skinoverlay/storage/gson/SerializableBufferedImageTypeAdapter.java index f8aec797..694f7ae3 100644 --- a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SerializableBufferedImageTypeAdapter.java +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SerializableBufferedImageTypeAdapter.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.storage.gsonAdapters; +package com.georgev22.skinoverlay.storage.gson; import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; import com.google.gson.*; @@ -79,4 +79,4 @@ public static SerializableBufferedImage fromJson(String json) { if (json.isEmpty()) throw new IllegalArgumentException("Json cannot be empty"); return SerializableBufferedImage.fromJson(json); } -} +} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SkinPartsTypeAdapter.java b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SkinPartsTypeAdapter.java similarity index 96% rename from core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SkinPartsTypeAdapter.java rename to common/src/main/java/com/georgev22/skinoverlay/storage/gson/SkinPartsTypeAdapter.java index 51b08d97..05c56b1b 100644 --- a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SkinPartsTypeAdapter.java +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SkinPartsTypeAdapter.java @@ -1,7 +1,7 @@ -package com.georgev22.skinoverlay.storage.gsonAdapters; +package com.georgev22.skinoverlay.storage.gson; -import com.georgev22.skinoverlay.handler.skin.SkinParts; import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; +import com.georgev22.skinoverlay.utilities.skin.SkinParts; import com.google.gson.*; import org.jetbrains.annotations.NotNull; @@ -75,4 +75,4 @@ public static SkinParts fromJson(@NotNull String json) { if (json.isEmpty()) throw new IllegalArgumentException("Json cannot be empty"); return SkinParts.fromJson(json); } -} +} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SkinTypeAdapter.java b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SkinTypeAdapter.java similarity index 76% rename from core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SkinTypeAdapter.java rename to common/src/main/java/com/georgev22/skinoverlay/storage/gson/SkinTypeAdapter.java index a71dab7f..94ff1c4a 100644 --- a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SkinTypeAdapter.java +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SkinTypeAdapter.java @@ -1,20 +1,15 @@ -package com.georgev22.skinoverlay.storage.gsonAdapters; +package com.georgev22.skinoverlay.storage.gson; -import com.georgev22.skinoverlay.handler.SProperty; -import com.georgev22.skinoverlay.handler.skin.SkinParts; +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.skin.SProperty; import com.georgev22.skinoverlay.storage.data.Skin; +import com.georgev22.skinoverlay.utilities.skin.SkinParts; import com.google.gson.*; import org.jetbrains.annotations.NotNull; import java.lang.reflect.Type; import java.util.UUID; -/** - * Gson TypeAdapter for serializing and deserializing {@link Skin} objects. - *

- * This adapter is responsible for converting {@link Skin} objects to and from JSON format. - * It handles the serialization and deserialization of the skin's ID, property, skin parts, and skin name properties. - */ public class SkinTypeAdapter implements JsonSerializer, JsonDeserializer { /** @@ -30,9 +25,8 @@ public JsonElement serialize(@NotNull Skin skin, Type type, @NotNull JsonSeriali JsonObject jsonObject = new JsonObject(); jsonObject.addProperty("entity_id", skin.getId().toString()); - jsonObject.add("property", jsonSerializationContext.serialize(skin.skinProperty())); - jsonObject.add("skinParts", jsonSerializationContext.serialize(skin.skinParts())); - jsonObject.addProperty("skinName", skin.skinName()); + jsonObject.add("property", jsonSerializationContext.serialize(skin.getProperty())); + jsonObject.add("skinParts", jsonSerializationContext.serialize(skin.getSkinParts())); return jsonObject; } @@ -52,10 +46,10 @@ public Skin deserialize(@NotNull JsonElement jsonElement, Type type, @NotNull Js UUID entityId = UUID.fromString(jsonObject.get("entity_id").getAsString()); SProperty property = jsonDeserializationContext.deserialize(jsonObject.get("property"), SProperty.class); SkinParts skinParts = jsonDeserializationContext.deserialize(jsonObject.get("skinParts"), SkinParts.class); - String skinName = jsonObject.get("skinName").getAsString(); - Skin skin = new Skin(entityId, property, skinParts); - skin.setSkinName(skinName); + Skin skin = new Skin(entityId); + skin.setProperty(property); + skin.setSkinParts(skinParts); return skin; } @@ -67,10 +61,10 @@ public Skin deserialize(@NotNull JsonElement jsonElement, Type type, @NotNull Js * @return A {@link Skin} object deserialized from the provided JSON string. * @throws IllegalArgumentException If the provided JSON string is null or empty. */ - public static Skin fromJson(String json) { - if (json == null) throw new IllegalArgumentException("Json cannot be null"); - if (json.isEmpty()) throw new IllegalArgumentException("Json cannot be empty"); - return Skin.fromJson(json); + public static Skin fromJson(String entityId, String json) { + if (json == null) throw new IllegalArgumentException("Json cannot be null (entityId: " + entityId + ")"); + if (json.isEmpty()) throw new IllegalArgumentException("Json cannot be empty (entityId: " + entityId + ")"); + return SkinOverlay.getInstance().getGson().fromJson(json, Skin.class); } /** @@ -82,6 +76,6 @@ public static Skin fromJson(String json) { */ public static String toJson(Skin skin) { if (skin == null) throw new IllegalArgumentException("Skin cannot be null"); - return skin.toJson(); + return SkinOverlay.getInstance().getGson().toJson(skin); } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/manager/PlayerEntityManager.java b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/PlayerEntityManager.java new file mode 100644 index 00000000..9c20c13b --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/PlayerEntityManager.java @@ -0,0 +1,153 @@ +package com.georgev22.skinoverlay.storage.manager; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.maps.ObservableObjectMap; +import com.georgev22.skinoverlay.storage.EntityManager; +import com.georgev22.skinoverlay.storage.data.PlayerData; +import com.georgev22.skinoverlay.utilities.config.OptionsUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.logging.Level; + +public abstract class PlayerEntityManager implements EntityManager { + + protected final ObservableObjectMap loadedEntities = new ObservableObjectMap<>(); + protected final SkinOverlay mainPlugin = SkinOverlay.getInstance(); + protected final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); + protected final Set saveQueue = ConcurrentHashMap.newKeySet(); + protected boolean isShuttingDown = false; + protected boolean isSaving = false; + + public PlayerEntityManager() { + long saveInterval = OptionsUtil.SAVE_INTERVAL.getLongValue(); + executorService.scheduleAtFixedRate(this::batchSave, saveInterval, saveInterval, TimeUnit.SECONDS); + } + + /** + * Saves the provided entity. + * + * @param entity the entity to be saved + */ + @Override + public void save(@NotNull PlayerData entity) { + // Update memory cache + this.loadedEntities.append(entity.getId().toString(), entity); + // Add to save queue + saveQueue.add(entity.getId()); + } + + @Override + public Optional findById(@NotNull String id) { + return this.loadedEntities.get(id) == null ? this.getEntity(id, true) : Optional.ofNullable(this.loadedEntities.get(id)); + } + + @Override + public void saveAll(Consumer consumer) { + this.mainPlugin.getLogger().info("Saving all player data..."); + batchSave(); + for (PlayerData entity : new ArrayList<>(loadedEntities.values())) { + consumer.accept(entity); + saveEntityWithRetry(entity); + } + } + + @Override + public List getAll() { + return new ArrayList<>(loadedEntities.values()); + } + + @Override + public Optional create(@NotNull String id, @NotNull Consumer consumer) { + PlayerData entity = new PlayerData(UUID.fromString(id)); + consumer.accept(entity); + saveEntityWithRetry(entity); + return Optional.of(entity); + } + + @Override + public Optional getEntity(@NotNull String id, boolean loadIfExists) { + if (this.loadedEntities.containsKey(id)) { + return Optional.ofNullable(this.loadedEntities.get(id)); + } + if (loadIfExists && this.exists(id)) { + Optional entity = this.load(id); + if (entity.isPresent()) { + this.loadedEntities.put(id, entity.get()); + return entity; + } + } + return create(id, entity -> { + }); + } + + @Override + public String getName() { + return this.getClass().getCanonicalName(); + } + + @Override + public String getSimpleName() { + return this.getClass().getSimpleName(); + } + + @Override + public void shutdown(Consumer consumer) { + isShuttingDown = true; + executorService.shutdownNow(); + this.mainPlugin.getLogger().info("Saving all player data synchronously on shutdown..."); + List entities = new ArrayList<>(loadedEntities.values()); + for (PlayerData entity : entities) { + consumer.accept(entity); + saveEntityWithRetry(entity); + } + } + + protected void saveEntityWithRetry(PlayerData entity) { + if (!SkinOverlay.getInstance().isProxy() && OptionsUtil.PROXY.getBooleanValue()) { + return; + } + int maxRetries = 3; + int retryCount = 0; + boolean success = false; + + while (retryCount < maxRetries && !success) { + success = save0(entity, isShuttingDown); + if (!success) { + retryCount++; + mainPlugin.getLogger().log(Level.WARNING, "Failed to save player data for {0}, retry {1}/{2}", + new Object[]{entity.getId(), retryCount, maxRetries}); + } + } + + if (!success) { + mainPlugin.getLogger().log(Level.SEVERE, "Failed to save player data for {0} after {1} retries", + new Object[]{entity.getId(), maxRetries}); + } + } + + protected void batchSave() { + if (isSaving || saveQueue.isEmpty()) return; + + isSaving = true; + Set toSave = new HashSet<>(saveQueue); + saveQueue.clear(); + + for (UUID entityId : toSave) { + if (this.loadedEntities.get(entityId.toString()) == null) { + continue; + } + saveEntityWithRetry(this.loadedEntities.get(entityId.toString())); + } + + isSaving = false; + } + + protected abstract boolean save0(PlayerData entity, boolean isShuttingDown); + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/manager/SkinEntityManager.java b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/SkinEntityManager.java new file mode 100644 index 00000000..d8478047 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/SkinEntityManager.java @@ -0,0 +1,155 @@ +package com.georgev22.skinoverlay.storage.manager; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.maps.ObservableObjectMap; +import com.georgev22.skinoverlay.storage.EntityManager; +import com.georgev22.skinoverlay.storage.data.Skin; +import com.georgev22.skinoverlay.utilities.config.OptionsUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.logging.Level; + +public abstract class SkinEntityManager implements EntityManager { + protected final SkinOverlay mainPlugin = SkinOverlay.getInstance(); + protected final ObservableObjectMap loadedEntities = new ObservableObjectMap<>(); + protected final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); + protected final Set saveQueue = ConcurrentHashMap.newKeySet(); + protected boolean isSaving = false; + protected boolean isShuttingDown = false; + + public SkinEntityManager() { + long saveInterval = OptionsUtil.SAVE_INTERVAL.getLongValue(); + executorService.scheduleAtFixedRate(this::batchSave, saveInterval, saveInterval, TimeUnit.SECONDS); + } + + @Override + public void save(@NotNull Skin entity) { + // Update memory cache + this.loadedEntities.append(entity.getId().toString(), entity); + // Add to save queue + saveQueue.add(entity.getId()); + } + + @Override + public Optional getEntity(@NotNull String id, boolean loadIfExists) { + if (loadedEntities.containsKey(id)) { + return Optional.ofNullable(loadedEntities.get(id)); + } + if (loadIfExists && exists(id)) { + Optional entity = load(id); + if (entity.isPresent()) { + loadedEntities.put(id, entity.get()); + return entity; + } + } + return Optional.empty(); + } + + @Override + public Optional findById(@NotNull String id) { + return loadedEntities.get(id) == null ? this.getEntity(id, true) : Optional.ofNullable(loadedEntities.get(id)); + } + + @Override + public void saveAll(Consumer consumer) { + this.mainPlugin.getLogger().info("Saving all skins..."); + batchSave(); + List entities = new ArrayList<>(loadedEntities.values()); + for (Skin entity : entities) { + consumer.accept(entity); + saveEntityWithRetry(entity); + } + } + + @Override + public List getAll() { + return new ArrayList<>(loadedEntities.values()); + } + + @Override + public String getName() { + return this.getClass().getCanonicalName(); + } + + @Override + public String getSimpleName() { + return this.getClass().getSimpleName(); + } + + @Override + public Optional create(@NotNull String id, @NotNull Consumer consumer) { + Skin skin = new Skin(UUID.fromString(id)); + consumer.accept(skin); + saveEntityWithRetry(skin); + return Optional.of(skin); + } + + protected void batchSave() { + if (isSaving || saveQueue.isEmpty()) return; + + isSaving = true; + Set toSave = new HashSet<>(saveQueue); + saveQueue.clear(); + + for (UUID entityId : toSave) { + if (this.loadedEntities.get(entityId.toString()) == null) { + continue; + } + saveEntityWithRetry(this.loadedEntities.get(entityId.toString())); + } + + isSaving = false; + } + + protected void saveEntityWithRetry(Skin entity) { + if (!SkinOverlay.getInstance().isProxy() && OptionsUtil.PROXY.getBooleanValue()) { + return; + } + int maxRetries = 3; + int retryCount = 0; + boolean success = false; + + while (retryCount < maxRetries && !success) { + success = save0(entity, isShuttingDown); + if (!success) { + retryCount++; + mainPlugin.getLogger().log(Level.WARNING, "Failed to save skin data for {0}, retry {1}/{2}", + new Object[]{entity.getId(), retryCount, maxRetries}); + } + } + + if (!success) { + mainPlugin.getLogger().log(Level.SEVERE, "Failed to save skin data for {0} after {1} retries", + new Object[]{entity.getId(), maxRetries}); + } + } + + protected abstract boolean save0(Skin entity, boolean isShuttingDown); + + protected void postLoad(Skin entity) { +// VoidChestAPI.getInstance().voidChestCacheController().add(entity, entity.blockLocation()); +// new VoidChestLoadEvent(entity).call(); + } + + protected void postSave(Skin entity) { +// new VoidChestSaveEvent(entity).call(); + } + + @Override + public void shutdown(Consumer consumer) { + isShuttingDown = true; + executorService.shutdownNow(); + mainPlugin.getLogger().info("Saving all skins synchronously on shutdown..."); + List entities = new ArrayList<>(loadedEntities.values()); + for (Skin entity : entities) { + consumer.accept(entity); + saveEntityWithRetry(entity); + } + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/manager/gson/PlayerFileManager.java b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/gson/PlayerFileManager.java new file mode 100644 index 00000000..fe4af93f --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/gson/PlayerFileManager.java @@ -0,0 +1,312 @@ +package com.georgev22.skinoverlay.storage.manager.gson; + +import com.georgev22.skinoverlay.event.events.player.PlayerDataLoadEvent; +import com.georgev22.skinoverlay.event.events.player.PlayerDataSaveEvent; +import com.georgev22.skinoverlay.registry.EntityManagerRegistry; +import com.georgev22.skinoverlay.storage.EntityManager; +import com.georgev22.skinoverlay.storage.data.PlayerData; +import com.georgev22.skinoverlay.storage.data.Skin; +import com.georgev22.skinoverlay.storage.gson.PlayerDataTypeAdapter; +import com.georgev22.skinoverlay.storage.manager.PlayerEntityManager; +import com.georgev22.skinoverlay.utilities.config.OptionsUtil; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousFileChannel; +import java.nio.channels.CompletionHandler; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.*; +import java.util.concurrent.ForkJoinPool; +import java.util.function.Consumer; +import java.util.logging.Level; + +@SuppressWarnings("DuplicatedCode") +public class PlayerFileManager extends PlayerEntityManager { + + private final File entitiesDirectory; + + public PlayerFileManager(File folder) { + super(); + this.entitiesDirectory = folder; + if (!this.entitiesDirectory.exists()) { + //noinspection ResultOfMethodCallIgnored + this.entitiesDirectory.mkdirs(); + } + } + + /** + * Deletes the specified entity. + * + * @param entity the entity to delete + */ + @Override + public void delete(@NotNull PlayerData entity) { + if (entitiesDirectory != null) { + File file = new File(entitiesDirectory, entity.getId() + ".json"); + saveQueue.remove(entity.getId()); + + if (file.exists()) { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + this.loadedEntities.remove(entity.getId().toString()); + // Delete skins + + @NotNull Optional> entityManager = EntityManagerRegistry.getManager(Skin.class); + if (entityManager.isPresent()) { + Optional currentSkin = entityManager.get().getEntity(entity.getCurrentSkin().getId(), true); + currentSkin.ifPresent(entityManager.get()::delete); + Optional defaultSkin = entityManager.get().getEntity(entity.getDefaultSkin().getId(), true); + defaultSkin.ifPresent(entityManager.get()::delete); + } + + } + } + + /** + * Loads an entity by its unique identifier. + * + * @param id the unique identifier + * @return the entity, or {@code null} if not found + */ + @Override + public Optional load(@NotNull String id) { + File jsonFile = new File(entitiesDirectory, id + ".json"); + + if (jsonFile.exists()) { + try { + String jsonData = Files.readString(jsonFile.toPath(), StandardCharsets.UTF_8); + PlayerData playerData = PlayerDataTypeAdapter.fromJson(id, jsonData); + + if (playerData == null) { + this.mainPlugin.getLogger().log(Level.SEVERE, "Failed to load player " + id); + return Optional.empty(); + } + this.loadedEntities.append(playerData.getId().toString(), playerData); + this.mainPlugin.getEventBus().post(new PlayerDataLoadEvent(playerData)); + return Optional.of(playerData); + } catch (IOException e) { + this.mainPlugin.getLogger().log(Level.SEVERE, "Error while trying to load " + id + " player", e); + return Optional.empty(); + } + } else { + return Optional.empty(); + } + } + + /** + * Loads all entities from the file system. + */ + @Override + public void loadAll() { + File[] files = this.entitiesDirectory.listFiles((dir, name) -> name.endsWith(".json")); + if (files == null || files.length == 0) { + mainPlugin.getLogger().info("No player data files found to load."); + return; + } + + int availableThreads = Math.max(4, Runtime.getRuntime().availableProcessors() / 2); + ForkJoinPool customPool = new ForkJoinPool(availableThreads); + + List successList = Collections.synchronizedList(new ArrayList<>()); + List failedList = Collections.synchronizedList(new ArrayList<>()); + + long startTime = System.nanoTime(); + + try { + customPool.submit(() -> Arrays.stream(files).parallel().forEach(file -> { + String id = file.getName().replace(".json", ""); + + try { + long fileStart = System.nanoTime(); + Optional playerData = this.load(id); + if (playerData.isPresent()) { + successList.add(id); + if (OptionsUtil.DEBUG.getBooleanValue()) { + this.mainPlugin.getLogger().info("Loaded player " + playerData.get().getId()); + } + if (OptionsUtil.DEBUG.getBooleanValue()) { + long fileDuration = System.nanoTime() - fileStart; + this.mainPlugin.getLogger().info("Loaded " + id + " in " + (fileDuration / 1_000_000.0) + " ms"); + } + } else { + failedList.add(id); + this.mainPlugin.getLogger().warning("Failed to load player " + id); + } + } catch (Exception e) { + failedList.add(id); + this.mainPlugin.getLogger().log(Level.SEVERE, "Exception while loading player " + id, e); + } + })).get(); + } catch (Exception e) { + this.mainPlugin.getLogger().log(Level.SEVERE, "Fatal error during parallel player load", e); + } + + long duration = System.nanoTime() - startTime; + + this.mainPlugin.getLogger().info("Finished loading player data:"); + this.mainPlugin.getLogger().info(" ✔ Loaded: " + successList.size()); + this.mainPlugin.getLogger().info(" ✘ Failed: " + failedList.size()); + this.mainPlugin.getLogger().info(" ⏱ Duration: " + (duration / 1_000_000.0) + " ms"); + + if (!failedList.isEmpty()) { + if (!OptionsUtil.DEBUG.getBooleanValue()) + this.mainPlugin.getLogger().info("Enable DEBUG mode for more details."); + else + this.mainPlugin.getLogger().warning("Failed to load the following players: " + String.join(", ", failedList)); + } + } + + @Override + public void saveAll(Consumer consumer) { + this.mainPlugin.getLogger().info("Saving all player data..."); + batchSave(); + List entities = new ArrayList<>(loadedEntities.values()); + for (PlayerData entity : entities) { + consumer.accept(entity); + saveEntityWithRetry(entity); + } + } + + /** + * Returns all entities managed by this manager. + * + * @return a list of all entities + */ + @Override + public List getAll() { + return new ArrayList<>(this.loadedEntities.values()); + } + + /** + * Checks if an entity with the specified identifier exists. + * + * @param id the unique identifier + * @return {@code true} if the entity exists, {@code false} otherwise + */ + @Override + public boolean exists(@NotNull String id) { + if (this.loadedEntities.containsKey(id)) { + return true; + } + if (this.entitiesDirectory != null) { + return new File(this.entitiesDirectory, id + ".json").exists(); + } else { + return false; + } + } + + /** + * Saves all entities in the save queue, executing the save operation in batches. + */ + protected void batchSave() { + if (isSaving || saveQueue.isEmpty()) { + return; + } + + isSaving = true; + + Set toSave = new HashSet<>(saveQueue); + saveQueue.clear(); + + for (UUID entityId : toSave) { + if (this.loadedEntities.get(entityId.toString()) == null) { + continue; + } + saveEntityWithRetry(this.loadedEntities.get(entityId.toString())); + } + + isSaving = false; + } + + /** + * Saves the entity to a temporary file before moving it to its final location. + * + * @param entity the entity to be saved + * @return {@code true} if the save was successful, {@code false} otherwise + */ + @SuppressWarnings("DuplicatedCode") + protected boolean save0(PlayerData entity, boolean synchronous) { + File tempFile; + try { + String jsonData = mainPlugin.getGson().toJson(entity); + if (jsonData == null) { + mainPlugin.getLogger().log(Level.SEVERE, "Failed to serialize player data for {0}", entity.getId()); + return false; + } + + File targetFile = new File(entitiesDirectory, entity.getId() + ".json"); + tempFile = new File(entitiesDirectory, entity.getId() + ".tmp"); + + byte[] bytes = jsonData.getBytes(StandardCharsets.UTF_8); + + if (synchronous) { + Files.write(tempFile.toPath(), bytes, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + String readJson = Files.readString(tempFile.toPath()); + PlayerData parsed = mainPlugin.getGson().fromJson(readJson, PlayerData.class); + + if (parsed == null || !parsed.getId().equals(entity.getId())) { + throw new IOException("Parsed data is invalid or ID mismatch"); + } + + Files.move(tempFile.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + this.mainPlugin.getEventBus().post(new PlayerDataSaveEvent(entity)); + } else { + //noinspection resource + AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open( + tempFile.toPath(), + StandardOpenOption.WRITE, StandardOpenOption.CREATE + ); + + ByteBuffer buffer = ByteBuffer.wrap(bytes); + + fileChannel.write(buffer, 0, entity.getId(), new CompletionHandler() { + @Override + public void completed(Integer result, Object attachment) { + try { + String readJson = Files.readString(tempFile.toPath()); + PlayerData parsed = mainPlugin.getGson().fromJson(readJson, PlayerData.class); + + if (parsed == null || !parsed.getId().equals(entity.getId())) { + throw new IOException("Parsed data is invalid or ID mismatch"); + } + + Files.move(tempFile.toPath(), targetFile.toPath(), + StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + PlayerFileManager.this.mainPlugin.getEventBus().post(new PlayerDataSaveEvent(entity)); + } catch (IOException e) { + mainPlugin.getLogger().log(Level.SEVERE, "Failed to save player data for " + entity.getId(), e); + } finally { + try { + fileChannel.close(); + } catch (IOException e) { + mainPlugin.getLogger().log(Level.WARNING, "Failed to close file channel for " + entity.getId(), e); + } + } + } + + @Override + public void failed(Throwable exc, Object attachment) { + mainPlugin.getLogger().log(Level.SEVERE, "Asynchronous file write failed for " + entity.getId(), exc); + try { + fileChannel.close(); + } catch (IOException e) { + mainPlugin.getLogger().log(Level.WARNING, "Failed to close file channel after failure for " + entity.getId(), e); + } + } + }); + + } + return true; + + } catch (Exception e) { + mainPlugin.getLogger().log(Level.SEVERE, "Error while saving player data for " + entity.getId(), e); + } + return false; + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/manager/gson/SkinFileManager.java b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/gson/SkinFileManager.java new file mode 100644 index 00000000..fac9de6e --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/gson/SkinFileManager.java @@ -0,0 +1,225 @@ +package com.georgev22.skinoverlay.storage.manager.gson; + +import com.georgev22.skinoverlay.storage.data.Skin; +import com.georgev22.skinoverlay.storage.gson.SkinTypeAdapter; +import com.georgev22.skinoverlay.storage.manager.SkinEntityManager; + +import com.georgev22.skinoverlay.utilities.config.OptionsUtil; +import org.jetbrains.annotations.NotNull; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousFileChannel; +import java.nio.channels.CompletionHandler; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.*; +import java.util.concurrent.ForkJoinPool; +import java.util.logging.Level; + +@SuppressWarnings("DuplicatedCode") +public class SkinFileManager extends SkinEntityManager { + private final File dataFolder; + + public SkinFileManager(@NotNull File dataFolder) { + super(); + this.dataFolder = dataFolder; + if (!dataFolder.exists()) { + //noinspection ResultOfMethodCallIgnored + dataFolder.mkdirs(); + } + } + + @Override + public Optional load(@NotNull String id) { + File file = new File(dataFolder, id + ".json"); + if (file.exists()) { + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + Skin chest = SkinTypeAdapter.fromJson(id, sb.toString()); + if (chest == null) { + return Optional.empty(); + } + loadedEntities.append(chest.getId().toString(), chest); + this.postLoad(chest); + return Optional.of(chest); + } catch (IOException e) { + mainPlugin.getLogger().log(Level.SEVERE, "Failed to load void chest " + id, e); + } + } + return Optional.empty(); + } + + @Override + public void delete(@NotNull Skin entity) { + File file = new File(dataFolder, entity.getId() + ".json"); + saveQueue.remove(entity.getId()); + + if (file.exists()) { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } +// +// @NotNull Optional> entityManager = EntityManagerRegistry.getManager(PlayerData.class); +// if (entityManager.isEmpty()) return; +// Optional playerData = entityManager.get().getEntity(entity.ownerUUID().toString(), true); +// if (playerData.isPresent()) { +// // TODO Delete skin +// entityManager.get().save(playerData.get()); +// } +// loadedEntities.remove(entity.getId().toString()); + } + + @Override + public void loadAll() { + File[] files = this.dataFolder.listFiles((dir, name) -> name.endsWith(".json")); + if (files == null || files.length == 0) { + mainPlugin.getLogger().info("No skin data files found to load."); + return; + } + + int availableThreads = Math.max(4, Runtime.getRuntime().availableProcessors() / 2); + ForkJoinPool customPool = new ForkJoinPool(availableThreads); + + List successList = Collections.synchronizedList(new ArrayList<>()); + List failedList = Collections.synchronizedList(new ArrayList<>()); + + long startTime = System.nanoTime(); + + try { + customPool.submit(() -> Arrays.stream(files).parallel().forEach(file -> { + String id = file.getName().replace(".json", ""); + + try { + long fileStart = System.nanoTime(); + Optional playerData = this.load(id); + if (playerData.isPresent()) { + successList.add(id); + if (OptionsUtil.DEBUG.getBooleanValue()) { + this.mainPlugin.getLogger().info("Loaded skin " + playerData.get().getId()); + } + if (OptionsUtil.DEBUG.getBooleanValue()) { + long fileDuration = System.nanoTime() - fileStart; + this.mainPlugin.getLogger().info("Loaded " + id + " in " + (fileDuration / 1_000_000.0) + " ms"); + } + } else { + failedList.add(id); + this.mainPlugin.getLogger().warning("Failed to load skin " + id); + } + } catch (Exception e) { + failedList.add(id); + this.mainPlugin.getLogger().log(Level.SEVERE, "Exception while loading skin " + id, e); + } + })).get(); + } catch (Exception e) { + this.mainPlugin.getLogger().log(Level.SEVERE, "Fatal error during parallel skin load", e); + } + + long duration = System.nanoTime() - startTime; + + this.mainPlugin.getLogger().info("Finished loading skin data:"); + this.mainPlugin.getLogger().info(" ✔ Loaded: " + successList.size()); + this.mainPlugin.getLogger().info(" ✘ Failed: " + failedList.size()); + this.mainPlugin.getLogger().info(" ⏱ Duration: " + (duration / 1_000_000.0) + " ms"); + + if (!failedList.isEmpty()) { + if (!OptionsUtil.DEBUG.getBooleanValue()) + this.mainPlugin.getLogger().info("Enable DEBUG mode for more details."); + else + this.mainPlugin.getLogger().warning("Failed to load the following skins: " + String.join(", ", failedList)); + } + } + + @Override + public boolean exists(@NotNull String id) { + if (loadedEntities.containsKey(id)) return true; + return new File(dataFolder, id + ".json").exists(); + } + + protected boolean save0(Skin entity, boolean synchronous) { + File tempFile; + try { + String jsonData = mainPlugin.getGson().toJson(entity); + if (jsonData == null) { + mainPlugin.getLogger().log(Level.SEVERE, "Failed to serialize skin data for {0}", entity.getId()); + return false; + } + + File targetFile = new File(dataFolder, entity.getId() + ".json"); + tempFile = new File(dataFolder, entity.getId() + ".tmp"); + + byte[] bytes = jsonData.getBytes(StandardCharsets.UTF_8); + + if (synchronous) { + Files.write(tempFile.toPath(), bytes, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + String readJson = Files.readString(tempFile.toPath()); + Skin parsed = SkinTypeAdapter.fromJson(entity.getId().toString(), readJson); + + if (parsed == null || !parsed.getId().equals(entity.getId())) { + throw new IOException("Parsed data is invalid or ID mismatch"); + } + + Files.move(tempFile.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + this.postSave(entity); + } else { + //noinspection resource + AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open( + tempFile.toPath(), StandardOpenOption.WRITE, StandardOpenOption.CREATE); + + ByteBuffer buffer = ByteBuffer.wrap(bytes); + + fileChannel.write(buffer, 0, entity.getId(), new CompletionHandler() { + @Override + public void completed(Integer result, Object attachment) { + try { + String readJson = Files.readString(tempFile.toPath()); + Skin parsed = SkinTypeAdapter.fromJson(entity.getId().toString(), readJson); + + if (parsed == null || !parsed.getId().equals(entity.getId())) { + throw new IOException("Parsed data is invalid or ID mismatch"); + } + + Files.move(tempFile.toPath(), targetFile.toPath(), + StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + SkinFileManager.this.postSave(entity); + } catch (IOException e) { + mainPlugin.getLogger().log(Level.SEVERE, "Failed to save void chest data for " + entity.getId(), e); + } finally { + try { + fileChannel.close(); + } catch (IOException e) { + mainPlugin.getLogger().log(Level.WARNING, "Failed to close file channel for " + entity.getId(), e); + } + } + } + + @Override + public void failed(Throwable exc, Object attachment) { + mainPlugin.getLogger().log(Level.SEVERE, "Asynchronous file write failed for " + entity.getId(), exc); + try { + fileChannel.close(); + } catch (IOException e) { + mainPlugin.getLogger().log(Level.WARNING, "Failed to close file channel after failure for " + entity.getId(), e); + } + } + }); + + } + return true; + + } catch (Exception e) { + mainPlugin.getLogger().log(Level.SEVERE, "Error while saving void chest data for " + entity.getId(), e); + } + + return false; + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/Copyable.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/Copyable.java new file mode 100644 index 00000000..3cdb8fc0 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/Copyable.java @@ -0,0 +1,64 @@ +package com.georgev22.skinoverlay.utilities; + +import org.jetbrains.annotations.NotNull; + +/** + * A generic interface that defines a contract for objects that can be copied. + *

+ * Implementing classes must provide their own implementations of + * {@link #shallowCopy()} and {@link #deepCopy()} to create new instances + * of the object with different copy depths. + * + * @param the type of the object that can be copied + */ +public interface Copyable { + + /** + * Creates a shallow copy of the current object. + *

+ * Only the top-level fields are copied; nested objects are referenced directly. + * + * @return a shallow copy of type {@code T} + */ + @NotNull T shallowCopy(); + + /** + * Creates a deep copy of the current object. + *

+ * Both top-level fields and nested objects are fully copied. + * + * @return a deep copy of type {@code T} + */ + @NotNull T deepCopy(); + + /** + * Creates a copy of the current object according to the given {@link CopyType}. + * + * @param type the copy type (shallow or deep) + * @return a copy of type {@code T}, based on the specified {@link CopyType} + */ + @NotNull + default T copy(@NotNull CopyType type) { + return type.isDeep() ? deepCopy() : shallowCopy(); + } + + /** + * Represents the strategy for copying an object: shallow or deep. + */ + enum CopyType { + SHALLOW, + DEEP; + + public boolean isDeep() { + return this == DEEP; + } + + public boolean isShallow() { + return this == SHALLOW; + } + + public static CopyType fromBoolean(boolean deep) { + return deep ? DEEP : SHALLOW; + } + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/CustomData.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/CustomData.java new file mode 100644 index 00000000..c60900c9 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/CustomData.java @@ -0,0 +1,228 @@ +package com.georgev22.skinoverlay.utilities; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.maps.ObjectMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.Cloneable; +import java.util.Map; +import java.util.function.Supplier; +import java.util.logging.Level; + +/** + * The {@code CustomData} class provides storage and management for custom key-value data. + * It allows adding, retrieving, and managing data that can be associated with user-defined + * keys and arbitrary object values. The class ensures strong type safety when retrieving + * data by supports runtime type casting. + *

+ * This class is immutable in terms of the field reference but allows modification of its + * internal data structure. It is designed to support flexible extension of custom functionality. + */ +public final class CustomData implements Cloneable, Copyable { + + private ObjectMap customData; + + public CustomData() { + this.customData = ObjectMap.newConcurrentObjectMap(); + } + + /** + * Sets the custom data for the menu. This method clears any existing custom data + * and replaces it with the new data provided. + * + * @param customData The map of custom data to be set, where the keys are strings + * and the values are arbitrary objects. + */ + public void set(ObjectMap customData) { + this.customData.clear(); + this.customData.putAll(customData); + } + + /** + * Sets the custom data for the menu. This method clears any existing custom data + * and replaces it with the new data provided. + * + * @param customData The map of custom data to be set, where the keys are strings + * and the values are arbitrary objects. + */ + public void set(Map customData) { + this.customData.clear(); + this.customData.putAll(customData); + } + + /** + * Sets a custom data entry in the internal data store. + * + * @param key the key associated with the custom data, must not be null + * @param value the value associated with the key, can be null + */ + public void set(String key, Object value) { + customData.append(key, value); + } + + /** + * Sets a dynamic custom data entry using a {@link Supplier}. The value is computed + * each time it is retrieved. + * + * @param key the key associated with the custom data, must not be null + * @param supplier a {@link Supplier} that provides the value when requested, must not be null + * @param the type of the value provided by the supplier + */ + public void setDynamic(String key, Supplier supplier) { + this.customData.append(key, supplier); + } + + /** + * Retrieves the custom data associated with the menu. + * + * @return An ObjectMap containing the custom data, where the keys are strings, + * and the values are objects. If no custom data exists, returns an empty ObjectMap. + */ + public ObjectMap getCustomData() { + return customData; + } + + /** + * Retrieves a custom data object associated with the specified key. + * Casts the retrieved object to the specified type if present. + * Returns null if the key does not exist or if the cast fails. + * If the value is a {@link Supplier}, it is evaluated and the result is cast to the specified type. + * + * @param key the key associated with the desired custom data + * @param the type to cast the retrieved custom data to + * @return the custom data cast to the specified type, or null if the key does not exist or the cast fails + */ + @Nullable + public T get(String key) { + if (!customData.containsKey(key)) { + return null; + } + try { + Object value = customData.get(key); + //noinspection unchecked + return value instanceof Supplier ? ((Supplier) value).get() : (T) value; + } catch (ClassCastException e) { + SkinOverlay.getInstance().getLogger() + .log(Level.SEVERE, "Failed to cast custom data value for key " + key + " to the specified type.", e); + return null; + } + } + + /** + * Retrieves a custom data object associated with the specified key. + * If the key does not exist, returns the default value. + * + * @param key the key associated with the desired custom data + * @param defaultValue the default value to return if the key does not exist + * @param the type of the value + * @return the custom data object associated with the specified key, or the default value if the key does not exist + */ + @NotNull + public T getOr(String key, T defaultValue) { + T value = get(key); + return value != null ? value : defaultValue; + } + + /** + * Retrieves and casts a custom data object associated with the specified key. + * If the value is a {@link Supplier}, it is evaluated and the result is cast to the specified type each time. + * + * @param key The key associated with the desired custom data + * @param type The class to cast the retrieved custom data to + * @param The type to cast the retrieved custom data to + * @return The custom data cast to the specified type, or null if the key does not exist or the cast fails + */ + @Nullable + public T getAs(String key, Class type) { + if (!customData.containsKey(key)) { + return null; + } + try { + Object value = customData.get(key); + if (value instanceof Supplier supplier) { + Object suppliedValue = supplier.get(); + return type.cast(suppliedValue); + } else { + return type.cast(value); + } + } catch (ClassCastException e) { + SkinOverlay.getInstance().getLogger() + .log(Level.SEVERE, String.format("Failed to cast custom data value for key %s to the specified type.", key), e); + return null; + } + } + + /** + * Retrieves and casts a custom data object associated with the specified key. + * If the value is a {@link Supplier}, it is evaluated and the result is cast to the specified type each time. + * If the key does not exist, returns the default value. + * + * @param key The key associated with the desired custom data + * @param defaultValue The default value to return if the key does not exist + * @param type The class to cast the retrieved custom data to + * @param The type to cast the retrieved custom data to + * @return The custom data cast to the specified type, or the default value if the key does not exist or the cast fails + */ + @NotNull + public T getOr(String key, T defaultValue, Class type) { + T value = getAs(key, type); + return value != null ? value : defaultValue; + } + + /** + * Removes a custom data entry from the internal data store. + * + * @param key the key associated with the custom data, must not be null + */ + public void remove(String key) { + if (key == null) return; + if (!customData.containsKey(key)) return; + customData.remove(key); + } + + /** + * Returns a string representation of the custom data. + * + * @return a string representation of the custom data + */ + @Override + public String toString() { + return customData.toString(); + } + + @Override + public @NotNull CustomData clone() { + try { + CustomData clone = (CustomData) super.clone(); + clone.customData = ObjectMap.newConcurrentObjectMap(); + for (Map.Entry entry : customData.entrySet()) { + clone.customData.put(entry.getKey(), DeepCloner.cloneValue(entry.getValue())); + } + + return clone; + } catch (Exception e) { + throw new AssertionError("Something went wrong while cloning CustomData", e); + } + } + + @Override + public @NotNull CustomData shallowCopy() { + try { + return (CustomData) super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError("Something went wrong while shallow copying CustomData", e); + } + } + + @Override + public @NotNull CustomData deepCopy() { + CustomData copy = new CustomData(); + copy.customData = ObjectMap.newConcurrentObjectMap(); + for (Map.Entry entry : customData.entrySet()) { + copy.customData.put(entry.getKey(), DeepCloner.cloneValue(entry.getValue())); + } + + return copy; + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/DeepCloner.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/DeepCloner.java new file mode 100644 index 00000000..722b8292 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/DeepCloner.java @@ -0,0 +1,278 @@ +package com.georgev22.skinoverlay.utilities; + +import com.georgev22.skinoverlay.maps.*; + +import java.lang.reflect.Constructor; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.*; +import java.util.concurrent.*; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Utility for cloning objects with support for: + *

    + *
  • {@link Cloneable} objects (via public {@code clone()})
  • + *
  • Standard JDK collections/maps/queues/deques
  • + *
  • Concurrent collections
  • + *
  • Fallback reflection-based instantiation if no handler is registered
  • + *
  • Special handling of known immutables (returned as-is)
  • + *
+ * + *

Cloning modes:

+ *
    + *
  • {@code deep = true}: recursively clones contents.
  • + *
  • {@code deep = false}: creates a new container but reuses element references.
  • + *
  • If cloning fails, returns the same reference.
  • + *
+ * + *

This class is thread-safe and extensible: + * you can register custom cloners and immutables at runtime.

+ */ +@SuppressWarnings("unchecked") +public final class DeepCloner { + /** + * Registry of collection/map cloners (thread-safe). + */ + private static final ConcurrentMap, Function> CLONERS = new ConcurrentHashMap<>(); + + /** + * Registry of known immutable classes (thread-safe). + */ + private static final Set> IMMUTABLES = ConcurrentHashMap.newKeySet(); + + static { + // === Built-in immutables === + IMMUTABLES.addAll(Set.of( + String.class, + UUID.class, + + // Numbers + Integer.class, Long.class, Short.class, Byte.class, + Double.class, Float.class, BigDecimal.class, BigInteger.class, + + // Primitive wrappers + Boolean.class, Character.class, + + // Optionals + Optional.class, OptionalInt.class, OptionalLong.class, OptionalDouble.class + )); + + // === MAPS === + register(ObjectMap.class, o -> switch (o) { + case ObservableObjectMap ignored -> new ObservableObjectMap<>(); + case ConcurrentObjectMap ignored -> new ConcurrentObjectMap<>(); + case HashObjectMap ignored -> new HashObjectMap<>(); + case LinkedObjectMap ignored -> new LinkedObjectMap<>(); + case TreeObjectMap treeObjectMap -> new TreeObjectMap<>(treeObjectMap.comparator()); + case UnmodifiableObjectMap ignored -> + throw new UnsupportedOperationException("Cannot clone unmodifiable map"); + default -> throw new IllegalArgumentException("Unknown map type: " + o.getClass()); + }); + register(HashMap.class, o -> new HashMap<>()); + register(LinkedHashMap.class, o -> new LinkedHashMap<>()); + register(TreeMap.class, o -> new TreeMap<>(((TreeMap) o).comparator())); + register(WeakHashMap.class, o -> new WeakHashMap<>()); + register(IdentityHashMap.class, o -> new IdentityHashMap<>()); + register(EnumMap.class, o -> new EnumMap<>(((EnumMap) o))); + + register(ConcurrentHashMap.class, o -> new ConcurrentHashMap<>()); + register(ConcurrentSkipListMap.class, o -> new ConcurrentSkipListMap<>(((ConcurrentSkipListMap) o).comparator())); + + register(Hashtable.class, o -> new Hashtable<>()); + register(Properties.class, o -> new Properties()); + + // === LISTS === + register(ArrayList.class, o -> new ArrayList<>(((List) o).size())); + register(CopyOnWriteArrayList.class, o -> new CopyOnWriteArrayList<>()); + register(Vector.class, o -> new Vector<>(((List) o).size())); + + // === STACK === + register(Stack.class, o -> new Stack<>()); + + // === SETS === + register(HashSet.class, o -> new HashSet<>()); + register(LinkedHashSet.class, o -> new LinkedHashSet<>()); + register(TreeSet.class, o -> new TreeSet<>(((TreeSet) o).comparator())); + register(EnumSet.class, o -> EnumSet.copyOf((EnumSet) o)); + + register(CopyOnWriteArraySet.class, o -> new CopyOnWriteArraySet<>()); + register(ConcurrentSkipListSet.class, o -> new ConcurrentSkipListSet<>(((ConcurrentSkipListSet) o).comparator())); + + // === QUEUES === + register(ArrayDeque.class, o -> new ArrayDeque<>(((Queue) o).size())); + register(PriorityQueue.class, o -> new PriorityQueue<>(((PriorityQueue) o).size(), ((PriorityQueue) o).comparator())); + register(LinkedList.class, o -> new LinkedList<>()); + + register(ConcurrentLinkedQueue.class, o -> new ConcurrentLinkedQueue<>()); + register(PriorityBlockingQueue.class, o -> new PriorityBlockingQueue<>(((PriorityBlockingQueue) o).size(), ((PriorityBlockingQueue) o).comparator())); + register(LinkedBlockingQueue.class, o -> new LinkedBlockingQueue<>(((LinkedBlockingQueue) o).remainingCapacity() + ((LinkedBlockingQueue) o).size())); + register(ArrayBlockingQueue.class, o -> { + ArrayBlockingQueue q = (ArrayBlockingQueue) o; + return new ArrayBlockingQueue<>(q.size() + q.remainingCapacity(), false); + }); + register(DelayQueue.class, o -> new DelayQueue<>()); + register(SynchronousQueue.class, o -> new SynchronousQueue<>()); + register(LinkedTransferQueue.class, o -> new LinkedTransferQueue<>()); + + // === DEQUES === + register(ConcurrentLinkedDeque.class, o -> new ConcurrentLinkedDeque<>()); + } + + private DeepCloner() { + // utility class + } + + /** + * Register a custom cloner for a given type. + * Thread-safe. + * + * @param clazz the class to register + * @param factory a factory that produces a new empty instance of the given type + */ + public static void register(Class clazz, Function factory) { + CLONERS.put(clazz, factory); + } + + /** + * Register a custom immutable type. + * Thread-safe. + * + * @param clazz the immutable class to register + */ + public static void registerImmutable(Class clazz) { + IMMUTABLES.add(clazz); + } + + /** + * Deep clones an object if possible. + * + * @param value the object to clone + * @return a deep-cloned copy, or the same reference if immutable or unhandled + */ + public static Object cloneValue(Object value) { + return cloneValue(value, true); + } + + /** + * Clones an object with control over depth. + * + * @param value the object to clone + * @param deep whether to recursively clone contents (true) or shallow copy (false) + * @return a cloned copy of the object, or the same reference if immutable or unhandled + */ + public static Object cloneValue(Object value, boolean deep) { + if (value == null) return null; + + // Known immutables → return as-is + if (IMMUTABLES.contains(value.getClass())) return value; + + // Enum values are immutable + switch (value) { + case Enum anEnum -> { + return anEnum; + } + + // Skip cloning suppliers (generally stateless) + case Supplier supplier -> { + return supplier; + } + + // If Copyable with deepCopy() method + case Copyable cloneable -> { + return cloneable.deepCopy(); + } + + // If Cloneable with clone() method + case java.lang.Cloneable ignored -> { + try { + return value.getClass().getMethod("clone").invoke(value); + } catch (Exception ignored1) { + // fallback to registry + } + } + default -> { + } + } + + // Try registered cloners + for (Map.Entry, Function> entry : CLONERS.entrySet()) { + if (entry.getKey().isInstance(value)) { + Object cloned = entry.getValue().apply(value); + fillCollection(value, cloned, deep); + return cloned; + } + } + + // === GENERIC FALLBACK === + if (value instanceof Map map) { + Object cloned = tryReflectiveCreate(value.getClass(), HashMap::new); + for (Map.Entry e : map.entrySet()) { + ((Map) cloned).put( + deep ? cloneValue(e.getKey(), true) : e.getKey(), + deep ? cloneValue(e.getValue(), true) : e.getValue() + ); + } + return cloned; + } + + if (value instanceof Collection coll) { + Object cloned = tryReflectiveCreate(value.getClass(), () -> + switch (coll) { + case Set ignored -> new HashSet<>(); + case Queue ignored -> new ArrayDeque<>(); + default -> new ArrayList<>(); + }); + for (Object o : coll) { + ((Collection) cloned).add(deep ? cloneValue(o, true) : o); + } + return cloned; + } + + // Default: assume immutable + return value; + } + + /** + * Fill a cloned collection/map with contents from the original. + * + * @param original the original collection/map + * @param cloned the new collection/map + * @param deep whether to clone contents deeply + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private static void fillCollection(Object original, Object cloned, boolean deep) { + if (original instanceof Map mapOriginal && cloned instanceof Map mapCloned) { + for (Object entryObj : mapOriginal.entrySet()) { + Map.Entry e = (Map.Entry) entryObj; + mapCloned.put( + deep ? cloneValue(e.getKey(), true) : e.getKey(), + deep ? cloneValue(e.getValue(), true) : e.getValue() + ); + } + } else if (original instanceof Collection collOriginal && cloned instanceof Collection collCloned) { + for (Object o : collOriginal) { + collCloned.add(deep ? cloneValue(o, true) : o); + } + } + } + + /** + * Attempt to create a new instance of the given class reflectively. + * Falls back to the provided supplier if reflection fails. + * + * @param clazz the class to instantiate + * @param fallback a supplier of a fallback instance + * @return a new instance of the given class or a fallback + */ + private static Object tryReflectiveCreate(Class clazz, Supplier fallback) { + try { + Constructor ctor = clazz.getDeclaredConstructor(); + ctor.setAccessible(true); + return ctor.newInstance(); + } catch (Exception ignored) { + return fallback.get(); + } + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/EncryptUtils.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/EncryptUtils.java new file mode 100644 index 00000000..6974e5de --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/EncryptUtils.java @@ -0,0 +1,156 @@ +package com.georgev22.skinoverlay.utilities; + +import org.jetbrains.annotations.NotNull; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Arrays; +import java.util.Base64; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +/** + * Utility class providing AES-GCM encryption and decryption with password-based key derivation, + * combined with GZIP compression for efficient storage and transmission. + */ +public class EncryptUtils { + + private static final int SALT_LENGTH = 16; + private static final int IV_LENGTH = 12; + private static final int KEY_LENGTH = 256; + private static final int TAG_LENGTH = 128; + private static final int ITERATIONS = 65536; + + /** + * Encrypts the given plaintext using AES-GCM with a password-derived key and GZIP compression. + *

+ * The output includes the salt, IV, and ciphertext concatenated and Base64-encoded. + * + * @param plaintext The plaintext string to encrypt + * @param password The password used for key derivation (PBKDF2 with HMAC-SHA256) + * @return A Base64-encoded string containing salt, IV, and ciphertext + * @throws GeneralSecurityException If encryption or key derivation fails + * @throws IOException If compression fails + */ + public static @NotNull String encrypt(@NotNull String plaintext, @NotNull String password) + throws GeneralSecurityException, IOException { + + byte[] salt = new byte[SALT_LENGTH]; + byte[] iv = new byte[IV_LENGTH]; + SecureRandom random = new SecureRandom(); + random.nextBytes(salt); + random.nextBytes(iv); + + SecretKey key = deriveKey(password, salt); + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + GCMParameterSpec gcmSpec = new GCMParameterSpec(TAG_LENGTH, iv); + cipher.init(Cipher.ENCRYPT_MODE, key, gcmSpec); + + byte[] compressed = compress(plaintext.getBytes(StandardCharsets.UTF_8)); + byte[] encrypted = cipher.doFinal(compressed); + + byte[] output = new byte[SALT_LENGTH + IV_LENGTH + encrypted.length]; + System.arraycopy(salt, 0, output, 0, SALT_LENGTH); + System.arraycopy(iv, 0, output, SALT_LENGTH, IV_LENGTH); + System.arraycopy(encrypted, 0, output, SALT_LENGTH + IV_LENGTH, encrypted.length); + + return Base64.getEncoder().encodeToString(output); + } + + /** + * Decrypts the given Base64-encoded string containing salt, IV, and ciphertext, + * returning the original plaintext after GZIP decompression. + * + * @param encryptedText The Base64-encoded string to decrypt + * @param password The password used for key derivation (must match encryption password) + * @return The decrypted plaintext string + * @throws GeneralSecurityException If decryption or key derivation fails + * @throws IOException If decompression fails + */ + public static @NotNull String decrypt(@NotNull String encryptedText, @NotNull String password) + throws GeneralSecurityException, IOException { + + byte[] decoded = Base64.getDecoder().decode(encryptedText); + + byte[] salt = Arrays.copyOfRange(decoded, 0, SALT_LENGTH); + byte[] iv = Arrays.copyOfRange(decoded, SALT_LENGTH, SALT_LENGTH + IV_LENGTH); + byte[] ciphertext = Arrays.copyOfRange(decoded, SALT_LENGTH + IV_LENGTH, decoded.length); + + SecretKey key = deriveKey(password, salt); + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + GCMParameterSpec gcmSpec = new GCMParameterSpec(TAG_LENGTH, iv); + cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec); + + byte[] compressed = cipher.doFinal(ciphertext); + byte[] decompressed = decompress(compressed); + + return new String(decompressed, StandardCharsets.UTF_8); + } + + /** + * Derives an AES SecretKey from the given password and salt using PBKDF2 with HMAC-SHA256. + * + * @param password The password to derive the key from + * @param salt The salt used for key derivation + * @return A SecretKey suitable for AES encryption + * @throws NoSuchAlgorithmException If the PBKDF2 algorithm is unavailable + * @throws InvalidKeySpecException If the key specification is invalid + */ + private static @NotNull SecretKey deriveKey(@NotNull String password, byte[] salt) + throws NoSuchAlgorithmException, InvalidKeySpecException { + + KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, KEY_LENGTH); + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + byte[] keyBytes = factory.generateSecret(spec).getEncoded(); + return new SecretKeySpec(keyBytes, "AES"); + } + + /** + * Compresses the given byte array using GZIP. + * + * @param data The data to compress + * @return Compressed byte array + * @throws IOException If compression fails + */ + private static byte @NotNull [] compress(byte[] data) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (GZIPOutputStream gzip = new GZIPOutputStream(bos)) { + gzip.write(data); + } + return bos.toByteArray(); + } + + /** + * Decompresses the given GZIP-compressed byte array. + * + * @param data The compressed data to decompress + * @return Decompressed byte array + * @throws IOException If decompression fails + */ + private static byte @NotNull [] decompress(byte[] data) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(data))) { + byte[] buffer = new byte[4096]; + int len; + while ((len = gzip.read(buffer)) != -1) { + bos.write(buffer, 0, len); + } + } + return bos.toByteArray(); + } +} diff --git a/core/src/main/java/com/georgev22/skinoverlay/utilities/interfaces/ImageSupplier.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/ImageSupplier.java similarity index 87% rename from core/src/main/java/com/georgev22/skinoverlay/utilities/interfaces/ImageSupplier.java rename to common/src/main/java/com/georgev22/skinoverlay/utilities/ImageSupplier.java index 6740b627..3cfe62f6 100644 --- a/core/src/main/java/com/georgev22/skinoverlay/utilities/interfaces/ImageSupplier.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/ImageSupplier.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.utilities.interfaces; +package com.georgev22.skinoverlay.utilities; import java.awt.image.BufferedImage; import java.io.IOException; diff --git a/core/src/main/java/com/georgev22/skinoverlay/utilities/Locale.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/Locale.java similarity index 100% rename from core/src/main/java/com/georgev22/skinoverlay/utilities/Locale.java rename to common/src/main/java/com/georgev22/skinoverlay/utilities/Locale.java diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/LoggerWrapper.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/LoggerWrapper.java new file mode 100644 index 00000000..e63bae60 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/LoggerWrapper.java @@ -0,0 +1,187 @@ +package com.georgev22.skinoverlay.utilities; + +import org.jetbrains.annotations.NotNull; + +import java.text.MessageFormat; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +/** + * LoggerWrapper is a wrapper class used to integrate java.util.Logger and log4j/slf4j loggers. + * It takes logger as an input and logs the records based on their levels. + */ +public class LoggerWrapper extends Logger { + private final org.apache.logging.log4j.Logger log4j; + private final org.slf4j.Logger slf4j; + + /** + * Constructor to create a LoggerWrapper wrapping the given Log4j Logger object. + * + * @param logger A Log4j Logger object. + */ + public LoggerWrapper(org.apache.logging.log4j.Logger logger) { + super("logger", null); + this.log4j = logger; + this.slf4j = null; + } + + /** + * Constructor to create a LoggerWrapper wrapping the given SLF4J Logger object. + * + * @param logger An SLF4J Logger object. + */ + public LoggerWrapper(org.slf4j.Logger logger) { + super("logger", null); + this.log4j = null; + this.slf4j = logger; + } + + /** + * Logs a message at the given LogRecord level. + * + * @param record A LogRecord object containing the log level and message. + */ + @Override + public void log(@NotNull LogRecord record) { + log(record.getLevel(), record.getMessage()); + } + + /** + * Logs a message at the specified log level. + * + * @param level The log level. + * @param msg The message to be logged. + */ + @Override + public void log(Level level, String msg) { + if (level == Level.FINE) { + if (log4j != null) { + log4j.debug(msg); + } else if (slf4j != null) { + slf4j.debug(msg); + } + } else if (level == Level.WARNING) { + if (log4j != null) { + log4j.warn(msg); + } else if (slf4j != null) { + slf4j.warn(msg); + } + } else if (level == Level.SEVERE) { + if (log4j != null) { + log4j.error(msg); + } else if (slf4j != null) { + slf4j.error(msg); + } + } else if (level == Level.INFO) { + if (log4j != null) { + log4j.info(msg); + } else if (slf4j != null) { + slf4j.info(msg); + } + } else { + if (log4j != null) { + log4j.trace(msg); + } else if (slf4j != null) { + slf4j.trace(msg); + } + } + } + + /** + * Logs a message with a single parameter at the specified log level. + * + * @param level The log level. + * @param msg The message to be logged. + * @param param1 The single parameter. + */ + @Override + public void log(Level level, String msg, Object param1) { + if (level == Level.FINE) { + if (log4j != null) { + log4j.debug(msg, param1); + } else if (slf4j != null) { + slf4j.debug(msg, param1); + } + } else if (level == Level.WARNING) { + if (log4j != null) { + log4j.warn(msg, param1); + } else if (slf4j != null) { + slf4j.warn(msg, param1); + } + } else if (level == Level.SEVERE) { + if (log4j != null) { + log4j.error(msg, param1); + } else if (slf4j != null) { + slf4j.error(msg, param1); + } + } else if (level == Level.INFO) { + if (log4j != null) { + log4j.info(msg, param1); + } else if (slf4j != null) { + slf4j.info(msg, param1); + } + } else { + if (log4j != null) { + log4j.trace(msg, param1); + } else if (slf4j != null) { + slf4j.trace(msg, param1); + } + } + } + + /** + * Logs a message with multiple parameters at the specified log level. + * + * @param level The log level. + * @param msg The message to be logged. + * @param params An array of parameters. + */ + @Override + public void log(Level level, String msg, Object[] params) { + log(level, MessageFormat.format(msg, params)); // workaround not formatting correctly + } + + /** + * Logs a message with a Throwable at the specified log level. + * + * @param level The log level. + * @param msg The message to be logged. + * @param params The Throwable associated with the message. + */ + @Override + public void log(Level level, String msg, Throwable params) { + if (level == Level.FINE) { + if (log4j != null) { + log4j.debug(msg, params); + } else if (slf4j != null) { + slf4j.debug(msg, params); + } + } else if (level == Level.WARNING) { + if (log4j != null) { + log4j.warn(msg, params); + } else if (slf4j != null) { + slf4j.warn(msg, params); + } + } else if (level == Level.SEVERE) { + if (log4j != null) { + log4j.error(msg, params); + } else if (slf4j != null) { + slf4j.error(msg, params); + } + } else if (level == Level.INFO) { + if (log4j != null) { + log4j.info(msg, params); + } else if (slf4j != null) { + slf4j.info(msg, params); + } + } else { + if (log4j != null) { + log4j.trace(msg, params); + } else if (slf4j != null) { + slf4j.trace(msg, params); + } + } + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/utilities/SerializableBufferedImage.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/SerializableBufferedImage.java similarity index 96% rename from core/src/main/java/com/georgev22/skinoverlay/utilities/SerializableBufferedImage.java rename to common/src/main/java/com/georgev22/skinoverlay/utilities/SerializableBufferedImage.java index 02abc65b..a3c11e19 100644 --- a/core/src/main/java/com/georgev22/skinoverlay/utilities/SerializableBufferedImage.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/SerializableBufferedImage.java @@ -1,7 +1,5 @@ package com.georgev22.skinoverlay.utilities; -import com.georgev22.library.yaml.serialization.ConfigurationSerializable; - import com.georgev22.skinoverlay.SkinOverlay; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; @@ -16,7 +14,7 @@ * Represents a {@link BufferedImage} that can be serialized and deserialized. */ @SuppressWarnings("ClassCanBeRecord") -public final class SerializableBufferedImage implements ConfigurationSerializable { +public final class SerializableBufferedImage { private final BufferedImage bufferedImage; public SerializableBufferedImage(BufferedImage bufferedImage) { @@ -28,7 +26,6 @@ public SerializableBufferedImage(BufferedImage bufferedImage) { * * @return A YAML-compatible map containing width, height, and pixel information. */ - @Override public @NotNull @Unmodifiable Map serialize() { int width = bufferedImage.getWidth(); int height = bufferedImage.getHeight(); @@ -149,4 +146,4 @@ public BufferedImage getBufferedImage() { } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/Utils.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/Utils.java new file mode 100644 index 00000000..62a19be4 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/Utils.java @@ -0,0 +1,2276 @@ +package com.georgev22.skinoverlay.utilities; + +import com.georgev22.skinoverlay.exceptions.NotFoundException; +import com.georgev22.skinoverlay.exceptions.ReflectionException; +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.maps.TreeObjectMap; +import com.google.gson.Gson; +import org.bspfsystems.yamlconfiguration.file.FileConfiguration; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.crypto.*; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.HttpsURLConnection; +import java.io.*; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.*; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.ProtocolException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +public final class Utils { + + /** + * Converts a given number of seconds into a formatted time string using provided labels. + * If the provided array does not contain exactly 9 elements, default values are used. + * + * @param input The number of seconds to convert. + * @param inputs An array containing labels for singular/plural time units and an invalid input message. + * @return A formatted time string. + */ + public static String convertSeconds(long input, String @NotNull ... inputs) { + String[] defaultInputs = {"second", "seconds", "minute", "minutes", "hour", "hours", "day", "days", "invalid"}; + String[] processedInputs = new String[9]; + + System.arraycopy(inputs, 0, processedInputs, 0, inputs.length); + + System.arraycopy(defaultInputs, inputs.length, processedInputs, inputs.length, 9 - inputs.length); + + return convertSeconds( + input, + processedInputs[0], + processedInputs[1], + processedInputs[2], + processedInputs[3], + processedInputs[4], + processedInputs[5], + processedInputs[6], + processedInputs[7], + processedInputs[8] + ); + } + + /** + * Converts a given number of seconds into a formatted time string using provided labels. + * + * @param input The number of seconds to convert. + * @param secondInput Singular form of "second". + * @param secondsInput Plural form of "seconds". + * @param minuteInput Singular form of "minute". + * @param minutesInput Plural form of "minutes". + * @param hourInput Singular form of "hour". + * @param hoursInput Plural form of "hours". + * @param dayInput Singular form of "day". + * @param daysInput Plural form of "days". + * @param invalidInput Message to return if the input is invalid. + * @return A formatted time string. + */ + public static String convertSeconds(long input, String secondInput, + String secondsInput, + String minuteInput, + String minutesInput, + String hourInput, + String hoursInput, + String dayInput, + String daysInput, + String invalidInput) { + if (input < 0) { + System.out.println( + "An attempt to convert a negative number was made for: " + input + ", making the number absolute."); + input = Math.abs(input); + } + + final StringBuilder builder = new StringBuilder(); + + boolean comma = false; + + /* Days */ + final long days = TimeUnit.SECONDS.toDays(input); + if (days > 0) { + builder.append(days).append(" ").append(days == 1 ? dayInput : daysInput); + comma = true; + } + + /* Hours */ + final long hours = (TimeUnit.SECONDS.toHours(input) - TimeUnit.DAYS.toHours(days)); + if (hours > 0) { + if (comma) { + builder.append(", "); + } + builder.append(hours).append(" ").append(hours == 1 ? hourInput : hoursInput); + comma = true; + } + + /* Minutes */ + final long minutes = (TimeUnit.SECONDS.toMinutes(input) - TimeUnit.HOURS.toMinutes(hours) + - TimeUnit.DAYS.toMinutes(days)); + if (minutes > 0) { + if (comma) { + builder.append(", "); + } + builder.append(minutes).append(" ").append(minutes == 1 ? minuteInput : minutesInput); + comma = true; + } + + /* Seconds */ + final long seconds = (TimeUnit.SECONDS.toSeconds(input) - TimeUnit.MINUTES.toSeconds(minutes) + - TimeUnit.HOURS.toSeconds(hours) - TimeUnit.DAYS.toSeconds(days)); + if (seconds > 0) { + if (comma) { + builder.append(", "); + } + builder.append(seconds).append(" ").append(seconds == 1 ? secondInput : secondsInput); + } + + /* Result */ + final String result = builder.toString(); + return result.isEmpty() ? invalidInput : result; + } + + /** + * Converts a given number of seconds into a formatted time string using default labels. + * + * @param input The number of seconds to convert. + * @return A formatted time string. + */ + public static String convertSeconds(long input) { + return convertSeconds(input, "second", "seconds", "minute", "minutes", + "hour", "hours", "day", "days", + "invalid time"); + } + + /** + * Checks if the input is of type long + * + * @param input input to check if it is long + * @return true if the input is long, + * false if it is not + */ + public static boolean isLong(final @NotNull String input) { + try { + Long.parseLong(input.trim()); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * Checks if the input is of type double + * + * @param input input to check if it is double + * @return true if the input is double, + * false if it is not + */ + public static boolean isDouble(final @NotNull String input) { + try { + Double.parseDouble(input.trim()); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * Checks if the input is of type int + * + * @param input input to check if it is int + * @return true if the input is int, + * false if it is not + */ + public static boolean isInt(final @NotNull String input) { + try { + Integer.parseInt(input.trim()); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * Checks if the input is a List + * + * @param input input to check if it is a List + * @return true if the input is a List, + * false if it is not + */ + public static boolean isList(final Object input) { + return input instanceof List; + } + + public static boolean isList(final @NotNull FileConfiguration file, final String path) { + return Utils.isList(file.get(path)); + } + + /** + * Converts an object to int + * + * @param object Object to convert. + * @return the converted object as int. + */ + public static int toInt(Object object) { + if (object instanceof Number) { + return ((Number) object).intValue(); + } + + try { + return Integer.parseInt(object.toString()); + } catch (NumberFormatException | NullPointerException ignored) { + } + return 0; + } + + /** + * Converts an object to double + * + * @param object Object to convert. + * @return the converted object as double. + */ + public static double toDouble(Object object) { + if (object instanceof Number) { + return ((Number) object).doubleValue(); + } + + try { + return Double.parseDouble(object.toString()); + } catch (NumberFormatException | NullPointerException ignored) { + } + + return 0; + } + + /** + * Converts an object to long + * + * @param object Object to convert. + * @return the converted object as long. + */ + public static long toLong(Object object) { + if (object instanceof Number) { + return ((Number) object).longValue(); + } + + try { + return Long.parseLong(object.toString()); + } catch (NumberFormatException | NullPointerException ignored) { + } + return 0; + } + + /** + * Translates all the placeholders of the string from the map + * + * @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 placeHolder(String str, final Map map, final boolean ignoreCase) { + if (str == null) throw new IllegalArgumentException("str cannot be null"); + if (map == null) { + return str; + } + for (final Entry entry : map.entrySet()) { + str = ignoreCase ? replaceIgnoreCase(str, entry.getKey(), entry.getValue()) + : str.replace(entry.getKey(), entry.getValue()); + } + return str; + } + + /** + * Replaces a string, without distinguishing between lowercase and uppercase + * + * @param text the input string + * @param searchString the string to be replaced + * @param replacement the replacement of the string + * @return the new string with the replacement + */ + public static String replaceIgnoreCase(final String text, String searchString, final String replacement) { + + if (text == null || text.isEmpty()) { + return text; + } + if (searchString == null || searchString.isEmpty()) { + return text; + } + if (replacement == null) { + return text; + } + + int max = -1; + + final String searchText = text.toLowerCase(); + searchString = searchString.toLowerCase(); + int start = 0; + int end = searchText.indexOf(searchString, start); + if (end == -1) { + return text; + } + final int replacementLength = searchString.length(); + int increase = replacement.length() - replacementLength; + increase = Math.max(increase, 0); + increase *= 16; + + final StringBuilder buf = new StringBuilder(text.length() + increase); + while (end != -1) { + buf.append(text, start, end).append(replacement); + start = end + replacementLength; + if (--max == 0) { + break; + } + end = searchText.indexOf(searchString, start); + } + return buf.append(text, start, text.length()).toString(); + } + + /** + * Translates all the placeholders of the string from the map + * + * @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 [] placeHolder(final String[] array, final Map map, final boolean ignoreCase) { + if (array == null) throw new IllegalArgumentException("array cannot be null"); + if (Arrays.stream(array).anyMatch(Objects::isNull)) + throw new IllegalArgumentException("array cannot have null elements"); + + final String[] newArray = Arrays.copyOf(array, array.length); + if (map == null) { + return newArray; + } + for (int i = 0; i < newArray.length; i++) { + newArray[i] = placeHolder(newArray[i], map, ignoreCase); + } + return newArray; + } + + /** + * Translates all the placeholders of the string from the map + * + * @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 placeHolder(final List coll, final Map map, + final boolean ignoreCase) { + if (coll == null) throw new IllegalArgumentException("coll cannot be null"); + if (coll.stream().anyMatch(Objects::isNull)) + throw new IllegalArgumentException("coll cannot have null elements"); + return map == null ? coll + : coll.stream().map(str -> placeHolder(str, map, ignoreCase)).collect(Collectors.toList()); + } + + /** + * Formats a number to a specific Locale + * + * @param lang the desired locale + * @param input the input number + * @return the formatted String + */ + public static String formatNumber(java.util.Locale lang, double input) { + if (lang == null) throw new IllegalArgumentException("lang cannot be null"); + return NumberFormat.getInstance(lang).format(input); + } + + /** + * Formats a number to the US Locale + * + * @param input the input number + * @return the formatted String + */ + public static String formatNumber(double input) { + return formatNumber(java.util.Locale.US, input); + } + + /** + * Finds and retrieves the greatest values of a map. + * + * @param map The map to get the greatest values + * @param n The number of values you want to get + * @param The map key + * @param The map value + * @return Map + */ + public static > @NotNull List> findGreatest(@NotNull Map map, int n) { + Comparator> comparator = (Comparator>) (e0, e1) -> { + V v0 = e0.getValue(); + V v1 = e1.getValue(); + return v1.compareTo(v0); + }; + PriorityQueue> highest = new PriorityQueue<>(n, comparator); + for (Entry entry : map.entrySet()) { + highest.offer(entry); + while (highest.size() > n) { + highest.poll(); + } + } + + List> result = new ArrayList<>(); + while (!highest.isEmpty()) { + result.add(highest.poll()); + } + return result; + } + + public static @NotNull String getArgumentsToString(String @NotNull [] args, int num) { + StringBuilder sb = new StringBuilder(); + for (int i = num; i < args.length; i++) { + sb.append(args[i]).append(" "); + } + return sb.toString().trim(); + } + + public static String @NotNull [] getArgumentsToArray(String @NotNull [] args, int num) { + if (args.length == 0) { + return args; + } + StringBuilder sb = new StringBuilder(); + for (int i = num; i < args.length; i++) { + sb.append(args[i]).append(" "); + } + return sb.toString().trim().split(" "); + } + + public static String @NotNull [] reverse(String[] a) { + List list = Arrays.asList(a); + Collections.reverse(list); + return list.toArray(new String[0]); + } + + /** + * Serialize Object to string using google Gson + * + * @param object object to serialize + * @return string output of the serialized object + * @since v5.0.1 + */ + public static String serialize(Object object) { + ByteArrayOutputStream byteaOut = new ByteArrayOutputStream(); + GZIPOutputStream gzipOut = null; + try { + gzipOut = new GZIPOutputStream(byteaOut); + gzipOut.write(new Gson().toJson(object).getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (gzipOut != null) try { + gzipOut.close(); + } catch (IOException exception) { + exception.printStackTrace(); + } + } + return byteaOut.toString(); + } + + /** + * Deserialize a string back to object + * see {@link #serialize(Object)} + * + * @param string serialized string + * @param the original object type (eg: {@code deserialize(stringToDeserialize, new TypeToken>(){}.getType())}) + * @return the deserialized object + * @since v5.0.1 + */ + public static T deserialize(@NotNull String string, @NotNull Type type) { + ByteArrayOutputStream byteaOut = new ByteArrayOutputStream(); + GZIPInputStream gzipIn = null; + try { + gzipIn = new GZIPInputStream(new ByteArrayInputStream(string.getBytes(StandardCharsets.UTF_8))); + for (int data; (data = gzipIn.read()) > -1; ) { + byteaOut.write(data); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (gzipIn != null) try { + gzipIn.close(); + } catch (IOException exception) { + exception.printStackTrace(); + } + } + + return new Gson().fromJson(byteaOut.toString(), type); + } + + /** + * Serializes an object and saves it to the specified file path. + *

+ * Serializes the specified object using the {@link ObjectOutputStream} and saves the serialized data to the file + * specified by the given file path. + *

+ * + * @param obj the object to be serialized + * @param filePath the file path where the serialized data will be saved + * @throws IOException if there is an error accessing or writing to the file + */ + public static void serializeObject(@NotNull Object obj, @NotNull String filePath) throws IOException { + FileOutputStream fileOutputStream = new FileOutputStream(filePath); + ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); + objectOutputStream.writeObject(obj); + objectOutputStream.flush(); + objectOutputStream.close(); + fileOutputStream.close(); + } + + /** + * Serializes an object and returns the serialized data as a string. + *

+ * Serializes the specified object using the {@link ObjectOutputStream} and returns the serialized data as a string. + * + * @param obj the object to be serialized + * @return the serialized data as a string + * @throws IOException if there is an error during serialization + */ + @Contract("_ -> new") + public static @NotNull String serializeObjectToString(@NotNull Object obj) throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); + objectOutputStream.writeObject(obj); + objectOutputStream.flush(); + objectOutputStream.close(); + byteArrayOutputStream.close(); + + byte[] bytes = byteArrayOutputStream.toByteArray(); + return Base64.getEncoder().encodeToString(bytes); + } + + /** + * Serializes an object and returns the serialized data as a byte array. + *

+ * Serializes the specified object using the {@link ObjectOutputStream} and returns the serialized data as a byte array. + * + * @param obj the object to be serialized + * @return the serialized data as a byte array + * @throws IOException if there is an error during serialization + */ + public static byte @NotNull [] serializeObjectToBytes(@NotNull Object obj) throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); + objectOutputStream.writeObject(obj); + objectOutputStream.flush(); + objectOutputStream.close(); + byteArrayOutputStream.close(); + return byteArrayOutputStream.toByteArray(); + } + + /** + * Deserializes an object from the specified file path. + *

+ * Deserializes the object + * stored in the file specified by the given file path using the {@link ObjectInputStream} and + * returns the deserialized object. + * + * @param filePath the file path where the serialized object is stored + * @return the deserialized object + * @throws IOException if there is an error accessing or reading from the file + * @throws ClassNotFoundException if the class of the serialized object cannot be found + */ + public static Object deserializeObject(@NotNull String filePath) throws IOException, ClassNotFoundException { + FileInputStream fileInputStream = new FileInputStream(filePath); + ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); + Object obj = objectInputStream.readObject(); + objectInputStream.close(); + fileInputStream.close(); + return obj; + } + + /** + * Deserializes an object from the specified serialized data. + *

+ * Deserializes the object from the specified serialized data using the {@link ObjectInputStream} and returns the + * deserialized object. + * + * @param serializedData the serialized object data as a string + * @return the deserialized object + * @throws IOException if there is an error accessing or reading from the serialized data + * @throws ClassNotFoundException if the class of the serialized object cannot be found + */ + public static Object deserializeObjectFromString(@NotNull String serializedData) throws IOException, ClassNotFoundException { + byte[] bytes = Base64.getDecoder().decode(serializedData); + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); + ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream); + Object obj = objectInputStream.readObject(); + objectInputStream.close(); + byteArrayInputStream.close(); + + return obj; + } + + /** + * Deserializes an object from the specified serialized data. + *

+ * Deserializes the object from the specified serialized data using the {@link ObjectInputStream} and returns the + * deserialized object. + * + * @param byteArray the serialized object data as a byte array + * @return the deserialized object + * @throws IOException if there is an error accessing or reading from the serialized data + * @throws ClassNotFoundException if the class of the serialized object cannot be found + */ + public static Object deserializeObjectFromBytes(byte @NotNull [] byteArray) throws IOException, ClassNotFoundException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArray); + ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream); + Object obj = objectInputStream.readObject(); + objectInputStream.close(); + byteArrayInputStream.close(); + return obj; + } + + /** + * Converts a string list to string. + * + * @param stringList The String List to convert. + * @return a String that contains the String List contents. + */ + public static @NotNull String stringListToString(@NotNull List stringList) { + return stringList.toString().replace("[", "").replace("]", "").replace(" ", ""); + } + + /** + * Convert a string to string list. + * + * @param string The string to convert. + * @return a String List that contains the String contents. + */ + public static @NotNull List stringToStringList(@NotNull String string) { + return string.replace(" ", "").isEmpty() ? new ArrayList<>() : new ArrayList<>(Arrays.asList(string.split(","))); + } + + /** + * Converts a map to string list. + * + * @param objectMap The {@link ObjectMap} to convert. + * @return a String List with the {@link ObjectMap} contents. + */ + public static @NotNull List objectMapToStringList(@NotNull ObjectMap objectMap) { + return mapToStringList(objectMap); + } + + /** + * Converts a map to string list. + * + * @param map The {@link Map} to convert. + * @return a String List with the {@link Map} contents. + */ + public static @NotNull List mapToStringList(@NotNull Map map) { + List stringList = new ArrayList<>(); + for (Entry entry : map.entrySet()) { + stringList.add(entry.getKey() + "=" + entry.getValue()); + } + return stringList; + } + + /** + * Converts a String List to {@link ObjectMap}. + * + * @param stringList the String List to convert. + * @param clazz The class type of the value. + * @param The class type. + * @param Type of the key. + * @param Type of the value. + * @return a {@link ObjectMap#newHashObjectMap(Map)} with the String List contents. + */ + public static @NotNull ObjectMap stringListToObjectMap(List stringList, final Class clazz) { + return new HashObjectMap<>(stringListToHashMap(stringList, clazz)); + } + + /** + * Converts a String List to {@link HashMap}. + * + * @param stringList the String List to convert. + * @param clazz The class type of the value. + * @param The class type. + * @param Type of the key. + * @param Type of the value. + * @return a {@link HashMap} with the String List contents. + */ + public static @NotNull Map stringListToMap(List stringList, final Class clazz) { + return new HashMap<>(stringListToHashMap(stringList, clazz)); + } + + /** + * Converts a String List to {@link Map}. + * + * @param stringList the String List to convert. + * @param clazz The class type of the value. + * @param The class type. + * @param Type of the key. + * @param Type of the value. + * @return a {@link HashMap} with the String List contents. + */ + public static @NotNull Map stringListToHashMap(@NotNull List stringList, final Class clazz) { + Map map = new HashMap<>(); + + if (stringList.isEmpty()) { + return map; + } + + for (String string : stringList) { + if (!string.contains("=")) continue; + String[] arguments = string.split("="); + if (clazz != null) { + Method method; + try { + method = Reflection.fetchDeclaredMethod(clazz, "valueOf", String.class); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + continue; + } + + try { + map.put((K) arguments[0], (V) method.invoke(null, arguments[1])); + } catch (InvocationTargetException | IllegalAccessException ex) { + ex.printStackTrace(); + System.out.println("Failure: " + arguments[1] + " is not of type " + clazz.getName()); + } + } else { + map.put((K) arguments[0], (V) arguments[1]); + } + + } + + return map; + } + + /** + * Generates a specific amount of random HEX colors + * + * @param amount the amount of the colors to be generated + * @return a String List that contains the generated color codes + */ + public static @NotNull List randomColors(int amount) { + List randomColorList = new ArrayList<>(); + for (int i = 0; i < amount; i++) { + randomColorList.add(String.format("#%06x", new Random().nextInt(0xffffff + 1))); + } + return randomColorList; + } + + /** + * Converts a number to Roman Number format + * + * @param number number to convert + * @return a string of the number at Roman Number format + */ + public static String toRoman(int number) { + if (number <= 0) { + return String.valueOf(number); + } + TreeObjectMap map = ObjectMap.newTreeObjectMap(); + map + .append(1000, "M") + .append(900, "CM") + .append(500, "D") + .append(400, "CD") + .append(100, "C") + .append(90, "XC") + .append(50, "L") + .append(40, "XL") + .append(10, "X") + .append(9, "IX") + .append(5, "V") + .append(4, "IV") + .append(1, "I") + ; + int l = map.floorKey(number); + if (number == l) { + return map.get(number); + } + return map.get(l) + toRoman(number - l); + } + + /** + * Returns element closest to target in arr[] + */ + public static int findClosest(@NotNull List list, int target) { + Integer[] arr = list.toArray(new Integer[0]); + int n = arr.length; + if (target <= arr[0]) + return arr[0]; + if (target >= arr[n - 1]) + return arr[n - 1]; + int i = 0, j = n, mid = 0; + while (i < j) { + mid = (i + j) / 2; + if (arr[mid] == target) + return arr[mid]; + if (target < arr[mid]) { + if (mid > 0 && target > arr[mid - 1]) + return getClosest(arr[mid - 1], + arr[mid], target); + j = mid; + } else { + if (mid < n - 1 && target < arr[mid + 1]) + return getClosest(arr[mid], + arr[mid + 1], target); + i = mid + 1; + } + } + + return arr[mid]; + } + + /** + * Method to compare which one is the more close + */ + public static int getClosest(int val1, int val2, + int target) { + if (target - val1 >= val2 - target) + return val2; + else + return val1; + } + + /** + * Generate a random alphanumeric string + * + * @param n string length + * @return a random alphanumeric string + */ + public static @NotNull String generateAlphaNumericString(int n) { + String AlphaNumericString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "0123456789" + + "abcdefghijklmnopqrstuvxyz"; + StringBuilder sb = new StringBuilder(n); + + for (int i = 0; i < n; i++) { + int index + = (int) (AlphaNumericString.length() + * Math.random()); + sb.append(AlphaNumericString + .charAt(index)); + } + + return sb.toString(); + } + + /** + * Returns an element at random from a List + * + * @param list List to select a random element + * @return a random element from the specified List + */ + public static Object getRandomElement(@NotNull List list) { + Random rand = new Random(); + return list.get(rand.nextInt(list.size())); + } + + /** + * Returns an element at random from a Set + * + * @param set Set to select a random element + * @return a random element from the specified Set + */ + public static Object getRandomElement(@NotNull Set set) { + List b = new ArrayList<>(); + b.addAll(set); + return getRandomElement(b); + } + + public static void saveResource(@NotNull String resourcePath, boolean replace, File dataFolder, Class clazz) throws Exception { + if (resourcePath.isEmpty()) { + throw new Exception("ResourcePath cannot be null or empty"); + } + + //noinspection DuplicatedCode + resourcePath = resourcePath.replace('\\', '/'); + InputStream in = getResource(resourcePath, clazz); + if (in == null) { + throw new Exception("The embedded resource '" + resourcePath + "' cannot be found in " + new File(clazz.getProtectionDomain() + .getCodeSource() + .getLocation() + .getPath()) + .getName()); + } + + File outFile = new File(dataFolder, resourcePath); + int lastIndex = resourcePath.lastIndexOf('/'); + File outDir = new File(dataFolder, resourcePath.substring(0, Math.max(lastIndex, 0))); + + if (!outDir.exists()) { + outDir.mkdirs(); + } + + try { + if (!outFile.exists() || replace) { + OutputStream out = Files.newOutputStream(outFile.toPath()); + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + out.close(); + in.close(); + } else { + Utils.getLogger().warning("Could not save " + outFile.getName() + " to " + outFile + " because " + outFile.getName() + " already exists."); + } + } catch (IOException ex) { + throw new Exception("Could not save " + outFile.getName() + " to " + outFile, ex); + } + } + + public static Logger getLogger() { + return Logger.getGlobal(); + } + + @Nullable + public static InputStream getResource(@NotNull String filename, @NotNull Class clazz) { + //noinspection DuplicatedCode + try { + URL url = clazz.getClassLoader().getResource(filename); + + if (url == null) { + return null; + } + + URLConnection connection = url.openConnection(); + connection.setUseCaches(false); + return connection.getInputStream(); + } catch (IOException ex) { + return null; + } + } + + public static void debug(final String name, String version, Logger logger, final Map map, String @NotNull ... messages) { + for (final String msg : messages) { + printMsg(Utils.placeHolder("[" + name + "] [Debug] [Version: " + version + "] " + msg, map, true), logger); + } + } + + public static void debug(final String name, String version, final Logger logger, String... messages) { + debug(name, version, logger, new HashObjectMap<>(), messages); + } + + public static void debug(final String name, String version, final Logger logger, @NotNull List messages) { + debug(name, version, logger, new HashObjectMap<>(), messages.toArray(new String[0])); + } + + public static void printMsg(final String input, Logger logger) { + logger.info(input); + } + + + /** + * Converts the given double `input` to a string representation. + * If the input value has no decimal part, it returns the integer part as a string. + * Otherwise, it returns the full double value as a string. + * + * @param input The double value to be converted. + * @return The string representation of the given double value. + */ + @Contract(pure = true) + public static @NotNull String convertDoubleInt(final double input) { + return input % 1d == 0 ? String.valueOf((int) input) : String.valueOf(input); + } + + /** + * Splits the given `value` of type BigDecimal into a list of BigDecimal values. + * Each element in the list represents a portion of the original value that does not exceed Double.MAX_VALUE. + * + * @param value The BigDecimal value to split. + * @return A list of BigDecimal values representing portions of the original value. + */ + @NotNull + public static List splitBigDecimal(@NotNull BigDecimal value) { + List splitValues = new ArrayList<>(); + + if (value.compareTo(BigDecimal.valueOf(Double.MAX_VALUE)) <= 0) { + splitValues.add(value); + } else { + BigDecimal maxValue = BigDecimal.valueOf(Double.MAX_VALUE); + BigDecimal remainingValue = value; + + while (remainingValue.compareTo(maxValue) > 0) { + splitValues.add(maxValue); + remainingValue = remainingValue.subtract(maxValue); + } + + if (remainingValue.compareTo(BigDecimal.ZERO) > 0) { + splitValues.addAll(splitBigDecimal(remainingValue)); + } + } + + return splitValues; + } + + /** + * Formats the given `number` into a human-readable string representation of money using the US locale. + * + * @param number The number to be formatted as money. + * @return The formatted money string. + */ + @NotNull + public static String formatMoney(Number number) { + return formatMoney(number, java.util.Locale.US); + } + + /** + * Formats the given `amount` into a human-readable string representation of money using the specified `locale`. + * + * @param amount The number to be formatted as money. + * @param locale The locale to use for formatting the money string. + * @return The formatted money string. + */ + @NotNull + public static String formatMoney(Number amount, java.util.Locale locale) { + NumberFormat numberFormat = NumberFormat.getCurrencyInstance(locale); + return numberFormat.format(amount); + } + + public static @NotNull String formatNumber(Number number, String format) { + return formatNumber(number, java.util.Locale.US, format); + } + + public static @NotNull String formatNumber(Number number, java.util.Locale locale, String format) { + DecimalFormatSymbols symbols = new DecimalFormatSymbols(locale); + symbols.setGroupingSeparator(' '); + + DecimalFormat decimalFormat = new DecimalFormat(format, symbols); + return decimalFormat.format(number); + } + + /** + * Generates a deterministic UUID from a given seed using the SHA-256 hash function. + * + * @param seed the input seed used to generate the UUID + * @return a UUID generated from the seed + */ + @Contract("_ -> new") + public static @NotNull UUID generateUUID(@NotNull String seed) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update(seed.getBytes(StandardCharsets.UTF_8)); + byte[] hash = md.digest(); + long msb = 0; + long lsb = 0; + for (int i = 0; i < 8; i++) + msb = (msb << 8) | (hash[i] & 0xff); + for (int i = 8; i < 16; i++) + lsb = (lsb << 8) | (hash[i] & 0xff); + return new UUID(msb, lsb); + } catch (NoSuchAlgorithmException exception) { + throw new RuntimeException(exception); + } + } + + /** + * Splits the given `value` of type BigInteger into a list of BigInteger values. + * Each element in the list represents a portion of the original value that does not exceed Integer.MAX_VALUE. + * + * @param value The BigInteger value to split. + * @return A list of BigInteger values representing portions of the original value. + */ + public static @NotNull List splitBigInteger(@NotNull BigInteger value) { + List splitValues = new ArrayList<>(); + + if (value.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) <= 0) { + splitValues.add(value); + } else { + BigInteger maxValue = BigInteger.valueOf(Integer.MAX_VALUE); + BigInteger remainingValue = value; + + while (remainingValue.compareTo(maxValue) > 0) { + splitValues.add(maxValue); + remainingValue = remainingValue.subtract(maxValue); + } + + if (remainingValue.compareTo(BigInteger.ZERO) > 0) { + splitValues.addAll(splitBigInteger(remainingValue)); + } + } + + return splitValues; + } + + /** + * Splits the given `value` of type BigInteger into a list of BigInteger values. + * Each element in the list represents a portion of the original value that does not exceed the limit. + * + * @param value The BigInteger value to split. + * @param limit The limit to split the value into. + * @return A list of BigInteger values representing portions of the original value. + */ + public static @NotNull List splitBigIntegerTo(@NotNull BigInteger value, int limit) { + List splitValues = new ArrayList<>(); + + if (value.compareTo(BigInteger.valueOf(limit)) <= 0) { + splitValues.add(value); + } else { + BigInteger maxValue = BigInteger.valueOf(limit); + BigInteger remainingValue = value; + + while (remainingValue.compareTo(maxValue) > 0) { + splitValues.add(maxValue); + remainingValue = remainingValue.subtract(maxValue); + } + + if (remainingValue.compareTo(BigInteger.ZERO) > 0) { + splitValues.addAll(splitBigIntegerTo(remainingValue, limit)); + } + } + + return splitValues; + } + + /** + * Returns the floor of the given double value. + * + * @param num The double value to get the floor of. + * @return The floor of the given double value. + */ + public static int floor(double num) { + int floor = (int) num; + return (double) floor == num ? floor : floor - (int) (Double.doubleToRawLongBits(num) >>> 63); + } + + public static boolean hasClass(String className) { + try { + Class.forName(className); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + public static final class Assertions { + + /** + * Throw IllegalArgumentException if the value is null. + * + * @param name the parameter name + * @param value the value that should not be null + * @param the value type + * @return the value + * @throws IllegalArgumentException if value is null + */ + @Contract(value = "_, null -> fail; _, !null -> param2", pure = true) + public static @NotNull T notNull(final String name, final T value) { + if (value == null) { + throw new IllegalArgumentException(name + " can not be null"); + } + return value; + } + + /** + * Throw IllegalStateException if the condition if false. + * + * @param name the name of the state that is being checked + * @param condition the condition about the parameter to check + * @throws IllegalStateException if the condition is false + */ + public static void isTrue(final String name, final boolean condition) { + if (!condition) { + throw new IllegalStateException("state should be: " + name); + } + } + + /** + * Throw IllegalArgumentException if the condition if false. + * + * @param name the name of the state that is being checked + * @param condition the condition about the parameter to check + * @throws IllegalArgumentException if the condition is false + */ + public static void isTrueArgument(final String name, final boolean condition) { + if (!condition) { + throw new IllegalArgumentException("state should be: " + name); + } + } + + /** + * Throw IllegalArgumentException if the condition if false, otherwise return the value. This is useful when arguments must be checked + * within an expression, as when using {@code this} to call another constructor, which must be the first line of the calling + * constructor. + * + * @param the value type + * @param name the name of the state that is being checked + * @param value the value of the argument + * @param condition the condition about the parameter to check + * @return the value + * @throws IllegalArgumentException if the condition is false + */ + public static T isTrueArgument(final String name, final T value, final boolean condition) { + if (!condition) { + throw new IllegalArgumentException("state should be: " + name); + } + return value; + } + + /** + * Cast an object to the given class and return it, or throw IllegalArgumentException if it's not assignable to that class. + * + * @param clazz the class to cast to + * @param value the value to cast + * @param errorMessage the error message to include in the exception + * @param the Class type + * @return value cast to clazz + * @throws IllegalArgumentException if value is not assignable to clazz + */ + public static @NotNull T convertToType(final @NotNull Class clazz, final @NotNull Object value, final String errorMessage) { + if (!clazz.isAssignableFrom(value.getClass())) { + throw new IllegalArgumentException(errorMessage); + } + return (T) value; + } + + public static void notEmpty(Object object, String message) { + if (object instanceof Collection) { + if (((Collection) object).isEmpty()) { + throw new IllegalArgumentException(message); + } + } else if (object instanceof Map) { + if (((Map) object).isEmpty()) { + throw new IllegalArgumentException(message); + } + } else if (object instanceof String) { + if (((String) object).isEmpty()) { + throw new IllegalArgumentException(message); + } + } else + throw new IllegalArgumentException("Object must be a Collection, Map or String"); + } + + private Assertions() { + } + } + + public static class Reflection { + + private static volatile Object theUnsafe; + private static final ObjectMap, Class> classClassMap = new HashObjectMap<>(); + + static { + try { + synchronized (Reflection.class) { + if (theUnsafe == null) { + Class unsafeClass = Class.forName("sun.misc.Unsafe"); + Field theUnsafeField = unsafeClass.getDeclaredField("theUnsafe"); + theUnsafeField.setAccessible(true); + theUnsafe = theUnsafeField.get(null); + } + classClassMap + .append(Integer.class, Integer.TYPE) + .append(Long.class, Long.TYPE) + .append(Double.class, Double.TYPE) + .append(Float.class, Float.TYPE) + .append(Boolean.class, Boolean.TYPE) + .append(Character.class, Character.TYPE) + .append(Byte.class, Byte.TYPE) + .append(Short.class, Short.TYPE); + } + } catch (Exception e) { + theUnsafe = null; + } + } + + public static boolean isUnsafeAvailable() { + return theUnsafe != null; + } + + /** + * Returns the {@link Class} object associated with the class or + * interface with the given string name, using the given class loader. + * Given the fully qualified name for a class or interface (in the same + * format returned by {@link Class#getName}) this method attempts to + * locate, load, and link the class or interface. The specified class + * loader is used to load the class or interface. If the parameter + * {@code loader} is null, the class is loaded through the bootstrap + * class loader. The class is initialized only if it has + * not been initialized earlier. + * + * @param name fully qualified name of the desired class + * @param loader class loader from which the class must be loaded + * @return class object representing the desired class + * @throws LinkageError if the linkage fails + * @throws ExceptionInInitializerError if the initialization provoked + * by this method fails + * @throws ReflectionException if the class cannot be located by + * the specified class loader + * @see Class#forName(String) + * @see ClassLoader + */ + public static @NotNull Class getClass(String name, ClassLoader loader) throws ReflectionException { + try { + return Class.forName(name, true, loader); + } catch (ClassNotFoundException exception) { + throw new ReflectionException("Class not found " + name, exception); + } + } + + /** + * Returns the {@link Class} object associated with the class or + * interface with the given string name, using the given class loader. + * Given the fully qualified name for a class or interface (in the same + * format returned by {@link Class#getName}) this method attempts to + * locate, load, and link the class or interface. The specified class + * loader is used to load the class or interface. If the parameter + * {@code loader} is null, the class is loaded through the bootstrap + * class loader. The class is initialized only if it has + * not been initialized earlier. + * + * @param name fully qualified name of the desired class + * @param loader class loader from which the class must be loaded + * @return class object representing the desired class, + * If a {@link ClassNotFoundException} occurs the return value is {@link Optional#empty()} + * @see Class#forName(String) + * @see ClassLoader + */ + public static Optional> optionalClass(String name, ClassLoader loader) { + try { + return Optional.of(Class.forName(name, true, loader)); + } catch (ClassNotFoundException e) { + return Optional.empty(); + } + } + + public static @NotNull Object enumValueOf(@NotNull Class enumClass, String ordinal) { + return Enum.valueOf(enumClass.asSubclass(Enum.class), ordinal); + } + + public static Object enumValueOf(Class enumClass, String constant, int fallbackOrdinal) throws NotFoundException { + try { + return enumValueOf(enumClass, constant); + } catch (IllegalArgumentException e) { + Object[] constants = enumClass.getEnumConstants(); + if (constants.length > fallbackOrdinal) { + return constants[fallbackOrdinal]; + } + throw new NotFoundException("Enum constant not found " + constant, e); + } + } + + public static @NotNull Enum getEnum(@NotNull Class clazz, String constant) throws NotFoundException { + Enum[] enumConstants = (Enum[]) clazz.getEnumConstants(); + + for (Enum e : enumConstants) + if (e.name().equalsIgnoreCase(constant)) + return e; + + throw new NotFoundException("Enum constant not found " + constant); + } + + public static Enum getEnum(@NotNull Class clazz, int ordinal) throws NotFoundException { + try { + return (Enum) clazz.getEnumConstants()[ordinal]; + } catch (ArrayIndexOutOfBoundsException e) { + throw new NotFoundException("Enum constant not found " + ordinal); + } + } + + public static @NotNull Enum getEnum(Class clazz, String enumName, String constant) throws NotFoundException, ReflectionException { + return getEnum(getSubClass(clazz, enumName), constant); + } + + public static Enum getEnum(Class clazz, String enumName, int ordinal) throws NotFoundException, ReflectionException { + return getEnum(getSubClass(clazz, enumName), ordinal); + } + + public static @NotNull Class getSubClass(@NotNull Class clazz, String className) throws ReflectionException { + for (Class subClass : clazz.getDeclaredClasses()) { + if (subClass.getSimpleName().equals(className)) + return subClass; + } + + for (Class subClass : clazz.getClasses()) { + if (subClass.getSimpleName().equals(className)) + return subClass; + } + + throw new ReflectionException("Sub class " + className + " of " + clazz.getSimpleName() + " not found!"); + } + + public static @NotNull Object invokeConstructor(Class clazz, Class[] args, Object... initArgs) throws ReflectionException { + try { + return getConstructor(clazz, args).newInstance(initArgs); + } catch (Exception e) { + throw new ReflectionException(e); + } + } + + public static @NotNull Object invokeConstructor(Class clazz, Object... initArgs) throws ReflectionException { + try { + return getConstructorByArgs(clazz, initArgs).newInstance(initArgs); + } catch (Exception e) { + throw new ReflectionException(e); + } + } + + public static @NotNull Constructor getConstructor(@NotNull Class clazz, Class... args) throws NoSuchMethodException { + Constructor c = clazz.getConstructor(args); + c.setAccessible(true); + + return c; + } + + public static @NotNull Constructor getConstructorByArgs(@NotNull Class clazz, Object... args) throws ReflectionException { + for (Constructor constructor : clazz.getConstructors()) { + if (constructor.getParameterTypes().length != args.length) + continue; + + int i = 0; + for (Class parameter : constructor.getParameterTypes()) { + if (!isAssignable(parameter, args[i])) + break; + + i++; + } + + if (i == args.length) + return constructor; + } + + throw new ReflectionException("Could not find constructor with args " + Arrays.stream(args).map(Object::getClass).map(Class::getSimpleName).collect(Collectors.joining(", ")) + " in " + clazz.getSimpleName()); + } + + public static boolean isAssignable(Class clazz, Object obj) { + clazz = convertToPrimitive(clazz); + + return clazz.isInstance(obj) || clazz == convertToPrimitive(obj.getClass()); + } + + public static Class convertToPrimitive(Class clazz) { + return classClassMap.getOrDefault(clazz, clazz); + } + + public static Object getFieldByType(Object obj, String typeName) throws ReflectionException { + return getFieldByType(obj, obj.getClass(), typeName); + } + + public static Object getFieldByType(Object obj, Class superClass, String typeName) throws ReflectionException { + return getFieldByTypeList(obj, superClass, typeName).get(0); + } + + public static @NotNull List getFieldByTypeList(Object obj, String typeName) throws ReflectionException { + return getFieldByTypeList(obj, obj.getClass(), typeName); + } + + public static @NotNull List getFieldByTypeList(Object obj, Class superClass, String typeName) throws ReflectionException { + List fields = new ArrayList<>(); + + try { + for (Field f : superClass.getDeclaredFields()) { + if (f.getType().getSimpleName().equalsIgnoreCase(typeName)) { + f.setAccessible(true); + + fields.add(f.get(obj)); + } + } + + if (superClass.getSuperclass() != null) { + fields.addAll(getFieldByTypeList(obj, superClass.getSuperclass(), typeName)); + } + + if (fields.isEmpty() && obj.getClass() == superClass) { + throw new ReflectionException("Could not find field of type " + typeName + " in " + obj.getClass().getSimpleName()); + } else { + return fields; + } + } catch (Exception e) { + throw new ReflectionException(e); + } + } + + /** + * Returns the inner-{@link Class} object associated with the class or + * interface with the given parent class, using the given class predicate. + * + * @param parentClass the parent class + * @param classPredicate the class predicate to test for the inner class + * @return class object representing the desired inner-class, + * @throws ReflectionException if the inner-class does not exist + */ + static Class innerClass(@NotNull Class parentClass, Predicate> classPredicate) throws ReflectionException { + for (Class innerClass : parentClass.getDeclaredClasses()) { + if (classPredicate.test(innerClass)) { + return innerClass; + } + } + throw new ReflectionException("No class in " + parentClass.getCanonicalName() + " matches the predicate."); + } + + public static @NotNull Utils.Reflection.Constructor0 findConstructor(Class clazz, MethodHandles.@NotNull Lookup lookup) throws NoSuchMethodException, IllegalAccessException { + if (isUnsafeAvailable()) { + MethodType allocateMethodType = MethodType.methodType(Object.class, Class.class); + MethodHandle allocateMethod = lookup.findVirtual(theUnsafe.getClass(), "allocateInstance", allocateMethodType); + return () -> allocateMethod.invoke(theUnsafe, clazz); + } + try { + MethodHandle constructor = lookup.findConstructor(clazz, MethodType.methodType(void.class)); + return constructor::invoke; + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new UnsupportedOperationException(); + } + } + + /** + * Returns the value of the field represented by this {@code Field}, on + * the specified object. The value is automatically wrapped in an + * object if it has a primitive type. + * + *

The underlying field's value is obtained as follows: + * + *

If the underlying field is a static field, the {@code object} argument + * is ignored; it may be null. + * + *

Otherwise, the underlying field is an instance field. If the + * specified {@code object} argument is null, the method throws a + * {@code NullPointerException}. If the specified object is not an + * instance of the class or interface declaring the underlying + * field, the method throws an {@code IllegalArgumentException}. + * + *

If this {@code Field} object is enforcing Java language access control, and + * the underlying field is inaccessible, the method throws an + * {@code IllegalAccessException}. + * If the underlying field is static, the class that declared the + * field is initialized if it has not already been initialized. + * + *

Otherwise, the value is retrieved from the underlying instance + * or static field. If the field has a primitive type, the value + * is wrapped in an object before being returned, otherwise it is + * returned as is. + * + *

If the field is hidden in the type of {@code object}, + * the field's value is obtained according to the preceding rules. + * + * @param clazz class that contains the field + * @param object object from which the represented field's value is + * to be extracted + * @param name field name + * @return the value of the represented field in object + * {@code object}; primitive values are wrapped in an appropriate + * object before being returned + * @throws IllegalAccessException if this {@code Field} object + * is enforcing Java language access control and the underlying + * field is inaccessible. + * @throws IllegalArgumentException if the specified object is not an + * instance of the class or interface declaring the underlying + * field (or a subclass or implementor thereof). + * @throws InvocationTargetException if the underlying method + * throws an exception. + * @throws NullPointerException if the specified object is null + * and the field is an instance field. + * @throws ExceptionInInitializerError if the initialization provoked + * by this method fails. + */ + public static Object fetchDeclaredField(final Class clazz, final Object object, final String name) throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + if (isUnsafeAvailable() & object != null) { + Field field = clazz.getDeclaredField(name); + long offset = (long) fetchDeclaredMethodAndInvoke(theUnsafe.getClass(), "objectFieldOffset", theUnsafe, new Object[]{field}, new Class[]{Field.class}); + return fetchDeclaredMethodAndInvoke(theUnsafe.getClass(), "getObject", theUnsafe, new Object[]{object, offset}, new Class[]{Object.class, long.class}); + } + try { + Field field = clazz.getDeclaredField(name); + field.setAccessible(true); + return field.get(object); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new UnsupportedOperationException(); + } + } + + /** + * Sets the field represented by this {@code Field} object on the + * specified object argument to the specified new value. The new + * value is automatically unwrapped if the underlying field has a + * primitive type. + * + *

The operation proceeds as follows: + * + *

If the underlying field is static, the {@code object} argument is + * ignored; it may be null. + * + *

Otherwise the underlying field is an instance field. If the + * specified object argument is null, the method throws a + * {@code NullPointerException}. If the specified object argument is not + * an instance of the class or interface declaring the underlying + * field, the method throws an {@code IllegalArgumentException}. + * + *

If this {@code Field} object is enforcing Java language access control, and + * the underlying field is inaccessible, the method throws an + * {@code IllegalAccessException}. + * + *

If the underlying field is final, the method throws an + * {@code IllegalAccessException} unless {@code setAccessible(true)} + * has succeeded for this {@code Field} object + * and the field is non-static. Setting a final field in this way + * is meaningful only during deserialization or reconstruction of + * instances of classes with blank final fields, before they are + * made available for access by other parts of a program. Use in + * any other context may have unpredictable effects, including cases + * in which other parts of a program continue to use the original + * value of this field. + * + *

If the underlying field is of a primitive type, an unwrapping + * conversion is attempted to convert the new value to a value of + * a primitive type. If this attempt fails, the method throws an + * {@code IllegalArgumentException}. + * + *

If, after possible unwrapping, the new value cannot be + * converted to the type of the underlying field by an identity or + * widening conversion, the method throws an + * {@code IllegalArgumentException}. + * + *

If the underlying field is static, the class that declared the + * field is initialized if it has not already been initialized. + * + *

The field is set to the possibly unwrapped and widened new value. + * + *

If the field is hidden in the type of {@code object}, + * the field's value is set according to the preceding rules. + * + * @param clazz class that contains the specific field + * @param object object whose field should be modified + * @param name field name + * @param value new value for the field of {@code object} + * being modified + * @throws IllegalAccessException if this {@code Field} object + * is enforcing Java language access control and the underlying + * field is either inaccessible or final. + * @throws IllegalArgumentException if the specified object is not an + * instance of the class or interface declaring the underlying + * field (or a subclass or implementor thereof), + * or if an unwrapping conversion fails. + * @throws InvocationTargetException if the underlying method + * throws an exception. + * @throws NullPointerException if the specified object is null + * and the field is an instance field. + * @throws ExceptionInInitializerError if the initialization provoked + * by this method fails. + */ + public static void setDeclaredFieldValue(final Class clazz, final Object object, final String name, Object value) throws NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IllegalAccessException { + if (isUnsafeAvailable()) { + Field field = clazz.getDeclaredField(name); + long offset = (long) fetchDeclaredMethodAndInvoke(theUnsafe.getClass(), "objectFieldOffset", theUnsafe, new Object[]{field}, new Class[]{Field.class}); + fetchDeclaredMethodAndInvoke(theUnsafe.getClass(), "putObject", theUnsafe, new Object[]{object, offset, value}, new Class[]{Object.class, long.class, Object.class}); + } + + try { + Field field = clazz.getDeclaredField(name); + field.setAccessible(true); + field.set(object, value); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new UnsupportedOperationException(); + } + } + + /** + * Returns the value of the field represented by this {@code Field}, on + * the specified object. The value is automatically wrapped in an + * object if it has a primitive type. + * + *

The underlying field's value is obtained as follows: + * + *

If the underlying field is a static field, the {@code object} argument + * is ignored; it may be null. + * + *

Otherwise, the underlying field is an instance field. If the + * specified {@code object} argument is null, the method throws a + * {@code NullPointerException}. If the specified object is not an + * instance of the class or interface declaring the underlying + * field, the method throws an {@code IllegalArgumentException}. + * + *

If this {@code Field} object is enforcing Java language access control, and + * the underlying field is inaccessible, the method throws an + * {@code IllegalAccessException}. + * If the underlying field is static, the class that declared the + * field is initialized if it has not already been initialized. + * + *

Otherwise, the value is retrieved from the underlying instance + * or static field. If the field has a primitive type, the value + * is wrapped in an object before being returned, otherwise it is + * returned as is. + * + *

If the field is hidden in the type of {@code object}, + * the field's value is obtained according to the preceding rules. + * + * @param clazz class that contains the field + * @param object object from which the represented field's value is + * to be extracted + * @param name field name + * @return the value of the represented field in object + * {@code object}; primitive values are wrapped in an appropriate + * object before being returned + * @throws IllegalAccessException if this {@code Field} object + * is enforcing Java language access control and the underlying + * field is inaccessible. + * @throws IllegalArgumentException if the specified object is not an + * instance of the class or interface declaring the underlying + * field (or a subclass or implementor thereof). + * @throws InvocationTargetException if the underlying method + * throws an exception. + * @throws NullPointerException if the specified object is null + * and the field is an instance field. + * @throws ExceptionInInitializerError if the initialization provoked + * by this method fails. + */ + public static Object fetchField(final Class clazz, final Object object, final String name) throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + if (isUnsafeAvailable() & object != null) { + Field field = clazz.getField(name); + long offset = (long) fetchDeclaredMethodAndInvoke(theUnsafe.getClass(), "objectFieldOffset", theUnsafe, new Object[]{field}, new Class[]{Field.class}); + return fetchDeclaredMethodAndInvoke(theUnsafe.getClass(), "getObject", theUnsafe, new Object[]{object, offset}, new Class[]{Object.class, long.class}); + } + try { + Field field = clazz.getField(name); + field.setAccessible(true); + return field.get(object); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new UnsupportedOperationException(); + } + } + + /** + * Sets the field represented by this {@code Field} object on the + * specified object argument to the specified new value. The new + * value is automatically unwrapped if the underlying field has a + * primitive type. + * + *

The operation proceeds as follows: + * + *

If the underlying field is static, the {@code object} argument is + * ignored; it may be null. + * + *

Otherwise the underlying field is an instance field. If the + * specified object argument is null, the method throws a + * {@code NullPointerException}. If the specified object argument is not + * an instance of the class or interface declaring the underlying + * field, the method throws an {@code IllegalArgumentException}. + * + *

If this {@code Field} object is enforcing Java language access control, and + * the underlying field is inaccessible, the method throws an + * {@code IllegalAccessException}. + * + *

If the underlying field is final, the method throws an + * {@code IllegalAccessException} unless {@code setAccessible(true)} + * has succeeded for this {@code Field} object + * and the field is non-static. Setting a final field in this way + * is meaningful only during deserialization or reconstruction of + * instances of classes with blank final fields, before they are + * made available for access by other parts of a program. Use in + * any other context may have unpredictable effects, including cases + * in which other parts of a program continue to use the original + * value of this field. + * + *

If the underlying field is of a primitive type, an unwrapping + * conversion is attempted to convert the new value to a value of + * a primitive type. If this attempt fails, the method throws an + * {@code IllegalArgumentException}. + * + *

If, after possible unwrapping, the new value cannot be + * converted to the type of the underlying field by an identity or + * widening conversion, the method throws an + * {@code IllegalArgumentException}. + * + *

If the underlying field is static, the class that declared the + * field is initialized if it has not already been initialized. + * + *

The field is set to the possibly unwrapped and widened new value. + * + *

If the field is hidden in the type of {@code object}, + * the field's value is set according to the preceding rules. + * + * @param clazz class that contains the specific field + * @param object object whose field should be modified + * @param name field name + * @param value new value for the field of {@code object} + * being modified + * @throws IllegalAccessException if this {@code Field} object + * is enforcing Java language access control and the underlying + * field is either inaccessible or final. + * @throws IllegalArgumentException if the specified object is not an + * instance of the class or interface declaring the underlying + * field (or a subclass or implementor thereof), + * or if an unwrapping conversion fails. + * @throws InvocationTargetException if the underlying method + * throws an exception. + * @throws NullPointerException if the specified object is null + * and the field is an instance field. + * @throws ExceptionInInitializerError if the initialization provoked + * by this method fails. + */ + public static void setFieldValue(final Class clazz, final Object object, final String name, Object value) throws NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IllegalAccessException { + if (isUnsafeAvailable()) { + Field field = clazz.getField(name); + long offset = (long) fetchDeclaredMethodAndInvoke(theUnsafe.getClass(), "objectFieldOffset", theUnsafe, new Object[]{field}, new Class[]{Field.class}); + fetchDeclaredMethodAndInvoke(theUnsafe.getClass(), "putObject", theUnsafe, new Object[]{object, offset, value}, new Class[]{Object.class, long.class, Object.class}); + } + + try { + Field field = clazz.getField(name); + field.setAccessible(true); + field.set(object, value); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new UnsupportedOperationException(); + } + } + + /** + * Returns a {@code Method} object that reflects the specified + * declared method of the class or interface represented by this + * {@code Class} object. The {@code name} parameter is a + * {@code String} that specifies the simple name of the desired + * method, and the {@code parameterTypes} parameter is an array of + * {@code Class} objects that identify the method's formal parameter + * types, in declared order. If more than one method with the same + * parameter types is declared in a class, and one of these methods has a + * return type that is more specific than any of the others, that method is + * returned; otherwise one of the methods is chosen arbitrarily. If the + * name is "<init>"or "<clinit>" a {@code NoSuchMethodException} + * is raised. + * + *

If this {@code Class} object represents an array type, then this + * method does not find the {@code clone()} method. + * + * @param clazz the class that contains the method + * @param name the name of the method + * @param parameterTypes the parameter array + * @return the {@code Method} object for the method of this class + * matching the specified name and parameters + * @throws NoSuchMethodException if a matching method is not found. + * @throws NullPointerException if {@code name} is {@code null} + */ + public static @NotNull Method fetchDeclaredMethod(final @NotNull Class clazz, final String name, Class... parameterTypes) throws NoSuchMethodException { + return clazz.getDeclaredMethod(name, parameterTypes); + } + + /** + * Check {@link Reflection#fetchDeclaredMethod(Class, String, Class...)} for + * the fetch method. + * Invokes the underlying method represented by this {@code Method} + * object, on the specified object with the specified parameters. + * Individual parameters are automatically unwrapped to match + * primitive formal parameters, and both primitive and reference + * parameters are subject to method invocation conversions as + * necessary. + * + *

If the underlying method is static, then the specified {@code obj} + * argument is ignored. It may be null. + * + *

If the number of formal parameters required by the underlying method is + * 0, the supplied {@code args} array may be of length 0 or null. + * + *

If the underlying method is an instance method, it is invoked + * using dynamic method lookup as documented in The Java Language + * Specification, Second Edition, section 15.12.4.4; in particular, + * overriding based on the runtime type of the target object will occur. + * + *

If the underlying method is static, the class that declared + * the method is initialized if it has not already been initialized. + * + *

If the method completes normally, the value it returns is + * returned to the caller of invoke; if the value has a primitive + * type, it is first appropriately wrapped in an object. However, + * if the value has the type of array of a primitive type, the + * elements of the array are not wrapped in objects; in + * other words, an array of primitive type is returned. If the + * underlying method return type is void, the invocation returns + * null. + * + * @param clazz the class that contains the method + * @param name the name of the method + * @param parameterTypes the parameter array + * @param obj the object the underlying method is invoked from + * @param arguments the arguments used for the method call + * @return the result of dispatching the method represented by + * this object on {@code obj} with parameters + * {@code args} + * @throws IllegalAccessException if this {@code Method} object + * is enforcing Java language access control and the underlying + * method is inaccessible. + * @throws IllegalArgumentException if the method is an + * instance method and the specified object argument + * is not an instance of the class or interface + * declaring the underlying method (or of a subclass + * or implementor thereof); if the number of actual + * and formal parameters differ; if an unwrapping + * conversion for primitive arguments fails; or if, + * after possible unwrapping, a parameter value + * cannot be converted to the corresponding formal + * parameter type by a method invocation conversion. + * @throws InvocationTargetException if the underlying method + * throws an exception. + * @throws NullPointerException if the specified object is null + * and the method is an instance method. + * @throws ExceptionInInitializerError if the initialization + * provoked by this method fails. + */ + public static Object fetchDeclaredMethodAndInvoke(final Class clazz, final String name, Object obj, Object[] arguments, Class[] parameterTypes) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Method method = fetchDeclaredMethod(clazz, name, parameterTypes); + method.setAccessible(true); + return method.invoke(obj, arguments); + } + + /** + * Returns a {@code Method} object that reflects the specified + * method of the class or interface represented by this + * {@code Class} object. The {@code name} parameter is a + * {@code String} that specifies the simple name of the desired + * method, and the {@code parameterTypes} parameter is an array of + * {@code Class} objects that identify the method's formal parameter + * types, in declared order. If more than one method with the same + * parameter types is declared in a class, and one of these methods has a + * return type that is more specific than any of the others, that method is + * returned; otherwise one of the methods is chosen arbitrarily. If the + * name is "<init>"or "<clinit>" a {@code NoSuchMethodException} + * is raised. + * + *

If this {@code Class} object represents an array type, then this + * method does not find the {@code clone()} method. + * + * @param clazz the class that contains the method + * @param name the name of the method + * @param parameterTypes the parameter array + * @return the {@code Method} object for the method of this class + * matching the specified name and parameters + * @throws NoSuchMethodException if a matching method is not found. + * @throws NullPointerException if {@code name} is {@code null} + */ + public static @NotNull Method fetchMethod(final @NotNull Class clazz, final String name, Class... parameterTypes) throws NoSuchMethodException { + return clazz.getMethod(name, parameterTypes); + } + + /** + * Check {@link Reflection#fetchMethod(Class, String, Class...)} for + * the fetch method. + * Invokes the underlying method represented by this {@code Method} + * object, on the specified object with the specified parameters. + * Individual parameters are automatically unwrapped to match + * primitive formal parameters, and both primitive and reference + * parameters are subject to method invocation conversions as + * necessary. + * + *

If the underlying method is static, then the specified {@code obj} + * argument is ignored. It may be null. + * + *

If the number of formal parameters required by the underlying method is + * 0, the supplied {@code args} array may be of length 0 or null. + * + *

If the underlying method is an instance method, it is invoked + * using dynamic method lookup as documented in The Java Language + * Specification, Second Edition, section 15.12.4.4; in particular, + * overriding based on the runtime type of the target object will occur. + * + *

If the underlying method is static, the class that declared + * the method is initialized if it has not already been initialized. + * + *

If the method completes normally, the value it returns is + * returned to the caller of invoke; if the value has a primitive + * type, it is first appropriately wrapped in an object. However, + * if the value has the type of array of a primitive type, the + * elements of the array are not wrapped in objects; in + * other words, an array of primitive type is returned. If the + * underlying method return type is void, the invocation returns + * null. + * + * @param clazz the class that contains the method + * @param name the name of the method + * @param parameterTypes the parameter array + * @param obj the object the underlying method is invoked from + * @param arguments the arguments used for the method call + * @return the result of dispatching the method represented by + * this object on {@code obj} with parameters + * {@code args} + * @throws IllegalAccessException if this {@code Method} object + * is enforcing Java language access control and the underlying + * method is inaccessible. + * @throws IllegalArgumentException if the method is an + * instance method and the specified object argument + * is not an instance of the class or interface + * declaring the underlying method (or of a subclass + * or implementor thereof); if the number of actual + * and formal parameters differ; if an unwrapping + * conversion for primitive arguments fails; or if, + * after possible unwrapping, a parameter value + * cannot be converted to the corresponding formal + * parameter type by a method invocation conversion. + * @throws InvocationTargetException if the underlying method + * throws an exception. + * @throws NullPointerException if the specified object is null + * and the method is an instance method. + * @throws ExceptionInInitializerError if the initialization + * provoked by this method fails. + */ + public static Object fetchMethodAndInvoke(final Class clazz, final String name, Object obj, Object[] arguments, Class[] parameterTypes) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Method method = fetchMethod(clazz, name, parameterTypes); + method.setAccessible(true); + return method.invoke(obj, arguments); + } + + @FunctionalInterface + interface Constructor0 { + Object invoke() throws Throwable; + } + } + + public static class Cooldown { + private static final ObjectMap cooldownManagerObjectMap = ObjectMap.newHashObjectMap(); + private long start; + private final int timeInSeconds; + private final UUID id; + private final String cooldownName; + + public Cooldown(UUID id, String cooldownName, int timeInSeconds) { + this.id = id; + this.cooldownName = cooldownName; + this.timeInSeconds = timeInSeconds; + } + + public static boolean isInCooldown(UUID id, String cooldownName) { + if (Cooldown.getTimeLeft(id, cooldownName) >= 1) { + return true; + } + Cooldown.stop(id, cooldownName); + return false; + } + + private static void stop(UUID id, String cooldownName) { + cooldownManagerObjectMap.remove(id + cooldownName); + } + + private static Cooldown getCooldown(@NotNull UUID id, String cooldownName) { + return cooldownManagerObjectMap.get(id + cooldownName); + } + + public static int getTimeLeft(UUID id, String cooldownName) { + Cooldown cooldown = Cooldown.getCooldown(id, cooldownName); + int f = -1; + if (cooldown != null) { + long now = System.currentTimeMillis(); + long cooldownTime = cooldown.start; + int r = (int) (now - cooldownTime) / 1000; + f = (r - cooldown.timeInSeconds) * -1; + } + return f; + } + + public void start() { + this.start = System.currentTimeMillis(); + cooldownManagerObjectMap.put(this.id.toString() + this.cooldownName, this); + } + + public static @NotNull String getTimeLeft(int secondTime) { + TimeZone tz = Calendar.getInstance().getTimeZone(); + SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss"); + df.setTimeZone(tz); + return df.format(new Date(secondTime * 1000L)); + } + + public static ObjectMap getCooldowns() { + return cooldownManagerObjectMap; + } + + public static ObjectMap appendToCooldowns(ObjectMap cooldownObjectMap) { + return cooldownManagerObjectMap.append(cooldownObjectMap); + } + + } + + public static abstract class Callback { + + public abstract T onSuccess(); + + public abstract T onFailure(); + + public T onFailure(Throwable throwable) { + throw new RuntimeException(throwable); + } + } + + + /** + * The Request class provides methods for making HTTPS requests. + */ + public static class Request { + /** + * The address of the HTTP(S) endpoint. + */ + private String address; + + /** + * The bytes returned from the HTTP(S) response. + */ + private byte[] bytes; + + /** + * The HTTP status code returned by the endpoint. + */ + private int httpCode; + + /** + * The HTTPS URL connection used to make the request. + */ + private HttpsURLConnection httpsURLConnection; + + /** + * Sets the HTTP request method to "GET". + * + * @return This Request object. + * @throws ProtocolException If the request method could not be set. + */ + public Request getRequest() throws ProtocolException { + this.httpsURLConnection.setRequestMethod("GET"); + this.httpsURLConnection.setRequestProperty("User-Agent", "SkinOverlay"); + return this; + } + + /** + * Sets the HTTP request method to "POST" and sets several request properties. + * + * @return This Request object. + * @throws ProtocolException If the request method could not be set. + */ + public Request postRequest() throws ProtocolException { + this.httpsURLConnection.setRequestMethod("POST"); + this.httpsURLConnection.setRequestProperty("Connection", "Keep-Alive"); + this.httpsURLConnection.setRequestProperty("Cache-Control", "no-cache"); + this.httpsURLConnection.setRequestProperty("User-Agent", "SkinOverlay"); + this.httpsURLConnection.setDoOutput(true); + return this; + } + + /** + * Opens a connection to the specified HTTP(S) endpoint. + * + * @param address The address of the endpoint. + * @return This Request object. + * @throws IOException If a connection to the endpoint could not be established. + */ + public Request openConnection(String address) throws IOException { + this.address = address; + final URL url = new URL(address); + this.httpsURLConnection = (HttpsURLConnection) url.openConnection(); + return this; + } + + /** + * Sets a request property to the specified key-value pair. + * + * @param key The key for the property. + * @param value The value for the property. + * @return This Request object. + */ + public Request setRequestProperty(String key, String value) { + this.httpsURLConnection.setRequestProperty(key, value); + return this; + } + + /** + * Writes one or more strings to the request output stream. + * + * @param data The strings to write. + * @return This Request object. + * @throws IOException If an I/O error occurs. + */ + public Request writeToOutputStream(final String @NotNull ... data) throws IOException { + for (final String str : data) { + this.httpsURLConnection.getOutputStream().write(str.getBytes()); + } + return this; + } + + /** + * Writes one or more byte arrays to the request output stream. + * + * @param data The byte arrays to write. + * @return This Request object. + * @throws IOException If an I/O error occurs. + */ + public Request writeToOutputStream(final byte @NotNull []... data) throws IOException { + for (final byte[] bytes : data) { + this.httpsURLConnection.getOutputStream().write(bytes); + } + return this; + } + + /** + * Closes the request output stream. + * + * @return This Request object. + * @throws IOException If an I/O error occurs. + */ + public Request closeOutputStream() throws IOException { + this.httpsURLConnection.getOutputStream().close(); + return this; + } + + /** + * Finalizes the request by getting the HTTP response code and reading the response body bytes. + * + * @return The updated Request object. + * @throws IOException If an I/O error occurs while finalizing the request. + */ + public Request finalizeRequest() throws IOException { + this.httpCode = this.httpsURLConnection.getResponseCode(); + this.bytes = this.httpsURLConnection.getInputStream().readAllBytes(); + return this; + } + + /** + * Gets the HTTP response code of the request. + * + * @return The HTTP response code. + */ + public int getHttpCode() { + return this.httpCode; + } + + /** + * Gets the bytes of the response body of the request. + * + * @return The response body bytes. + */ + public byte[] getBytes() { + return this.bytes; + } + + /** + * Gets the address of the request. + * + * @return The request address. + */ + public String getAddress() { + return this.address; + } + + /** + * Gets the underlying HTTPS connection object. + * + * @return The HTTPS connection object. + */ + public HttpsURLConnection getHttpsURLConnection() { + return httpsURLConnection; + } + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/color/Color.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/color/Color.java new file mode 100644 index 00000000..69bf8c38 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/color/Color.java @@ -0,0 +1,188 @@ +package com.georgev22.skinoverlay.utilities.color; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Represents a color with support for hexadecimal, RGB, and various formatting tags. + */ +@SuppressWarnings("UnnecessaryUnicodeEscape") +public class Color { + + private final String colorCode; + private final int r; + private final int g; + private final int b; + private static java.awt.Color javaColor; + + /** + * Creates a {@link Color} instance from a hex string (with or without the # prefix). + * + * @param colorCode The hex color code. + * @return A new {@link Color} instance. + */ + @Contract("_ -> new") + public static @NotNull Color from(String colorCode) { + return new Color(colorCode); + } + + /** + * Creates a {@link Color} instance from RGB values. + * + * @param r Red component (0–255). + * @param g Green component (0–255). + * @param b Blue component (0–255). + * @return A new {@link Color} instance. + */ + public static @NotNull Color from(int r, int g, int b) { + javaColor = new java.awt.Color(r, g, b); + return from(Integer.toHexString(javaColor.getRGB()).substring(2)); + } + + /** + * Constructs a {@link Color} from a hex code. + * + * @param colorCode Hexadecimal color code (e.g., #ff5733 or ff5733). + */ + private Color(@NotNull String colorCode) { + this.colorCode = colorCode.replace("#", ""); + javaColor = new java.awt.Color(Integer.parseInt(this.colorCode, 16)); + this.r = javaColor.getRed(); + this.g = javaColor.getGreen(); + this.b = javaColor.getBlue(); + } + + /** + * @return The hexadecimal color code (without the # prefix). + */ + public String getColorCode() { + return this.colorCode; + } + + /** + * @return The red component of the color. + */ + public int getRed() { + return this.r; + } + + /** + * @return The green component of the color. + */ + public int getGreen() { + return this.g; + } + + /** + * @return The blue component of the color. + */ + public int getBlue() { + return this.b; + } + + /** + * @return A {@link java.awt.Color} representation of this color. + */ + public java.awt.Color getJavaColor() { + return javaColor; + } + + /** + * @return A darker version of this color. + */ + public @NotNull Color darker() { + return Color.from(javaColor.darker().getRed(), javaColor.darker().getGreen(), javaColor.darker().getBlue()); + } + + /** + * @return A brighter version of this color. + */ + public @NotNull Color brighter() { + return Color.from(javaColor.brighter().getRed(), javaColor.brighter().getGreen(), javaColor.brighter().getBlue()); + } + + /** + * Gets the color code formatted as a legacy Minecraft text color code or as the closest fallback. + * + * @param ver Whether to return the full §x-style hex format (Minecraft 1.16+). + * @param closest A fallback color code to return if ver is false. + * @return The formatted color code. + */ + public String getAppliedTag(boolean ver, String closest) { + return ver + ? "\u00A7x" + Arrays.stream(this.colorCode.split("")) + .map((paramString) -> "\u00A7" + paramString) + .collect(Collectors.joining()) + : closest; + } + + /** + * @return The color with a leading '#' (e.g., #ff5733). + */ + public String toHex() { + return "#" + this.colorCode; + } + + /** + * @return The color formatted in MiniMessage tag format (e.g., <color:#ff5733>). + */ + public String getMiniMessageTag() { + return ""; + } + + /** + * Wraps text in a MiniMessage color tag using this color. + * + * @param text The text to wrap. + * @return The colorized MiniMessage string. + */ + public String toMiniMessage(String text) { + return this.getMiniMessageTag() + text + ""; + } + + /** + * Calculates the RGB difference between two {@link Color} instances. + * + * @param color1 The first color. + * @param color2 The second color. + * @return The sum of absolute RGB differences. + */ + public static int difference(@NotNull Color color1, @NotNull Color color2) { + return Math.abs(color1.r - color2.r) + + Math.abs(color1.g - color2.g) + + Math.abs(color1.b - color2.b); + } + + /** + * @return A string representation using the Minecraft §x hex format. + */ + @Override + public String toString() { + return this.getAppliedTag(true, ""); + } + + /** + * Compares this color to another for equality based on RGB and hex code. + * + * @param o The object to compare. + * @return True if equal. + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Color color)) return false; + return r == color.r && g == color.g && b == color.b && Objects.equals(colorCode, color.colorCode); + } + + /** + * @return A hash code based on RGB and hex code. + */ + @Override + public int hashCode() { + return Objects.hash(colorCode, r, g, b); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/color/ColorCalculations.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/color/ColorCalculations.java new file mode 100644 index 00000000..cf6ead5f --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/color/ColorCalculations.java @@ -0,0 +1,32 @@ +package com.georgev22.skinoverlay.utilities.color; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + + +public class ColorCalculations { + public static @NotNull List getColorsInBetween(@NotNull Color color, @NotNull Color color2, int n) { + double n2 = (double) (color2.getRed() - color.getRed()) / (double) n; + double n3 = (double) (color2.getGreen() - color.getGreen()) / (double) n; + double n4 = (double) (color2.getBlue() - color.getBlue()) / (double) n; + ArrayList list = new ArrayList<>(); + + for (int i = 1; i <= n; ++i) { + list.add(Color.from((int) Math.round((double) color.getRed() + n2 * (double) i), (int) Math.round((double) color.getGreen() + n3 * (double) i), (int) Math.round((double) color.getBlue() + n4 * (double) i))); + } + + return list; + } + + public static @NotNull Color interpolateColor(@NotNull Color color1, @NotNull Color color2, float ratio) { + ratio = Math.max(0f, Math.min(1f, ratio)); // Clamp between 0 and 1 + + int red = Math.round(color1.getRed() + ratio * (color2.getRed() - color1.getRed())); + int green = Math.round(color1.getGreen() + ratio * (color2.getGreen() - color1.getGreen())); + int blue = Math.round(color1.getBlue() + ratio * (color2.getBlue() - color1.getBlue())); + + return Color.from(red, green, blue); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/color/MinecraftColor.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/color/MinecraftColor.java new file mode 100644 index 00000000..809055d0 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/color/MinecraftColor.java @@ -0,0 +1,71 @@ +package com.georgev22.skinoverlay.utilities.color; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +public enum MinecraftColor { + + DARK_RED("&4", "#AA0000"), + RED("&c", "#FF5555"), + GOLD("&6", "#FFAA00"), + YELLOW("&e", "#FFFF55"), + DARK_GREEN("&2", "#00AA00"), + GREEN("&a", "#55FF55"), + AQUA("&b", "#55FFFF"), + DARK_AQUA("&3", "#00AAAA"), + DARK_BLUE("&1", "#0000AA"), + BLUE("&9", "#5555FF"), + LIGHT_PURPLE("&d", "#FF55FF"), + DARK_PURPLE("&5", "#AA00AA"), + WHITE("&f", "#FFFFFF"), + GRAY("&7", "#AAAAAA"), + DARK_GRAY("&8", "#555555"), + BLACK("&0", "#000000"); + + private final String chatColor; + private final Color color; + + public static MinecraftColor getClosest(Color color) { + MinecraftColor minecraftColor = null; + int i = 0; + MinecraftColor[] minecraftColors = values(); + + for (MinecraftColor minecraftColor1 : minecraftColors) { + int j = Color.difference(color, minecraftColor1.getColor()); + if (minecraftColor == null || i > j) { + i = j; + minecraftColor = minecraftColor1; + } + } + + return minecraftColor; + } + + MinecraftColor(String chatColor, String hexColor) { + this.chatColor = chatColor; + this.color = Color.from(hexColor); + } + + public @NotNull String getName() { + return this.name().toLowerCase(); + } + + public Color getColor() { + return this.color; + } + + public String getTag() { + return this.chatColor; + } + + @Contract(pure = true) + public @NotNull String getAppliedTag() { + return this.chatColor.replace("&", "\u00A7"); + } + + @Contract(pure = true) + @Override + public @NotNull String toString() { + return this.getAppliedTag(); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/config/CFG.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/CFG.java new file mode 100644 index 00000000..be8ec729 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/CFG.java @@ -0,0 +1,213 @@ +package com.georgev22.skinoverlay.utilities.config; + +import org.bspfsystems.yamlconfiguration.file.FileConfiguration; +import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; +import java.nio.file.Files; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +public final class CFG { + + private final static Set cachedFiles = new HashSet<>(); + /* The file's name (without the .yml) */ + private final String fileName; + private final Logger logger; + private final File dataFolder; + private final Class clazz; + private final boolean saveResource; + private final boolean replace; + /* The yml file configuration. */ + private FileConfiguration fileConfiguration; + /* The file. */ + private File file; + + public CFG( + final String string, + final File dataFolder, + final boolean saveResource, + final boolean replace, + final Logger logger, + final Class clazz + ) { + this.fileName = string + ".yml"; + this.dataFolder = dataFolder; + this.saveResource = saveResource; + this.replace = replace; + this.logger = logger; + this.clazz = clazz; + this.setup(); + cachedFiles.add(this); + } + + public static void reloadFiles() { + cachedFiles.forEach(CFG::reloadFile); + } + + /** + * Attempts to load the file. + * + * @see #reloadFile() + */ + public void setup() { + if (!dataFolder.exists()) { + if (dataFolder.mkdir()) { + logger.info("Folder " + dataFolder.getName() + " has been created!"); + } + } + + this.file = new File(dataFolder, this.fileName); + + if (!this.file.exists()) { + try { + if (this.file.createNewFile()) { + logger.info("File " + this.file.getName() + " has been created!"); + } + } catch (final IOException e) { + this.logger.log(Level.SEVERE, "Could not create file " + this.file.getName(), e); + } + if (saveResource) { + saveResource(this.fileName, this.dataFolder, this.clazz); + } + } + + this.reloadFile(); + } + + private void saveResource(@NotNull String resourcePath, File dataFolder, Class clazz) { + if (resourcePath.isEmpty()) { + this.logger.warning("ResourcePath cannot be null or empty"); + return; + } + + resourcePath = resourcePath.replace('\\', '/'); + InputStream in = getResource(resourcePath, clazz); + if (in == null) { + this.logger.warning("The embedded resource '" + resourcePath + "' cannot be found in " + new java.io.File(clazz.getProtectionDomain() + .getCodeSource() + .getLocation() + .getPath()) + .getName()); + } + + File outFile = new File(dataFolder, resourcePath); + int lastIndex = resourcePath.lastIndexOf('/'); + File outDir = new File(dataFolder, resourcePath.substring(0, Math.max(lastIndex, 0))); + + if (!outDir.exists()) { + if (outDir.mkdirs()) { + logger.info("Folder " + outDir.getPath() + " has been created!"); + } + } + + try { + if (!outFile.exists() || replace) { + OutputStream out = Files.newOutputStream(outFile.toPath()); + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + out.close(); + in.close(); + } else { + logger.warning("Could not save " + outFile.getName() + " to " + outFile + " because " + outFile.getName() + " already exists."); + } + } catch (IOException ex) { + this.logger.log(Level.SEVERE, "Could not save " + outFile.getName() + " to " + outFile, ex); + } + } + + @Nullable + private InputStream getResource(@NotNull String filename, @NotNull Class clazz) { + try { + URL url = clazz.getClassLoader().getResource(filename); + + if (url == null) { + return null; + } + + URLConnection connection = url.openConnection(); + connection.setUseCaches(false); + return connection.getInputStream(); + } catch (IOException ex) { + return null; + } + } + + /** + * Saves the file configuration. + * + * @see FileConfiguration#save(File) + * @see #getFileConfiguration() + */ + public void saveFile() { + try { + this.getFileConfiguration().save(this.file); + } catch (final IOException e) { + e.printStackTrace(); + } + } + + /** + * Reloads the file. + * + * @see YamlConfiguration#loadConfiguration(File) + * @see #file + */ + public void reloadFile() { + this.fileConfiguration = YamlConfiguration.loadConfiguration(this.file); + } + + /** + * @return the file - The {@link FileConfiguration}. + */ + public FileConfiguration getFileConfiguration() { + return this.fileConfiguration; + } + + /** + * Get the file + * + * @return the file + */ + public File getFile() { + return file; + } + + @Override + public int hashCode() { + return Objects.hash(fileName, fileConfiguration, file, saveResource); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CFG cfg = (CFG) o; + return saveResource == cfg.saveResource && Objects.equals(fileName, cfg.fileName) && Objects.equals(fileConfiguration, cfg.fileConfiguration) && Objects.equals(file, cfg.file); + } + + @Contract(pure = true) + @Override + public @NotNull String toString() { + return "CFG{" + + "fileName='" + fileName + '\'' + + ", fileConfiguration=" + fileConfiguration + + ", file=" + file + + ", saveResource=" + saveResource + + '}'; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/utilities/config/FileManager.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/FileManager.java similarity index 92% rename from core/src/main/java/com/georgev22/skinoverlay/utilities/config/FileManager.java rename to common/src/main/java/com/georgev22/skinoverlay/utilities/config/FileManager.java index 3c29b74d..449889a5 100644 --- a/core/src/main/java/com/georgev22/skinoverlay/utilities/config/FileManager.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/FileManager.java @@ -1,6 +1,5 @@ package com.georgev22.skinoverlay.utilities.config; -import com.georgev22.library.yaml.configmanager.CFG; import com.georgev22.skinoverlay.SkinOverlay; public final class FileManager { diff --git a/core/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java similarity index 78% rename from core/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java rename to common/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java index 1c2587a5..a53be8d4 100644 --- a/core/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.utilities.config; -import com.georgev22.library.utilities.Color; import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.utilities.color.Color; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; @@ -20,38 +20,6 @@ public enum OptionsUtil { COMMAND_SKINOVERLAY("commands.skinoverlay", true, Optional.empty()), - DATABASE_HOST("database.SQL.host", "localhost", Optional.empty()), - - DATABASE_PORT("database.SQL.port", 3306, Optional.empty()), - - DATABASE_USER("database.SQL.user", "youruser", Optional.empty()), - - DATABASE_PASSWORD("database.SQL.password", "yourpassword", Optional.empty()), - - DATABASE_DATABASE("database.SQL.database", "SkinOverlay", Optional.empty()), - - DATABASE_USERS_TABLE_NAME("database.SQL.users table name", "skinoverlay_users", Optional.of("database.SQL.table name")), - - DATABASE_SKINS_TABLE_NAME("database.SQL.skins table name", "skinoverlay_skins", Optional.empty()), - - DATABASE_FILE_NAME("database.SQL.SQLite file name", "skinoverlay", Optional.of("database.SQLite.file name")), - - DATABASE_MONGO_HOST("database.MongoDB.host", "localhost", Optional.empty()), - - DATABASE_MONGO_PORT("database.MongoDB.port", 27017, Optional.empty()), - - DATABASE_MONGO_USER("database.MongoDB.user", "youruser", Optional.empty()), - - DATABASE_MONGO_PASSWORD("database.MongoDB.password", "yourpassword", Optional.empty()), - - DATABASE_MONGO_DATABASE("database.MongoDB.database", "skinoverlay", Optional.empty()), - - DATABASE_MONGO_USERS_COLLECTION("database.MongoDB.users collection", "skinoverlay_users", Optional.of("database.MongoDB.collection")), - - DATABASE_MONGO_SKINS_COLLECTION("database.MongoDB.users collection", "skinoverlay_users", Optional.empty()), - - DATABASE_TYPE("database.type", "SQLite", Optional.empty()), - EXPERIMENTAL_FEATURES("experimental features", false, Optional.empty()), METRICS("metrics", true, Optional.empty()), @@ -64,6 +32,11 @@ public enum OptionsUtil { SKIN_HOOK("skin hook", "SkinsRestorer", Optional.empty()), LOCALE("locale", "en_US", Optional.empty()), + SAVE_INTERVAL("save interval", 20, Optional.empty()), + CONNECTION_TYPE("connection type", "PluginMessage", Optional.empty()), + REDIS_HOST("redis.host", "localhost", Optional.empty()), + REDIS_PORT("redis.port", 6379, Optional.empty()), + REDIS_PASSWORD("redis.password", "", Optional.empty()), ; private static final SkinOverlay mainPlugin = SkinOverlay.getInstance(); private final String pathName; @@ -148,7 +121,7 @@ public String getStringValue(String arg) { * * @return a List of Color classes that represent the colors. */ - public @NotNull List getColors(String arg) { + public @NotNull List getColors(String arg) { return getStringList().stream().map(Color::from).collect(Collectors.toList()); } diff --git a/core/src/main/java/com/georgev22/skinoverlay/utilities/config/OverlayOptionsUtil.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OverlayOptionsUtil.java similarity index 97% rename from core/src/main/java/com/georgev22/skinoverlay/utilities/config/OverlayOptionsUtil.java rename to common/src/main/java/com/georgev22/skinoverlay/utilities/config/OverlayOptionsUtil.java index 8c1b7f6d..87f86b43 100644 --- a/core/src/main/java/com/georgev22/skinoverlay/utilities/config/OverlayOptionsUtil.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OverlayOptionsUtil.java @@ -1,8 +1,8 @@ package com.georgev22.skinoverlay.utilities.config; -import com.georgev22.library.utilities.Color; -import com.georgev22.library.yaml.file.FileConfiguration; import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.utilities.color.Color; +import org.bspfsystems.yamlconfiguration.file.FileConfiguration; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; diff --git a/core/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinConfigurationFile.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinConfigurationFile.java similarity index 92% rename from core/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinConfigurationFile.java rename to common/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinConfigurationFile.java index bffca909..7391476c 100644 --- a/core/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinConfigurationFile.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinConfigurationFile.java @@ -1,9 +1,8 @@ package com.georgev22.skinoverlay.utilities.config; - -import com.georgev22.library.yaml.file.FileConfiguration; -import com.georgev22.library.yaml.file.YamlConfiguration; import com.georgev22.skinoverlay.SkinOverlay; +import org.bspfsystems.yamlconfiguration.file.FileConfiguration; +import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; import java.io.File; diff --git a/core/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinFileCache.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinFileCache.java similarity index 95% rename from core/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinFileCache.java rename to common/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinFileCache.java index 3c3d8482..16769e49 100644 --- a/core/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinFileCache.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinFileCache.java @@ -1,11 +1,11 @@ package com.georgev22.skinoverlay.utilities.config; -import com.georgev22.library.maps.HashObjectMap; -import com.georgev22.library.maps.ObjectMap; -import com.georgev22.library.utilities.Utils; import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.maps.ObjectMap; import com.georgev22.skinoverlay.storage.data.Skin; import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; +import com.georgev22.skinoverlay.utilities.Utils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -130,7 +130,7 @@ public SkinConfigurationFile getCacheSkinConfig(final String name) { @Nullable public SkinConfigurationFile getCacheSkinConfig(final @NotNull Skin skin) { - return this.getCacheSkinConfig(skin.skinName()); + return this.getCacheSkinConfig(skin.getSkinParts().getSkinName()); } @Nullable @@ -149,6 +149,6 @@ public SerializableBufferedImage getSkinImage(final String skinName) { @Nullable public SerializableBufferedImage getSkinImage(final @NotNull Skin skin) { - return this.getSkinImage(skin.skinName()); + return this.getSkinImage(skin.getSkinParts().getSkinName()); } } diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/MinecraftSkinRenderer.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/MinecraftSkinRenderer.java new file mode 100644 index 00000000..ad742c46 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/MinecraftSkinRenderer.java @@ -0,0 +1,67 @@ +package com.georgev22.skinoverlay.utilities.skin; + +import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; + +import java.awt.*; +import java.awt.image.BufferedImage; + +/** + * Renders a full Minecraft skin by compositing multiple {@link Part} images onto a single 64x64 canvas. + */ +public class MinecraftSkinRenderer { + + /** + * The final rendered full skin image. + */ + private SerializableBufferedImage fullSkinImage; + + /** + * The array of {@link Part} objects to render. + */ + private final Part[] parts; + + /** + * Constructs a new MinecraftSkinRenderer with the specified parts. + * + * @param parts The parts to render onto the skin. + */ + public MinecraftSkinRenderer(Part... parts) { + this.parts = parts; + } + + /** + * Creates the full skin image by drawing each part onto a new 64x64 {@link BufferedImage}. + *

+ * The resulting image can be retrieved using {@link #getFullSkinImage()}. + *

+ */ + public void createFullSkinImage() { + fullSkinImage = new SerializableBufferedImage(new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB)); + + Graphics g = fullSkinImage.getBufferedImage().getGraphics(); + + for (Part part : parts) { + g.drawImage(part.image().getBufferedImage(), part.x(), part.y(), part.width(), part.height(), null); + } + + g.dispose(); + } + + /** + * Returns the rendered full skin image. + * + * @return The full skin image as a {@link SerializableBufferedImage}. + */ + public SerializableBufferedImage getFullSkinImage() { + return fullSkinImage; + } + + /** + * Returns the parts used for rendering this skin. + * + * @return An array of {@link Part} objects. + */ + public Part[] getParts() { + return parts; + } +} diff --git a/core/src/main/java/com/georgev22/skinoverlay/handler/skin/Part.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/Part.java similarity index 89% rename from core/src/main/java/com/georgev22/skinoverlay/handler/skin/Part.java rename to common/src/main/java/com/georgev22/skinoverlay/utilities/skin/Part.java index c9ee4841..64bdf65e 100644 --- a/core/src/main/java/com/georgev22/skinoverlay/handler/skin/Part.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/Part.java @@ -1,7 +1,5 @@ -package com.georgev22.skinoverlay.handler.skin; +package com.georgev22.skinoverlay.utilities.skin; -import com.georgev22.library.yaml.serialization.ConfigurationSerializable; -import com.georgev22.library.yaml.serialization.SerializableAs; import com.georgev22.skinoverlay.SkinOverlay; import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; import org.jetbrains.annotations.Contract; @@ -13,9 +11,8 @@ /** * Represents a part of a player's skin. */ -@SerializableAs("SkinPart") public record Part(String name, SerializableBufferedImage image, int x, int y, int width, int height, - boolean isEmpty) implements ConfigurationSerializable { + boolean isEmpty) { /** * Returns a string representation of the Part object. @@ -41,7 +38,6 @@ public record Part(String name, SerializableBufferedImage image, int x, int y, i * * @return A map containing the serialized data. */ - @Override public @NotNull Map serialize() { Map map = new HashMap<>(); map.put("name", name); diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/Section.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/Section.java new file mode 100644 index 00000000..de7ced5b --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/Section.java @@ -0,0 +1,180 @@ +package com.georgev22.skinoverlay.utilities.skin; + +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.Optional; + +/** + * Represents a rectangular section of a skin texture. + * + * @param x1 The starting X coordinate. + * @param y1 The starting Y coordinate. + * @param x2 The ending X coordinate. + * @param y2 The ending Y coordinate. + * @param width The width of the section. + * @param height The height of the section. + */ +public record Section(int x1, int y1, int x2, int y2, int width, int height) { + + /** + * Creates a Section based on two points. Calculates width and height automatically. + * + * @param x1 The starting X coordinate. + * @param y1 The starting Y coordinate. + * @param x2 The ending X coordinate. + * @param y2 The ending Y coordinate. + */ + public Section(int x1, int y1, int x2, int y2) { + this(x1, y1, x2, y2, x2 - x1, Math.max(y1, y2) - Math.min(y1, y2)); + } + + @Override + public String toString() { + return "Section{" + + "x1=" + x1 + + ", y1=" + y1 + + ", x2=" + x2 + + ", y2=" + y2 + + ", width=" + width + + ", height=" + height + + '}'; + } + + /** + * Enum representing all skin texture sections with their coordinates. + */ + public enum SectionType { + // Head + Head_Top(8, 0, 16, 8), + Head_Bottom(16, 0, 24, 8), + Head_Right(0, 8, 8, 16), + Head_Front(8, 8, 16, 16), + Head_Left(16, 8, 24, 16), + Head_Back(24, 8, 32, 16), + // Hat + Hat_Top(40, 0, 48, 8), + Hat_Bottom(48, 0, 56, 8), + Hat_Right(32, 8, 40, 16), + Hat_Front(40, 8, 48, 16), + Hat_Left(48, 8, 56, 16), + Hat_Back(56, 8, 64, 16), + // Right Leg + Right_Leg_Top(4, 16, 8, 20), + Right_Leg_Bottom(8, 16, 12, 20), + Right_Leg_Right(0, 20, 4, 32), + Right_Leg_Front(4, 20, 8, 32), + Right_Leg_Left(8, 20, 12, 32), + Right_Leg_Back(12, 20, 16, 32), + // Torso + Torso_Top(20, 16, 28, 20), + Torso_Bottom(28, 16, 36, 20), + Torso_Right(16, 20, 20, 32), + Torso_Front(20, 20, 28, 32), + Torso_Left(36, 20, 40, 32), + Torso_Back(28, 20, 36, 32), + // Right Arm + Right_Arm_Top(44, 16, 48, 20), + Right_Arm_Bottom(48, 16, 52, 20), + Right_Arm_Right(40, 20, 44, 32), + Right_Arm_Front(44, 20, 48, 32), + Right_Arm_Left(48, 20, 52, 32), + Right_Arm_Back(52, 20, 56, 32), + // Left Leg + Left_Leg_Top(20, 48, 24, 52), + Left_Leg_Bottom(24, 48, 28, 52), + Left_Leg_Right(16, 52, 20, 64), + Left_Leg_Front(20, 52, 24, 64), + Left_Leg_Left(24, 52, 28, 64), + Left_Leg_Back(28, 52, 32, 64), + // Left Arm + Left_Arm_Top(36, 48, 40, 52), + Left_Arm_Bottom(40, 48, 44, 52), + Left_Arm_Right(32, 52, 36, 64), + Left_Arm_Front(36, 52, 40, 64), + Left_Arm_Left(40, 52, 44, 64), + Left_Arm_Back(44, 52, 48, 64), + // Right Pants Leg + Right_Pants_Leg_Top(4, 32, 8, 36), + Right_Pants_Leg_Bottom(8, 32, 12, 36), + Right_Pants_Leg_Right(0, 36, 4, 48), + Right_Pants_Leg_Front(4, 36, 8, 48), + Right_Pants_Leg_Left(8, 36, 12, 48), + Right_Pants_Leg_Back(12, 36, 16, 48), + // Jacket + Jacket_Top(20, 32, 28, 36), + Jacket_Bottom(28, 32, 36, 36), + Jacket_Right(16, 36, 20, 48), + Jacket_Front(20, 36, 28, 48), + Jacket_Left(36, 36, 40, 48), + Jacket_Back(28, 36, 36, 48), + // Right Sleeve + Right_Sleeve_Top(44, 32, 48, 36), + Right_Sleeve_Bottom(48, 32, 52, 36), + Right_Sleeve_Right(40, 36, 44, 48), + Right_Sleeve_Front(44, 36, 48, 48), + Right_Sleeve_Left(48, 36, 52, 48), + Right_Sleeve_Back(52, 36, 56, 48), + // Left Pants Leg + Left_Pants_Leg_Top(4, 48, 8, 52), + Left_Pants_Leg_Bottom(8, 48, 12, 52), + Left_Pants_Leg_Right(0, 52, 4, 64), + Left_Pants_Leg_Front(4, 52, 8, 64), + Left_Pants_Leg_Left(8, 52, 12, 64), + Left_Pants_Leg_Back(12, 52, 16, 64), + // Left Sleeve + Left_Sleeve_Top(52, 48, 56, 52), + Left_Sleeve_Bottom(56, 48, 60, 52), + Left_Sleeve_Right(48, 52, 52, 64), + Left_Sleeve_Front(52, 52, 56, 64), + Left_Sleeve_Left(56, 52, 60, 64), + Left_Sleeve_Back(60, 52, 64, 64); + + private final Section section; + + /** + * Constructs a SectionType with the specified section coordinates. + * + * @param x1 The starting X coordinate. + * @param y1 The starting Y coordinate. + * @param x2 The ending X coordinate. + * @param y2 The ending Y coordinate. + */ + SectionType(int x1, int y1, int x2, int y2) { + this.section = new Section(x1, y1, x2, y2); + } + + /** + * Gets the Section represented by this SectionType. + * + * @return The Section. + */ + public Section getSection() { + return section; + } + + /** + * Retrieves a SectionType by its name (case-insensitive). + * + * @param name The name to search for. + * @return An Optional containing the SectionType if found, otherwise empty. + */ + public static @NotNull Optional getByName(String name) { + return Arrays.stream(values()) + .filter(sectionType -> sectionType.name().equalsIgnoreCase(name)) + .findFirst(); + } + + /** + * Filters SectionTypes that start with the specified prefix. + * + * @param prefix The prefix to filter by. + * @return An array of matching SectionTypes. + */ + public static SectionType @NotNull [] filterByPrefix(String prefix) { + return Arrays.stream(values()) + .filter(sectionType -> sectionType.name().startsWith(prefix)) + .toArray(SectionType[]::new); + } + } +} diff --git a/core/src/main/java/com/georgev22/skinoverlay/handler/skin/SkinParts.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/SkinParts.java similarity index 53% rename from core/src/main/java/com/georgev22/skinoverlay/handler/skin/SkinParts.java rename to common/src/main/java/com/georgev22/skinoverlay/utilities/skin/SkinParts.java index c0b2a7ec..19f4c14a 100644 --- a/core/src/main/java/com/georgev22/skinoverlay/handler/skin/SkinParts.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/SkinParts.java @@ -1,31 +1,29 @@ -package com.georgev22.skinoverlay.handler.skin; +package com.georgev22.skinoverlay.utilities.skin; -import com.georgev22.library.yaml.serialization.ConfigurationSerializable; -import com.georgev22.library.yaml.serialization.SerializableAs; import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.handler.skin.Section.*; +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.maps.UnmodifiableObjectMap; import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; -import lombok.Getter; +import com.georgev22.skinoverlay.utilities.skin.Section.SectionType; import org.jetbrains.annotations.NotNull; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.*; import java.net.URL; -import java.util.HashMap; import java.util.Map; /** * Represents different sections and parts of a player's skin. */ -@Getter -@SerializableAs("SkinParts") -public class SkinParts implements ConfigurationSerializable { +public class SkinParts { private static final SerializableBufferedImage steveSkin; static { try { + @SuppressWarnings("deprecation") URL url = new URL("https://s.namemc.com/i/12b92a9206470fe2.png"); ByteArrayOutputStream output = new ByteArrayOutputStream(); @@ -54,7 +52,7 @@ public class SkinParts implements ConfigurationSerializable { private String skinName; - private final Map parts; + private final ObjectMap parts; /** * Default constructor for SkinParts using the Steve skin. @@ -70,7 +68,7 @@ public SkinParts() { * @param skinName The name of the skin. */ public SkinParts(SerializableBufferedImage fullSkin, String skinName) { - this.parts = new HashMap<>(); + this.parts = new HashObjectMap<>(); if (fullSkin != null) { this.fullSkin = fullSkin; } @@ -91,78 +89,9 @@ public void createParts() { SerializableBufferedImage fullSkin = this.fullSkin; this.fullSkin = convertSkin(fullSkin); } - parts.put("Head_Top", createPart(new Head_Top())); - parts.put("Head_Bottom", createPart(new Head_Bottom())); - parts.put("Head_Right", createPart(new Head_Right())); - parts.put("Head_Front", createPart(new Head_Front())); - parts.put("Head_Left", createPart(new Head_Left())); - parts.put("Head_Back", createPart(new Head_Back())); - parts.put("Hat_Top", createPart(new Hat_Top())); - parts.put("Hat_Bottom", createPart(new Hat_Bottom())); - parts.put("Hat_Right", createPart(new Hat_Right())); - parts.put("Hat_Front", createPart(new Hat_Front())); - parts.put("Hat_Left", createPart(new Hat_Left())); - parts.put("Hat_Back", createPart(new Hat_Back())); - parts.put("Right_Leg_Top", createPart(new Right_Leg_Top())); - parts.put("Right_Leg_Bottom", createPart(new Right_Leg_Bottom())); - parts.put("Right_Leg_Right", createPart(new Right_Leg_Right())); - parts.put("Right_Leg_Front", createPart(new Right_Leg_Front())); - parts.put("Right_Leg_Left", createPart(new Right_Leg_Left())); - parts.put("Right_Leg_Back", createPart(new Right_Leg_Back())); - parts.put("Torso_Top", createPart(new Torso_Top())); - parts.put("Torso_Bottom", createPart(new Torso_Bottom())); - parts.put("Torso_Right", createPart(new Torso_Right())); - parts.put("Torso_Front", createPart(new Torso_Front())); - parts.put("Torso_Left", createPart(new Torso_Left())); - parts.put("Torso_Back", createPart(new Torso_Back())); - parts.put("Right_Arm_Top", createPart(new Right_Arm_Top())); - parts.put("Right_Arm_Bottom", createPart(new Right_Arm_Bottom())); - parts.put("Right_Arm_Right", createPart(new Right_Arm_Right())); - parts.put("Right_Arm_Front", createPart(new Right_Arm_Front())); - parts.put("Right_Arm_Left", createPart(new Right_Arm_Left())); - parts.put("Right_Arm_Back", createPart(new Right_Arm_Back())); - parts.put("Left_Leg_Top", createPart(new Left_Leg_Top())); - parts.put("Left_Leg_Bottom", createPart(new Left_Leg_Bottom())); - parts.put("Left_Leg_Right", createPart(new Left_Leg_Right())); - parts.put("Left_Leg_Front", createPart(new Left_Leg_Front())); - parts.put("Left_Leg_Left", createPart(new Left_Leg_Left())); - parts.put("Left_Leg_Back", createPart(new Left_Leg_Back())); - parts.put("Left_Arm_Top", createPart(new Left_Arm_Top())); - parts.put("Left_Arm_Bottom", createPart(new Left_Arm_Bottom())); - parts.put("Left_Arm_Right", createPart(new Left_Arm_Right())); - parts.put("Left_Arm_Front", createPart(new Left_Arm_Front())); - parts.put("Left_Arm_Left", createPart(new Left_Arm_Left())); - parts.put("Left_Arm_Back", createPart(new Left_Arm_Back())); - parts.put("Right_Pants_Leg_Top", createPart(new Right_Pants_Leg_Top())); - parts.put("Right_Pants_Leg_Bottom", createPart(new Right_Pants_Leg_Bottom())); - parts.put("Right_Pants_Leg_Right", createPart(new Right_Pants_Leg_Right())); - parts.put("Right_Pants_Leg_Front", createPart(new Right_Pants_Leg_Front())); - parts.put("Right_Pants_Leg_Left", createPart(new Right_Pants_Leg_Left())); - parts.put("Right_Pants_Leg_Back", createPart(new Right_Pants_Leg_Back())); - parts.put("Jacket_Top", createPart(new Jacket_Top())); - parts.put("Jacket_Bottom", createPart(new Jacket_Bottom())); - parts.put("Jacket_Right", createPart(new Jacket_Right())); - parts.put("Jacket_Front", createPart(new Jacket_Front())); - parts.put("Jacket_Left", createPart(new Jacket_Left())); - parts.put("Jacket_Back", createPart(new Jacket_Back())); - parts.put("Right_Sleeve_Top", createPart(new Right_Sleeve_Top())); - parts.put("Right_Sleeve_Bottom", createPart(new Right_Sleeve_Bottom())); - parts.put("Right_Sleeve_Right", createPart(new Right_Sleeve_Right())); - parts.put("Right_Sleeve_Front", createPart(new Right_Sleeve_Front())); - parts.put("Right_Sleeve_Left", createPart(new Right_Sleeve_Left())); - parts.put("Right_Sleeve_Back", createPart(new Right_Sleeve_Back())); - parts.put("Left_Pants_Leg_Top", createPart(new Left_Pants_Leg_Top())); - parts.put("Left_Pants_Leg_Bottom", createPart(new Left_Pants_Leg_Bottom())); - parts.put("Left_Pants_Leg_Right", createPart(new Left_Pants_Leg_Right())); - parts.put("Left_Pants_Leg_Front", createPart(new Left_Pants_Leg_Front())); - parts.put("Left_Pants_Leg_Left", createPart(new Left_Pants_Leg_Left())); - parts.put("Left_Pants_Leg_Back", createPart(new Left_Pants_Leg_Back())); - parts.put("Left_Sleeve_Top", createPart(new Left_Sleeve_Top())); - parts.put("Left_Sleeve_Bottom", createPart(new Left_Sleeve_Bottom())); - parts.put("Left_Sleeve_Right", createPart(new Left_Sleeve_Right())); - parts.put("Left_Sleeve_Front", createPart(new Left_Sleeve_Front())); - parts.put("Left_Sleeve_Left", createPart(new Left_Sleeve_Left())); - parts.put("Left_Sleeve_Back", createPart(new Left_Sleeve_Back())); + for (SectionType sectionType : SectionType.values()) { + parts.put(sectionType.name(), createPart(sectionType.getSection(), sectionType.name())); + } } /** @@ -171,11 +100,11 @@ public void createParts() { * @param section The section for which the part image is created. * @return A Part object representing the created part. */ - private @NotNull Part createPart(@NotNull Section section) { - int x = section.getX1(); - int y = section.getY1(); - int width = section.getWidth(); - int height = section.getHeight(); + private @NotNull Part createPart(@NotNull Section section, String partName) { + int x = section.x1(); + int y = section.y1(); + int width = section.width(); + int height = section.height(); BufferedImage partImage = fullSkin.getBufferedImage().getSubimage(x, y, width, height); int[] pixelData = partImage.getRGB(0, 0, width, height, null, 0, width); @@ -189,7 +118,6 @@ public void createParts() { } } - String partName = section.getClass().getSimpleName(); return new Part(partName, new SerializableBufferedImage(partImage), x, y, width, height, isAreaTransparent); } @@ -230,6 +158,15 @@ public boolean isOldSkin() { return image.getWidth() == 64 && image.getHeight() == 32; } + /** + * Retrieves the full skin image. + * + * @return The full skin image. + */ + public SerializableBufferedImage getFullSkin() { + return fullSkin; + } + /** * Converts an old format skin to the new format (64x64). * @@ -254,13 +191,29 @@ public SerializableBufferedImage convertSkin(@NotNull SerializableBufferedImage return new SerializableBufferedImage(newImage); } + /** + * Retrieves the name of the skin. + * + * @return The name of the skin. + */ + public String getSkinName() { + return skinName; + } + + /** + * Retrieves the parts of the skin. + * + * @return A map containing the parts of the skin. + */ + public UnmodifiableObjectMap getParts() { + return new UnmodifiableObjectMap<>(parts); + } /** * Serializes the SkinParts object to a map for YAML serialization. * * @return A map containing the serialized data. */ - @Override public @NotNull Map serialize() { return Map.of("bufferedImage", this.fullSkin, "skinName", this.skinName); } @@ -307,4 +260,4 @@ public String toString() { ", parts=" + parts + '}'; } -} +} \ No newline at end of file diff --git a/src/main/resources/bungee.yml b/common/src/main/resources/bungee.yml similarity index 100% rename from src/main/resources/bungee.yml rename to common/src/main/resources/bungee.yml diff --git a/common/src/main/resources/config.yml b/common/src/main/resources/config.yml new file mode 100644 index 00000000..7cd07588 --- /dev/null +++ b/common/src/main/resources/config.yml @@ -0,0 +1,45 @@ +Options: + experimental features: false #Turn on experimental features + debug: false + proxy: false + secret: "SECRET HERE" + mineskin api key: "none" + # Supported locales: en, fr, de, it, es, pt, nl, no, sv, da, fi, pl, cs, sk, hu, ro, bg, el, ru, tr, zh, ja, ko, en_US, en_GR, es_ES, es_MX, pt_BR, fr_CA + # You must have a file with the locale you want for example messages_en_US.yml or messages_el.yml + locale: "en_US" + commands: #Disable or enable plugin commands + skinoverlay: true + updater: + enabled: true + default skin uuid: "8667ba71-b85a-4004-af54-457a9734eed7" + #Skin hooks are only used to retrieve player skin properties for offline players or players who have changed their skin. + #Currently available Skin Hooks: None, SkinsRestorer and Custom. + #With the custom option, you need to have your own plugin that sets the SkinOverlay.getInstance().setSkinHook(SkinHook). + skin hook: None + + # WARNING: Only use this feature if you fully understand the implications. + # The value represents the time (in SECONDS) between automatic saves of player and skin data. + # The primary benefit of batching saves is to reduce performance overhead by saving changes less frequently, + # rather than saving each change individually. + # + # However, please note: If the server crashes before changes are saved, you may lose unsaved data. + # + # The reason for introducing this feature is that, in previous versions, concurrent saves to the disk could occur + # if one save operation was in progress while another change was made. + # This could lead to file corruption, as two threads would be attempting to write to the same file simultaneously. + # + # Recommended Usage: + # - Set a reasonable save interval to balance between performance and data safety. + # - If you're running a highly active server with frequent changes, consider a lower save interval to reduce data loss risk. + save interval: 20 # Time in SECONDS between saves. Adjust based on server load and performance needs. + + # The following option is used to specify which protocol the proxy/backend server will use to send/listen the skin data. + # Available options: "PluginMessage" and "Redis" + # If you are using a backend server that doesn't support the Bungee/Velocity PluginMessage protocol, please set it to "Redis". + connection type: "PluginMessage" + + # Redis connection details + redis: + host: "localhost" + port: 6379 + password: "" \ No newline at end of file diff --git a/src/main/resources/messages_en_US.yml b/common/src/main/resources/messages_en_US.yml similarity index 51% rename from src/main/resources/messages_en_US.yml rename to common/src/main/resources/messages_en_US.yml index ca21fe49..e3e4cd20 100644 --- a/src/main/resources/messages_en_US.yml +++ b/common/src/main/resources/messages_en_US.yml @@ -3,14 +3,9 @@ Messages: Only Player Command: '&c&l(!)&c Only players can run this command!' Offline Player: '&c&l(!)&c Player %player% is offline!' Overlay Applied: '&a&l(!)&a Overlay %url% applied!' - Overlay Reset: '&a&l(!)&a Default skin applied(%player%)!!' + Overlay Reset: '&a&l(!)&a Default skin applied (%player%)!' Overlay Not Found: '&c&l(!)&c Overlay %overlay% not found!' Insufficient arguments: '&c&l(!)&c Insufficient arguments (%command%)' -Commands: - Descriptions: - SkinOverlay: - help: 'Shows the help page' - overlay: 'Wear a specific overlay from the plugin files' - clear: 'Removes the skin overlay' - url: 'Wear a specific overlay from a URL' - reload: 'Reload the plugin configuration files (some settings need server restart)' + Invalid URL: '&c&l(!)&c Invalid URL or failed to download image: %url%' + Error: '&c&l(!)&c An error occurred: %error%' + Failed To Retrieve Or Generate Skin: '&c&l(!)&c Failed to retrieve or generate skin for %player%!' diff --git a/src/main/resources/messages_tr.yml b/common/src/main/resources/messages_tr.yml similarity index 100% rename from src/main/resources/messages_tr.yml rename to common/src/main/resources/messages_tr.yml diff --git a/common/src/main/resources/plugin.yml b/common/src/main/resources/plugin.yml new file mode 100644 index 00000000..b25bad14 --- /dev/null +++ b/common/src/main/resources/plugin.yml @@ -0,0 +1,11 @@ +main: ${bukkitMain} +name: ${pluginName} +version: ${version} +authors: + - ${author} +api-version: 1.13 +load: POSTWORLD +folia-supported: true +softdepend: + - PlaceholderAPI + - SkinsRestorer diff --git a/src/main/resources/skins-cfg/alley.yml b/common/src/main/resources/skins-cfg/alley.yml similarity index 100% rename from src/main/resources/skins-cfg/alley.yml rename to common/src/main/resources/skins-cfg/alley.yml diff --git a/src/main/resources/skins-cfg/bubbo_transparent.yml b/common/src/main/resources/skins-cfg/bubbo_transparent.yml similarity index 100% rename from src/main/resources/skins-cfg/bubbo_transparent.yml rename to common/src/main/resources/skins-cfg/bubbo_transparent.yml diff --git a/src/main/resources/skins-cfg/fire_demon.yml b/common/src/main/resources/skins-cfg/fire_demon.yml similarity index 100% rename from src/main/resources/skins-cfg/fire_demon.yml rename to common/src/main/resources/skins-cfg/fire_demon.yml diff --git a/src/main/resources/skins-cfg/flame.yml b/common/src/main/resources/skins-cfg/flame.yml similarity index 100% rename from src/main/resources/skins-cfg/flame.yml rename to common/src/main/resources/skins-cfg/flame.yml diff --git a/src/main/resources/skins-cfg/frog_pijama.yml b/common/src/main/resources/skins-cfg/frog_pijama.yml similarity index 100% rename from src/main/resources/skins-cfg/frog_pijama.yml rename to common/src/main/resources/skins-cfg/frog_pijama.yml diff --git a/src/main/resources/skins-cfg/glare.yml b/common/src/main/resources/skins-cfg/glare.yml similarity index 100% rename from src/main/resources/skins-cfg/glare.yml rename to common/src/main/resources/skins-cfg/glare.yml diff --git a/src/main/resources/skins-cfg/hoodie.yml b/common/src/main/resources/skins-cfg/hoodie.yml similarity index 100% rename from src/main/resources/skins-cfg/hoodie.yml rename to common/src/main/resources/skins-cfg/hoodie.yml diff --git a/src/main/resources/skins-cfg/migrator.yml b/common/src/main/resources/skins-cfg/migrator.yml similarity index 100% rename from src/main/resources/skins-cfg/migrator.yml rename to common/src/main/resources/skins-cfg/migrator.yml diff --git a/src/main/resources/skins-cfg/mustache.yml b/common/src/main/resources/skins-cfg/mustache.yml similarity index 100% rename from src/main/resources/skins-cfg/mustache.yml rename to common/src/main/resources/skins-cfg/mustache.yml diff --git a/src/main/resources/skins-cfg/pirate.yml b/common/src/main/resources/skins-cfg/pirate.yml similarity index 100% rename from src/main/resources/skins-cfg/pirate.yml rename to common/src/main/resources/skins-cfg/pirate.yml diff --git a/src/main/resources/skins-cfg/policeman.yml b/common/src/main/resources/skins-cfg/policeman.yml similarity index 100% rename from src/main/resources/skins-cfg/policeman.yml rename to common/src/main/resources/skins-cfg/policeman.yml diff --git a/src/main/resources/skins-cfg/smoking.yml b/common/src/main/resources/skins-cfg/smoking.yml similarity index 100% rename from src/main/resources/skins-cfg/smoking.yml rename to common/src/main/resources/skins-cfg/smoking.yml diff --git a/src/main/resources/skins/alley.png b/common/src/main/resources/skins/alley.png similarity index 100% rename from src/main/resources/skins/alley.png rename to common/src/main/resources/skins/alley.png diff --git a/src/main/resources/skins/bubbo_transparent.png b/common/src/main/resources/skins/bubbo_transparent.png similarity index 100% rename from src/main/resources/skins/bubbo_transparent.png rename to common/src/main/resources/skins/bubbo_transparent.png diff --git a/src/main/resources/skins/fire_demon.png b/common/src/main/resources/skins/fire_demon.png similarity index 100% rename from src/main/resources/skins/fire_demon.png rename to common/src/main/resources/skins/fire_demon.png diff --git a/src/main/resources/skins/flame.png b/common/src/main/resources/skins/flame.png similarity index 100% rename from src/main/resources/skins/flame.png rename to common/src/main/resources/skins/flame.png diff --git a/src/main/resources/skins/frog_pijama.png b/common/src/main/resources/skins/frog_pijama.png similarity index 100% rename from src/main/resources/skins/frog_pijama.png rename to common/src/main/resources/skins/frog_pijama.png diff --git a/src/main/resources/skins/glare.png b/common/src/main/resources/skins/glare.png similarity index 100% rename from src/main/resources/skins/glare.png rename to common/src/main/resources/skins/glare.png diff --git a/src/main/resources/skins/hoodie.png b/common/src/main/resources/skins/hoodie.png similarity index 100% rename from src/main/resources/skins/hoodie.png rename to common/src/main/resources/skins/hoodie.png diff --git a/src/main/resources/skins/migrator.png b/common/src/main/resources/skins/migrator.png similarity index 100% rename from src/main/resources/skins/migrator.png rename to common/src/main/resources/skins/migrator.png diff --git a/src/main/resources/skins/mustache.png b/common/src/main/resources/skins/mustache.png similarity index 100% rename from src/main/resources/skins/mustache.png rename to common/src/main/resources/skins/mustache.png diff --git a/src/main/resources/skins/pirate.png b/common/src/main/resources/skins/pirate.png similarity index 100% rename from src/main/resources/skins/pirate.png rename to common/src/main/resources/skins/pirate.png diff --git a/src/main/resources/skins/policeman.png b/common/src/main/resources/skins/policeman.png similarity index 100% rename from src/main/resources/skins/policeman.png rename to common/src/main/resources/skins/policeman.png diff --git a/src/main/resources/skins/smoking.png b/common/src/main/resources/skins/smoking.png similarity index 100% rename from src/main/resources/skins/smoking.png rename to common/src/main/resources/skins/smoking.png diff --git a/core/build.gradle b/core/build.gradle deleted file mode 100644 index fdc3a300..00000000 --- a/core/build.gradle +++ /dev/null @@ -1,3 +0,0 @@ -dependencies { - compileOnly "net.kyori:adventure-platform-api:${adventurePlatformVersion}" -} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java b/core/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java deleted file mode 100644 index b7b532b4..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java +++ /dev/null @@ -1,576 +0,0 @@ -package com.georgev22.skinoverlay; - -import co.aikar.commands.*; -import com.georgev22.library.database.DatabaseType; -import com.georgev22.library.database.DatabaseWrapper; -import com.georgev22.library.maps.HashObjectMap; -import com.georgev22.library.maps.ObjectMap; -import com.georgev22.library.maps.ObjectMap.Pair; -import com.georgev22.library.maps.ObservableObjectMap; -import com.georgev22.library.maps.UnmodifiableObjectMap; -import com.georgev22.library.minecraft.scheduler.MinecraftScheduler; -import com.georgev22.library.utilities.EntityManager; -import com.georgev22.library.yaml.file.FileConfiguration; -import com.georgev22.library.yaml.serialization.ConfigurationSerialization; -import com.georgev22.skinoverlay.commands.SkinOverlayCommand; -import com.georgev22.skinoverlay.event.EventManager; -import com.georgev22.skinoverlay.event.HandlerList; -import com.georgev22.skinoverlay.handler.SProperty; -import com.georgev22.skinoverlay.handler.SkinHandler; -import com.georgev22.skinoverlay.handler.skin.Part; -import com.georgev22.skinoverlay.handler.skin.SkinParts; -import com.georgev22.skinoverlay.hook.SkinHook; -import com.georgev22.skinoverlay.hook.hooks.SkinHookImpl; -import com.georgev22.skinoverlay.hook.hooks.SkinsRestorerHook; -import com.georgev22.skinoverlay.listeners.DebugListeners; -import com.georgev22.skinoverlay.listeners.ObservableListener; -import com.georgev22.skinoverlay.listeners.PlayerListeners; -import com.georgev22.skinoverlay.storage.data.Skin; -import com.georgev22.skinoverlay.storage.data.User; -import com.georgev22.skinoverlay.storage.gsonAdapters.*; -import com.georgev22.skinoverlay.storage.manager.SkinManager; -import com.georgev22.skinoverlay.storage.manager.UserManager; -import com.georgev22.skinoverlay.utilities.Locale; -import com.georgev22.skinoverlay.utilities.PluginMessageUtils; -import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; -import com.georgev22.skinoverlay.utilities.Updater; -import com.georgev22.skinoverlay.utilities.config.FileManager; -import com.georgev22.skinoverlay.utilities.config.MessagesUtil; -import com.georgev22.skinoverlay.utilities.config.OptionsUtil; -import com.georgev22.skinoverlay.utilities.config.SkinFileCache; -import com.georgev22.skinoverlay.utilities.interfaces.SkinOverlayImpl; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import lombok.Getter; -import lombok.Setter; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -import java.io.File; -import java.sql.SQLException; -import java.util.*; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -public final class SkinOverlay { - - @Getter - @Setter - private static SkinOverlay instance = null; - - @Getter - private final SkinOverlayImpl skinOverlay; - - @Getter - @Setter - private SkinHandler skinHandler; - - @Getter - @Setter - private SkinHook skinHook; - - @Getter - private final SkinHook defaultSkinHook = new SkinHookImpl(); - - @Getter - @Setter - @ApiStatus.Internal - private PluginMessageUtils pluginMessageUtils; - - @Getter - private FileManager fileManager; - - @Getter - private DatabaseWrapper databaseWrapper = null; - - @Getter - private File skinsDataFolder; - - @Setter - @ApiStatus.Internal - private CommandManager commandManager; - - @Getter - private UserManager userManager; - - @Getter - private SkinManager skinManager; - - @Getter - private EventManager eventManager; - - @Getter - private SkinFileCache skinFileCache; - - @Setter - @Getter - private MinecraftScheduler minecraftScheduler; - - @Getter - private Gson gson; - - public SkinOverlay(SkinOverlayImpl skinOverlay) { - this.skinOverlay = skinOverlay; - instance = this; - } - - public void onLoad() { - fileManager = FileManager.getInstance(); - try { - fileManager.loadFiles(); - } catch (Exception e) { - throw new RuntimeException(e); - } - try { - MessagesUtil.repairPaths(Locale.fromString(OptionsUtil.LOCALE.getStringValue())); - } catch (Exception e) { - getLogger().log(Level.SEVERE, "Error loading the language file: ", e); - } - eventManager = new EventManager(getLogger(), this.getClass()); - skinFileCache = new SkinFileCache(); - } - - public void onEnable() { - ConfigurationSerialization.registerClass(User.class, "SkinOverlayUser"); - ConfigurationSerialization.registerClass(Skin.class, "SkinOverlaySkin"); - ConfigurationSerialization.registerClass(SkinParts.class, "SkinParts"); - ConfigurationSerialization.registerClass(Part.class, "SkinPart"); - ConfigurationSerialization.registerClass(SProperty.class, "SProperty"); - ConfigurationSerialization.registerClass(SerializableBufferedImage.class, "SerializableBufferedImage"); - this.gson = new GsonBuilder() - .registerTypeAdapter(SerializableBufferedImage.class, new SerializableBufferedImageTypeAdapter()) - .registerTypeAdapter(Part.class, new PartTypeAdapter()) - .registerTypeAdapter(SkinParts.class, new SkinPartsTypeAdapter()) - .registerTypeAdapter(SProperty.class, new SPropertyTypeAdapter()) - .registerTypeAdapter(Skin.class, new SkinTypeAdapter()) - .registerTypeAdapter(User.class, new UserTypeAdapter()) - //.setPrettyPrinting() - .create(); - switch (OptionsUtil.SKIN_HOOK.getStringValue().toLowerCase(Locale.ENGLISH_US.getLocale())) { - case "skinsrestorer" -> { - if (skinOverlay.isPluginEnabled(type().equals(SkinOverlayImpl.Type.VELOCITY) ? "skinsrestorer" : "SkinsRestorer")) { - setSkinHook(new SkinsRestorerHook()); - } - } - case "skinoverlay" -> setSkinHook(new SkinHookImpl()); - default -> setSkinHook(defaultSkinHook); - } - this.skinsDataFolder = new File(this.getDataFolder(), "skins"); - this.skinFileCache.cache(); - - try { - setupDatabase(); - } catch (Exception e) { - this.getLogger().log(Level.SEVERE, "Cannot initialize the database: ", e.getCause()); - } - - new Updater(); - HandlerList.bakeAll(); - eventManager.register(new PlayerListeners()); - if (OptionsUtil.DEBUG.getBooleanValue()) { - getLogger().warning("Debug mode is enabled!"); - getLogger().warning("Be prepared for a lot of log messages"); - eventManager.register(new DebugListeners()); - } - this.setupCommands(); - } - - public void onDisable() { - userManager.saveAll(); - skinManager.saveAll(); - if (databaseWrapper != null && databaseWrapper.isConnected()) { - databaseWrapper.disconnect(); - } - unregisterCommands(); - this.minecraftScheduler.cancelTasks(this.skinOverlay.plugin()); - HandlerList.unregisterAll(); - } - - /** - * Returns the type of server implementation (Bukkit, Bungee or Velocity). - * - * @return The type of server implementation. - */ - public SkinOverlayImpl.Type type() { - return skinOverlay.type(); - } - - /** - * Returns the plugin's data folder. - * - * @return The plugin's data folder. - */ - public File getDataFolder() { - return skinOverlay.dataFolder(); - } - - /** - * Returns the plugin's logger. - * - * @return The plugin's logger. - */ - public Logger getLogger() { - return skinOverlay.logger(); - } - - /** - * Returns the plugin's description. - * - * @return The plugin's description. - */ - public SkinOverlayImpl.Description getDescription() { - return skinOverlay.description(); - } - - /** - * Enables or disables the plugin. - * - * @param enable true to enable, false to disable. - * @return True if the plugin was successfully enabled or disabled, false otherwise. - */ - public boolean setEnable(boolean enable) { - return skinOverlay.enable(enable); - } - - /** - * Returns whether the plugin is currently enabled. - * - * @return True if the plugin is enabled, false otherwise. - */ - public boolean isEnabled() { - return skinOverlay.enabled(); - } - - /** - * Saves a resource from the plugin's JAR file to the plugin's data folder. - * - * @param resource The path of the resource to save. - * @param replace True to replace the file if it already exists, false otherwise. - */ - public void saveResource(@NotNull String resource, boolean replace) { - skinOverlay.saveResource(resource, replace); - } - - /** - * Returns whether the server is running in online mode. - * - * @return True if the server is running in online mode, false otherwise. - */ - public boolean isOnlineMode() { - return skinOverlay.onlineMode(); - } - - /** - * Returns a boolean value indicating whether a specified plugin is enabled. - * - * @param pluginName the name of the plugin to check - * @return {@code true} if the plugin is enabled, {@code false} otherwise. - */ - public boolean isPluginEnabled(String pluginName) { - return skinOverlay.isPluginEnabled(pluginName); - } - - /** - * Returns the plugin instance. - * - * @return The plugin instance. - */ - public T getPlugin() { - return skinOverlay.plugin(); - } - - /** - * Returns the server implementation instance. - * - * @return The server implementation instance. - */ - public Object getServerImplementation() { - return skinOverlay.serverImpl(); - } - - /** - * Returns the version of the server implementation. - * - * @return The version of the server implementation. - */ - public String getServerVersion() { - return skinOverlay.serverVersion(); - } - - /** - * Prints a message to the console. - * - * @param msg The message(s) to print. - */ - public void print(String... msg) { - skinOverlay.print(msg); - } - - /** - * Returns a list of PlayerObjects for all online players. - */ - public List onlinePlayers() { - return new ArrayList<>(skinOverlay.onlinePlayers().values()); - } - - /** - * Returns an UnmodifiableObjectMap of PlayerObject instances representing all online players on the server. - * - * @return An UnmodifiableObjectMap of PlayerObject instances representing all online players on the server. - */ - public UnmodifiableObjectMap onlinePlayersMap() { - return new UnmodifiableObjectMap<>(skinOverlay.onlinePlayers()); - } - - /** - * Returns true if the player with the given name is currently online, and false otherwise. - * - * @param playerName the name of the player to check - */ - public boolean isOnline(String playerName) { - return onlinePlayers().stream().anyMatch(playerObject -> playerObject.playerName().equalsIgnoreCase(playerName)); - } - - /** - * Returns true if the player with the given UUID is currently online, and false otherwise. - * - * @param uuid the UUID of the player to check - */ - public boolean isOnline(UUID uuid) { - return onlinePlayers().stream().anyMatch(playerObject -> playerObject.playerUUID().equals(uuid)); - } - - /** - * Returns an Optional containing the PlayerObject for the player with the given name, - * or an empty Optional if the player is not online. - * - * @param playerName the name of the player to get the PlayerObject for - */ - public Optional getPlayer(String playerName) { - return onlinePlayers().stream().filter(playerObject -> playerObject.playerName().equalsIgnoreCase(playerName)).findFirst(); - } - - /** - * Returns an Optional containing the PlayerObject for the player with the given UUID, - * or an empty Optional if the player is not online. - * - * @param uuid the UUID of the player to get the PlayerObject for - */ - public Optional getPlayer(UUID uuid) { - return onlinePlayers().stream().filter(playerObject -> playerObject.playerUUID().equals(uuid)).findFirst(); - } - - /** - * Returns the configuration for this SkinOverlay. - */ - public FileConfiguration getConfig() { - return fileManager.getConfig().getFileConfiguration(); - } - - /** - * Returns a list of all available overlay names. - */ - public List getOverlayList() { - return Arrays.stream(Objects.requireNonNull(getSkinsDataFolder().listFiles())).map(File::getName).filter(file -> file.endsWith(".png")).map(file -> file.substring(0, file.length() - 4)).collect(Collectors.toList()); - } - - /** - * Sets up a connection to the specified database type, which can be File, MySQL, SQLite, PostgreSQL or MongoDB. - * This method throws a SQLException if there's an issue with the connection or configuration, - * and a ClassNotFoundException - * if the specified database driver can't be found. - * - * @throws SQLException If there's an issue with the connection or configuration. - * @throws ClassNotFoundException If the specified database driver can't be found. - */ - private void setupDatabase() throws Exception { - ObjectMap> map = new HashObjectMap>() - .append("entity_id", Pair.create("VARCHAR(38)", "NULL")) - .append("data", Pair.create("LONGTEXT", "NULL")); - switch (OptionsUtil.DATABASE_TYPE.getStringValue()) { - case "MySQL" -> { - if (databaseWrapper == null || !databaseWrapper.isConnected()) { - databaseWrapper = new DatabaseWrapper(DatabaseType.MYSQL, - OptionsUtil.DATABASE_HOST.getStringValue(), - OptionsUtil.DATABASE_PORT.getIntValue(), - OptionsUtil.DATABASE_USER.getStringValue(), - OptionsUtil.DATABASE_PASSWORD.getStringValue(), - OptionsUtil.DATABASE_DATABASE.getStringValue(), - getLogger()); - sqlConnect(map); - getLogger().log(Level.INFO, "[" + getDescription().name() + "] [" + getDescription().version() + "] Database: MySQL"); - } - } - case "PostgreSQL" -> { - if (databaseWrapper == null || !databaseWrapper.isConnected()) { - databaseWrapper = new DatabaseWrapper(DatabaseType.POSTGRESQL, - OptionsUtil.DATABASE_HOST.getStringValue(), - OptionsUtil.DATABASE_PORT.getIntValue(), - OptionsUtil.DATABASE_USER.getStringValue(), - OptionsUtil.DATABASE_PASSWORD.getStringValue(), - OptionsUtil.DATABASE_DATABASE.getStringValue(), - getLogger()); - sqlConnect(map); - getLogger().log(Level.INFO, "[" + getDescription().name() + "] [" + getDescription().version() + "] Database: PostgreSQL"); - } - } - case "SQLite" -> { - if (databaseWrapper == null || !databaseWrapper.isConnected()) { - databaseWrapper = new DatabaseWrapper(DatabaseType.SQLITE, - getDataFolder().getAbsolutePath(), - 0, - "", - "", - OptionsUtil.DATABASE_FILE_NAME.getStringValue(), - getLogger()); - sqlConnect(map); - getLogger().log(Level.INFO, "[" + getDescription().name() + "] [" + getDescription().version() + "] Database: SQLite"); - } - } - case "MongoDB" -> { - if (databaseWrapper == null || !databaseWrapper.isConnected()) { - databaseWrapper = new DatabaseWrapper(DatabaseType.MONGO, - OptionsUtil.DATABASE_MONGO_HOST.getStringValue(), - OptionsUtil.DATABASE_MONGO_PORT.getIntValue(), - OptionsUtil.DATABASE_MONGO_USER.getStringValue(), - OptionsUtil.DATABASE_MONGO_PASSWORD.getStringValue(), - OptionsUtil.DATABASE_MONGO_DATABASE.getStringValue(), - getLogger()); - databaseWrapper.connect(); - this.userManager = new UserManager(databaseWrapper, OptionsUtil.DATABASE_MONGO_USERS_COLLECTION.getStringValue()); - this.skinManager = new SkinManager(databaseWrapper, OptionsUtil.DATABASE_MONGO_SKINS_COLLECTION.getStringValue()); - getLogger().log(Level.INFO, "[" + getDescription().name() + "] [" + getDescription().version() + "] Database: MongoDB"); - } - } - default -> { - databaseWrapper = null; - this.userManager = new UserManager(new File(this.getDataFolder(), "userdata"), null); - this.skinManager = new SkinManager(new File(this.getDataFolder(), "skindata"), null); - getLogger().log(Level.INFO, "[" + getDescription().name() + "] [" + getDescription().version() + "] Database: File"); - } - } - - userManager.loadAll(); - skinManager.loadAll(); - - onlinePlayers().forEach(player -> userManager.getEntity(player.playerUUID()).handle((user, throwable) -> { - if (throwable != null) { - getLogger().log(Level.SEVERE, "Error retrieving user: ", throwable); - return null; - } - return user; - })); - - } - - private void sqlConnect(ObjectMap> map) throws SQLException, ClassNotFoundException { - databaseWrapper.connect(); - if (databaseWrapper.getSQLDatabase() == null) { - this.getLogger().log(Level.SEVERE, "[" + getDescription().name() + "] [" + getDescription().version() + "] Database is not connected"); - return; - } - databaseWrapper.getSQLDatabase().createTable(OptionsUtil.DATABASE_USERS_TABLE_NAME.getStringValue(), map); - databaseWrapper.getSQLDatabase().createTable(OptionsUtil.DATABASE_SKINS_TABLE_NAME.getStringValue(), map); - this.userManager = new UserManager(databaseWrapper, OptionsUtil.DATABASE_USERS_TABLE_NAME.getStringValue()); - this.skinManager = new SkinManager(databaseWrapper, OptionsUtil.DATABASE_SKINS_TABLE_NAME.getStringValue()); - } - - - /** - * Registers and sets up the commands for this SkinOverlay. - * If the SkinOverlay is not a proxy and the 'PROXY' option is enabled, no commands will be registered. - */ - private void setupCommands() { - if (!type().isProxy() && OptionsUtil.PROXY.getBooleanValue()) { - return; - } - // Enable unstable API for deprecated 'help' command - //noinspection deprecation - commandManager.enableUnstableAPI("help"); - // Load command locales - loadCommandLocales(); - // Register SkinOverlayCommand and command completions if 'COMMAND_SKINOVERLAY' option is enabled - if (OptionsUtil.COMMAND_SKINOVERLAY.getBooleanValue()) { - commandManager.registerCommand(new SkinOverlayCommand()); - commandManager.getCommandCompletions().registerCompletion("overlays", context -> getOverlayList()); - } - } - - /** - * Adds the specified list of {@link ObservableListener}s to this {@link EntityManager} instance. - * Each listener in the - * list will be registered with the {@link ObservableObjectMap} - * that holds the loaded users in the user manager, - * and - * will be notified whenever a new user is added or removed to the map. - * - * @param managerListeners the list of listeners to be added - */ - public void registerUserManagerListeners(@NotNull List> managerListeners) { - for (ObservableListener managerListener : managerListeners) { - this.userManager.getLoadedEntities().addListener(managerListener); - } - } - - /** - * Adds the specified list of {@link ObservableListener}s to this {@link SkinOverlayImpl} instance. - * Each listener in the - * list will be registered with the {@link ObservableObjectMap} - * that holds the online players in the server, - * and - * will be notified whenever a new player is added or removed to the map. - * - * @param pListeners the list of listeners to be added - */ - public void registerOnlinePlayersListeners(@NotNull List> pListeners) { - for (ObservableListener pListener : pListeners) { - this.skinOverlay.onlinePlayers().addListener(pListener); - } - } - - /** - * Unregisters the commands for this SkinOverlay. - * If the SkinOverlay is not a proxy and the 'PROXY' option is enabled, no commands will be unregistered. - */ - private void unregisterCommands() { - if (!type().isProxy() && OptionsUtil.PROXY.getBooleanValue()) - return; - List rootCommands = new ArrayList<>(commandManager.getRegisteredRootCommands()); - rootCommands.forEach(rootCommand -> { - switch (type()) { - case BUKKIT -> ((PaperCommandManager) commandManager).unregisterCommand(rootCommand.getDefCommand()); - case BUNGEE -> ((BungeeCommandManager) commandManager).unregisterCommand(rootCommand.getDefCommand()); - case VELOCITY -> - ((VelocityCommandManager) commandManager).unregisterCommand(rootCommand.getDefCommand()); - } - }); - } - - /** - * Loads the command locales for this SkinOverlay. - * If a 'lang_en.yml' language file exists in the data folder, it will be used as the default language file. - * Otherwise, the default English language file provided by the command manager will be used. - */ - @ApiStatus.Internal - public void loadCommandLocales() { - try { - // Set the default locale to English - commandManager.getLocales().setDefaultLocale(MessagesUtil.getLocale().getLocale()); - // Load the language file based on the server platform - switch (type()) { - case BUNGEE -> ((BungeeCommandManager) commandManager).getLocales() - .loadYamlLanguageFile(MessagesUtil.getMessagesCFG().getFile(), MessagesUtil.getLocale().getLocale()); - case BUKKIT -> ((PaperCommandManager) commandManager).getLocales() - .loadYamlLanguageFile(MessagesUtil.getMessagesCFG().getFile(), MessagesUtil.getLocale().getLocale()); - case VELOCITY -> ((VelocityCommandManager) commandManager).getLocales() - .loadYamlLanguageFile(MessagesUtil.getMessagesCFG().getFile(), MessagesUtil.getLocale().getLocale()); - } - // Enable per-issuer locale support - commandManager.usePerIssuerLocale(true); - } catch (Exception e) { - getLogger().log(Level.SEVERE, "Failed to load language config messages_" + OptionsUtil.LOCALE.getStringValue() + "': ", e); - } - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/commands/SkinOverlayCommand.java b/core/src/main/java/com/georgev22/skinoverlay/commands/SkinOverlayCommand.java deleted file mode 100644 index 3a9430e2..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/commands/SkinOverlayCommand.java +++ /dev/null @@ -1,253 +0,0 @@ -package com.georgev22.skinoverlay.commands; - -import co.aikar.commands.BaseCommand; -import co.aikar.commands.CommandIssuer; -import co.aikar.commands.annotation.*; -import com.georgev22.library.maps.HashObjectMap; -import com.georgev22.library.maps.ObjectMap; -import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.handler.skin.SkinParts; -import com.georgev22.skinoverlay.utilities.Locale; -import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; -import com.georgev22.skinoverlay.utilities.Utilities; -import com.georgev22.skinoverlay.utilities.config.FileManager; -import com.georgev22.skinoverlay.utilities.config.MessagesUtil; -import com.georgev22.skinoverlay.utilities.config.OptionsUtil; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import org.jetbrains.annotations.NotNull; - -import javax.imageio.ImageIO; -import java.io.*; -import java.net.URL; -import java.util.Arrays; -import java.util.Optional; -import java.util.UUID; -import java.util.logging.Level; - -@CommandAlias("skinoverlay|soverlay|skino") -public final class SkinOverlayCommand extends BaseCommand { - private final ObjectMap placeholders = new HashObjectMap<>(); - private final FileManager fm = SkinOverlay.getInstance().getFileManager(); - private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); - - @Default - @HelpCommand - @Subcommand("help") - @CommandAlias("shelp") - @Description("{@@Commands.Descriptions.SkinOverlay.help}") - @CommandPermission("skinoverlay.default") - public void onHelp(final @NotNull CommandIssuer issuer) { - for (String input : Arrays.asList( - "&c&l(!)&c Commands &c&l(!)", - "&6/skinoverlay reload", - "&6/skinoverlay overlay", - "&6/skinoverlay url", - "&6/skinoverlay clear", - "&c&l==============")) { - issuer.sendMessage(LegacyComponentSerializer.legacySection().serialize(LegacyComponentSerializer.legacy('&').deserialize(input))); - } - } - - @Subcommand("reload") - @CommandAlias("skinoverlayreload|soverlayreload|skinoreload|sreload") - @Description("{@@Commands.Descriptions.SkinOverlay.reload}") - @CommandPermission("skinoverlay.reload") - public void reload(final @NotNull CommandIssuer issuer) { - //TODO RELOAD SKIN HANDLER - skinOverlay.getFileManager().getConfig().reloadFile(); - try { - MessagesUtil.repairPaths(Locale.fromString(OptionsUtil.LOCALE.getStringValue())); - MessagesUtil.getMessagesCFG().reloadFile(); - skinOverlay.loadCommandLocales(); - } catch (Exception e) { - skinOverlay.getLogger().log(Level.SEVERE, "Error loading the language file: ", e); - } - issuer.sendMessage(LegacyComponentSerializer.legacySection().serialize(LegacyComponentSerializer.legacy('&').deserialize("&a&l(!)&a Plugin reloaded!"))); - } - - @Subcommand("overlay") - @CommandAlias("wear|swear") - @CommandCompletion("@overlays @players ") - @Description("{@@Commands.Descriptions.SkinOverlay.overlay}") - @CommandPermission("skinoverlay.wear.overlay") - @Syntax("wear [player]") - public void overlay(@NotNull CommandIssuer issuer, String @NotNull [] args) { - if (args.length < 1) { - MessagesUtil.INSUFFICIENT_ARGUMENTS.msg(issuer, new HashObjectMap().append("%command%", "wear "), true); - return; - } - - var overlay = args[0]; - Optional target; - - if (args.length > 1) { - target = getPlayerObject(issuer, args[1]); - if (target.isEmpty()) return; - } else if (!(issuer.isPlayer())) { - MessagesUtil.INSUFFICIENT_ARGUMENTS.msg(issuer, new HashObjectMap().append("%command%", "wear skin "), true); - return; - } else { - target = skinOverlay.getPlayer(issuer.getUniqueId()); - } - SkinParts skinParts; - try { - File overlayFile = new File(skinOverlay.getSkinsDataFolder(), overlay + ".png"); - if (!overlayFile.exists()) { - MessagesUtil.OVERLAY_NOT_FOUND.msg(issuer, new HashObjectMap().append("%overlay%", overlay), true); - return; - } - skinParts = new SkinParts(new SerializableBufferedImage(ImageIO.read(overlayFile)), overlay); - } catch (IOException e) { - skinOverlay.getLogger().log(Level.SEVERE, "Error while trying to load the skin: ", e); - return; - } - skinOverlay.getSkinHandler().retrieveOrGenerateSkin( - target.orElseThrow(), - () -> skinParts.getFullSkin().getBufferedImage(), - skinParts) - .thenAccept(skin -> { - if (skin != null) { - skinOverlay.getSkinHandler().setSkin(target.orElseThrow(), skin); - MessagesUtil.DONE.msg( - issuer, - new HashObjectMap() - .append("%player%", target.orElseThrow().playerName()) - .append("%url%", skin.skinURL()) - .append("%name%", skin.skinParts().getSkinName()) - .append("%skinParts%", skin.skinParts().toString()), - true - ); - } else { - skinOverlay.getLogger().info("Skin is null"); - } - }); - } - - @Subcommand("url") - @CommandAlias("wurl|swurl|wearurl") - @CommandCompletion(" false|true|@players false|true false|true false|true false|true false|true false|true @players") - @Description("{@@Commands.Descriptions.SkinOverlay.url}") - @CommandPermission("skinoverlay.wear.url") - @Syntax("url [player]") - public void url(@NotNull CommandIssuer issuer, String @NotNull [] args) { - if (args.length < 1) { - MessagesUtil.INSUFFICIENT_ARGUMENTS.msg(issuer, new HashObjectMap().append("%command%", "url "), true); - return; - } - - try { - URL url = new URL(args[0]); - Optional target = skinOverlay.getPlayer(issuer.getUniqueId()); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - - try (InputStream stream = url.openStream()) { - byte[] buffer = new byte[4096]; - int bytesRead; - while ((bytesRead = stream.read(buffer)) > -1) { - output.write(buffer, 0, bytesRead); - } - } - - SkinParts skinParts = new SkinParts(new SerializableBufferedImage(ImageIO.read(new ByteArrayInputStream(output.toByteArray()))), "custom"); - - if (args.length > 1) { - target = getPlayerObject(issuer, args[1]); - if (target.isEmpty()) return; - } else if (!(issuer.isPlayer())) { - MessagesUtil.INSUFFICIENT_ARGUMENTS.msg(issuer, new HashObjectMap().append("%command%", "url "), true); - return; - } - - Optional finalTarget = target; - UUID skinUUID = Utilities.generateUUID(url + finalTarget.orElseThrow().playerUUID().toString()); - skinOverlay.getSkinHandler().retrieveOrGenerateSkin( - target.orElseThrow(), - () -> skinParts.getFullSkin().getBufferedImage(), - skinParts).thenAccept(skin -> { - if (skin != null) { - skinOverlay.getSkinHandler().setSkin(finalTarget.orElseThrow(), skin); - MessagesUtil.DONE.msg( - issuer, - new HashObjectMap() - .append("%player%", finalTarget.orElseThrow().playerName()) - .append("%url%", skin.skinURL()), - true - ); - } - }); - } catch (Exception e) { - skinOverlay.getLogger().log(Level.SEVERE, "Error:", e); - } - - } - - private Optional getPlayerObject(@NotNull CommandIssuer issuer, String name) { - Optional target; - if (issuer.hasPermission("skinoverlay.wear.overlay.others")) { - target = skinOverlay.isOnline(name) ? skinOverlay.getPlayer(name) : Optional.empty(); - if (target.isEmpty()) { - MessagesUtil.OFFLINE_PLAYER.msg(issuer, new HashObjectMap().append("%player%", name), true); - return Optional.empty(); - } - } else { - MessagesUtil.NO_PERMISSION.msg(issuer); - return Optional.empty(); - } - return target; - } - - @Subcommand("clear") - @CommandAlias("sclear") - @CommandCompletion("@players ") - @Description("{@@Commands.Descriptions.SkinOverlay.clear}") - @CommandPermission("skinoverlay.wear.clear") - @Syntax("clear [player]") - public void clear(@NotNull CommandIssuer issuer, String @NotNull [] args) { - if (args.length == 0) { - if (!issuer.isPlayer()) { - MessagesUtil.INSUFFICIENT_ARGUMENTS.msg(issuer, new HashObjectMap().append("%command%", "clear "), true); - return; - } - PlayerObject playerObject = skinOverlay.getPlayer(issuer.getUniqueId()).orElseThrow(); - SkinParts skinParts = new SkinParts(null, "default"); - UUID skinUUID = Utilities.generateUUID(skinParts.getSkinName() + playerObject.playerUUID().toString()); - skinOverlay.getSkinHandler().retrieveOrGenerateSkin( - playerObject, - null, - skinParts) - .thenAccept(skin -> { - if (skin != null) { - skinOverlay.getSkinHandler().setSkin(playerObject, skin); - MessagesUtil.RESET.msg( - issuer, - new HashObjectMap().append("%player%", playerObject.playerName()), - true - ); - } - }); - } else { - Optional optionalPlayerObject = skinOverlay.getPlayer(args[0]); - if (optionalPlayerObject.isEmpty()) { - MessagesUtil.OFFLINE_PLAYER.msg(issuer, new HashObjectMap().append("%player%", args[0]), true); - return; - } - SkinParts skinParts = new SkinParts(null, "default"); - UUID skinUUID = Utilities.generateUUID(skinParts.getSkinName() + optionalPlayerObject.orElseThrow().playerUUID().toString()); - skinOverlay.getSkinHandler().retrieveOrGenerateSkin( - optionalPlayerObject.orElseThrow(), - () -> skinParts.getFullSkin().getBufferedImage(), - skinParts) - .thenAccept(skin -> { - if (skin != null) { - skinOverlay.getSkinHandler().setSkin(optionalPlayerObject.orElseThrow(), skin); - MessagesUtil.RESET.msg( - issuer, - new HashObjectMap().append("%player%", optionalPlayerObject.orElseThrow().playerName()), - true - ); - } - }); - } - } -} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/Event.java b/core/src/main/java/com/georgev22/skinoverlay/event/Event.java deleted file mode 100644 index 7440a6a7..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/Event.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.georgev22.skinoverlay.event; - -import org.jetbrains.annotations.NotNull; - -/** - * An interface that represents an event. - */ -public abstract class Event { - - private String name; - private final boolean async; - - /** - * The default constructor is defined for cleaner code. This constructor - * assumes the event is synchronous. - */ - public Event() { - this(false); - } - - /** - * This constructor is used to explicitly declare an event as synchronous - * or asynchronous. - * - * @param isAsync true indicates the event will fire asynchronously, false - * by default from default constructor - */ - public Event(boolean isAsync) { - this.async = isAsync; - } - - @NotNull - public abstract HandlerList getHandlers(); - - /** - * Returns whether this event should be run asynchronously. - * - * @return {@code true} if this event should be run asynchronously, {@code false} otherwise - */ - public final boolean isAsynchronous() { - return async; - } - - /** - * Convenience method for providing a user-friendly identifier. By - * default, it is the event's class's {@linkplain Class#getSimpleName() - * simple name}. - * - * @return name of this event - */ - public String getEventName() { - if (name == null) { - name = getClass().getSimpleName(); - } - return name; - } - -} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/EventManager.java b/core/src/main/java/com/georgev22/skinoverlay/event/EventManager.java deleted file mode 100644 index 2f3c1fbb..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/EventManager.java +++ /dev/null @@ -1,157 +0,0 @@ -package com.georgev22.skinoverlay.event; - -import com.georgev22.library.maps.HashObjectMap; -import com.georgev22.library.maps.ObjectMap; -import com.georgev22.skinoverlay.event.annotations.EventHandler; -import com.georgev22.skinoverlay.event.interfaces.EventListener; -import org.jetbrains.annotations.NotNull; - -import java.lang.reflect.Method; -import java.util.*; -import java.util.logging.Logger; - -/** - * A class that manages events and listeners. - */ -public class EventManager { - - /** - * The logger used for this event manager. - */ - private final Logger logger; - - /** - * The class that owns this event manager. - */ - private final Class clazz; - - /** - * Constructs a new EventManager with the given logger and class. - * - * @param logger the logger to use - * @param clazz the class that owns this event manager - */ - public EventManager(Logger logger, Class clazz) { - this.logger = logger; - this.clazz = clazz; - } - - /** - * Registers one or more listeners to this event manager. - * - * @param listeners the listeners to register - */ - public void register(EventListener @NotNull ... listeners) { - for (EventListener listener : listeners) { - for (Map.Entry, Set> entry : createRegisteredListeners(listener).entrySet()) { - getEventListeners(getRegistrationClass(entry.getKey())).registerAll(entry.getValue()); - } - } - } - - /** - * Unregisters one or more listeners from this event manager. - * - * @param listeners the listeners to unregister - */ - public void unregister(EventListener @NotNull ... listeners) { - for (EventListener listener : listeners) { - HandlerList.unregisterAll(listener.getClass()); - } - } - - /** - * Calls the given event either synchronously or asynchronously, - * depending on the event's {@link Event#isAsynchronous()} - * value. - * Asynchronous event handling is recommended for long-running or potentially blocking tasks, as it allows the - * main thread to continue processing other tasks while the event is being handled. - * Synchronous event handling is recommended - * for quick, lightweight tasks that can be handled quickly without blocking the main thread. - * - * @param event the event to call - */ - public Event callEvent(@NotNull Event event) { - for (ListenerWrapper listenerWrapper : event.getHandlers().getListenerWrappers()) { - listenerWrapper.callEvent(event); - } - return event; - } - - /** - * Creates a map of event classes to listener wrappers for the given listener. - * - * @param listener the listener to create the map for - * @return the map of event classes to listener wrappers - */ - @NotNull - public ObjectMap, Set> createRegisteredListeners(@NotNull EventListener listener) { - ObjectMap, Set> ret = new HashObjectMap<>(); - List methods; - try { - methods = new ArrayList<>(Arrays.asList(listener.getClass().getDeclaredMethods())); - } catch (NoClassDefFoundError e) { - logger.severe("Failed to register events for " + listener.getClass() + " in EventManager of " + clazz.getName() + ". The required class or resource (" + e.getMessage() + ") could not be found."); - return ret; - } - - for (final Method method : methods) { - final EventHandler eh = method.getAnnotation(EventHandler.class); - if (eh == null) continue; - if (method.isBridge() || method.isSynthetic()) { - continue; - } - final Class checkClass; - if (method.getParameterTypes().length != 1 || !Event.class.isAssignableFrom(checkClass = method.getParameterTypes()[0])) { - logger.severe("Failed to register events for " + listener + " in EventManager of " + clazz.getName() + ". The method " + method.getName() + " must have a single parameter of type Event."); - continue; - } - final Class eventClass = checkClass.asSubclass(Event.class); - method.setAccessible(true); - Set eventSet = ret.computeIfAbsent(eventClass, k -> new HashSet<>()); - - eventSet.add(new ListenerWrapper(clazz, listener, method, eh.priority(), eh.ignoreCancelled())); - - } - return ret; - } - - /** - * Gets the handler list for the given event class. - * - * @param type the event class to get the handler list for - * @return the handler list for the given event class - */ - @NotNull - public HandlerList getEventListeners(@NotNull Class type) { - try { - Method method = getRegistrationClass(type).getDeclaredMethod("getHandlerList"); - method.setAccessible(true); - return (HandlerList) method.invoke(null); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - /** - * Gets the registration class for the given event class. - * - * @param clazz the event class to get the registration class for - * @return the registration class for the given event class - */ - @NotNull - public Class getRegistrationClass(@NotNull Class clazz) { - try { - clazz.getDeclaredMethod("getHandlerList"); - return clazz; - } catch (NoSuchMethodException e) { - if (clazz.getSuperclass() != null - && !clazz.getSuperclass().equals(Event.class) - && Event.class.isAssignableFrom(clazz.getSuperclass())) { - return getRegistrationClass(clazz.getSuperclass().asSubclass(Event.class)); - } else { - throw new RuntimeException("Unable to locate the handler list for event " + clazz.getName() + ". Please ensure that a static getHandlerList method is defined."); - } - } - } -} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/EventPriority.java b/core/src/main/java/com/georgev22/skinoverlay/event/EventPriority.java deleted file mode 100644 index c1bac624..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/EventPriority.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.georgev22.skinoverlay.event; - -/** - * Represents the priority level of an event listener. - * Listeners with lower priority will be called before those with higher priority. - * The execution order of listeners with the same priority is undefined. - *

- * It is not recommended to make data changes in listeners with priority {@link #LOWEST}. - * Data changes should be made in listeners with priority {@link #NORMAL} or {@link #HIGHEST}. - *

- * The recommended priority levels for most listeners are {@link #NORMAL} and {@link #HIGHEST}. - */ -public enum EventPriority { - /** - * Lowest priority. Executed first. - * Not recommended for data changes. - */ - LOWEST(-1000), - - /** - * Low priority. - */ - LOW(-500), - - /** - * Normal priority. - * Recommended for most listeners. - */ - NORMAL(0), - - /** - * High priority. - * Recommended for data changes. - */ - HIGH(500), - - /** - * Highest priority. Executed last. - * Recommended for data changes. - */ - HIGHEST(1000); - - private final int value; - - /** - * Constructs an {@code EventPriority} with the given value. - * - * @param value the priority value - */ - EventPriority(int value) { - this.value = value; - } - - /** - * Gets the integer value of this priority level. - * - * @return the integer value of this priority level - */ - public int getValue() { - return value; - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/HandlerList.java b/core/src/main/java/com/georgev22/skinoverlay/event/HandlerList.java deleted file mode 100644 index 6f5d2792..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/HandlerList.java +++ /dev/null @@ -1,264 +0,0 @@ -package com.georgev22.skinoverlay.event; - -import com.georgev22.library.maps.ConcurrentObjectMap; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.UnmodifiableView; - -import java.util.*; - -/** - * This class represents a list of event handlers, sorted by priority. - *

- * It provides methods for registering and unregistering listeners, as well as - * for retrieving a list of all registered listeners. - */ -public class HandlerList { - - /** - * An array of ListenerWrapper objects that represents the registered listeners. - * This array is sorted by priority, with higher-priority listeners appearing - * first in the array. - */ - private volatile ListenerWrapper[] handlers = null; - - /** - * A map that associates each event priority with a list of listeners that - * have that priority. - */ - private final EnumMap> handlerSlots; - - /** - * A static list of all HandlerList objects. - */ - private static final ArrayList allLists = new ArrayList<>(); - - /** - * A set of all event types for which there is at least one registered listener. - */ - private static final Set EVENT_TYPES = ConcurrentObjectMap.newKeySet(); - - /** - * Bakes the handler array for all registered HandlerList objects. This method - * iterates over all HandlerList objects stored in the allLists list and calls the - * bake method on each one in a synchronized block. - *

This method should be called after all listeners have been registered to ensure - * that the handler array is up-to-date and can be used for efficient event dispatching. - */ - public static void bakeAll() { - synchronized (allLists) { - for (HandlerList handlerList : allLists) { - handlerList.bake(); - } - } - } - - /** - * Unregisters all ListenerWrapper objects that are equal to the specified - * object from all HandlerList objects. - */ - public static void unregisterAll() { - synchronized (allLists) { - for (HandlerList handlerList : allLists) { - synchronized (handlerList) { - for (List list : handlerList.handlerSlots.values()) { - list.clear(); - } - handlerList.handlers = null; - } - } - } - } - - /** - * Unregisters all ListenerWrapper objects that are equal to the specified - * object from all HandlerList objects. - * - * @param clazz the Class object to unregister - */ - public static void unregisterAll(@NotNull Class clazz) { - synchronized (allLists) { - for (HandlerList h : allLists) { - h.unregister(clazz); - } - } - } - - /** - * Unregisters all ListenerWrapper objects that are equal to the specified - * object from all HandlerList objects. - * - * @param listener the ListenerWrapper object to unregister - */ - public static void unregisterAll(@NotNull EventListener listener) { - synchronized (allLists) { - for (HandlerList h : allLists) { - h.unregister(listener); - } - } - } - - /** - * Constructor that initializes the HandlerList object. - */ - public HandlerList() { - StackWalker.getInstance(EnumSet.of(StackWalker.Option.RETAIN_CLASS_REFERENCE), 4) - .walk(s -> s.filter(f -> Event.class.isAssignableFrom(f.getDeclaringClass())).findFirst()) - .map(f -> f.getDeclaringClass().getName()) - .ifPresent(EVENT_TYPES::add); - handlerSlots = new EnumMap<>(EventPriority.class); - for (EventPriority o : EventPriority.values()) { - handlerSlots.put(o, new ArrayList<>()); - } - synchronized (allLists) { - allLists.add(this); - } - } - - /** - * Registers a ListenerWrapper object with this HandlerList object. - * - * @param listener the ListenerWrapper object to register - * @throws IllegalStateException if the listener is already registered to - * the specified priority - */ - public synchronized void register(@NotNull ListenerWrapper listener) { - if (handlerSlots.get(listener.eventPriority()).contains(listener)) - throw new IllegalStateException("This listener is already registered to priority " + listener.eventPriority().toString()); - handlers = null; - handlerSlots.get(listener.eventPriority()).add(listener); - } - - /** - * Registers all ListenerWrapper objects in the specified collection with - * this HandlerList object. - * - * @param listeners a collection of ListenerWrapper objects to register - */ - public void registerAll(@NotNull Collection listeners) { - for (ListenerWrapper listener : listeners) { - register(listener); - } - } - - /** - * Unregisters a ListenerWrapper object from this HandlerList object. - * - * @param listener the ListenerWrapper object to unregister - */ - public synchronized void unregister(@NotNull ListenerWrapper listener) { - if (handlerSlots.get(listener.eventPriority()).remove(listener)) { - handlers = null; - } - } - - /** - * Unregisters all ListenerWrapper objects whose associated event type is - * equal to the specified class from all HandlerList objects. - * - * @param clazz the class that represents the event type - */ - public synchronized void unregister(@NotNull Class clazz) { - boolean changed = false; - for (List list : handlerSlots.values()) { - for (ListIterator listenerWrapperListIterator = list.listIterator(); listenerWrapperListIterator.hasNext(); ) { - if (listenerWrapperListIterator.next().aClass().equals(clazz)) { - listenerWrapperListIterator.remove(); - changed = true; - } - } - } - if (changed) handlers = null; - } - - /** - * Unregisters all ListenerWrapper objects whose associated event type is - * equal to the specified class from all HandlerList objects. - * - * @param listener the EventListener that represents the event type - */ - @SuppressWarnings("EqualsBetweenInconvertibleTypes") - public synchronized void unregister(@NotNull EventListener listener) { - boolean changed = false; - for (List list : handlerSlots.values()) { - for (ListIterator listenerWrapperListIterator = list.listIterator(); listenerWrapperListIterator.hasNext(); ) { - if (listener.equals(listenerWrapperListIterator.next().listener())) { - listenerWrapperListIterator.remove(); - changed = true; - } - } - } - if (changed) handlers = null; - } - - /** - * Bakes an array of ListenerWrapper objects by collecting all listeners from the - * handlerSlots map and assigning them to the handler array, which can be used for - * efficient event dispatching. This method does nothing if the handler array has - * already been baked. - *

The order of ListenerWrapper objects in the handler array corresponds to the - * order of their associated EventPriority values, with lower priority values coming - * before higher ones. - */ - public synchronized void bake() { - if (handlers != null) return; - List entries = new ArrayList<>(); - for (Map.Entry> entry : handlerSlots.entrySet()) { - entries.addAll(entry.getValue()); - } - handlers = entries.toArray(new ListenerWrapper[0]); - } - - /** - * Returns a list of all ListenerWrapper objects that are registered with - * this HandlerList object. - * - * @return an array of all registered ListenerWrapper objects - */ - @NotNull - public ListenerWrapper[] getListenerWrappers() { - ListenerWrapper[] handlers; - while ((handlers = this.handlers) == null) bake(); - return handlers; - } - - /** - * Returns a list of all ListenerWrapper objects that are registered with - * any HandlerList object and whose associated event type is equal to the - * specified class. - * - * @param clazz the class that represents the event type - * @return a list of all registered ListenerWrapper objects with the specified event type - */ - @NotNull - public static ArrayList getListenerWrappers(@NotNull Class clazz) { - ArrayList listeners = new ArrayList<>(); - synchronized (allLists) { - for (HandlerList handlerList : allLists) { - synchronized (handlerList) { - for (List list : handlerList.handlerSlots.values()) { - for (ListenerWrapper listener : list) { - if (listener.aClass().equals(clazz)) { - listeners.add(listener); - } - } - } - } - } - } - return listeners; - } - - /** - * Returns an unmodifiable list of all HandlerList objects. - * - * @return an unmodifiable list of all HandlerList objects - */ - @Contract(pure = true) - @NotNull - public static @UnmodifiableView List getHandlerLists() { - synchronized (allLists) { - return Collections.unmodifiableList(allLists); - } - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/ListenerWrapper.java b/core/src/main/java/com/georgev22/skinoverlay/event/ListenerWrapper.java deleted file mode 100644 index 79a84594..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/ListenerWrapper.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.georgev22.skinoverlay.event; - -import com.georgev22.skinoverlay.event.interfaces.Cancellable; -import com.georgev22.skinoverlay.event.interfaces.EventListener; -import com.georgev22.skinoverlay.exceptions.EventException; -import org.jetbrains.annotations.NotNull; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.concurrent.Executor; - -/** - * A wrapper for event listeners, containing metadata about the listener and the method it should invoke when an event - * occurs. - */ -public record ListenerWrapper( - Class aClass, - EventListener listener, - Method method, - EventPriority eventPriority, - boolean ignoreCancelled -) { - - /** - * Creates a new listener wrapper instance. - * - * @param aClass The class that this listener belongs to. - * @param listener The listener objects itself. - * @param method The method to invoke when an event occurs. - * @param eventPriority The priority of this listener relative to other listeners. - * @param ignoreCancelled Whether this listener should ignore cancelled events. - */ - public ListenerWrapper( - Class aClass, - EventListener listener, - Method method, - EventPriority eventPriority, - boolean ignoreCancelled - ) { - this.aClass = aClass; - this.listener = listener; - this.method = method; - this.eventPriority = eventPriority; - this.ignoreCancelled = ignoreCancelled; - this.method.setAccessible(true); - } - - /** - * Invokes the wrapped listener's method with the given event. - * - * @param event The event to pass to the listener. - */ - public void callEvent(@NotNull final Event event) { - if (event instanceof Cancellable) { - if (((Cancellable) event).isCancelled() && ignoreCancelled()) { - return; - } - } - (event.isAsynchronous() ? new AsyncExecutor() : new SyncExecutor()).execute(() -> { - try { - method.invoke(listener, event); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new EventException("Event " + event.getEventName() + " failed to fire due to an error", e); - } - }); - } - - @Override - public String toString() { - return "ListenerWrapper{" + - "EventManager caller=" + aClass.getSimpleName() + - ", listener=" + listener.getClass().getSimpleName() + - ", method=" + method.getName() + - ", eventPriority=" + eventPriority.name() + " (slot: " + eventPriority.getValue() + ")" + - ", ignoreCancelled=" + ignoreCancelled + - '}'; - } - - private static class AsyncExecutor implements Executor { - - /** - * Executes the given command at some time in the future. - * The command - * may execute in a new thread, in a pooled thread, or in the calling - * thread, at the discretion of the {@code Executor} implementation. - * - * @param runnable the runnable task - * @throws java.util.concurrent.RejectedExecutionException if this task cannot be - * accepted for execution - * @throws NullPointerException if command is null - */ - @Override - public void execute(@NotNull Runnable runnable) { - new Thread(runnable).start(); - } - } - - private static class SyncExecutor implements Executor { - - /** - * Executes the given command at some time in the future. - * The command - * may execute in a new thread, in a pooled thread, or in the calling - * thread, at the discretion of the {@code Executor} implementation. - * - * @param runnable the runnable task - * @throws java.util.concurrent.RejectedExecutionException if this task cannot be - * accepted for execution - * @throws NullPointerException if command is null - */ - @Override - public void execute(@NotNull Runnable runnable) { - runnable.run(); - } - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/annotations/EventHandler.java b/core/src/main/java/com/georgev22/skinoverlay/event/annotations/EventHandler.java deleted file mode 100644 index 8e90b4cc..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/annotations/EventHandler.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.georgev22.skinoverlay.event.annotations; - -import com.georgev22.skinoverlay.event.Event; -import com.georgev22.skinoverlay.event.EventPriority; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Annotation that marks a method as an event handler. - * The method must have a single parameter that is a subclass of - * the {@link Event} class. - * By default, the priority is set to {@link EventPriority#NORMAL} and cancellation of the - * event is not ignored. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) -public @interface EventHandler { - - /** - * The priority of the event handler. - * Events are processed in order from lowest to highest priority. - * By default, the - * priority is set to {@link EventPriority#NORMAL}. - * - * @return the priority of the event handler. - */ - EventPriority priority() default EventPriority.NORMAL; - - /** - * Define if the handler ignores a cancelled event. - *

- * If ignoreCancelled is true and the event is cancelled, the method is - * not called. - * Otherwise, the method is always called. - * - * @return whether cancelled events should be ignored - */ - boolean ignoreCancelled() default false; -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerObjectConnectionEvent.java b/core/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerObjectConnectionEvent.java deleted file mode 100644 index b4c6cc58..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerObjectConnectionEvent.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.georgev22.skinoverlay.event.events.player; - -import com.georgev22.skinoverlay.event.HandlerList; -import com.georgev22.skinoverlay.event.interfaces.Cancellable; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import org.jetbrains.annotations.NotNull; - -public class PlayerObjectConnectionEvent extends PlayerObjectEvent implements Cancellable { - - private static final HandlerList handlers = new HandlerList(); - - private final ConnectionType connectionType; - private boolean cancelled = false; - - /** - * Constructs a {@code PlayerObjectConnectionEvent} with the specified player object, connection type and asynchronous status. - * - * @param playerObject the player object associated with this event - * @param connectionType the player connection type - * @param async whether this event should be run asynchronously - */ - public PlayerObjectConnectionEvent(PlayerObject playerObject, ConnectionType connectionType, boolean async) { - super(playerObject, async); - this.connectionType = connectionType; - } - - public ConnectionType getConnectionType() { - return connectionType; - } - - /** - * Cancels the event. - * - * @return {@code true} if the event was successfully cancelled, {@code false} otherwise - */ - @Override - public boolean cancel() { - return cancelled = true; - } - - /** - * Returns whether the event has been cancelled. - * - * @return {@code true} if the event has been cancelled, {@code false} otherwise - */ - @Override - public boolean isCancelled() { - return cancelled; - } - - @NotNull - @Override - public HandlerList getHandlers() { - return handlers; - } - - public static HandlerList getHandlerList() { - return handlers; - } - - public enum ConnectionType { - CONNECT, - DISCONNECT - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerObjectEvent.java b/core/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerObjectEvent.java deleted file mode 100644 index c2cd957f..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerObjectEvent.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.georgev22.skinoverlay.event.events.player; - -import com.georgev22.skinoverlay.event.Event; -import com.georgev22.skinoverlay.event.HandlerList; -import com.georgev22.skinoverlay.event.interfaces.Cancellable; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import org.jetbrains.annotations.NotNull; - -/** - * An event that represents a player object event. - */ -public class PlayerObjectEvent extends Event implements Cancellable { - - private static final HandlerList handlers = new HandlerList(); - - private boolean cancelled = false; - - /** - * The player object associated with this event. - */ - private final PlayerObject playerObject; - - /** - * Constructs a {@code PlayerObjectEvent} with the specified player object and asynchronous status. - * - * @param playerObject the player object associated with this event - * @param async whether this event should be run asynchronously - */ - public PlayerObjectEvent(PlayerObject playerObject, boolean async) { - super(async); - this.playerObject = playerObject; - } - - /** - * Returns the player object associated with this event. - * - * @return the player object associated with this event - */ - public PlayerObject getPlayerObject() { - return playerObject; - } - - @NotNull - @Override - public HandlerList getHandlers() { - return handlers; - } - - public static HandlerList getHandlerList() { - return handlers; - } - - /** - * Cancels the event. - * - * @return {@code true} if the event was successfully cancelled, {@code false} otherwise - */ - @Override - public boolean cancel() { - return cancelled = true; - } - - /** - * Returns whether the event has been cancelled. - * - * @return {@code true} if the event has been cancelled, {@code false} otherwise - */ - @Override - public boolean isCancelled() { - return cancelled; - } -} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerObjectUserEvent.java b/core/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerObjectUserEvent.java deleted file mode 100644 index e467d053..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerObjectUserEvent.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.georgev22.skinoverlay.event.events.player; - -import com.georgev22.skinoverlay.event.HandlerList; -import com.georgev22.skinoverlay.event.events.user.UserEvent; -import com.georgev22.skinoverlay.event.interfaces.Cancellable; -import com.georgev22.skinoverlay.storage.data.User; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import org.jetbrains.annotations.NotNull; - -/** - * An event that represents a user player object event. - */ -public class PlayerObjectUserEvent extends UserEvent implements Cancellable { - - private final static HandlerList handlers = new HandlerList(); - - /** - * The player object associated with this event. - */ - private final PlayerObject playerObject; - private boolean cancelled = false; - - /** - * Constructs a {@code PlayerObjectUserEvent} with the specified user, player object, and asynchronous status. - *

- * - * @param playerObject the player object associated with this event - * @param user the user associated with this event - * @param async whether this event should be run asynchronously - */ - public PlayerObjectUserEvent(@NotNull PlayerObject playerObject, @NotNull User user, boolean async) { - super(user, async); - this.playerObject = playerObject; - } - - /** - * Returns the player object associated with this event. - * - * @return the player object associated with this event - */ - public PlayerObject getPlayerObject() { - return playerObject; - } - - /** - * Cancels the event. - * - * @return {@code true} if the event was successfully cancelled, {@code false} otherwise - */ - @Override - public boolean cancel() { - return cancelled = true; - } - - /** - * Returns whether the event has been cancelled. - * - * @return {@code true} if the event has been cancelled, {@code false} otherwise - */ - @Override - public boolean isCancelled() { - return cancelled; - } - - @NotNull - @Override - public HandlerList getHandlers() { - return handlers; - } - - public static HandlerList getHandlerList() { - return handlers; - } -} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerSkinPartOptionsChangedEvent.java b/core/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerSkinPartOptionsChangedEvent.java deleted file mode 100644 index 63d59681..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerSkinPartOptionsChangedEvent.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.georgev22.skinoverlay.event.events.player; - -import com.georgev22.skinoverlay.event.HandlerList; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import org.jetbrains.annotations.NotNull; - -public class PlayerSkinPartOptionsChangedEvent extends PlayerObjectEvent { - - private static final HandlerList handlers = new HandlerList(); - private final boolean hasSkinPartsChanged; - - /** - * Constructs a {@code PlayerSkinPartOptionsChangedEvent} with the specified player object and asynchronous status. - * - * @param playerObject the player object associated with this event - * @param async whether this event should be run asynchronously - */ - public PlayerSkinPartOptionsChangedEvent(PlayerObject playerObject, boolean hasSkinPartsChanged, boolean async) { - super(playerObject, async); - this.hasSkinPartsChanged = hasSkinPartsChanged; - } - - public boolean hasSkinPartsChanged() { - return hasSkinPartsChanged; - } - - @NotNull - @Override - public HandlerList getHandlers() { - return handlers; - } - - public static HandlerList getHandlerList() { - return handlers; - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/events/player/skin/PlayerObjectPreUpdateSkinEvent.java b/core/src/main/java/com/georgev22/skinoverlay/event/events/player/skin/PlayerObjectPreUpdateSkinEvent.java deleted file mode 100644 index 44a880f6..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/events/player/skin/PlayerObjectPreUpdateSkinEvent.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.georgev22.skinoverlay.event.events.player.skin; - -import com.georgev22.skinoverlay.event.HandlerList; -import com.georgev22.skinoverlay.event.events.player.PlayerObjectUserEvent; -import com.georgev22.skinoverlay.event.interfaces.Cancellable; -import com.georgev22.skinoverlay.storage.data.User; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import org.jetbrains.annotations.NotNull; - -/** - * Represents an event that is called before the player's skin is updated. - */ -public class PlayerObjectPreUpdateSkinEvent extends PlayerObjectUserEvent implements Cancellable { - - private static final HandlerList handlers = new HandlerList(); - - private boolean cancelled = false; - - /** - * This event is called when a player's skin is about to be updated. - * It is called before the new skin options are applied to the player - * and allows other plugins to modify the skin options - * or cancel the skin update entirely. - * - *

The event can be cancelled to prevent the skin from being updated. If the event is cancelled, the player's skin will not be changed.

- * - *

The event is fired asynchronously by default.

- * - * @param playerObject The player object being updated. - * @param user The user associated with the player object. - * @param async Whether the event is asynchronous. - */ - public PlayerObjectPreUpdateSkinEvent(PlayerObject playerObject, User user, boolean async) { - super(playerObject, user, async); - } - - /** - * Cancels the event. - * - * @return {@code true} if the event was successfully cancelled, {@code false} otherwise - */ - @Override - public boolean cancel() { - return cancelled = true; - } - - /** - * Returns whether the event has been cancelled. - * - * @return {@code true} if the event has been cancelled, {@code false} otherwise - */ - @Override - public boolean isCancelled() { - return cancelled; - } - - - @NotNull - @Override - public HandlerList getHandlers() { - return handlers; - } - - public static HandlerList getHandlerList() { - return handlers; - } - -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/events/player/skin/PlayerObjectUpdateSkinEvent.java b/core/src/main/java/com/georgev22/skinoverlay/event/events/player/skin/PlayerObjectUpdateSkinEvent.java deleted file mode 100644 index 63580fa2..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/events/player/skin/PlayerObjectUpdateSkinEvent.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.georgev22.skinoverlay.event.events.player.skin; - -import com.georgev22.skinoverlay.event.HandlerList; -import com.georgev22.skinoverlay.event.events.player.PlayerObjectUserEvent; -import com.georgev22.skinoverlay.event.interfaces.Cancellable; -import com.georgev22.skinoverlay.storage.data.Skin; -import com.georgev22.skinoverlay.storage.data.User; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * This event is called when a player's skin is ready to be updated in the game. - * This event is cancellable, which allows you to prevent the player's skin from being updated. - * If you want to modify the skin options of a player, use {@link PlayerObjectPreUpdateSkinEvent}. - */ -public class PlayerObjectUpdateSkinEvent extends PlayerObjectUserEvent implements Cancellable { - - /** - * A list of all registered handlers for this event. - */ - private static final HandlerList handlers = new HandlerList(); - - /** - * The skin of the player. - */ - private Skin skin; - private boolean cancelled = false; - - /** - * Constructs a new PlayerObjectUpdateSkinEvent. - * - * @param playerObject the player object related to this event. - * @param user the user who owns the player object. - * @param skin the skin to update the player's skin to. - * @param async whether this event should be handled asynchronously or not. - * If true, the event will be handled on a separate thread. - * If false, the event will be handled on the main thread. - */ - public PlayerObjectUpdateSkinEvent(PlayerObject playerObject, User user, @Nullable Skin skin, boolean async) { - super(playerObject, user, async); - this.skin = skin; - } - - /** - * Returns the skin options of the player. - * If the skin options are null, this method will try to retrieve the skin options from the user's custom data. - * - * @return the skin options of the player. - */ - public Skin getSkin() { - return skin == null ? getUser().skin() : skin; - } - - /** - * Sets the skin options of the player. - * - * @param skin the new skin of the player. - */ - public void setSkin(Skin skin) { - getUser().addCustomData("skin", this.skin = skin); - } - - /** - * Cancels the event. - * - * @return {@code true} if the event was successfully cancelled, {@code false} otherwise - */ - @Override - public boolean cancel() { - return cancelled = true; - } - - /** - * Returns whether the event has been cancelled. - * - * @return {@code true} if the event has been cancelled, {@code false} otherwise - */ - @Override - public boolean isCancelled() { - return cancelled; - } - - /** - * Returns a list of all registered handlers for this event. - * - * @return a list of event handlers - */ - @NotNull - @Override - public HandlerList getHandlers() { - return handlers; - } - - /** - * Returns a list of all registered handlers for this event. - * - * @return a list of event handlers - */ - public static HandlerList getHandlerList() { - return handlers; - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/events/profile/ProfileCreatedEvent.java b/core/src/main/java/com/georgev22/skinoverlay/event/events/profile/ProfileCreatedEvent.java deleted file mode 100644 index 813d12de..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/events/profile/ProfileCreatedEvent.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.georgev22.skinoverlay.event.events.profile; - -import com.georgev22.library.maps.UnmodifiableObjectMap; -import com.georgev22.skinoverlay.event.Event; -import com.georgev22.skinoverlay.event.HandlerList; -import com.georgev22.skinoverlay.handler.SGameProfile; -import com.georgev22.skinoverlay.handler.SProperty; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.UnmodifiableView; - -import java.util.UUID; - -/** - * An event that is fired when a new SGameProfile is created. - */ -public class ProfileCreatedEvent extends Event { - - private static final HandlerList handlers = new HandlerList(); - - private final String name; - private final UUID uuid; - private final UnmodifiableObjectMap properties; - private final SGameProfile profile; - - /** - * Constructs a new ProfileCreatedEvent with the given SGameProfile and whether the event should be run asynchronously. - * - * @param sGameProfile the SGameProfile that was created - * @param async whether the event should be run asynchronously - */ - public ProfileCreatedEvent(@NotNull SGameProfile sGameProfile, boolean async) { - super(async); - this.name = sGameProfile.getName(); - this.uuid = sGameProfile.getUUID(); - this.properties = sGameProfile.getProperties(); - this.profile = sGameProfile; - } - - /** - * Returns the name associated with the created profile. - * - * @return the name associated with the created profile - */ - public String getName() { - return name; - } - - /** - * Returns the UUID associated with the created profile. - * - * @return the UUID associated with the created profile - */ - public UUID getUUID() { - return uuid; - } - - /** - * Returns an unmodifiable view of the properties associated with the created profile. - * - * @return an unmodifiable view of the properties associated with the created profile - */ - @UnmodifiableView - public UnmodifiableObjectMap getProperties() { - return properties; - } - - /** - * Returns the SGameProfile that was created. - * - * @return the SGameProfile that was created - */ - public SGameProfile getProfile() { - return profile; - } - - @NotNull - @Override - public HandlerList getHandlers() { - return handlers; - } - - public static HandlerList getHandlerList() { - return handlers; - } - -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/events/profile/property/SPropertyAddEvent.java b/core/src/main/java/com/georgev22/skinoverlay/event/events/profile/property/SPropertyAddEvent.java deleted file mode 100644 index 973acba8..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/events/profile/property/SPropertyAddEvent.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.georgev22.skinoverlay.event.events.profile.property; - -import com.georgev22.skinoverlay.event.Event; -import com.georgev22.skinoverlay.event.HandlerList; -import com.georgev22.skinoverlay.event.interfaces.Cancellable; -import com.georgev22.skinoverlay.handler.SProperty; -import org.jetbrains.annotations.NotNull; - -/** - * An event that is fired when a SProperty is added. - */ -public class SPropertyAddEvent extends Event implements Cancellable { - - private static final HandlerList handlers = new HandlerList(); - - private String propertyName; - private SProperty property; - private boolean cancelled = false; - - /** - * Constructs a new SPropertyAddEvent with the given property name, SProperty, and whether the event should be run asynchronously. - * - * @param propertyName the name of the property that was added - * @param property the SProperty that was added - * @param async whether the event should be run asynchronously - */ - public SPropertyAddEvent(String propertyName, SProperty property, boolean async) { - super(async); - this.propertyName = propertyName; - this.property = property; - } - - /** - * Returns the name of the property that was added. - * - * @return the name of the property that was added - */ - public String getPropertyName() { - return propertyName; - } - - /** - * Returns the SProperty that was added. - * - * @return the SProperty that was added - */ - public SProperty getProperty() { - return property; - } - - /** - * Sets the name of the property that was added. - * - * @param propertyName the new name of the property that was added - */ - public void setPropertyName(String propertyName) { - this.propertyName = propertyName; - } - - /** - * Sets the SProperty that was added. - * - * @param property the new SProperty that was added - */ - public void setProperty(SProperty property) { - this.property = property; - } - - /** - * Cancels the event. - * - * @return {@code true} if the event was successfully cancelled, {@code false} otherwise - */ - @Override - public boolean cancel() { - return cancelled = true; - } - - /** - * Returns whether the event has been cancelled. - * - * @return {@code true} if the event has been cancelled, {@code false} otherwise - */ - @Override - public boolean isCancelled() { - return cancelled; - } - - @NotNull - @Override - public HandlerList getHandlers() { - return handlers; - } - - public static HandlerList getHandlerList() { - return handlers; - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/events/profile/property/SPropertyRemoveEvent.java b/core/src/main/java/com/georgev22/skinoverlay/event/events/profile/property/SPropertyRemoveEvent.java deleted file mode 100644 index 3d6af261..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/events/profile/property/SPropertyRemoveEvent.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.georgev22.skinoverlay.event.events.profile.property; - -import com.georgev22.skinoverlay.event.Event; -import com.georgev22.skinoverlay.event.HandlerList; -import com.georgev22.skinoverlay.event.interfaces.Cancellable; -import com.georgev22.skinoverlay.handler.SProperty; -import org.jetbrains.annotations.NotNull; - -/** - * An event that is fired when a SProperty is removed. - */ -public class SPropertyRemoveEvent extends Event implements Cancellable { - - private static final HandlerList handlers = new HandlerList(); - - private final String propertyName; - private final SProperty property; - private boolean cancelled = false; - - /** - * Constructs a new SPropertyRemoveEvent with the given property name, SProperty, and whether the event should be run asynchronously. - * - * @param propertyName the name of the property that was removed - * @param property the SProperty that was removed - * @param async whether the event should be run asynchronously - */ - public SPropertyRemoveEvent(String propertyName, SProperty property, boolean async) { - super(async); - this.propertyName = propertyName; - this.property = property; - } - - /** - * Returns the name of the property that was removed. - * - * @return the name of the property that was removed - */ - public String getPropertyName() { - return propertyName; - } - - /** - * Returns the SProperty that was removed. - * - * @return the SProperty that was removed - */ - public SProperty getProperty() { - return property; - } - - /** - * Cancels the event. - * - * @return {@code true} if the event was successfully cancelled, {@code false} otherwise - */ - @Override - public boolean cancel() { - return cancelled = true; - } - - /** - * Returns whether the event has been cancelled. - * - * @return {@code true} if the event has been cancelled, {@code false} otherwise - */ - @Override - public boolean isCancelled() { - return cancelled; - } - - @NotNull - @Override - public HandlerList getHandlers() { - return handlers; - } - - public static HandlerList getHandlerList() { - return handlers; - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/events/user/UserEvent.java b/core/src/main/java/com/georgev22/skinoverlay/event/events/user/UserEvent.java deleted file mode 100644 index bbfaa02b..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/events/user/UserEvent.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.georgev22.skinoverlay.event.events.user; - -import com.georgev22.skinoverlay.event.Event; -import com.georgev22.skinoverlay.event.HandlerList; -import com.georgev22.skinoverlay.event.interfaces.Cancellable; -import com.georgev22.skinoverlay.storage.data.User; -import org.jetbrains.annotations.NotNull; - -/** - * An event that represents a user event. - */ -public class UserEvent extends Event implements Cancellable { - - private static final HandlerList handlers = new HandlerList(); - - /** - * The user associated with this event. - */ - private final @NotNull User user; - - /** - * Whether this event has been cancelled. - */ - private boolean cancelled = false; - - /** - * Constructs a {@code UserEvent} with the specified user and asynchronous status. - * - * @param user the user associated with this event - * @param async whether this event should be run asynchronously - */ - public UserEvent(@NotNull User user, boolean async) { - super(async); - this.user = user; - } - - /** - * Returns the user associated with this event. - * - * @return the user associated with this event - */ - public @NotNull User getUser() { - return user; - } - - /** - * Cancels this event. - * - * @return {@code true} if the event was cancelled, {@code false} otherwise - */ - @Override - public boolean cancel() { - return cancelled = true; - } - - /** - * Returns whether this event has been cancelled. - * - * @return {@code true} if this event has been cancelled, {@code false} otherwise - */ - @Override - public boolean isCancelled() { - return cancelled; - } - - @Override - public @NotNull HandlerList getHandlers() { - return handlers; - } - - public static HandlerList getHandlerList() { - return handlers; - } -} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/events/user/data/UserModifyDataEvent.java b/core/src/main/java/com/georgev22/skinoverlay/event/events/user/data/UserModifyDataEvent.java deleted file mode 100644 index cbd60cf8..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/events/user/data/UserModifyDataEvent.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.georgev22.skinoverlay.event.events.user.data; - -import com.georgev22.skinoverlay.event.HandlerList; -import com.georgev22.skinoverlay.event.events.user.UserEvent; -import com.georgev22.skinoverlay.event.interfaces.Cancellable; -import com.georgev22.skinoverlay.storage.data.User; -import org.jetbrains.annotations.NotNull; - -/** - * An event that represents a modification of user data. - */ -public class UserModifyDataEvent extends UserEvent implements Cancellable { - - private static final HandlerList handlers = new HandlerList(); - - private boolean cancelled = false; - - /** - * Constructs a {@code UserModifyDataEvent} with the specified user and asynchronous status. - * - * @param user the user associated with this event - * @param async whether this event should be run asynchronously - */ - public UserModifyDataEvent(@NotNull User user, boolean async) { - super(user, async); - } - - /** - * Cancels the event. - * - * @return {@code true} if the event was successfully cancelled, {@code false} otherwise - */ - @Override - public boolean cancel() { - return cancelled = true; - } - - /** - * Returns whether the event has been cancelled. - * - * @return {@code true} if the event has been cancelled, {@code false} otherwise - */ - @Override - public boolean isCancelled() { - return cancelled; - } - - @NotNull - @Override - public HandlerList getHandlers() { - return handlers; - } - - public static HandlerList getHandlerList() { - return handlers; - } -} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/events/user/data/add/UserAddDataEvent.java b/core/src/main/java/com/georgev22/skinoverlay/event/events/user/data/add/UserAddDataEvent.java deleted file mode 100644 index d34c7421..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/events/user/data/add/UserAddDataEvent.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.georgev22.skinoverlay.event.events.user.data.add; - -import com.georgev22.library.maps.ObjectMap; -import com.georgev22.skinoverlay.event.HandlerList; -import com.georgev22.skinoverlay.event.events.user.data.UserModifyDataEvent; -import com.georgev22.skinoverlay.event.interfaces.Cancellable; -import com.georgev22.skinoverlay.storage.data.User; -import org.jetbrains.annotations.NotNull; - -/** - * An event that represents the addition of data to a user. - */ -public class UserAddDataEvent extends UserModifyDataEvent implements Cancellable { - - private static final HandlerList handlers = new HandlerList(); - - private ObjectMap.Pair objectPair; - private boolean cancelled = false; - - /** - * Constructs a {@code UserAddDataEvent} with the specified user, asynchronous status, and data. - * - * @param user the user associated with this event - * @param async whether this event should be run asynchronously - * @param objectPair the data being added to the user - */ - public UserAddDataEvent(@NotNull User user, ObjectMap.@NotNull Pair objectPair, boolean async) { - super(user, async); - this.objectPair = objectPair; - } - - /** - * Returns the data being added to the user. - * - * @return the data being added to the user - */ - public ObjectMap.Pair getData() { - return objectPair; - } - - /** - * Sets the data being added to the user. - * - * @param objectPair the data being added to the user - */ - public void setData(ObjectMap.Pair objectPair) { - this.objectPair = objectPair; - } - - /** - * Cancels the event. - * - * @return {@code true} if the event was successfully cancelled, {@code false} otherwise - */ - @Override - public boolean cancel() { - return cancelled = true; - } - - /** - * Returns whether the event has been cancelled. - * - * @return {@code true} if the event has been cancelled, {@code false} otherwise - */ - @Override - public boolean isCancelled() { - return cancelled; - } - - @NotNull - @Override - public HandlerList getHandlers() { - return handlers; - } - - public static HandlerList getHandlerList() { - return handlers; - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/events/user/data/load/UserPostLoadEvent.java b/core/src/main/java/com/georgev22/skinoverlay/event/events/user/data/load/UserPostLoadEvent.java deleted file mode 100644 index 35da95a7..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/events/user/data/load/UserPostLoadEvent.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.georgev22.skinoverlay.event.events.user.data.load; - -import com.georgev22.skinoverlay.event.HandlerList; -import com.georgev22.skinoverlay.event.events.user.UserEvent; -import com.georgev22.skinoverlay.event.interfaces.Cancellable; -import com.georgev22.skinoverlay.storage.data.User; -import org.jetbrains.annotations.NotNull; - -/** - * An event that is triggered after a user's data is loaded. - */ -public class UserPostLoadEvent extends UserEvent implements Cancellable { - - private static final HandlerList handlers = new HandlerList(); - - private boolean cancelled = false; - - /** - * Constructs a {@code UserPostLoadEvent} with the specified user and asynchronous status. - * - * @param user the user associated with this event - * @param async whether this event should be run asynchronously - */ - public UserPostLoadEvent(@NotNull User user, boolean async) { - super(user, async); - } - - /** - * Cancels the event. - * - * @return {@code true} if the event was successfully cancelled, {@code false} otherwise - */ - @Override - public boolean cancel() { - return cancelled = true; - } - - /** - * Returns whether the event has been cancelled. - * - * @return {@code true} if the event has been cancelled, {@code false} otherwise - */ - @Override - public boolean isCancelled() { - return cancelled; - } - - @NotNull - @Override - public HandlerList getHandlers() { - return handlers; - } - - public static HandlerList getHandlerList() { - return handlers; - } -} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/events/user/data/load/UserPreLoadEvent.java b/core/src/main/java/com/georgev22/skinoverlay/event/events/user/data/load/UserPreLoadEvent.java deleted file mode 100644 index e0a76a89..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/events/user/data/load/UserPreLoadEvent.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.georgev22.skinoverlay.event.events.user.data.load; - -import com.georgev22.skinoverlay.event.HandlerList; -import com.georgev22.skinoverlay.event.events.user.UserEvent; -import com.georgev22.skinoverlay.event.interfaces.Cancellable; -import com.georgev22.skinoverlay.storage.data.User; -import org.jetbrains.annotations.NotNull; - -import java.util.UUID; - -/** - * An event that is triggered before a user's data is loaded. - */ -public class UserPreLoadEvent extends UserEvent implements Cancellable { - - private static final HandlerList handlers = new HandlerList(); - - private final UUID uuid; - private boolean cancelled = false; - - /** - * Constructs a {@code UserPreLoadEvent} with the specified UUID and asynchronous status. - * - * @param uuid the UUID of the user associated with this event - * @param async whether this event should be run asynchronously - */ - public UserPreLoadEvent(@NotNull UUID uuid, boolean async) { - super(new User(uuid), async); - this.uuid = uuid; - } - - /** - * Returns the UUID of the user associated with this event. - * - * @return the UUID of the user associated with this event - */ - public UUID getUUID() { - return uuid; - } - - /** - * Cancels the event. - * - * @return {@code true} if the event was successfully cancelled, {@code false} otherwise - */ - @Override - public boolean cancel() { - return cancelled = true; - } - - /** - * Returns whether the event has been cancelled. - * - * @return {@code true} if the event has been cancelled, {@code false} otherwise - */ - @Override - public boolean isCancelled() { - return cancelled; - } - - @NotNull - @Override - public HandlerList getHandlers() { - return handlers; - } - - public static HandlerList getHandlerList() { - return handlers; - } -} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/interfaces/Cancellable.java b/core/src/main/java/com/georgev22/skinoverlay/event/interfaces/Cancellable.java deleted file mode 100644 index 17fb2f10..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/interfaces/Cancellable.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.georgev22.skinoverlay.event.interfaces; - -/** - * An interface representing an event that can be cancelled. - */ -public interface Cancellable { - - /** - * Cancels the event. - * - * @return {@code true} if the event was successfully cancelled, {@code false} otherwise - */ - boolean cancel(); - - /** - * Returns whether the event has been cancelled. - * - * @return {@code true} if the event has been cancelled, {@code false} otherwise - */ - boolean isCancelled(); - -} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/event/interfaces/EventListener.java b/core/src/main/java/com/georgev22/skinoverlay/event/interfaces/EventListener.java deleted file mode 100644 index 9ebca4ef..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/event/interfaces/EventListener.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.georgev22.skinoverlay.event.interfaces; - -/** - * An interface representing an event listener. - */ -public interface EventListener { - -} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/exceptions/EventException.java b/core/src/main/java/com/georgev22/skinoverlay/exceptions/EventException.java deleted file mode 100644 index 416bd941..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/exceptions/EventException.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.georgev22.skinoverlay.exceptions; - -public class EventException extends RuntimeException { - - public EventException(String message) { - super(message); - } - - public EventException(String message, Throwable cause) { - super(message, cause); - } - - public EventException(Throwable cause) { - super(cause); - } - - public EventException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/exceptions/SkinException.java b/core/src/main/java/com/georgev22/skinoverlay/exceptions/SkinException.java deleted file mode 100644 index 72c5f28f..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/exceptions/SkinException.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.georgev22.skinoverlay.exceptions; - -public class SkinException extends RuntimeException { - - public SkinException(String message) { - super(message); - } - - public SkinException(String message, Throwable cause) { - super(message, cause); - } - - public SkinException(Throwable cause) { - super(cause); - } - - public SkinException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/exceptions/UserException.java b/core/src/main/java/com/georgev22/skinoverlay/exceptions/UserException.java deleted file mode 100644 index c0102ca3..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/exceptions/UserException.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.georgev22.skinoverlay.exceptions; - -public class UserException extends RuntimeException { - - public UserException(String message) { - super(message); - } - - public UserException(String message, Throwable cause) { - super(message, cause); - } - - public UserException(Throwable cause) { - super(cause); - } - - public UserException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } - -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/handler/SGameProfile.java b/core/src/main/java/com/georgev22/skinoverlay/handler/SGameProfile.java deleted file mode 100755 index 4315d394..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/handler/SGameProfile.java +++ /dev/null @@ -1,187 +0,0 @@ -package com.georgev22.skinoverlay.handler; - -import com.georgev22.library.maps.HashObjectMap; -import com.georgev22.library.maps.ObjectMap; -import com.georgev22.library.maps.UnmodifiableObjectMap; -import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.event.events.profile.ProfileCreatedEvent; -import com.georgev22.skinoverlay.event.events.profile.property.SPropertyAddEvent; -import com.georgev22.skinoverlay.event.events.profile.property.SPropertyRemoveEvent; -import com.georgev22.skinoverlay.storage.data.Skin; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.UnmodifiableView; - -import java.util.UUID; - -/** - * The SGameProfile class represents a simplified version of a GameProfile, containing a player's name, UUID, and properties. - */ -@ApiStatus.OverrideOnly -public abstract class SGameProfile { - - protected final SkinOverlay skinOverlay = SkinOverlay.getInstance(); - - /** - * The player's name. - */ - private final String name; - - /** - * The player's UUID. - */ - private final UUID uuid; - - @Nullable - private final Skin skin; - - /** - * The player's properties. - */ - private final ObjectMap properties; - - /** - * Constructs an {@link SGameProfile} object with the specified name and UUID, and an empty skin and properties map. - * - * @param name the player's name - * @param uuid the player's UUID - */ - public SGameProfile(String name, UUID uuid) { - this.name = name; - this.uuid = uuid; - this.properties = new HashObjectMap<>(); - this.skin = null; - this.skinOverlay.getEventManager().callEvent(new ProfileCreatedEvent(this, false)); - } - - /** - * Constructs an {@link SGameProfile} object with the specified name, UUID, and skin from the property map. - * - * @param name the player's name - * @param uuid the player's UUID - * @param properties the player's properties map - */ - public SGameProfile(String name, UUID uuid, @NotNull ObjectMap properties) { - this.name = name; - this.uuid = uuid; - this.properties = properties; - this.skin = null; - this.skinOverlay.getEventManager().callEvent(new ProfileCreatedEvent(this, false)); - } - - /** - * Constructs an {@link SGameProfile} object with the specified name, UUID, and skin. - * - * @param name the player's name - * @param uuid the player's UUID - * @param skin the player's skin - */ - public SGameProfile(String name, UUID uuid, @NotNull Skin skin) { - this.name = name; - this.uuid = uuid; - this.skin = skin; - this.properties = new HashObjectMap().append("textures", skin.skinProperty()); - this.skinOverlay.getEventManager().callEvent(new ProfileCreatedEvent(this, false)); - } - - - /** - * Returns the player's name. - * - * @return the player's name - */ - public String getName() { - return name; - } - - /** - * Returns the player's UUID. - * - * @return the player's UUID - */ - public UUID getUUID() { - return uuid; - } - - /** - * Returns the player's Skin object - * - * @return the player's Skin object - */ - public @Nullable Skin getSkin() { - return skin; - } - - /** - * Returns an unmodifiable view of the player's properties map. - * - * @return an unmodifiable view of the player's properties map - */ - @UnmodifiableView - public UnmodifiableObjectMap getProperties() { - return new UnmodifiableObjectMap<>(properties); - } - - /** - * Adds a property to the player's properties map. - * - * @param propertyName the name of the property to add - * @param sProperty the {@link SProperty} object representing the property to add - * @return this {@link SGameProfile} object - */ - public SGameProfile addProperty(String propertyName, SProperty sProperty) { - if (sProperty == null) { - return this; - } - SPropertyAddEvent sPropertyAddEvent = new SPropertyAddEvent(propertyName, sProperty, false); - skinOverlay.getEventManager().callEvent(sPropertyAddEvent); - if (sPropertyAddEvent.isCancelled()) { - return this; - } - this.properties.append(sPropertyAddEvent.getPropertyName(), sPropertyAddEvent.getProperty()); - this.apply(); - return this; - } - - /** - * Removes a property from the player's properties map. - * - * @param propertyName the name of the property to remove - * @return this {@link SGameProfile} object - */ - public SGameProfile removeProperty(String propertyName) { - if (!this.properties.containsKey(propertyName)) { - return this; - } - SPropertyRemoveEvent sPropertyRemoveEvent = new SPropertyRemoveEvent(propertyName, this.properties.get(propertyName), false); - skinOverlay.getEventManager().callEvent(sPropertyRemoveEvent); - if (sPropertyRemoveEvent.isCancelled()) { - return this; - } - this.properties.remove(sPropertyRemoveEvent.getPropertyName()); - this.apply(); - return this; - } - - /** - * Returns the {@link SProperty} associated with the specified property name. - * - * @param propertyName the name of the property to retrieve - * @return the {@code SProperty} object associated with the given property name - */ - public SProperty getProperty(String propertyName) { - return this.properties.get(propertyName); - } - - public abstract void apply(); - - @Override - public String toString() { - return "SGameProfile{" + - "name='" + name + '\'' + - ", uuid=" + uuid + - ", properties=" + properties + - '}'; - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/handler/SProperty.java b/core/src/main/java/com/georgev22/skinoverlay/handler/SProperty.java deleted file mode 100755 index b1215e63..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/handler/SProperty.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.georgev22.skinoverlay.handler; - -import com.georgev22.library.yaml.serialization.ConfigurationSerializable; -import com.georgev22.library.yaml.serialization.SerializableAs; -import com.georgev22.skinoverlay.SkinOverlay; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Unmodifiable; - -import java.util.Map; - -/** - * Represents a property with a name, value, and signature. - */ -@SerializableAs("SProperty") -public record SProperty(String name, String value, String signature) implements ConfigurationSerializable { - - /** - * Constructs a new {@code SProperty} with the given name, value, and signature. - * - * @param name the name of the property - * @param value the value of the property - * @param signature the signature of the property - */ - public SProperty { - } - - /** - * Returns the name of this property. - * - * @return the name of this property - */ - public String name() { - return name; - } - - /** - * Returns the value of this property. - * - * @return the value of this property - */ - public String value() { - return value; - } - - /** - * Returns the signature of this property. - * - * @return the signature of this property - */ - public String signature() { - return signature; - } - - /** - * Returns a string representation of this property. - * - * @return a string representation of this property - */ - @Override - public String toString() { - return "SProperty{" + - "name='" + name + '\'' + - ", value='" + value + '\'' + - ", signature='" + signature + '\'' + - '}'; - } - - /** - * Serializes the SProperty object to a JSON string. - * - * @return The serialized JSON string. - */ - public String toJson() { - return SkinOverlay.getInstance().getGson().toJson(this); - } - - /** - * Deserializes a JSON string to a SProperty object. - * - * @param json The JSON string to deserialize. - * @return The deserialized SProperty object. - */ - public static SProperty fromJson(String json) { - return SkinOverlay.getInstance().getGson().fromJson(json, SProperty.class); - } - - /** - * Serializes the {@code SProperty} to a YAML-compatible map for serialization. - * - * @return A YAML-compatible map containing name, value, and signature information. - */ - @Contract(pure = true) - @Override - public @NotNull @Unmodifiable Map serialize() { - return Map.of("name", name, "value", value, "signature", signature); - } - - /** - * Deserializes the YAML-compatible map to an {@code SProperty}. - * - * @param map The YAML-compatible map containing name, value, and signature information. - * @return The deserialized {@code SProperty}. - */ - @Contract(pure = true) - public static @NotNull SProperty deserialize(@NotNull Map map) { - return new SProperty((String) map.get("name"), (String) map.get("value"), (String) map.get("signature")); - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_Unsupported.java b/core/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_Unsupported.java deleted file mode 100644 index e2a79240..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_Unsupported.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.georgev22.skinoverlay.handler.handlers; - -import com.georgev22.library.maps.HashObjectMap; -import com.georgev22.library.maps.ObjectMap; -import com.georgev22.skinoverlay.handler.SGameProfile; -import com.georgev22.skinoverlay.handler.SProperty; -import com.georgev22.skinoverlay.handler.SkinHandler; -import com.georgev22.skinoverlay.handler.profile.SGameProfileBukkit; -import com.georgev22.skinoverlay.storage.data.Skin; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import com.mojang.authlib.GameProfile; -import com.mojang.authlib.properties.Property; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.logging.Level; - -@ApiStatus.Internal -@ApiStatus.NonExtendable -public class SkinHandler_Unsupported extends SkinHandler { - - @Override - public CompletableFuture updateSkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - return CompletableFuture.supplyAsync(() -> { - throw new UnsupportedOperationException("Unsupported Minecraft Version"); - }); - } - - @Override - public void applySkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - skinOverlay.getSkinHandler().updateSkin(playerObject, skin).handleAsync((aBoolean, throwable) -> { - if (throwable != null) { - skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); - return false; - } - return aBoolean; - }); - } - - @Override - public GameProfile getInternalGameProfile(@NotNull PlayerObject playerObject) throws IOException, ExecutionException, InterruptedException { - GameProfile gameProfile = new GameProfile(playerObject.playerUUID(), playerObject.playerName()); - if (!gameProfile.getProperties().containsKey("textures")) { - SProperty property = getSkin(playerObject); - gameProfile.getProperties().put("textures", new Property(property.name(), property.value(), property.signature())); - } - return gameProfile; - } - - @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); - } - - public static @NotNull SGameProfile wrapper(@NotNull GameProfile gameProfile) { - ObjectMap propertyObjectMap = new HashObjectMap<>(); - gameProfile.getProperties().forEach((s, property) -> propertyObjectMap.append(s, new SProperty(property.getName(), property.getValue(), property.getSignature()))); - return new SGameProfileBukkit(gameProfile.getName(), gameProfile.getId(), propertyObjectMap); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/handler/profile/SGameProfileBukkit.java b/core/src/main/java/com/georgev22/skinoverlay/handler/profile/SGameProfileBukkit.java deleted file mode 100644 index d603bcc0..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/handler/profile/SGameProfileBukkit.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.georgev22.skinoverlay.handler.profile; - -import com.georgev22.library.maps.ObjectMap; -import com.georgev22.skinoverlay.handler.SGameProfile; -import com.georgev22.skinoverlay.handler.SProperty; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import com.mojang.authlib.GameProfile; -import com.mojang.authlib.properties.Property; -import org.jetbrains.annotations.ApiStatus; - -import java.util.UUID; - -@ApiStatus.Internal -@ApiStatus.NonExtendable -public class SGameProfileBukkit extends SGameProfile { - - public SGameProfileBukkit(String name, UUID uuid) { - super(name, uuid); - } - - public SGameProfileBukkit(String name, UUID uuid, ObjectMap properties) { - super(name, uuid, properties); - } - - @Override - public void apply() { - PlayerObject playerObject = skinOverlay.getPlayer(getUUID()).orElseThrow(); - ((GameProfile) playerObject.internalGameProfile()).getProperties().clear(); - this.getProperties().forEach((s, sProperty) -> ((GameProfile) playerObject.internalGameProfile()).getProperties().put(s, new Property(sProperty.name(), sProperty.value(), sProperty.signature()))); - } - -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/handler/skin/MinecraftSkinRenderer.java b/core/src/main/java/com/georgev22/skinoverlay/handler/skin/MinecraftSkinRenderer.java deleted file mode 100644 index 307b73cf..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/handler/skin/MinecraftSkinRenderer.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.georgev22.skinoverlay.handler.skin; - -import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; -import lombok.Getter; - -import java.awt.*; -import java.awt.image.BufferedImage; - -@Getter -public class MinecraftSkinRenderer { - private SerializableBufferedImage fullSkinImage; - - private final Part[] parts; - - public MinecraftSkinRenderer(Part... parts) { - this.parts = parts; - } - - public void createFullSkinImage() { - fullSkinImage = new SerializableBufferedImage(new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB)); - - Graphics g = fullSkinImage.getBufferedImage().getGraphics(); - - for (Part part : parts) { - g.drawImage(part.image().getBufferedImage(), part.x(), part.y(), part.width(), part.height(), null); - } - - g.dispose(); - } - -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/handler/skin/Section.java b/core/src/main/java/com/georgev22/skinoverlay/handler/skin/Section.java deleted file mode 100644 index 4aeb14f2..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/handler/skin/Section.java +++ /dev/null @@ -1,475 +0,0 @@ -package com.georgev22.skinoverlay.handler.skin; - -import lombok.Getter; - -@Getter -public abstract class Section { - private final int x1; - private final int y1; - private final int x2; - private final int y2; - private final int width; - private final int height; - - public Section(int x1, int y1, int x2, int y2) { - this.x1 = x1; - this.y1 = y1; - this.x2 = x2; - this.y2 = y2; - - if (y1 > y2) { - int temp = y1; - y1 = y2; - y2 = temp; - } - - this.width = x2 - x1; - this.height = y2 - y1; - } - - @Override - public String toString() { - return "Section{" + - "type=" + getClass().getSimpleName() + - ", x1=" + x1 + - ", y1=" + y1 + - ", x2=" + x2 + - ", y2=" + y2 + - ", width=" + width + - ", height=" + height + - '}'; - } - - public static class Head_Top extends Section { - public Head_Top() { - super(8, 0, 16, 8); - } - } - - public static class Head_Bottom extends Section { - public Head_Bottom() { - super(16, 0, 24, 8); - } - } - - public static class Head_Right extends Section { - public Head_Right() { - super(0, 8, 8, 16); - } - } - - public static class Head_Front extends Section { - public Head_Front() { - super(8, 8, 16, 16); - } - } - - public static class Head_Left extends Section { - public Head_Left() { - super(16, 8, 24, 16); - } - } - - public static class Head_Back extends Section { - public Head_Back() { - super(24, 8, 32, 16); - } - } - - public static class Hat_Top extends Section { - public Hat_Top() { - super(40, 0, 48, 8); - } - } - - public static class Hat_Bottom extends Section { - public Hat_Bottom() { - super(48, 0, 56, 8); - } - } - - public static class Hat_Right extends Section { - public Hat_Right() { - super(32, 8, 40, 16); - } - } - - public static class Hat_Front extends Section { - public Hat_Front() { - super(40, 8, 48, 16); - } - } - - public static class Hat_Left extends Section { - public Hat_Left() { - super(48, 8, 56, 16); - } - } - - public static class Hat_Back extends Section { - public Hat_Back() { - super(56, 8, 64, 16); - } - } - - public static class Right_Leg_Top extends Section { - public Right_Leg_Top() { - super(4, 16, 8, 20); - } - } - - public static class Right_Leg_Bottom extends Section { - public Right_Leg_Bottom() { - super(8, 16, 12, 20); - } - } - - public static class Right_Leg_Right extends Section { - public Right_Leg_Right() { - super(0, 20, 4, 32); - } - } - - public static class Right_Leg_Front extends Section { - public Right_Leg_Front() { - super(4, 20, 8, 32); - } - } - - public static class Right_Leg_Left extends Section { - public Right_Leg_Left() { - super(8, 20, 12, 32); - } - } - - public static class Right_Leg_Back extends Section { - public Right_Leg_Back() { - super(12, 20, 16, 32); - } - } - - public static class Torso_Top extends Section { - public Torso_Top() { - super(20, 16, 28, 20); - } - } - - public static class Torso_Bottom extends Section { - public Torso_Bottom() { - super(28, 16, 36, 20); - } - } - - public static class Torso_Right extends Section { - public Torso_Right() { - super(16, 20, 20, 32); - } - } - - public static class Torso_Front extends Section { - public Torso_Front() { - super(20, 20, 28, 32); - } - } - - public static class Torso_Left extends Section { - public Torso_Left() { - super(36, 20, 40, 32); - } - } - - public static class Torso_Back extends Section { - public Torso_Back() { - super(28, 20, 36, 32); - } - } - - public static class Right_Arm_Top extends Section { - public Right_Arm_Top() { - super(44, 16, 48, 20); - } - } - - public static class Right_Arm_Bottom extends Section { - public Right_Arm_Bottom() { - super(48, 16, 52, 20); - } - } - - public static class Right_Arm_Right extends Section { - public Right_Arm_Right() { - super(40, 20, 44, 32); - } - } - - public static class Right_Arm_Front extends Section { - public Right_Arm_Front() { - super(44, 20, 48, 32); - } - } - - public static class Right_Arm_Left extends Section { - public Right_Arm_Left() { - super(48, 20, 52, 32); - } - } - - public static class Right_Arm_Back extends Section { - public Right_Arm_Back() { - super(52, 20, 56, 32); - } - } - - public static class Left_Leg_Top extends Section { - public Left_Leg_Top() { - super(20, 48, 24, 52); - } - } - - public static class Left_Leg_Bottom extends Section { - public Left_Leg_Bottom() { - super(24, 48, 28, 52); - } - } - - public static class Left_Leg_Right extends Section { - public Left_Leg_Right() { - super(16, 52, 20, 64); - } - } - - public static class Left_Leg_Front extends Section { - public Left_Leg_Front() { - super(20, 52, 24, 64); - } - } - - public static class Left_Leg_Left extends Section { - public Left_Leg_Left() { - super(24, 52, 28, 64); - } - } - - public static class Left_Leg_Back extends Section { - public Left_Leg_Back() { - super(28, 52, 32, 64); - } - } - - public static class Left_Arm_Top extends Section { - public Left_Arm_Top() { - super(36, 48, 40, 52); - } - } - - public static class Left_Arm_Bottom extends Section { - public Left_Arm_Bottom() { - super(40, 48, 44, 52); - } - } - - public static class Left_Arm_Right extends Section { - public Left_Arm_Right() { - super(32, 52, 36, 64); - } - } - - public static class Left_Arm_Front extends Section { - public Left_Arm_Front() { - super(36, 52, 40, 64); - } - } - - public static class Left_Arm_Left extends Section { - public Left_Arm_Left() { - super(40, 52, 44, 64); - } - } - - public static class Left_Arm_Back extends Section { - public Left_Arm_Back() { - super(44, 52, 48, 64); - } - } - - public static class Right_Pants_Leg_Top extends Section { - public Right_Pants_Leg_Top() { - super(4, 32, 8, 36); - } - } - - public static class Right_Pants_Leg_Bottom extends Section { - public Right_Pants_Leg_Bottom() { - super(8, 32, 12, 36); - } - } - - public static class Right_Pants_Leg_Right extends Section { - public Right_Pants_Leg_Right() { - super(0, 36, 4, 48); - } - } - - public static class Right_Pants_Leg_Front extends Section { - public Right_Pants_Leg_Front() { - super(4, 36, 8, 48); - } - } - - public static class Right_Pants_Leg_Left extends Section { - public Right_Pants_Leg_Left() { - super(8, 36, 12, 48); - } - } - - public static class Right_Pants_Leg_Back extends Section { - public Right_Pants_Leg_Back() { - super(12, 36, 16, 48); - } - } - - public static class Jacket_Top extends Section { - public Jacket_Top() { - super(20, 32, 28, 36); - } - } - - public static class Jacket_Bottom extends Section { - public Jacket_Bottom() { - super(28, 32, 36, 36); - } - } - - public static class Jacket_Right extends Section { - public Jacket_Right() { - super(16, 36, 20, 48); - } - } - - public static class Jacket_Front extends Section { - public Jacket_Front() { - super(20, 36, 28, 48); - } - } - - public static class Jacket_Left extends Section { - public Jacket_Left() { - super(36, 36, 40, 48); - } - } - - public static class Jacket_Back extends Section { - public Jacket_Back() { - super(28, 36, 36, 48); - } - } - - public static class Right_Sleeve_Top extends Section { - public Right_Sleeve_Top() { - super(44, 32, 48, 36); - } - } - - public static class Right_Sleeve_Bottom extends Section { - public Right_Sleeve_Bottom() { - super(48, 32, 52, 36); - } - } - - public static class Right_Sleeve_Right extends Section { - public Right_Sleeve_Right() { - super(40, 36, 44, 48); - } - } - - public static class Right_Sleeve_Front extends Section { - public Right_Sleeve_Front() { - super(44, 36, 48, 48); - } - } - - public static class Right_Sleeve_Left extends Section { - public Right_Sleeve_Left() { - super(48, 36, 52, 48); - } - } - - public static class Right_Sleeve_Back extends Section { - public Right_Sleeve_Back() { - super(52, 36, 56, 48); - } - } - - public static class Left_Pants_Leg_Top extends Section { - public Left_Pants_Leg_Top() { - super(4, 48, 8, 52); - } - } - - public static class Left_Pants_Leg_Bottom extends Section { - public Left_Pants_Leg_Bottom() { - super(8, 48, 12, 52); - } - } - - public static class Left_Pants_Leg_Right extends Section { - public Left_Pants_Leg_Right() { - super(0, 52, 4, 64); - } - } - - public static class Left_Pants_Leg_Front extends Section { - public Left_Pants_Leg_Front() { - super(4, 52, 8, 64); - } - } - - public static class Left_Pants_Leg_Left extends Section { - public Left_Pants_Leg_Left() { - super(8, 52, 12, 64); - } - } - - public static class Left_Pants_Leg_Back extends Section { - public Left_Pants_Leg_Back() { - super(12, 52, 16, 64); - } - } - - public static class Left_Sleeve_Top extends Section { - public Left_Sleeve_Top() { - super(52, 48, 56, 52); - } - } - - public static class Left_Sleeve_Bottom extends Section { - public Left_Sleeve_Bottom() { - super(56, 48, 60, 52); - } - } - - public static class Left_Sleeve_Right extends Section { - public Left_Sleeve_Right() { - super(48, 52, 52, 64); - } - } - - public static class Left_Sleeve_Front extends Section { - public Left_Sleeve_Front() { - super(52, 52, 56, 64); - } - } - - public static class Left_Sleeve_Left extends Section { - public Left_Sleeve_Left() { - super(56, 52, 60, 64); - } - } - - public static class Left_Sleeve_Back extends Section { - public Left_Sleeve_Back() { - super(60, 52, 64, 64); - } - } - -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/hook/SkinHook.java b/core/src/main/java/com/georgev22/skinoverlay/hook/SkinHook.java deleted file mode 100644 index d76cb089..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/hook/SkinHook.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.georgev22.skinoverlay.hook; - -import com.georgev22.skinoverlay.handler.SProperty; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.util.concurrent.ExecutionException; - -/** - * The SkinHook interface defines the methods required for a skin hook. - * A skin hook is responsible for retrieving the skin property for a given player object. - */ -public interface SkinHook { - - /** - * Retrieves the SProperty for the given PlayerObject. - * - * @param playerObject The player object to retrieve the SProperty for. - * @return The SProperty for the given PlayerObject, or null if the property cannot be retrieved. - * @throws IOException if an IO exception occurs while retrieving the property. - * @throws ExecutionException if an exception occurs while executing the property retrieval. - * @throws InterruptedException if the current thread is interrupted while waiting for the retrieval to complete. - */ - @Nullable SProperty getProperty(@NotNull PlayerObject playerObject) throws IOException, ExecutionException, InterruptedException; - -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/hook/hooks/SkinHookImpl.java b/core/src/main/java/com/georgev22/skinoverlay/hook/hooks/SkinHookImpl.java deleted file mode 100644 index 8a18dec1..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/hook/hooks/SkinHookImpl.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.georgev22.skinoverlay.hook.hooks; - -import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.handler.SProperty; -import com.georgev22.skinoverlay.hook.SkinHook; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonParser; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.util.concurrent.ExecutionException; - -@ApiStatus.Internal -@ApiStatus.NonExtendable -public class SkinHookImpl implements SkinHook { - - - /** - * Retrieves the SProperty for the given PlayerObject. - * - * @param playerObject The player object to retrieve the SProperty for. - * @return The SProperty for the given PlayerObject, or null if the property cannot be retrieved. - * @throws IOException if an IO exception occurs while retrieving the property. - * @throws ExecutionException if an exception occurs while executing the property retrieval. - * @throws InterruptedException if the current thread is interrupted while waiting for the retrieval to complete. - */ - @Override - public @Nullable SProperty getProperty(@NotNull PlayerObject playerObject) throws IOException, ExecutionException, InterruptedException { - final JsonElement json = JsonParser.parseString(new String(SkinOverlay.getInstance().getSkinHandler().getProfileBytes(playerObject, null))); - final JsonArray properties = json.getAsJsonObject().get("properties").getAsJsonArray(); - SProperty property = null; - for (final JsonElement object : properties) { - if (object.getAsJsonObject().get("name").getAsString().equals("textures")) { - property = new SProperty("textures", object.getAsJsonObject().get("value").getAsString(), object.getAsJsonObject().get("signature").getAsString()); - } - } - return property; - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/listeners/DebugListeners.java b/core/src/main/java/com/georgev22/skinoverlay/listeners/DebugListeners.java deleted file mode 100644 index 9d6324b6..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/listeners/DebugListeners.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.georgev22.skinoverlay.listeners; - -import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.event.EventPriority; -import com.georgev22.skinoverlay.event.annotations.EventHandler; -import com.georgev22.skinoverlay.event.events.player.PlayerObjectEvent; -import com.georgev22.skinoverlay.event.events.player.PlayerObjectUserEvent; -import com.georgev22.skinoverlay.event.events.player.skin.PlayerObjectPreUpdateSkinEvent; -import com.georgev22.skinoverlay.event.events.player.skin.PlayerObjectUpdateSkinEvent; -import com.georgev22.skinoverlay.event.events.profile.ProfileCreatedEvent; -import com.georgev22.skinoverlay.event.events.profile.property.SPropertyAddEvent; -import com.georgev22.skinoverlay.event.events.profile.property.SPropertyRemoveEvent; -import com.georgev22.skinoverlay.event.events.user.UserEvent; -import com.georgev22.skinoverlay.event.events.user.data.UserModifyDataEvent; -import com.georgev22.skinoverlay.event.events.user.data.add.UserAddDataEvent; -import com.georgev22.skinoverlay.event.events.user.data.load.UserPostLoadEvent; -import com.georgev22.skinoverlay.event.events.user.data.load.UserPreLoadEvent; -import com.georgev22.skinoverlay.event.interfaces.EventListener; -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -@ApiStatus.NonExtendable -public class DebugListeners implements EventListener { - - private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); - - @EventHandler(priority = EventPriority.HIGHEST) - public void onPlayerObjectPreUpdateSkin(PlayerObjectPreUpdateSkinEvent event) { - if (event.isCancelled()) return; - skinOverlay.getLogger().info("===== Debug ====="); - skinOverlay.getLogger().info("PlayerObjectPreUpdateSkinEvent: " + event.getPlayerObject().playerName()); - skinOverlay.getLogger().info("===== Debug ====="); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onPlayerObjectUpdateSkinEvent(PlayerObjectUpdateSkinEvent event) { - if (event.isCancelled()) return; - skinOverlay.getLogger().info("===== Debug ====="); - skinOverlay.getLogger().info("PlayerObjectUpdateSkinEvent: " + event.getPlayerObject().playerName()); - skinOverlay.getLogger().info("PlayerObjectUpdateSkinEvent: " + event.getSkin().toString()); - skinOverlay.getLogger().info("===== Debug ====="); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onPlayerObjectEvent(PlayerObjectEvent event) { - if (event.isCancelled()) return; - skinOverlay.getLogger().info("===== Debug ====="); - skinOverlay.getLogger().info("PlayerObjectEvent: " + event.getPlayerObject().playerName()); - skinOverlay.getLogger().info("===== Debug ====="); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onUserPlayerObjectEvent(PlayerObjectUserEvent event) { - if (event.isCancelled()) return; - skinOverlay.getLogger().info("===== Debug ====="); - skinOverlay.getLogger().info("PlayerObjectUserEvent: " + event.getPlayerObject().playerName()); - skinOverlay.getLogger().info("PlayerObjectUserEvent: " + event.getUser()); - skinOverlay.getLogger().info("===== Debug ====="); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onUserEvent(UserEvent event) { - if (event.isCancelled()) return; - skinOverlay.getLogger().info("===== Debug ====="); - skinOverlay.getLogger().info("UserEvent: " + event.getUser()); - skinOverlay.getLogger().info("===== Debug ====="); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onUserAddDataEvent(UserAddDataEvent event) { - if (event.isCancelled()) return; - skinOverlay.getLogger().info("===== Debug ====="); - skinOverlay.getLogger().info("UserAddDataEvent: " + event.getUser()); - skinOverlay.getLogger().info("UserAddDataEvent: " + event.getData()); - skinOverlay.getLogger().info("===== Debug ====="); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onUserPostLoadEvent(UserPostLoadEvent event) { - if (event.isCancelled()) return; - skinOverlay.getLogger().info("===== Debug ====="); - skinOverlay.getLogger().info("UserPostLoadEvent: " + event.getUser()); - skinOverlay.getLogger().info("===== Debug ====="); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onUserPreLoadEvent(UserPreLoadEvent event) { - if (event.isCancelled()) return; - skinOverlay.getLogger().info("===== Debug ====="); - skinOverlay.getLogger().info("UserPreLoadEvent: " + event.getUser()); - skinOverlay.getLogger().info("===== Debug ====="); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onUserModifyDataEvent(UserModifyDataEvent event) { - if (event.isCancelled()) return; - skinOverlay.getLogger().info("===== Debug ====="); - skinOverlay.getLogger().info("UserModifyDataEvent: " + event.getUser()); - skinOverlay.getLogger().info("===== Debug ====="); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onProfileCreatedEvent(ProfileCreatedEvent event) { - skinOverlay.getLogger().info("===== Debug ====="); - skinOverlay.getLogger().info("ProfileCreatedEvent: " + event.getProfile()); - skinOverlay.getLogger().info("===== Debug ====="); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onSPropertyAddEvent(SPropertyAddEvent event) { - skinOverlay.getLogger().info("===== Debug ====="); - skinOverlay.getLogger().info("SPropertyAddEvent: " + event.getPropertyName()); - skinOverlay.getLogger().info("SPropertyAddEvent: " + event.getProperty()); - skinOverlay.getLogger().info("===== Debug ====="); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onSPropertyRemoveEvent(SPropertyRemoveEvent event) { - skinOverlay.getLogger().info("===== Debug ====="); - skinOverlay.getLogger().info("SPropertyRemoveEvent: " + event.getPropertyName()); - skinOverlay.getLogger().info("SPropertyRemoveEvent: " + event.getProperty()); - skinOverlay.getLogger().info("===== Debug ====="); - } - -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/listeners/ObservableListener.java b/core/src/main/java/com/georgev22/skinoverlay/listeners/ObservableListener.java deleted file mode 100644 index 41710a2a..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/listeners/ObservableListener.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.georgev22.skinoverlay.listeners; - -import com.georgev22.library.maps.ObservableObjectMap; -import org.jetbrains.annotations.Nullable; - -public abstract class ObservableListener implements ObservableObjectMap.MapChangeListener { - - @Override - public abstract void entryAdded(K key, V value); - - @Override - public abstract void entryRemoved(Object key, @Nullable Object value); -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java b/core/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java deleted file mode 100644 index a65f1407..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.georgev22.skinoverlay.listeners; - -import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.event.EventPriority; -import com.georgev22.skinoverlay.event.annotations.EventHandler; -import com.georgev22.skinoverlay.event.events.player.PlayerObjectConnectionEvent; -import com.georgev22.skinoverlay.event.events.player.PlayerSkinPartOptionsChangedEvent; -import com.georgev22.skinoverlay.event.interfaces.EventListener; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -@ApiStatus.Internal -@ApiStatus.NonExtendable -public class PlayerListeners implements EventListener { - - private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); - - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) - public void onConnection(@NotNull PlayerObjectConnectionEvent event) { - switch (event.getConnectionType()) { - case CONNECT -> event.getPlayerObject().playerJoin(); - case DISCONNECT -> event.getPlayerObject().playerQuit(); - } - } - - public void onSettingsChange(PlayerSkinPartOptionsChangedEvent event) { - event.getPlayerObject().updateSkin(); - } - - -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/storage/data/Data.java b/core/src/main/java/com/georgev22/skinoverlay/storage/data/Data.java deleted file mode 100644 index d63c51e7..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/storage/data/Data.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.georgev22.skinoverlay.storage.data; - -import com.georgev22.library.maps.ConcurrentObjectMap; -import com.georgev22.library.utilities.Entity; - -import java.util.UUID; - -/** - * Represents data associated with a player identified by a UUID. - * It implements the {@link Entity} interface, providing a unique identifier and custom data storage. - */ -public class Data implements Entity { - - /** - * Custom data storage for this entity. - */ - private final ConcurrentObjectMap customData; - - /** - * The UUID of the player associated with this data. - */ - private final UUID uuid; - - /** - * Constructs a new Data object with the given UUID. - * - * @param uuid The UUID of the player associated with this data. - */ - public Data(UUID uuid) { - this.customData = new ConcurrentObjectMap<>(); - this.uuid = uuid; - } - - /** - * Gets the UUID of the player associated with this data. - * - * @return The UUID of the player. - */ - @Override - public UUID getId() { - return this.uuid; - } - - /** - * Gets the custom data storage for this entity. - * - * @return The custom data storage. - */ - @Override - public ConcurrentObjectMap getCustomData() { - return this.customData; - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/storage/data/Skin.java b/core/src/main/java/com/georgev22/skinoverlay/storage/data/Skin.java deleted file mode 100644 index ef6a78eb..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/storage/data/Skin.java +++ /dev/null @@ -1,221 +0,0 @@ -package com.georgev22.skinoverlay.storage.data; - -import com.georgev22.library.yaml.serialization.ConfigurationSerializable; -import com.georgev22.library.yaml.serialization.SerializableAs; -import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.handler.SProperty; -import com.georgev22.skinoverlay.handler.skin.SkinParts; -import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; -import com.google.gson.*; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.*; -import java.util.Base64; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; -import java.util.logging.Level; - -/** - * Represents a player's skin data, including properties, skin parts, and skin name. - * Extends {@link Data} and implements {@link ConfigurationSerializable}. - */ -@ApiStatus.NonExtendable -@SerializableAs("SkinOverlaySkin") -public class Skin extends Data implements ConfigurationSerializable { - - private SProperty property; - private SkinParts skinParts; - - private String skinName; - - /** - * Creates a new Skin object with the given UUID, initializing custom data. - * - * @param uuid The UUID of the player associated with this skin. - */ - public Skin(UUID uuid) { - super(uuid); - addCustomData("entity_id", uuid.toString()); - addCustomData("skinParts", this.skinParts = new SkinParts()); - addCustomData("skinName", this.skinName = "empty"); - } - - /** - * Creates a new Skin object with the given UUID, SProperty, and skin name, initializing custom data. - * - * @param uuid The UUID of the player associated with this skin. - * @param sProperty The SProperty representing the player's skin properties. - * @param skinName The name of the skin. - */ - public Skin(UUID uuid, SProperty sProperty, String skinName) { - super(uuid); - addCustomData("entity_id", uuid.toString()); - addCustomData("property", this.property = sProperty); - addCustomData("skinName", this.skinName = skinName); - try { - addCustomData("skinParts", this.skinParts = new SkinParts( - new SerializableBufferedImage(SkinOverlay.getInstance().getSkinHandler().getSkinImage(sProperty)), - skinName - )); - } catch (IOException e) { - SkinOverlay.getInstance().getLogger().log(Level.SEVERE, "Could not load skin " + skinName, e); - addCustomData("skinParts", this.skinParts = new SkinParts(null, skinName)); - } - } - - /** - * Creates a new Skin object with the given UUID, SProperty, and SkinParts, initializing custom data. - * - * @param uuid The UUID of the player associated with this skin. - * @param sProperty The SProperty representing the player's skin properties. - * @param skinParts The SkinParts containing the skin image. - */ - public Skin(UUID uuid, SProperty sProperty, @NotNull SkinParts skinParts) { - super(uuid); - addCustomData("entity_id", uuid.toString()); - addCustomData("property", this.property = sProperty); - addCustomData("skinParts", this.skinParts = skinParts); - addCustomData("skinName", this.skinName = skinParts.getSkinName()); - } - - /** - * Gets the SProperty representing the player's skin properties. - * - * @return The SProperty object. - */ - public @Nullable SProperty skinProperty() { - return getCustomData("property") != null ? getCustomData("property") : property; - } - - /** - * Gets the SkinParts containing the skin image. - * - * @return The SkinParts object. - */ - public SkinParts skinParts() { - return getCustomData("skinParts") != null ? getCustomData("skinParts") : skinParts; - } - - /** - * Gets the name of the skin. - * - * @return The name of the skin. - */ - public String skinName() { - return skinName; - } - - /** - * Sets the SkinParts for this skin. - * - * @param skinParts The SkinParts to set. - */ - public void setSkinParts(SkinParts skinParts) { - addCustomData("skinParts", this.skinParts = skinParts); - } - - /** - * Sets the SProperty for this skin. - * - * @param property The SProperty to set. - */ - public void setProperty(SProperty property) { - addCustomData("property", this.property = property); - } - - /** - * Sets the name of the skin. - * - * @param skinName The name of the skin. - */ - public void setSkinName(String skinName) { - this.skinName = skinName; - } - - /** - * Gets the URL of the skin based on the SProperty. - * - * @return The URL of the skin. - */ - public @Nullable String skinURL() { - return JsonParser.parseString(new String(Base64.getDecoder().decode(property.value()))) - .getAsJsonObject() - .getAsJsonObject("textures") - .getAsJsonObject("SKIN") - .get("url") - .getAsString(); - } - - /** - * Returns a string representation of the Skin object. - * - * @return A string containing information about the Skin object. - */ - @Override - public String toString() { - return "Skin{" + - "property=" + property + - ", skinParts=" + skinParts + - ", skinURL=" + skinURL() + - '}'; - } - - /** - * Gets the UUID of the player associated with this skin. - * - * @return The UUID of the player. - */ - @Override - public UUID getId() { - return this.getCustomData("entity_id") != null ? UUID.fromString(this.getCustomData("entity_id")) : null; - } - - /** - * Serializes the Skin object to a JSON string using the Gson library. - * - * @return The serialized JSON string. - */ - public String toJson() { - return SkinOverlay.getInstance().getGson().toJson(this); - } - - /** - * Deserializes a JSON string to a Skin object using the Gson library. - * - * @param json The JSON string to deserialize. - * @return The deserialized Skin object. - */ - public static Skin fromJson(String json) { - return SkinOverlay.getInstance().getGson().fromJson(json, Skin.class); - } - - /** - * Serializes the Skin object to a map for YAML configuration serialization. - * - * @return A map containing the serialized data of the Skin object. - */ - @Override - public @NotNull Map serialize() { - Map map = new HashMap<>(); - map.put("entity_id", getCustomData("entity_id")); - map.put("property", property); - map.put("skinParts", skinParts); - map.put("skinName", skinName); - return map; - } - - /** - * Deserializes a map to a Skin object for YAML configuration deserialization. - * - * @param data The map containing the serialized data of the Skin object. - * @return The deserialized Skin object. - */ - @Contract("_ -> new") - public static @NotNull Skin deserialize(@NotNull Map data) { - return new Skin(UUID.fromString((String) data.get("entity_id")), (SProperty) data.get("property"), (String) data.get("skinName")); - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/storage/data/User.java b/core/src/main/java/com/georgev22/skinoverlay/storage/data/User.java deleted file mode 100644 index 3a73de92..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/storage/data/User.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.georgev22.skinoverlay.storage.data; - -import com.georgev22.library.yaml.serialization.ConfigurationSerializable; -import com.georgev22.library.yaml.serialization.SerializableAs; -import com.georgev22.skinoverlay.SkinOverlay; -import org.jetbrains.annotations.NotNull; - -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -/** - * Represents a user with associated skin data, including default and current skins. - * Extends {@link Data} and implements {@link ConfigurationSerializable}. - */ -@SerializableAs("SkinOverlayUser") -public class User extends Data implements ConfigurationSerializable { - - /** - * Creates a new User object with the given UUID, initializing custom data. - * - * @param uuid The UUID of the user. - */ - public User(UUID uuid) { - super(uuid); - addCustomData("entity_id", uuid.toString()); - } - - /** - * Gets the default skin associated with the user. - * - * @return The default skin. - */ - public Skin defaultSkin() { - return getCustomData("defaultSkin"); - } - - /** - * Gets the current skin associated with the user. - * - * @return The current skin. - */ - public Skin skin() { - return getCustomData("skin"); - } - - /** - * Sets the current skin for the user. - * - * @param skin The skin to set as the current skin. - */ - public void setSkin(Skin skin) { - addCustomData("skin", skin); - } - - /** - * Sets the default skin for the user. - * - * @param skin The skin to set as the default skin. - */ - public void setDefaultSkin(Skin skin) { - addCustomData("defaultSkin", skin); - } - - /** - * Gets the UUID of the user. - * - * @return The UUID of the user. - */ - @Override - public UUID getId() { - return this.getCustomData("entity_id") != null ? UUID.fromString(this.getCustomData("entity_id")) : null; - } - - /** - * Serializes the User object to a JSON string using the Gson library. - * - * @return The serialized JSON string. - */ - public String toJson() { - return SkinOverlay.getInstance().getGson().toJson(this); - } - - /** - * Deserializes a JSON string to a User object using the Gson library. - * - * @param json The JSON string to deserialize. - * @return The deserialized User object. - */ - public static User fromJson(String json) { - return SkinOverlay.getInstance().getGson().fromJson(json, User.class); - } - - /** - * Serializes the User object to a map for YAML configuration serialization. - * - * @return A map containing the serialized data of the User object. - */ - @Override - public @NotNull Map serialize() { - Map map = new HashMap<>(); - map.put("entity_id", this.getCustomData("entity_id")); - map.put("defaultSkin", defaultSkin()); - map.put("skin", skin()); - return map; - } - - /** - * Deserializes a map to a User object for YAML configuration deserialization. - * - * @param map The map containing the serialized data of the User object. - * @return The deserialized User object. - */ - public static @NotNull User deserialize(@NotNull Map map) { - User user = new User(UUID.fromString((String) map.get("entity_id"))); - user.setSkin((Skin) map.get("skin")); - user.setDefaultSkin((Skin) map.get("defaultSkin")); - return user; - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/storage/manager/SkinManager.java b/core/src/main/java/com/georgev22/skinoverlay/storage/manager/SkinManager.java deleted file mode 100644 index a39ee066..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/storage/manager/SkinManager.java +++ /dev/null @@ -1,254 +0,0 @@ -package com.georgev22.skinoverlay.storage.manager; - -import com.georgev22.library.database.DatabaseWrapper; -import com.georgev22.library.database.DatabaseWrapper.DatabaseObject; -import com.georgev22.library.maps.HashObjectMap; -import com.georgev22.library.maps.ObjectMap; -import com.georgev22.library.maps.ObjectMap.Pair; -import com.georgev22.library.maps.ObservableObjectMap; -import com.georgev22.library.utilities.EntityManager; -import com.georgev22.library.yaml.file.YamlConfiguration; -import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.storage.data.Skin; -import com.mongodb.annotations.Beta; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.util.*; -import java.util.concurrent.CompletableFuture; - -/** - * The {@link SkinManager} class is responsible for managing {@link Skin} objects in a persistence storage. - * It supports multiple storage types including MySQL, SQLite, PostgreSQL, MongoDB, and FILE. - * The class provides methods for checking if a {@link Skin} exists, - * loading a {@link Skin}, and creating a {@link Skin}. - * - * @author GeorgeV220 - */ -public class SkinManager implements EntityManager { - private final File entitiesDirectory; - private final DatabaseWrapper database; - private final String collection; - private final ObservableObjectMap loadedEntities = new ObservableObjectMap<>(); - private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); - - /** - * Constructor for the EntityManager class - * - * @param obj the object to be used for storage (DatabaseWrapper or File) - * @param collectionName the name of the collection to be used for MONGODB and SQL, null for other types - */ - public SkinManager(Object obj, @Nullable String collectionName) { - this.collection = collectionName; - if (obj instanceof File folder) { - this.entitiesDirectory = folder; - this.database = null; - if (!this.entitiesDirectory.exists()) { - if (this.entitiesDirectory.mkdirs()) { - this.skinOverlay.getLogger().info("Created entities directory: " + this.entitiesDirectory.getAbsolutePath()); - } - } - } else if (obj instanceof DatabaseWrapper databaseWrapper) { - this.entitiesDirectory = null; - this.database = databaseWrapper; - } else { - this.entitiesDirectory = null; - this.database = null; - } - } - - /** - * Loads the {@link Skin} with the specified ID - * - * @param entityId the {@link UUID} of the entity to be loaded - * @return a {@link CompletableFuture} containing the loaded {@link Skin} object - */ - @Override - public CompletableFuture load(UUID entityId) { - return exists(entityId) - .thenCompose(exists -> { - if (exists) { - return CompletableFuture.supplyAsync(() -> { - if (entitiesDirectory != null) { - File file = new File(entitiesDirectory, entityId + ".yml"); - try { - YamlConfiguration yamlConfiguration = YamlConfiguration.loadConfiguration(file); - Skin entity = (Skin) yamlConfiguration.get("entity"); - loadedEntities.append(entityId, entity); - return entity; - } catch (Exception e) { - throw new RuntimeException(e); - } - } else if (database != null) { - Pair> retrievedData = database.retrieveData(collection, Pair.create("entity_id", entityId.toString())); - Optional optionalEntity = retrievedData.value().stream() - .filter(databaseObject -> databaseObject.data().get("data") != null) - .map(databaseObject -> Skin.fromJson((String) databaseObject.data().get("data"))) - .findFirst(); - Skin entity = optionalEntity.orElseGet(() -> new Skin(entityId)); - loadedEntities.append(entityId, entity); - return entity; - } else { - return new Skin(entityId); - } - }); - } else { - return createEntity(entityId); - } - }); - } - - /** - * Saves the specified {@link Skin}. - * - * @param entity the {@link Skin} to save - * @return a {@link CompletableFuture} that completes when the {@link Skin} is saved - */ - @Override - public CompletableFuture save(Skin entity) { - return CompletableFuture.runAsync(() -> { - if (entitiesDirectory != null) { - File file = new File(entitiesDirectory, entity.getId() + ".yml"); - try { - YamlConfiguration yamlConfiguration = new YamlConfiguration(); - yamlConfiguration.set("entity", entity); - yamlConfiguration.save(file); - } catch (Exception e) { - throw new RuntimeException(e); - } - } else if (database != null) { - exists(entity.getId()).thenAccept(result -> { - ObjectMap entityData = new HashObjectMap<>(); - entityData.append("entity_id", entity.getId().toString()); - entityData.append("data", entity.toJson()); - if (result) { - database.updateData(collection, Pair.create("entity_id", entity.getId().toString()), Pair.create("$set", entityData.removeEntry("entity_id")), null); - } else { - database.addData(collection, Pair.create(entity.getId().toString(), entityData)); - } - }); - } - this.loadedEntities.append(entity.getId(), entity); - }); - } - - /** - * Deletes the specified entity. - * - * @param entity the {@link Skin} to delete - * @return a {@link CompletableFuture} that completes when the {@link Skin} is deleted - */ - @Override - public CompletableFuture delete(Skin entity) { - return CompletableFuture.runAsync(() -> { - if (entitiesDirectory != null) { - File file = new File(entitiesDirectory, entity.getId() + ".yml"); - if (file.exists()) { - if (file.delete()) { - this.skinOverlay.getLogger().info("Deleted Skin: " + file.getAbsolutePath()); - } - } - } else if (database != null) { - exists(entity.getId()).thenAccept(result -> { - ObjectMap entityData = new HashObjectMap<>(entity.getCustomData()); - if (result) { - database.removeData(collection, Pair.create("entity_id", entity.getId().toString()), null); - this.skinOverlay.getLogger().info("Deleted Skin: " + entity.getId()); - } - }); - } - this.loadedEntities.remove(entity.getId()); - }); - } - - /** - * Creates a new {@link Skin} with the specified entity ID. - * - * @param entityId the {@link UUID} of the entity to create - * @return a {@link CompletableFuture} that returns the newly created {@link Skin} - */ - @Override - public CompletableFuture createEntity(UUID entityId) { - return CompletableFuture.completedFuture(loadedEntities.append(entityId, new Skin(entityId)).get(entityId)); - } - - /** - * Determines if a {@link Skin} with the specified entity ID exists. - * - * @param entityId the {@link UUID} of the entity to check - * @return a {@link CompletableFuture} that returns true if a {@link Skin} with the specified ID exists, false otherwise - */ - @Override - public CompletableFuture exists(UUID entityId) { - return CompletableFuture.supplyAsync(() -> { - if (entitiesDirectory != null) { - return new File(entitiesDirectory, entityId + ".yml").exists(); - } else if (database != null) { - return database.exists(collection, Pair.create("entity_id", entityId.toString()), null); - } else { - return false; - } - }); - } - - /** - * Retrieves the {@link Skin} with the given {@link UUID}. - *

- * If the entity is already loaded, it is returned immediately. - * If not, it is loaded - * asynchronously and returned in a {@link CompletableFuture}. - * - * @param entityId the {@link UUID} of the entity to retrieve - * @return a {@link CompletableFuture} that will contain the {@link Skin} with the given id - */ - @Override - public CompletableFuture getEntity(UUID entityId) { - if (loadedEntities.containsKey(entityId)) { - return CompletableFuture.completedFuture(loadedEntities.get(entityId)); - } - - return load(entityId); - } - - /** - * Saves all the loaded {@link Skin}s in the {@link #loadedEntities} map. - * For each {@link Skin} in the map, - * this method calls the {@link #save(Skin)} method to persist the {@link Skin}. - */ - @Override - public void saveAll() { - ObjectMap entities = new ObservableObjectMap().append(loadedEntities); - entities.forEach((uuid, entity) -> save(entity)); - } - - /** - * Loads all the entities by retrieving their IDs and invoking the {@link #load(UUID)} method. - * If the entities directory is specified, it scans the directory for entity files and extracts their IDs. - * If the database is specified, it retrieves entity IDs from the database and loads them. - */ - @Beta - @Override - public void loadAll() { - List entityIDs = new ArrayList<>(); - if (entitiesDirectory != null) { - File[] files = this.entitiesDirectory.listFiles((dir, name) -> name.endsWith(".yml")); - if (files != null) { - Arrays.stream(files).forEach(file -> entityIDs.add(UUID.fromString(file.getName().replace(".yml", "")))); - } - } else if (database != null) { - Pair> data = database.retrieveData(collection, Pair.create("entity_id", null)); - data.value().forEach(databaseObject -> entityIDs.add(UUID.fromString(String.valueOf(databaseObject.data().get("entity_id"))))); - } - entityIDs.forEach(this::load); - } - - /** - * Retrieves the current map of loaded entities. - * - * @return the map of loaded entities with UUID as the key and Skin object as the value - */ - @Override - public ObservableObjectMap getLoadedEntities() { - return loadedEntities; - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/storage/manager/UserManager.java b/core/src/main/java/com/georgev22/skinoverlay/storage/manager/UserManager.java deleted file mode 100644 index 7c35ee45..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/storage/manager/UserManager.java +++ /dev/null @@ -1,256 +0,0 @@ -package com.georgev22.skinoverlay.storage.manager; - -import com.georgev22.library.database.DatabaseWrapper; -import com.georgev22.library.database.DatabaseWrapper.DatabaseObject; -import com.georgev22.library.maps.HashObjectMap; -import com.georgev22.library.maps.ObjectMap; -import com.georgev22.library.maps.ObjectMap.Pair; -import com.georgev22.library.maps.ObservableObjectMap; -import com.georgev22.library.utilities.EntityManager; -import com.georgev22.library.yaml.file.YamlConfiguration; -import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.storage.data.User; -import com.mongodb.annotations.Beta; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.io.IOException; -import java.util.*; -import java.util.concurrent.CompletableFuture; - -/** - * The {@link UserManager} class is responsible for managing {@link User} objects in a persistence storage. - * It supports multiple storage types including MySQL, SQLite, PostgreSQL, MongoDB, and FILE. - * The class provides methods for checking if a {@link User} exists, - * loading a {@link User}, and creating a {@link User}. - * - * @author GeorgeV220 - */ -public class UserManager implements EntityManager { - private final File entitiesDirectory; - private final DatabaseWrapper database; - private final String collection; - private final ObservableObjectMap loadedEntities = new ObservableObjectMap<>(); - - private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); - - /** - * Constructor for the EntityManager class - * - * @param obj the object to be used for storage (DatabaseWrapper or File) - * @param collectionName the name of the collection to be used for MONGODB and SQL, null for other types - */ - public UserManager(Object obj, @Nullable String collectionName) { - this.collection = collectionName; - if (obj instanceof File folder) { - this.entitiesDirectory = folder; - this.database = null; - if (!this.entitiesDirectory.exists()) { - if (this.entitiesDirectory.mkdirs()) { - this.skinOverlay.getLogger().info("Created entities directory: " + this.entitiesDirectory.getAbsolutePath()); - } - } - } else if (obj instanceof DatabaseWrapper databaseWrapper) { - this.entitiesDirectory = null; - this.database = databaseWrapper; - } else { - this.entitiesDirectory = null; - this.database = null; - } - } - - /** - * Loads the {@link User} with the specified ID - * - * @param entityId the {@link UUID} of the entity to be loaded - * @return a {@link CompletableFuture} containing the loaded {@link User} object - */ - @Override - public CompletableFuture load(UUID entityId) { - return exists(entityId) - .thenCompose(exists -> { - if (exists) { - return CompletableFuture.supplyAsync(() -> { - if (entitiesDirectory != null) { - File file = new File(entitiesDirectory, entityId + ".yml"); - try { - YamlConfiguration yamlConfiguration = YamlConfiguration.loadConfiguration(file); - User entity = (User) yamlConfiguration.get("entity"); - loadedEntities.append(entityId, entity); - return entity; - } catch (Exception e) { - throw new RuntimeException(e); - } - } else if (database != null) { - Pair> retrievedData = database.retrieveData(collection, Pair.create("entity_id", entityId.toString())); - Optional optionalEntity = retrievedData.value().stream() - .filter(databaseObject -> databaseObject.data().get("data") != null) - .map(databaseObject -> User.fromJson((String) databaseObject.data().get("data"))) - .findFirst(); - User entity = optionalEntity.orElseGet(() -> new User(entityId)); - loadedEntities.append(entityId, entity); - return entity; - } else { - return new User(entityId); - } - }); - } else { - return createEntity(entityId); - } - }); - } - - /** - * Saves the specified {@link User}. - * - * @param entity the {@link User} to save - * @return a {@link CompletableFuture} that completes when the {@link User} is saved - */ - @Override - public CompletableFuture save(User entity) { - return CompletableFuture.runAsync(() -> { - if (entitiesDirectory != null) { - File file = new File(entitiesDirectory, entity.getId() + ".yml"); - try { - YamlConfiguration yamlConfiguration = new YamlConfiguration(); - yamlConfiguration.set("entity", entity); - yamlConfiguration.save(file); - } catch (IOException e) { - throw new RuntimeException(e); - } - } else if (database != null) { - exists(entity.getId()).thenAccept(result -> { - ObjectMap entityData = new HashObjectMap<>(); - entityData.append("entity_id", entity.getId().toString()); - entityData.append("data", entity.toJson()); - if (result) { - database.updateData(collection, Pair.create("entity_id", entity.getId().toString()), Pair.create("$set", entityData.removeEntry("entity_id")), null); - } else { - database.addData(collection, Pair.create(entity.getId().toString(), entityData)); - } - }); - } - this.loadedEntities.append(entity.getId(), entity); - }); - } - - /** - * Deletes the specified entity. - * - * @param entity the {@link User} to delete - * @return a {@link CompletableFuture} that completes when the {@link User} is deleted - */ - @Override - public CompletableFuture delete(User entity) { - return CompletableFuture.runAsync(() -> { - if (entitiesDirectory != null) { - File file = new File(entitiesDirectory, entity.getId() + ".yml"); - if (file.exists()) { - if (file.delete()) { - this.skinOverlay.getLogger().info("Deleted User: " + file.getAbsolutePath()); - } - } - } else if (database != null) { - exists(entity.getId()).thenAccept(result -> { - ObjectMap entityData = new HashObjectMap<>(entity.getCustomData()); - if (result) { - database.removeData(collection, Pair.create("entity_id", entity.getId().toString()), null); - this.skinOverlay.getLogger().info("Deleted User: " + entity.getId()); - } - }); - } - this.loadedEntities.remove(entity.getId()); - }); - } - - /** - * Creates a new {@link User} with the specified entity ID. - * - * @param entityId the {@link UUID} of the entity to create - * @return a {@link CompletableFuture} that returns the newly created {@link User} - */ - @Override - public CompletableFuture createEntity(UUID entityId) { - return CompletableFuture.completedFuture(loadedEntities.append(entityId, new User(entityId)).get(entityId)); - } - - /** - * Determines if a {@link User} with the specified entity ID exists. - * - * @param entityId the {@link UUID} of the entity to check - * @return a {@link CompletableFuture} that returns true if a {@link User} with the specified ID exists, false otherwise - */ - @Override - public CompletableFuture exists(UUID entityId) { - return CompletableFuture.supplyAsync(() -> { - if (entitiesDirectory != null) { - return new File(entitiesDirectory, entityId + ".yml").exists(); - } else if (database != null) { - return database.exists(collection, Pair.create("entity_id", entityId.toString()), null); - } else { - return false; - } - }); - } - - /** - * Retrieves the {@link User} with the given {@link UUID}. - *

- * If the entity is already loaded, it is returned immediately. - * If not, it is loaded - * asynchronously and returned in a {@link CompletableFuture}. - * - * @param entityId the {@link UUID} of the entity to retrieve - * @return a {@link CompletableFuture} that will contain the {@link User} with the given id - */ - @Override - public CompletableFuture getEntity(UUID entityId) { - if (loadedEntities.containsKey(entityId)) { - return CompletableFuture.completedFuture(loadedEntities.get(entityId)); - } - - return load(entityId); - } - - /** - * Saves all the loaded {@link User}s in the {@link #loadedEntities} map. - * For each {@link User} in the map, - * this method calls the {@link #save(User)} method to persist the {@link User}. - */ - @Override - public void saveAll() { - ObjectMap entities = new ObservableObjectMap().append(loadedEntities); - entities.forEach((uuid, entity) -> save(entity)); - } - - /** - * Loads all the entities by retrieving their IDs and invoking the {@link #load(UUID)} method. - * If the entities directory is specified, it scans the directory for entity files and extracts their IDs. - * If the database is specified, it retrieves entity IDs from the database and loads them. - */ - @Beta - @Override - public void loadAll() { - List entityIDs = new ArrayList<>(); - if (entitiesDirectory != null) { - File[] files = this.entitiesDirectory.listFiles((dir, name) -> name.endsWith(".yml")); - if (files != null) { - Arrays.stream(files).forEach(file -> entityIDs.add(UUID.fromString(file.getName().replace(".yml", "")))); - } - } else if (database != null) { - Pair> data = database.retrieveData(collection, Pair.create("entity_id", null)); - data.value().forEach(databaseObject -> entityIDs.add(UUID.fromString(String.valueOf(databaseObject.data().get("entity_id"))))); - } - entityIDs.forEach(this::load); - } - - /** - * Retrieves the current map of loaded entities. - * - * @return the map of loaded entities with UUID as the key and User object as the value - */ - @Override - public ObservableObjectMap getLoadedEntities() { - return loadedEntities; - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/utilities/PluginMessageUtils.java b/core/src/main/java/com/georgev22/skinoverlay/utilities/PluginMessageUtils.java deleted file mode 100644 index de46848a..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/utilities/PluginMessageUtils.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.georgev22.skinoverlay.utilities; - -import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; -import lombok.Getter; -import lombok.Setter; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; - -import static com.georgev22.skinoverlay.utilities.Utilities.encrypt; - -/** - * Provides utility methods for sending plugin messages to players or the server. - */ -public abstract class PluginMessageUtils { - - /** - * The channel used to send the plugin message. - */ - @Setter - @Getter - private String channel; - - /** - * The object to send in the plugin message. - */ - @Setter - @Getter - private Object object; - - /** - * The SkinOverlay instance. - */ - protected final SkinOverlay skinOverlay = SkinOverlay.getInstance(); - - /** - * Sends a plugin message to the server. - * - * @param subChannel the sub-channel to send the message on. - * @param dataArray the data to send in the message. - */ - public abstract void sendDataToServer(@NotNull String subChannel, String... dataArray); - - /** - * Sends a plugin message to a player. - * - * @param subChannel the sub-channel to send the message on. - * @param player the player to send the message to. - * @param dataArray the data to send in the message. - */ - public abstract void sendDataToPlayer(@NotNull String subChannel, @NotNull PlayerObject player, String... dataArray); - - /** - * Sends a plugin message to all online players. - * - * @param subChannel the sub-channel to send the message on. - * @param dataArray the data to send in the message. - */ - public void sendDataToAllPlayers(@NotNull String subChannel, String... dataArray) { - skinOverlay.onlinePlayers().forEach(playerObject -> sendDataToPlayer(subChannel, playerObject, dataArray)); - } - - /** - * Creates a ByteArrayDataOutput object with the given sub-channel and data. - * - * @param subChannel the sub-channel to write to the output. - * @param dataArray the data to write to the output. - * @return a new ByteArrayDataOutput object. - */ - @NotNull - public ByteArrayDataOutput byteArrayDataOutput(@NotNull String subChannel, String @NotNull ... dataArray) { - ByteArrayDataOutput out = ByteStreams.newDataOutput(); - out.writeUTF(subChannel); - for (String data : dataArray) { - String encryptedData = encrypt(data); - out.writeUTF(Objects.requireNonNull(encryptedData)); - } - return out; - } - - /** - * Converts the data in the given channel and data array to a byte array. - * - * @param channel the channel to use. - * @param dataArray the data to convert. - * @return a byte array representation of the data. - */ - public byte @NotNull [] toByteArray(@NotNull String channel, String... dataArray) { - return this.byteArrayDataOutput(channel, dataArray).toByteArray(); - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/utilities/Updater.java b/core/src/main/java/com/georgev22/skinoverlay/utilities/Updater.java deleted file mode 100644 index 1a025eef..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/utilities/Updater.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.georgev22.skinoverlay.utilities; - -import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.utilities.config.OptionsUtil; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonParser; -import org.jetbrains.annotations.NotNull; - -import javax.net.ssl.HttpsURLConnection; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URL; -import java.nio.charset.StandardCharsets; - -public class Updater { - - private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); - private final String localVersion = skinOverlay.getDescription().version(); - private String onlineVersion; - - { - try { - onlineVersion = getOnlineVersion(); - } catch (IOException e) { - skinOverlay.getLogger().warning("Failed to check for an update on GitHub. Either GitHub is offline or has rejected the request due to rate limiting, or you are experiencing slow response times."); - } - } - - public Updater() { - if (!OptionsUtil.UPDATER.getBooleanValue()) - return; - if (onlineVersion == null) - return; - this.skinOverlay.getMinecraftScheduler().createAsyncRepeatingTask(skinOverlay.getPlugin(), () -> { - skinOverlay.getSkinOverlay().print("Checking for Updates ... "); - if (compareVersions(onlineVersion.replace("v", ""), localVersion.replace("v", "")) == 0) { - skinOverlay.getSkinOverlay().print("You are running the newest build."); - } else if (compareVersions(onlineVersion.replace("v", ""), localVersion.replace("v", "")) == 1) { - skinOverlay.getSkinOverlay().print( - "New stable version available!", - "Version: " + onlineVersion + ". You are running version: " + localVersion, - "Update at: https://github.com/GeorgeV220/SkinOverlay/releases/"); - } else { - skinOverlay.getSkinOverlay().print("You are currently using the " + localVersion + " version which is under development.", "Your version is " + localVersion, "Latest released version is " + onlineVersion, "If you have problems contact me on discord or github. Thank you for testing this version"); - } - }, 20L, 20 * 7200); - } - - public Updater(PlayerObject playerObject) { - if (!playerObject.permission("skinoverlay.updater")) - return; - if (!OptionsUtil.UPDATER.getBooleanValue()) - return; - if (onlineVersion == null) - return; - this.skinOverlay.getMinecraftScheduler().runAsyncTask(skinOverlay.getPlugin(), () -> { - playerObject.sendMessage("&e&lSkinOverlay Updater &8» &6Checking for Updates ..."); - if (compareVersions(onlineVersion.replace("v", ""), localVersion.replace("v", "")) == 0) { - playerObject.sendMessage("&e&lSkinOverlay Updater &8» &6You are running the newest build."); - } else if (compareVersions(onlineVersion.replace("v", ""), localVersion.replace("v", "")) == 1) { - playerObject.sendMessage("&e&lSkinOverlay Updater &8» &6New version available!"); - playerObject.sendMessage("&e&lSkinOverlay Updater &8» &6Version: &c" - + onlineVersion + ". &6You are running version: &c" + localVersion); - playerObject.sendMessage("&e&lSkinOverlay Updater &8» &6Update at: https://github.com/GeorgeV220/SkinOverlay/releases/"); - - } else { - playerObject.sendMessage("&e&lSkinOverlay Updater &8» &6You are currently using the &c" + localVersion + " &6version which is under development. If you have problems contact me on discord or github"); - playerObject.sendMessage("&e&lSkinOverlay Updater &8» &6Your version is &c" + localVersion); - playerObject.sendMessage("&e&lSkinOverlay Updater &8» &6Latest released version is &c" + onlineVersion); - } - }); - } - - - private int compareVersions(@NotNull String version1, @NotNull String version2) { - if (version1.contains("alpha") | version1.contains("beta")) { - return -1; - } - - int comparisonResult = 0; - - String[] version1Splits = version1.split("\\."); - String[] version2Splits = version2.split("\\."); - int maxLengthOfVersionSplits = Math.max(version1Splits.length, version2Splits.length); - - for (int i = 0; i < maxLengthOfVersionSplits; i++) { - Integer v1 = i < version1Splits.length ? Integer.parseInt(version1Splits[i]) : 0; - Integer v2 = i < version2Splits.length ? Integer.parseInt(version2Splits[i]) : 0; - int compare = v1.compareTo(v2); - if (compare != 0) { - comparisonResult = compare; - break; - } - } - return comparisonResult; - } - - private @NotNull String getOnlineVersion() throws IOException { - if (!OptionsUtil.UPDATER.getBooleanValue()) - return skinOverlay.getSkinOverlay().description().version(); - System.setProperty("http.agent", "Chrome"); - HttpsURLConnection con = (HttpsURLConnection) new URL("https://api.github.com/repos/GeorgeV220/SkinOverlay/tags").openConnection(); - - con.setDoOutput(true); - - con.setRequestMethod("GET"); - - BufferedReader rd = new BufferedReader(new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8)); - StringBuilder sb = new StringBuilder(); - int cp; - while ((cp = rd.read()) != -1) { - sb.append((char) cp); - } - String jsonText = sb.toString(); - JsonElement jsonElement = JsonParser.parseString(jsonText); - JsonArray jsonArray = jsonElement.getAsJsonArray(); - - return jsonArray.get(0).getAsJsonObject().get("name").getAsString().replace("\"", ""); - } - -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/utilities/Utilities.java b/core/src/main/java/com/georgev22/skinoverlay/utilities/Utilities.java deleted file mode 100644 index 39c42a27..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/utilities/Utilities.java +++ /dev/null @@ -1,311 +0,0 @@ -package com.georgev22.skinoverlay.utilities; - -import com.georgev22.skinoverlay.utilities.config.OptionsUtil; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import javax.crypto.Cipher; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.SecretKeySpec; -import javax.net.ssl.HttpsURLConnection; -import java.io.*; -import java.net.ProtocolException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.spec.KeySpec; -import java.util.Arrays; -import java.util.Base64; -import java.util.UUID; - -public class Utilities { - - /** - * Generates a deterministic UUID from a given seed using the SHA-256 hash function. - * - * @param seed the input seed used to generate the UUID - * @return a UUID generated from the seed - */ - @Contract("_ -> new") - public static @NotNull UUID generateUUID(@NotNull String seed) { - try { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - md.update(seed.getBytes(StandardCharsets.UTF_8)); - byte[] hash = md.digest(); - long msb = 0; - long lsb = 0; - for (int i = 0; i < 8; i++) - msb = (msb << 8) | (hash[i] & 0xff); - for (int i = 8; i < 16; i++) - lsb = (lsb << 8) | (hash[i] & 0xff); - return new UUID(msb, lsb); - } catch (NoSuchAlgorithmException exception) { - throw new RuntimeException(exception); - } - } - - /** - * Decodes the specified Base64-encoded string into an object. - * - * @param bytesString the Base64-encoded string - * @return the decoded object - * @throws RuntimeException if the decoding fails for any reason - */ - public static Object getObject(@NotNull String bytesString) { - // Decode the Base64 string into bytes - final byte[] bytes = Base64.getDecoder().decode(bytesString); - try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes); - ObjectInput in = new ObjectInputStream(bis)) { - // Read the object from the input stream - return in.readObject(); - } catch (ClassNotFoundException | IOException e) { - // Throw a runtime exception if the decoding fails - throw new RuntimeException(e); - } - } - - /** - * Encodes the specified object into a Base64-encoded string. - * - * @param object the object to encode - * @return the Base64-encoded string - * @throws RuntimeException if the encoding fails for any reason - */ - public static String objectToString(Object object) { - try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ObjectOutput out = new ObjectOutputStream(bos)) { - // Write the object to the output stream - out.writeObject(object); - final byte[] byteArray = bos.toByteArray(); - // Encode the byte array into a Base64 string - return Base64.getEncoder().encodeToString(byteArray); - } catch (IOException e) { - // Throw a runtime exception if the encoding fails - throw new RuntimeException(e); - } - } - - /** - * Decrypts a Base64-encoded string using AES encryption. - * - * @param encryptedText the Base64-encoded string to decrypt - * @return the decrypted string, or null if decryption failed - */ - public static @Nullable String decrypt(String encryptedText) { - try { - byte[] salt = Arrays.copyOfRange(Base64.getDecoder().decode(encryptedText), 0, 16); - KeySpec spec = new PBEKeySpec(OptionsUtil.SECRET.getStringValue().toCharArray(), salt, 65536, 256); - SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); - byte[] keyBytes = factory.generateSecret(spec).getEncoded(); - SecretKey key = new SecretKeySpec(keyBytes, "AES"); - Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); - cipher.init(Cipher.DECRYPT_MODE, key); - byte[] encryptedBytes = Arrays.copyOfRange(Base64.getDecoder().decode(encryptedText), 16, Base64.getDecoder().decode(encryptedText).length); - byte[] decryptedBytes = cipher.doFinal(encryptedBytes); - return new String(decryptedBytes, StandardCharsets.UTF_8); - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - - /** - * Encrypts a string using AES encryption and returns the result as a Base64-encoded string. - * - * @param plaintext the string to encrypt - * @return the Base64-encoded encrypted string, or null if encryption failed - */ - public static @Nullable String encrypt(String plaintext) { - try { - byte[] salt = new byte[16]; - SecureRandom random = new SecureRandom(); - random.nextBytes(salt); - KeySpec spec = new PBEKeySpec(OptionsUtil.SECRET.getStringValue().toCharArray(), salt, 65536, 256); - SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); - byte[] keyBytes = factory.generateSecret(spec).getEncoded(); - SecretKey key = new SecretKeySpec(keyBytes, "AES"); - Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); - cipher.init(Cipher.ENCRYPT_MODE, key); - byte[] encryptedBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); - byte[] combined = new byte[16 + encryptedBytes.length]; - System.arraycopy(salt, 0, combined, 0, 16); - System.arraycopy(encryptedBytes, 0, combined, 16, encryptedBytes.length); - return Base64.getEncoder().encodeToString(combined); - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - - - /** - * The Request class provides methods for making HTTPS requests. - */ - public static class Request { - /** - * The address of the HTTP(S) endpoint. - */ - private String address; - - /** - * The bytes returned from the HTTP(S) response. - */ - private byte[] bytes; - - /** - * The HTTP status code returned by the endpoint. - */ - private int httpCode; - - /** - * The HTTPS URL connection used to make the request. - */ - private HttpsURLConnection httpsURLConnection; - - /** - * Sets the HTTP request method to "GET". - * - * @return This Request object. - * @throws ProtocolException If the request method could not be set. - */ - public Request getRequest() throws ProtocolException { - this.httpsURLConnection.setRequestMethod("GET"); - this.httpsURLConnection.setRequestProperty("User-Agent", "SkinOverlay"); - return this; - } - - /** - * Sets the HTTP request method to "POST" and sets several request properties. - * - * @return This Request object. - * @throws ProtocolException If the request method could not be set. - */ - public Request postRequest() throws ProtocolException { - this.httpsURLConnection.setRequestMethod("POST"); - this.httpsURLConnection.setRequestProperty("Connection", "Keep-Alive"); - this.httpsURLConnection.setRequestProperty("Cache-Control", "no-cache"); - this.httpsURLConnection.setRequestProperty("User-Agent", "SkinOverlay"); - this.httpsURLConnection.setDoOutput(true); - return this; - } - - /** - * Opens a connection to the specified HTTP(S) endpoint. - * - * @param address The address of the endpoint. - * @return This Request object. - * @throws IOException If a connection to the endpoint could not be established. - */ - public Request openConnection(String address) throws IOException { - this.address = address; - final URL url = new URL(address); - this.httpsURLConnection = (HttpsURLConnection) url.openConnection(); - return this; - } - - /** - * Sets a request property to the specified key-value pair. - * - * @param key The key for the property. - * @param value The value for the property. - * @return This Request object. - */ - public Request setRequestProperty(String key, String value) { - this.httpsURLConnection.setRequestProperty(key, value); - return this; - } - - /** - * Writes one or more strings to the request output stream. - * - * @param data The strings to write. - * @return This Request object. - * @throws IOException If an I/O error occurs. - */ - public Request writeToOutputStream(final String @NotNull ... data) throws IOException { - for (final String str : data) { - this.httpsURLConnection.getOutputStream().write(str.getBytes()); - } - return this; - } - - /** - * Writes one or more byte arrays to the request output stream. - * - * @param data The byte arrays to write. - * @return This Request object. - * @throws IOException If an I/O error occurs. - */ - public Request writeToOutputStream(final byte @NotNull []... data) throws IOException { - for (final byte[] bytes : data) { - this.httpsURLConnection.getOutputStream().write(bytes); - } - return this; - } - - /** - * Closes the request output stream. - * - * @return This Request object. - * @throws IOException If an I/O error occurs. - */ - public Request closeOutputStream() throws IOException { - this.httpsURLConnection.getOutputStream().close(); - return this; - } - - /** - * Finalizes the request by getting the HTTP response code and reading the response body bytes. - * - * @return The updated Request object. - * @throws IOException If an I/O error occurs while finalizing the request. - */ - public Request finalizeRequest() throws IOException { - this.httpCode = this.httpsURLConnection.getResponseCode(); - this.bytes = this.httpsURLConnection.getInputStream().readAllBytes(); - return this; - } - - /** - * Gets the HTTP response code of the request. - * - * @return The HTTP response code. - */ - public int getHttpCode() { - return this.httpCode; - } - - /** - * Gets the bytes of the response body of the request. - * - * @return The response body bytes. - */ - public byte[] getBytes() { - return this.bytes; - } - - /** - * Gets the address of the request. - * - * @return The request address. - */ - public String getAddress() { - return this.address; - } - - /** - * Gets the underlying HTTPS connection object. - * - * @return The HTTPS connection object. - */ - public HttpsURLConnection getHttpsURLConnection() { - return httpsURLConnection; - } - } - -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/utilities/config/MessagesUtil.java b/core/src/main/java/com/georgev22/skinoverlay/utilities/config/MessagesUtil.java deleted file mode 100644 index f401c5e3..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/utilities/config/MessagesUtil.java +++ /dev/null @@ -1,149 +0,0 @@ -package com.georgev22.skinoverlay.utilities.config; - -import co.aikar.commands.CommandIssuer; -import com.georgev22.library.maps.HashObjectMap; -import com.georgev22.library.utilities.Utils; -import com.georgev22.library.yaml.configmanager.CFG; -import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.utilities.Locale; -import lombok.Getter; -import lombok.Setter; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import org.jetbrains.annotations.NotNull; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -import static com.georgev22.library.utilities.Utils.placeHolder; - -public enum MessagesUtil { - NO_PERMISSION("Messages.No Permission", "&c&l(!)&c You do not have the correct permissions to do this!"), - ONLY_PLAYER_COMMAND("Messages.Only Player Command", "&c&l(!)&c Only players can run this command!"), - OFFLINE_PLAYER("Messages.Offline Player", "&c&l(!)&c Player %player% is offline!"), - DONE("Messages.Overlay Applied", "&a&l(!)&a Overlay %url% applied!"), - RESET("Messages.Overlay Reset", "&a&l(!)&a Default skin applied(%player%)!!"), - OVERLAY_NOT_FOUND("Messages.Overlay Not Found", "&c&l(!)&c Overlay %overlay% not found!"), - INSUFFICIENT_ARGUMENTS("Messages.Insufficient arguments", "&c&l(!)&c Insufficient arguments (%command%)"), - COMMANDS_DESCRIPTIONS_SKINOVERLAY_HELP("Commands.Descriptions.SkinOverlay.help", "Shows the help page"), - COMMANDS_DESCRIPTIONS_SKINOVERLAY_OVERLAY("Commands.Descriptions.SkinOverlay.overlay", "Wear a specific overlay from the plugin files"), - COMMANDS_DESCRIPTIONS_SKINOVERLAY_CLEAR("Commands.Descriptions.SkinOverlay.clear", "Removes the skin overlay"), - COMMANDS_DESCRIPTIONS_SKINOVERLAY_URL("Commands.Descriptions.SkinOverlay.url", "Wear a specific overlay from a URL"), - COMMANDS_DESCRIPTIONS_SKINOVERLAY_RELOAD("Commands.Descriptions.SkinOverlay.reload", "Reload the plugin configuration files (some settings need server restart)"), - ; - private String[] messages; - private final String path; - private static final SkinOverlay skinOverlay = SkinOverlay.getInstance(); - - @Getter - @Setter - private static CFG messagesCFG; - @Getter - @Setter - private static Locale locale; - - MessagesUtil(String path, String... messages) { - this.messages = messages; - this.path = path; - } - - private boolean isMultiLined() { - return this.messages.length > 1; - } - - public static void repairPaths(Locale locale) throws Exception { - if (messagesCFG == null | !Objects.equals(MessagesUtil.locale, locale)) { - setLocale(locale); - setMessagesCFG(new CFG("messages_" + locale.getStringLocale(), skinOverlay.getDataFolder(), true, true, skinOverlay.getLogger(), skinOverlay.getClass())); - } - boolean changed = false; - for (MessagesUtil enumMessage : MessagesUtil.values()) { - if (messagesCFG.getFileConfiguration().contains(enumMessage.getPath())) { - setPathToMessage(messagesCFG, enumMessage); - continue; - } - setMessageToPath(messagesCFG, enumMessage); - if (changed) continue; - changed = true; - } - if (changed) { - messagesCFG.saveFile(); - } - } - - private static void setMessageToPath(CFG cfg, @NotNull MessagesUtil enumMessage) { - if (enumMessage.isMultiLined()) { - cfg.getFileConfiguration().set(enumMessage.getPath(), enumMessage.getMessages()); - } else { - cfg.getFileConfiguration().set(enumMessage.getPath(), enumMessage.getMessages()[0]); - } - } - - private static void setPathToMessage(@NotNull CFG cfg, @NotNull MessagesUtil enumMessage) { - if (Utils.isList(cfg.getFileConfiguration(), enumMessage.getPath())) { - enumMessage.setMessages(cfg.getFileConfiguration().getStringList(enumMessage.getPath()).toArray(new String[0])); - } else { - enumMessage.setMessages(cfg.getFileConfiguration().getString(enumMessage.getPath())); - } - } - - public String getPath() { - return this.path; - } - - public String[] getMessages() { - return this.messages; - } - - public void setMessages(String[] messages) { - this.messages = messages; - } - - public void setMessages(String messages) { - this.messages[0] = messages; - } - - public void msg(@NotNull CommandIssuer issuer) { - this.msg(issuer, new HashObjectMap<>(), false); - } - - public void msg(CommandIssuer issuer, Map map, boolean ignoreCase) { - if (this.isMultiLined()) { - for (String message : messages) { - issuer.sendMessage(LegacyComponentSerializer.legacySection().serialize(LegacyComponentSerializer.legacy('&').deserialize(placeHolder(message, map, ignoreCase)))); - } - } else { - issuer.sendMessage(LegacyComponentSerializer.legacySection().serialize(LegacyComponentSerializer.legacy('&').deserialize(placeHolder(this.getMessages()[0], map, ignoreCase)))); - } - } - - public void msgConsole() { - msgConsole(new HashMap<>(), false); - } - - public void msgConsole(Map map, boolean ignoreCase) { - if (this.isMultiLined()) { - skinOverlay.print(Utils.placeHolder(this.getMessages(), map, ignoreCase)); - } else { - skinOverlay.print(Utils.placeHolder(this.getMessages()[0], map, ignoreCase)); - } - } - - public void msgAll() { - if (this.isMultiLined()) { - skinOverlay.onlinePlayers().forEach(playerObject -> playerObject.sendMessage(this.getMessages())); - } else { - skinOverlay.onlinePlayers().forEach(playerObject -> playerObject.sendMessage(this.getMessages()[0])); - } - } - - public void msgAll(Map map, boolean ignoreCase) { - if (this.isMultiLined()) { - skinOverlay.onlinePlayers().forEach(playerObject -> playerObject.sendMessage(placeHolder(this.getMessages(), map, ignoreCase))); - } else { - skinOverlay.onlinePlayers().forEach(playerObject -> playerObject.sendMessage(placeHolder(this.getMessages()[0], map, ignoreCase))); - } - } - -} - diff --git a/core/src/main/java/com/georgev22/skinoverlay/utilities/interfaces/SkinOverlayImpl.java b/core/src/main/java/com/georgev22/skinoverlay/utilities/interfaces/SkinOverlayImpl.java deleted file mode 100644 index bdde58a4..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/utilities/interfaces/SkinOverlayImpl.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.georgev22.skinoverlay.utilities.interfaces; - -import com.georgev22.library.maps.ObservableObjectMap; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import net.kyori.adventure.platform.AudienceProvider; -import org.jetbrains.annotations.NotNull; - -import java.io.File; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; -import java.util.logging.Logger; - -/** - * An interface representing the core functionality of the SkinOverlay plugin. - */ -public interface SkinOverlayImpl { - - /** - * Returns the type of server implementation (Bukkit, Bungee or Velocity). - * - * @return The type of server implementation. - */ - Type type(); - - /** - * Returns the plugin's data folder. - * - * @return The plugin's data folder. - */ - File dataFolder(); - - /** - * Returns the plugin's logger. - * - * @return The plugin's logger. - */ - Logger logger(); - - /** - * Returns the plugin's description. - * - * @return The plugin's description. - */ - Description description(); - - /** - * Enables or disables the plugin. - * - * @param enable true to enable, false to disable. - * @return True if the plugin was successfully enabled or disabled, false otherwise. - */ - boolean enable(boolean enable); - - /** - * Returns whether the plugin is currently enabled. - * - * @return True if the plugin is enabled, false otherwise. - */ - boolean enabled(); - - /** - * Saves a resource from the plugin's JAR file to the plugin's data folder. - * - * @param resource The path of the resource to save. - * @param replace True to replace the file if it already exists, false otherwise. - */ - void saveResource(@NotNull String resource, boolean replace); - - /** - * Returns whether the server is running in online mode. - * - * @return True if the server is running in online mode, false otherwise. - */ - boolean onlineMode(); - - /** - * Returns an ObservableObjectMap of PlayerObject instances representing all online players on the server. - * - * @return An ObservableObjectMap of PlayerObject instances representing all online players on the server. - */ - ObservableObjectMap onlinePlayers(); - - /** - * Returns a boolean value indicating whether a specified plugin is enabled. - * - * @param pluginName the name of the plugin to check - * @return {@code true} if the plugin is enabled, {@code false} otherwise. - */ - boolean isPluginEnabled(String pluginName); - - /** - * Returns the plugin instance. - * - * @return The plugin instance. - */ - T plugin(); - - /** - * Returns the server implementation instance. - * - * @return The server implementation instance. - */ - T serverImpl(); - - /** - * Returns the version of the server implementation. - * - * @return The version of the server implementation. - */ - String serverVersion(); - - /** - * Prints a message to the console. - * - * @param msg The message(s) to print. - */ - default void print(String... msg) { - Arrays.stream(msg).forEach(s -> logger().info(s)); - } - - AudienceProvider adventure(); - - /** - * A record representing the plugin's description. - */ - record Description(String name, String version, String main, List authors) { - } - - /** - * An enum representing the type of server implementation. - */ - enum Type { - BUKKIT, - BUNGEE, - VELOCITY; - - /** - * Returns whether the server implementation is a proxy server. - * - * @return True if the server implementation is a proxy server, false otherwise. - */ - public boolean isProxy() { - return this.equals(VELOCITY) || this.equals(BUNGEE); - } - } -} \ No newline at end of file diff --git a/core/src/main/java/com/georgev22/skinoverlay/utilities/player/PlayerObject.java b/core/src/main/java/com/georgev22/skinoverlay/utilities/player/PlayerObject.java deleted file mode 100644 index 8fc3585c..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/utilities/player/PlayerObject.java +++ /dev/null @@ -1,453 +0,0 @@ -package com.georgev22.skinoverlay.utilities.player; - -import com.georgev22.library.maps.HashObjectMap; -import com.georgev22.library.maps.ObjectMap; -import com.georgev22.library.maps.ObjectMap.Pair; -import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.event.events.player.PlayerObjectUserEvent; -import com.georgev22.skinoverlay.event.events.player.skin.PlayerObjectPreUpdateSkinEvent; -import com.georgev22.skinoverlay.event.events.user.data.UserModifyDataEvent; -import com.georgev22.skinoverlay.event.events.user.data.add.UserAddDataEvent; -import com.georgev22.skinoverlay.event.events.user.data.load.UserPostLoadEvent; -import com.georgev22.skinoverlay.event.events.user.data.load.UserPreLoadEvent; -import com.georgev22.skinoverlay.exceptions.UserException; -import com.georgev22.skinoverlay.handler.SGameProfile; -import com.georgev22.skinoverlay.handler.skin.SkinParts; -import com.georgev22.skinoverlay.storage.data.Skin; -import com.georgev22.skinoverlay.utilities.Updater; -import com.georgev22.skinoverlay.utilities.Utilities; -import com.georgev22.skinoverlay.utilities.config.OptionsUtil; -import com.georgev22.skinoverlay.utilities.interfaces.SkinOverlayImpl.Type; -import net.kyori.adventure.audience.Audience; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.ExecutionException; -import java.util.logging.Level; - -/** - * The PlayerObject class represents a player in the game. - */ -public abstract class PlayerObject { - private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); - - /** - * Returns the PlayerObject instance. - * - * @return The PlayerObject instance. - */ - public PlayerObject playerObject() { - return this; - } - - /** - * Returns the player object. - * - * @return The player object. - */ - public abstract T player(); - - /** - * Returns the audience of the player. - * - * @return The audience of the player. - */ - public abstract Audience audience(); - - /** - * Returns the UUID of the player. - * - * @return The UUID of the player. - */ - public abstract UUID playerUUID(); - - /** - * Returns the name of the player. - * - * @return The name of the player. - */ - public abstract String playerName(); - - /** - * Checks if the player is using Bedrock Edition. - * - * @return true if the player is using Bedrock Edition, false otherwise. - */ - public boolean isBedrock() { - return this.playerUUID().toString().replace("-", "").startsWith("000000"); - } - - /** - * Sends a message to the player. - * - * @param input The message to be sent. - */ - public abstract void sendMessage(String input); - - /** - * Sends a list of messages to the player. - * - * @param input The list of messages to be sent. - */ - public abstract void sendMessage(List input); - - /** - * Sends an array of messages to the player. - * - * @param input The array of messages to be sent. - */ - public abstract void sendMessage(String... input); - - /** - * Sends a message to the player with placeholders. - * - * @param input The message to be sent. - * @param placeholders The map of placeholders and their values. - * @param ignoreCase Whether to ignore the case of the placeholders. - */ - public abstract void sendMessage(String input, ObjectMap placeholders, boolean ignoreCase); - - /** - * Sends a list of messages to the player with placeholders. - * - * @param input The list of messages to be sent. - * @param placeholders The map of placeholders and their values. - * @param ignoreCase Whether to ignore the case of the placeholders. - */ - public abstract void sendMessage(List input, ObjectMap placeholders, boolean ignoreCase); - - /** - * Sends an array of messages to the player with placeholders. - * - * @param input The array of messages to be sent. - * @param placeholders The map of placeholders and their values. - * @param ignoreCase Whether to ignore the case of the placeholders. - */ - public abstract void sendMessage(String[] input, ObjectMap placeholders, boolean ignoreCase); - - /** - * Checks if the player is online. - * - * @return true if the player is online, false otherwise. - */ - public abstract boolean isOnline(); - - /** - * Checks if the player has a permission. - * - * @param permission The permission to be checked. - * @return true if the player has the permission, false otherwise. - */ - public abstract boolean permission(String permission); - - /** - * Returns the game profile associated with this player object. - * - * @return the game profile associated with this player object - * @throws RuntimeException if there is an error retrieving the game profile - */ - public SGameProfile gameProfile() { - try { - return SkinOverlay.getInstance().getSkinHandler().getGameProfile(playerObject()); - } catch (IOException | ExecutionException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - /** - * Returns the internal game profile object associated with this player object. - * - * @return the internal game profile object associated with this player object - * @throws RuntimeException if there is an error retrieving the game profile - */ - public Object internalGameProfile() { - try { - return SkinOverlay.getInstance().getSkinHandler().getInternalGameProfile(playerObject()); - } catch (IOException | ExecutionException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - private final List> inform = Collections.singletonList( - Pair.create("GeorgeV22", UUID.fromString("a4f5cd7f-362f-4044-931e-7128b4e6bad9")) - ); - - /** - * Checks if a player is registered to receive developer information. - */ - public void developerInform() { - - final Pair pair = Pair.create(playerName(), playerUUID()); - - boolean found = false; - - for (Pair loop : this.inform) { - if (loop.key().equals(pair.key())) { - found = true; - break; - } - if (loop.value().equals(pair.value())) { - found = true; - break; - } - } - - if (!found) { - return; - } - - this.skinOverlay.getMinecraftScheduler().createDelayedTask(this.skinOverlay.getSkinOverlay().plugin(), () -> { - if (!isOnline() && player() == null) { - return; - } - - sendMessage(Arrays.asList( - - "", - - "", - - "&7Hey &f%player%&7, details are listed below.", - - "&7Version: &c%version%", - - "&7Java Version: &c%javaversion%", - - "&7Server Version: &c%serverversion%", - - "&7Name: &c%name%", - - "&7Author: &c%author%", - - "&7Main package: &c%package%", - - "&7Main path: &c%main%", - - "&7Experimental Features: &c" + OptionsUtil.EXPERIMENTAL_FEATURES.getBooleanValue(), - - "" - - ), new HashObjectMap() - .append("%player%", playerName()) - .append("%version%", skinOverlay.getDescription().version()) - .append("%package%", skinOverlay.getClass().getPackage().getName()) - .append("%name%", skinOverlay.getDescription().name()) - .append("%author%", String.join(", ", skinOverlay.getDescription().authors())) - .append("%main%", skinOverlay.getDescription().main()) - .append("%javaversion%", System.getProperty("java.version")) - .append("%serverversion%", skinOverlay.getSkinOverlay().serverVersion()), false); - }, 20L * 10L); - } - - /** - * Called when a player joins the server. - * If the player has the "skinoverlay.updater" permission, a new Updater instance is created. - * If the server is not a proxy server and the "proxy" option is set to true, the method does not continue. - * Retrieves the User object associated with the player's UUID from the UserManager and asynchronously handles it. - * Adds custom data to the user object, saves it, and updates the player's skin. - */ - public void playerJoin() { - new Updater(this); - UserPreLoadEvent userPreLoadEvent = (UserPreLoadEvent) skinOverlay.getEventManager() - .callEvent(new UserPreLoadEvent(this.playerUUID(), true)); - if (userPreLoadEvent.isCancelled()) { - return; - } - skinOverlay.getUserManager().getEntity(playerUUID()) - .handleAsync((entity, throwable) -> { - if (throwable != null) { - skinOverlay.getLogger().log(Level.SEVERE, "Error retrieving user: ", throwable); - return null; - } - if (entity == null) { - throw new UserException("User not found!"); - } - UserModifyDataEvent userModifyDataEvent = (UserModifyDataEvent) skinOverlay.getEventManager() - .callEvent(new UserModifyDataEvent(entity, true)); - if (userModifyDataEvent.isCancelled()) { - return entity; - } - UUID skinUUID = Utilities.generateUUID("default" + playerUUID().toString()); - try { - UserAddDataEvent event = (UserAddDataEvent) skinOverlay.getEventManager() - .callEvent(new UserAddDataEvent( - entity, - Pair.create( - "defaultSkin", - new Skin(skinUUID, - gameProfile().getProperties().get("textures") != null - ? gameProfile().getProperties().get("textures") - : skinOverlay.getSkinHandler().getSkin(playerObject()), - "default") - ), - true)); - if (!event.isCancelled()) { - entity.addCustomData(event.getData().key(), event.getData().value()); - if (!skinOverlay.getSkinOverlay().type().isProxy() && OptionsUtil.PROXY.getBooleanValue()) { - return event.getUser(); - } - skinOverlay.getSkinManager().save((Skin) event.getData().value()).handleAsync((unused, saveThrowable) -> { - if (saveThrowable != null) { - skinOverlay.getLogger().log(Level.SEVERE, "Error: ", saveThrowable); - return unused; - } - return unused; - }); - } - } catch (IOException | ExecutionException | InterruptedException e) { - throw new RuntimeException(e); - } - - UserAddDataEvent event = (UserAddDataEvent) skinOverlay.getEventManager() - .callEvent(new UserAddDataEvent( - entity, - Pair.create("skin", entity.defaultSkin()), - true) - ); - if (!event.isCancelled()) { - entity.addCustomDataIfNotExists(event.getData().key(), event.getData().value()); - } - - userModifyDataEvent = (UserModifyDataEvent) skinOverlay.getEventManager() - .callEvent(new UserModifyDataEvent(entity, true)); - if (!userModifyDataEvent.isCancelled()) { - if (!skinOverlay.getSkinOverlay().type().isProxy() && OptionsUtil.PROXY.getBooleanValue()) { - return userModifyDataEvent.getUser(); - } - skinOverlay.getUserManager().save(userModifyDataEvent.getUser()).handleAsync((unused, saveThrowable) -> { - if (saveThrowable != null) { - skinOverlay.getLogger().log(Level.SEVERE, "Error: ", saveThrowable); - return unused; - } - return unused; - }); - } - return userModifyDataEvent.getUser(); - }).handleAsync((user, throwable) -> { - if (throwable != null) { - skinOverlay.getLogger().log(Level.SEVERE, "Error: ", throwable); - return null; - } - return user; - }).thenApplyAsync(user -> { - if (user != null) { - PlayerObjectUserEvent playerObjectUserEvent = (PlayerObjectUserEvent) skinOverlay.getEventManager() - .callEvent(new PlayerObjectUserEvent(playerObject(), user, true)); - - if (playerObjectUserEvent.isCancelled()) - return playerObjectUserEvent.getUser(); - - UserPostLoadEvent userPostLoadEvent = (UserPostLoadEvent) skinOverlay.getEventManager() - .callEvent(new UserPostLoadEvent(user, true)); - if (userPostLoadEvent.isCancelled()) - return userPostLoadEvent.getUser(); - - if (!OptionsUtil.PROXY.getBooleanValue()) - updateSkin(); - - if (skinOverlay.type().equals(Type.BUKKIT) & OptionsUtil.PROXY.getBooleanValue()) { - this.skinOverlay.getMinecraftScheduler().runAsyncTask(this.skinOverlay.getSkinOverlay().plugin(), () -> { - skinOverlay.getPluginMessageUtils().setChannel("skinoverlay:message"); - if (isOnline()) - skinOverlay.getPluginMessageUtils().sendDataToPlayer("playerJoin", this, playerUUID().toString()); - else - skinOverlay.getLogger().warning("Player " + playerName() + " is not online"); - }); - } - return userPostLoadEvent.getUser(); - } - return null; - }); - } - - /** - * Called when a player quits the server. - * If the server is not a proxy server and the "proxy" option is set to true, the method does nothing. - * Retrieves the User object associated with the player's UUID from the UserManager and asynchronously handles it. - * Saves the user object. - */ - public void playerQuit() { - skinOverlay.getSkinOverlay().onlinePlayers().remove(playerUUID()); - if (!skinOverlay.getSkinOverlay().type().isProxy() && OptionsUtil.PROXY.getBooleanValue()) { - return; - } - skinOverlay.getUserManager().getEntity(playerUUID()).handleAsync((entity, throwable) -> { - if (throwable != null) { - skinOverlay.getLogger().log(Level.SEVERE, "Error retrieving user: ", throwable); - return null; - } - return entity; - }).thenAcceptAsync(entity -> { - if (entity != null) { - UserModifyDataEvent userModifyDataEvent = (UserModifyDataEvent) skinOverlay.getEventManager() - .callEvent(new UserModifyDataEvent(entity, true)); - if (!userModifyDataEvent.isCancelled()) { - if (!skinOverlay.getSkinOverlay().type().isProxy() && OptionsUtil.PROXY.getBooleanValue()) { - return; - } - skinOverlay.getUserManager().save(userModifyDataEvent.getUser()).handleAsync((unused, saveThrowable) -> { - if (saveThrowable != null) { - skinOverlay.getLogger().log(Level.SEVERE, "Error: ", saveThrowable); - return unused; - } - return unused; - }); - } - } - }); - } - - /** - * Updates the player's skin. - * Retrieves the User object associated with the player's UUID from the UserManager and asynchronously handles it. - * If the player is not online or their skin options have the default skin name, no further action is taken. - * Updates the player's skin using the SkinHandler. - */ - public void updateSkin() { - skinOverlay.getUserManager().getEntity(playerUUID()).handleAsync((entity, throwable) -> { - if (throwable != null) { - skinOverlay.getLogger().log(Level.SEVERE, "Error retrieving user: ", throwable); - return null; - } - if (!isOnline()) - return entity; - return entity; - }).thenAcceptAsync(entity -> { - if (entity != null) { - PlayerObjectPreUpdateSkinEvent event = (PlayerObjectPreUpdateSkinEvent) skinOverlay.getEventManager() - .callEvent(new PlayerObjectPreUpdateSkinEvent(this, entity, true)); - if (event.isCancelled()) - return; - SkinParts skinParts = entity.skin().skinParts(); - if (skinParts == null) - return; - skinOverlay.getLogger().info("Skin name " + skinParts.getSkinName()); - if (skinParts.getSkinName().equals("default")) - return; - skinOverlay.getSkinHandler().retrieveOrGenerateSkin(event.getPlayerObject(), null, skinParts) - .thenAccept(skin -> skinOverlay.getSkinHandler().setSkin(playerObject(), skin)); - } - }).handleAsync((unused, throwable) -> { - if (throwable != null) { - skinOverlay.getLogger().log(Level.SEVERE, "Error: ", throwable); - return null; - } - return unused; - }); - } - - - @Override - public String toString() { - return "PlayerObject{\n" + - "playerName: " + playerName() + "\n" + - "playerUUID: " + playerUUID() + "\n" + - "isBedrock: " + isBedrock() + "\n" + - "isOnline: " + isOnline() + "\n" + - "gameProfile: " + gameProfile().toString() + "\n" + - "}"; - } -} diff --git a/gradle.properties b/gradle.properties index afc1facb..bc1974e3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,24 +5,5 @@ bukkitMain = com.georgev22.skinoverlay.SkinOverlayBukkit bungeeMain = com.georgev22.skinoverlay.SkinOverlayBungee pluginName = SkinOverlay author = GeorgeV22 -version = 7.1.0 -mcVersion = 1.18.2 -apiVersion = 1.13 -libraryVersion = 11.7.0-beta.9 -libraryLoaderVersion = 1.6.1 -mineskinVersion = e00167583d -acfVersion = 0.5.1-G-4.0-SNAPSHOT -bstatsVersion = 3.0.2 -skinsRestorerVersion = 15.0.3 -adventureVersion = 4.14.0 -adventurePlatformVersion = 4.3.0 -log4jVersion = 2.20.0 -gsonVersion = 2.10.1 -guavaVersion = 32.0.1-jre -jetbrainsAnnotationsVersion = 24.0.1 -commonscodecVersion = 1.15 -commonsioVersion = 2.11.0 -commonslangVersion = 2.6 -mongodbVersion = 3.12.13 -authlibVersion = 3.11.50 -paperlibVersion = 1.0.8 +version = 8.0.0-beta.3 +description = SkinOverlay diff --git a/gradle/jacoco.gradle b/gradle/jacoco.gradle deleted file mode 100644 index 0886821f..00000000 --- a/gradle/jacoco.gradle +++ /dev/null @@ -1,10 +0,0 @@ -jacoco { toolVersion = "0.8.7" } - -jacocoTestReport { - reports { - xml.required = true - html.required = true - } -} - -tasks.check.dependsOn 'jacocoTestReport' \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..ce6bedba --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,49 @@ +[versions] +folia-api = "1.21.5-R0.1-SNAPSHOT" +bstats = "3.1.0" +adventure-api = "4.22.0" +adventure-platform = "4.4.0" +auth-lib-modern = "6.0.57" +auth-lib-legacy = "3.11.50" +gradleup-shadow = "8.3.5" +hikari = "4.0.3" +paperweight = "2.0.0-beta.17" +gson = "2.13.1" +placeholder-api = "2.11.6" +mineskinclient = "3.0.6-SNAPSHOT" +skinsrestorer = "15.7.3" +yamlconfiguration = "3.0.2" +jedis = "6.0.0" +jetbrains-annotations = "26.0.2" +log4j-api = "3.0.0-beta2" +reflect = "1.5.0" + +[plugins] +gradleup-shadow = { id = "com.gradleup.shadow", version.ref = "gradleup-shadow" } +paperweight-userdev = { id = "io.papermc.paperweight.userdev", version.ref = "paperweight" } +blossom = { id = "net.kyori.blossom", version = "2.1.0" } + +[libraries] +bstats-bukkit = { module = "org.bstats:bstats-bukkit", version.ref = "bstats" } +bstats-bungeecord = { module = "org.bstats:bstats-bungeecord", version.ref = "bstats" } +bstats-velocity = { module = "org.bstats:bstats-velocity", version.ref = "bstats" } +adventure-api = { module = "net.kyori:adventure-api", version.ref = "adventure-api" } +adventure-text-serializer-legacy = { module = "net.kyori:adventure-text-serializer-legacy", version.ref = "adventure-api" } +adventure-text-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure-api" } +adventure-platform-api = { module = "net.kyori:adventure-platform-api", version.ref = "adventure-platform" } +adventure-platform-bukkit = { module = "net.kyori:adventure-platform-bukkit", version.ref = "adventure-platform" } +adventure-platform-bungeecord = { module = "net.kyori:adventure-platform-bungeecord", version.ref = "adventure-platform" } +auth-lib-modern = { module = "com.mojang:authlib", version.ref = "auth-lib-modern" } +auth-lib-legacy = { module = "com.mojang:authlib", version.ref = "auth-lib-legacy" } +folia-api = { module = "dev.folia:folia-api", version.ref = "folia-api" } +hikari = { module = "com.zaxxer:HikariCP", version.ref = "hikari" } +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +placeholder-api = { module = "me.clip:placeholderapi", version.ref = "placeholder-api" } +mineskinclient-client = { module = "org.mineskin:java-client", version.ref = "mineskinclient" } +mineskinclient-java11 = { module = "org.mineskin:java-client-java11", version.ref = "mineskinclient" } +skinsrestorer-api = { module = "net.skinsrestorer:skinsrestorer-api", version.ref = "skinsrestorer" } +yamlconfiguration = { module = "org.bspfsystems:yamlconfiguration", version.ref = "yamlconfiguration" } +jedis = { module = "redis.clients:jedis", version.ref = "jedis" } +jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" } +log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j-api" } +reflect = { module = "net.lenni0451:Reflect", version.ref = "reflect" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba77..249e5832 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37aef8d3..5d4f6f3b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ +#Sat Jun 28 12:50:28 EEST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip -networkTimeout=10000 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew old mode 100755 new mode 100644 index 79a61d42..1b6c7873 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,11 +80,11 @@ do esac done -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' @@ -143,16 +143,12 @@ fi if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -209,12 +205,6 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 6689b85b..107acd32 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,92 +1,89 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/multiver/build.gradle b/multiver/build.gradle deleted file mode 100644 index 9776cb8f..00000000 --- a/multiver/build.gradle +++ /dev/null @@ -1,19 +0,0 @@ -plugins { - id 'java' -} - -group = 'com.georgev22.skinoverlay' -version = '6.0.0' - -repositories { - mavenCentral() -} - -dependencies { - testImplementation platform('org.junit:junit-bom:5.10.0') - testImplementation 'org.junit.jupiter:junit-jupiter' -} - -test { - useJUnitPlatform() -} \ No newline at end of file diff --git a/multiver/mc_1_17_R1/build.gradle b/multiver/mc_1_17_R1/build.gradle deleted file mode 100644 index 5602b12d..00000000 --- a/multiver/mc_1_17_R1/build.gradle +++ /dev/null @@ -1,14 +0,0 @@ -plugins { - id "io.papermc.paperweight.userdev" version "1.5.5" -} - -repositories { - mavenLocal() - mavenCentral() -} - -dependencies { - paperweightDevelopmentBundle("io.papermc.paper:dev-bundle:1.17.1-R0.1-SNAPSHOT") - - implementation project(path: ':core') -} diff --git a/multiver/mc_1_18_R1/build.gradle b/multiver/mc_1_18_R1/build.gradle deleted file mode 100644 index f12b9e46..00000000 --- a/multiver/mc_1_18_R1/build.gradle +++ /dev/null @@ -1,14 +0,0 @@ -plugins { - id "io.papermc.paperweight.userdev" version "1.5.5" -} - -repositories { - mavenLocal() - mavenCentral() -} - -dependencies { - paperweightDevelopmentBundle("io.papermc.paper:dev-bundle:1.18-R0.1-SNAPSHOT") - - implementation project(path: ':core') -} diff --git a/multiver/mc_1_18_R2/build.gradle b/multiver/mc_1_18_R2/build.gradle deleted file mode 100644 index ef7dc2be..00000000 --- a/multiver/mc_1_18_R2/build.gradle +++ /dev/null @@ -1,14 +0,0 @@ -plugins { - id "io.papermc.paperweight.userdev" version "1.5.5" -} - -repositories { - mavenLocal() - mavenCentral() -} - -dependencies { - paperweightDevelopmentBundle("io.papermc.paper:dev-bundle:1.18.2-R0.1-SNAPSHOT") - - implementation project(path: ':core') -} diff --git a/multiver/mc_1_18_R2/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_18_R2.java b/multiver/mc_1_18_R2/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_18_R2.java deleted file mode 100644 index 8bbc6e18..00000000 --- a/multiver/mc_1_18_R2/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_18_R2.java +++ /dev/null @@ -1,122 +0,0 @@ -package com.georgev22.skinoverlay.handler.handlers; - -import com.georgev22.skinoverlay.exceptions.SkinException; -import com.georgev22.skinoverlay.handler.SGameProfile; -import com.georgev22.skinoverlay.handler.SkinHandler; -import com.georgev22.skinoverlay.storage.data.Skin; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import com.mojang.authlib.GameProfile; -import net.minecraft.network.protocol.Packet; -import net.minecraft.network.protocol.game.ClientboundPlayerInfoPacket; -import net.minecraft.network.protocol.game.ClientboundRespawnPacket; -import net.minecraft.network.protocol.game.ClientboundUpdateMobEffectPacket; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.server.level.ServerPlayerGameMode; -import net.minecraft.server.players.PlayerList; -import net.minecraft.world.effect.MobEffectInstance; -import net.minecraft.world.level.biome.BiomeManager; -import org.bukkit.craftbukkit.v1_18_R2.entity.CraftPlayer; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.logging.Level; - -import static com.georgev22.skinoverlay.handler.handlers.SkinHandler_Unsupported.wrapper; - -public final class SkinHandler_1_18_R2 extends SkinHandler { - - @Override - public CompletableFuture updateSkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - return CompletableFuture.supplyAsync(() -> { - try { - Player player = playerObject.player(); - final CraftPlayer craftPlayer = (CraftPlayer) player; - final ServerPlayer entityPlayer = craftPlayer.getHandle(); - - ServerLevel world = entityPlayer.getLevel(); - ServerPlayerGameMode gamemode = entityPlayer.gameMode; - - ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( - world.dimensionTypeRegistration(), - world.dimension(), - BiomeManager.obfuscateSeed(world.getSeed()), - gamemode.getGameModeForPlayer(), - gamemode.getPreviousGameModeForPlayer(), - world.isDebug(), - world.isFlat(), - true); - - sendPacket(entityPlayer, new ClientboundPlayerInfoPacket(ClientboundPlayerInfoPacket.Action.REMOVE_PLAYER, List.of(entityPlayer))); - sendPacket(entityPlayer, new ClientboundPlayerInfoPacket(ClientboundPlayerInfoPacket.Action.ADD_PLAYER, List.of(entityPlayer))); - - sendPacket(entityPlayer, respawn); - - entityPlayer.onUpdateAbilities(); - - entityPlayer.connection.teleport(player.getLocation()); - - entityPlayer.resetSentInfo(); - - PlayerList playerList = entityPlayer.server.getPlayerList(); - playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); - playerList.sendAllPlayerInfo(entityPlayer); - - for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { - ClientboundUpdateMobEffectPacket effect = new ClientboundUpdateMobEffectPacket(entityPlayer.getId(), mobEffect); - sendPacket(entityPlayer, effect); - } - return true; - } catch (Exception exception) { - throw new SkinException(exception); - } - }, runnable -> this.skinOverlay.getMinecraftScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); - } - - @Override - public void applySkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - this.skinOverlay.getMinecraftScheduler().getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { - Player player = playerObject.player(); - player.hidePlayer(skinOverlay.getSkinOverlay().plugin(), player); - player.showPlayer(skinOverlay.getSkinOverlay().plugin(), 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(skinOverlay.getPlugin(), () -> { - if (aBoolean) - skinOverlay.onlinePlayers().stream().filter(playerObjects -> playerObjects != playerObject).forEach(playerObjects -> { - Player p = playerObjects.player(); - p.hidePlayer(skinOverlay.getPlugin(), player); - p.showPlayer(skinOverlay.getPlugin(), player); - }); - })); - }, 20L); - } - - @Override - public @NotNull GameProfile getInternalGameProfile(@NotNull PlayerObject playerObject) { - Player player = playerObject.player(); - final CraftPlayer craftPlayer = (CraftPlayer) player; - final ServerPlayer entityPlayer = craftPlayer.getHandle(); - return entityPlayer.getGameProfile(); - } - - @Override - public SGameProfile getGameProfile(@NotNull PlayerObject playerObject) { - if (sGameProfiles.containsKey(playerObject)) { - return sGameProfiles.get(playerObject); - } - return sGameProfiles.append(playerObject, wrapper(this.getInternalGameProfile(playerObject))).get(playerObject); - } - - private void sendPacket(@NotNull ServerPlayer player, Packet packet) { - player.connection.send(packet); - } -} - diff --git a/multiver/mc_1_19_R1/build.gradle b/multiver/mc_1_19_R1/build.gradle deleted file mode 100644 index bf002088..00000000 --- a/multiver/mc_1_19_R1/build.gradle +++ /dev/null @@ -1,16 +0,0 @@ -plugins { - id "io.papermc.paperweight.userdev" version "1.5.5" -} - -repositories { - mavenLocal() - mavenCentral() -} - -dependencies { - paperweightDevelopmentBundle("io.papermc.paper:dev-bundle:1.19-R0.1-SNAPSHOT") - - implementation project(path: ':core') - - compileOnly "io.papermc:paperlib:1.0.8" -} diff --git a/multiver/mc_1_19_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_19_R1.java b/multiver/mc_1_19_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_19_R1.java deleted file mode 100644 index 102d0f99..00000000 --- a/multiver/mc_1_19_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_19_R1.java +++ /dev/null @@ -1,139 +0,0 @@ -package com.georgev22.skinoverlay.handler.handlers; - -import com.georgev22.library.utilities.Utils; -import com.georgev22.skinoverlay.exceptions.SkinException; -import com.georgev22.skinoverlay.handler.SGameProfile; -import com.georgev22.skinoverlay.handler.SkinHandler; -import com.georgev22.skinoverlay.storage.data.Skin; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import com.mojang.authlib.GameProfile; -import io.papermc.lib.PaperLib; -import net.minecraft.network.protocol.Packet; -import net.minecraft.network.protocol.game.ClientboundPlayerInfoPacket; -import net.minecraft.network.protocol.game.ClientboundRespawnPacket; -import net.minecraft.network.protocol.game.ClientboundUpdateMobEffectPacket; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.server.level.ServerPlayerGameMode; -import net.minecraft.server.players.PlayerList; -import net.minecraft.world.effect.MobEffectInstance; -import net.minecraft.world.level.biome.BiomeManager; -import org.bukkit.craftbukkit.v1_19_R1.entity.CraftPlayer; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Modifier; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.logging.Level; - -import static com.georgev22.skinoverlay.handler.handlers.SkinHandler_Unsupported.wrapper; - -public final class SkinHandler_1_19_R1 extends SkinHandler { - - @Override - public CompletableFuture updateSkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - return CompletableFuture.supplyAsync(() -> { - try { - Player player = playerObject.player(); - final CraftPlayer craftPlayer = (CraftPlayer) player; - final ServerPlayer entityPlayer = craftPlayer.getHandle(); - - ServerLevel world = entityPlayer.getLevel(); - ServerPlayerGameMode gamemode = entityPlayer.gameMode; - - ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( - world.dimensionTypeId(), - world.dimension(), - BiomeManager.obfuscateSeed(world.getSeed()), - gamemode.getGameModeForPlayer(), - gamemode.getPreviousGameModeForPlayer(), - world.isDebug(), - world.isFlat(), - true, - entityPlayer.getLastDeathLocation() - ); - - sendPacket(entityPlayer, new ClientboundPlayerInfoPacket(ClientboundPlayerInfoPacket.Action.REMOVE_PLAYER, List.of(entityPlayer))); - sendPacket(entityPlayer, new ClientboundPlayerInfoPacket(ClientboundPlayerInfoPacket.Action.ADD_PLAYER, List.of(entityPlayer))); - - sendPacket(entityPlayer, respawn); - - entityPlayer.onUpdateAbilities(); - - entityPlayer.connection.teleport(player.getLocation()); - - entityPlayer.resetSentInfo(); - - PlayerList playerList = entityPlayer.server.getPlayerList(); - playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); - playerList.sendAllPlayerInfo(entityPlayer); - - for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { - ClientboundUpdateMobEffectPacket effect = new ClientboundUpdateMobEffectPacket(entityPlayer.getId(), mobEffect); - sendPacket(entityPlayer, effect); - } - return true; - } catch (Exception exception) { - throw new SkinException(exception); - } - }, runnable -> this.skinOverlay.getMinecraftScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); - } - - @Override - public void applySkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - this.skinOverlay.getMinecraftScheduler().getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { - Player player = playerObject.player(); - player.hidePlayer(skinOverlay.getSkinOverlay().plugin(), player); - player.showPlayer(skinOverlay.getSkinOverlay().plugin(), 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(skinOverlay.getPlugin(), () -> { - if (aBoolean) - skinOverlay.onlinePlayers().stream().filter(playerObjects -> playerObjects != playerObject).forEach(playerObjects -> { - Player p = playerObjects.player(); - p.hidePlayer(skinOverlay.getPlugin(), player); - p.showPlayer(skinOverlay.getPlugin(), player); - }); - })); - }, 20L); - } - - @Override - public GameProfile getInternalGameProfile(@NotNull PlayerObject playerObject) { - Player player = playerObject.player(); - final CraftPlayer craftPlayer = (CraftPlayer) player; - final ServerPlayer entityPlayer = craftPlayer.getHandle(); - if (PaperLib.isSpigot()) - try { - Field field = entityPlayer.getClass().getDeclaredField("ct"); - if (Modifier.isPrivate(field.getModifiers())) { - return (GameProfile) Utils.Reflection.fetchDeclaredField(entityPlayer.getClass().getSuperclass(), entityPlayer, "ct"); - } - } catch (NoSuchFieldException | IllegalAccessException | NoSuchMethodException | - InvocationTargetException exception) { - skinOverlay.getLogger().log(Level.SEVERE, exception.getMessage(), exception); - } - return entityPlayer.getGameProfile(); - } - - @Override - public SGameProfile getGameProfile(@NotNull PlayerObject playerObject) { - if (sGameProfiles.containsKey(playerObject)) { - return sGameProfiles.get(playerObject); - } - return sGameProfiles.append(playerObject, wrapper(this.getInternalGameProfile(playerObject))).get(playerObject); - } - - private void sendPacket(@NotNull ServerPlayer player, Packet packet) { - player.connection.send(packet); - } -} - diff --git a/multiver/mc_1_19_R2/build.gradle b/multiver/mc_1_19_R2/build.gradle deleted file mode 100644 index 71b84aba..00000000 --- a/multiver/mc_1_19_R2/build.gradle +++ /dev/null @@ -1,16 +0,0 @@ -plugins { - id "io.papermc.paperweight.userdev" version "1.5.5" -} - -repositories { - mavenLocal() - mavenCentral() -} - -dependencies { - paperweightDevelopmentBundle("io.papermc.paper:dev-bundle:1.19.3-R0.1-SNAPSHOT") - - implementation project(path: ':core') - - compileOnly "io.papermc:paperlib:1.0.8" -} diff --git a/multiver/mc_1_19_R2/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_19_R2.java b/multiver/mc_1_19_R2/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_19_R2.java deleted file mode 100644 index 2a23aebb..00000000 --- a/multiver/mc_1_19_R2/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_19_R2.java +++ /dev/null @@ -1,137 +0,0 @@ -package com.georgev22.skinoverlay.handler.handlers; - -import com.georgev22.library.utilities.Utils; -import com.georgev22.skinoverlay.exceptions.SkinException; -import com.georgev22.skinoverlay.handler.SGameProfile; -import com.georgev22.skinoverlay.handler.SkinHandler; -import com.georgev22.skinoverlay.storage.data.Skin; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import com.mojang.authlib.GameProfile; -import io.papermc.lib.PaperLib; -import net.minecraft.network.protocol.Packet; -import net.minecraft.network.protocol.game.*; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.server.level.ServerPlayerGameMode; -import net.minecraft.server.players.PlayerList; -import net.minecraft.world.effect.MobEffectInstance; -import net.minecraft.world.level.biome.BiomeManager; -import org.bukkit.craftbukkit.v1_19_R2.entity.CraftPlayer; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Modifier; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.logging.Level; - -import static com.georgev22.skinoverlay.handler.handlers.SkinHandler_Unsupported.wrapper; - -public final class SkinHandler_1_19_R2 extends SkinHandler { - - @Override - public CompletableFuture updateSkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - return CompletableFuture.supplyAsync(() -> { - try { - Player player = playerObject.player(); - final CraftPlayer craftPlayer = (CraftPlayer) player; - final ServerPlayer entityPlayer = craftPlayer.getHandle(); - - ServerLevel world = entityPlayer.getLevel(); - ServerPlayerGameMode gamemode = entityPlayer.gameMode; - - ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( - world.dimensionTypeId(), - world.dimension(), - BiomeManager.obfuscateSeed(world.getSeed()), - gamemode.getGameModeForPlayer(), - gamemode.getPreviousGameModeForPlayer(), - world.isDebug(), - world.isFlat(), - ClientboundRespawnPacket.KEEP_ALL_DATA, - entityPlayer.getLastDeathLocation() - ); - - sendPacket(entityPlayer, new ClientboundPlayerInfoRemovePacket(List.of(player.getUniqueId()))); - sendPacket(entityPlayer, ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityPlayer))); - - sendPacket(entityPlayer, respawn); - - entityPlayer.onUpdateAbilities(); - - entityPlayer.connection.teleport(player.getLocation()); - - entityPlayer.resetSentInfo(); - - PlayerList playerList = entityPlayer.server.getPlayerList(); - playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); - playerList.sendAllPlayerInfo(entityPlayer); - - for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { - ClientboundUpdateMobEffectPacket effect = new ClientboundUpdateMobEffectPacket(entityPlayer.getId(), mobEffect); - sendPacket(entityPlayer, effect); - } - return true; - } catch (Exception exception) { - throw new SkinException(exception); - } - }, runnable -> this.skinOverlay.getMinecraftScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); - } - - @Override - public void applySkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - this.skinOverlay.getMinecraftScheduler().getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { - Player player = playerObject.player(); - player.hidePlayer(skinOverlay.getSkinOverlay().plugin(), player); - player.showPlayer(skinOverlay.getSkinOverlay().plugin(), 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(skinOverlay.getPlugin(), () -> { - if (aBoolean) - skinOverlay.onlinePlayers().stream().filter(playerObjects -> playerObjects != playerObject).forEach(playerObjects -> { - Player p = playerObjects.player(); - p.hidePlayer(skinOverlay.getPlugin(), player); - p.showPlayer(skinOverlay.getPlugin(), player); - }); - })); - }, 20L); - } - - @Override - public GameProfile getInternalGameProfile(@NotNull PlayerObject playerObject) { - Player player = playerObject.player(); - final CraftPlayer craftPlayer = (CraftPlayer) player; - final ServerPlayer entityPlayer = craftPlayer.getHandle(); - if (PaperLib.isSpigot()) - try { - Field field = entityPlayer.getClass().getDeclaredField("cs"); - if (Modifier.isPrivate(field.getModifiers())) { - return (GameProfile) Utils.Reflection.fetchDeclaredField(entityPlayer.getClass().getSuperclass(), entityPlayer, "cs"); - } - } catch (NoSuchFieldException | IllegalAccessException | NoSuchMethodException | - InvocationTargetException exception) { - skinOverlay.getLogger().log(Level.SEVERE, exception.getMessage(), exception); - } - return entityPlayer.gameProfile; - } - - @Override - public SGameProfile getGameProfile(@NotNull PlayerObject playerObject) { - if (sGameProfiles.containsKey(playerObject)) { - return sGameProfiles.get(playerObject); - } - return sGameProfiles.append(playerObject, wrapper(this.getInternalGameProfile(playerObject))).get(playerObject); - } - - private void sendPacket(@NotNull ServerPlayer player, Packet packet) { - player.connection.send(packet); - } -} - diff --git a/multiver/mc_1_19_R3/build.gradle b/multiver/mc_1_19_R3/build.gradle deleted file mode 100644 index 419b9fc0..00000000 --- a/multiver/mc_1_19_R3/build.gradle +++ /dev/null @@ -1,16 +0,0 @@ -plugins { - id "io.papermc.paperweight.userdev" version "1.5.5" -} - -repositories { - mavenLocal() - mavenCentral() -} - -dependencies { - paperweightDevelopmentBundle("io.papermc.paper:dev-bundle:1.19.4-R0.1-SNAPSHOT") - - implementation project(path: ':core') - - compileOnly "io.papermc:paperlib:1.0.8" -} diff --git a/multiver/mc_1_19_R3/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_19_R3.java b/multiver/mc_1_19_R3/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_19_R3.java deleted file mode 100644 index cbc353f5..00000000 --- a/multiver/mc_1_19_R3/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_19_R3.java +++ /dev/null @@ -1,122 +0,0 @@ -package com.georgev22.skinoverlay.handler.handlers; - -import com.georgev22.skinoverlay.exceptions.SkinException; -import com.georgev22.skinoverlay.handler.SGameProfile; -import com.georgev22.skinoverlay.handler.SkinHandler; -import com.georgev22.skinoverlay.storage.data.Skin; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import com.mojang.authlib.GameProfile; -import net.minecraft.network.protocol.Packet; -import net.minecraft.network.protocol.game.*; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.server.level.ServerPlayerGameMode; -import net.minecraft.server.players.PlayerList; -import net.minecraft.world.effect.MobEffectInstance; -import net.minecraft.world.level.biome.BiomeManager; -import org.bukkit.craftbukkit.v1_19_R3.entity.CraftPlayer; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.logging.Level; - -import static com.georgev22.skinoverlay.handler.handlers.SkinHandler_Unsupported.wrapper; - -public final class SkinHandler_1_19_R3 extends SkinHandler { - - @Override - public CompletableFuture updateSkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - return CompletableFuture.supplyAsync(() -> { - try { - Player player = playerObject.player(); - final CraftPlayer craftPlayer = (CraftPlayer) player; - final ServerPlayer entityPlayer = craftPlayer.getHandle(); - - ServerLevel world = entityPlayer.getLevel(); - ServerPlayerGameMode gamemode = entityPlayer.gameMode; - - ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( - world.dimensionTypeId(), - world.dimension(), - BiomeManager.obfuscateSeed(world.getSeed()), - gamemode.getGameModeForPlayer(), - gamemode.getPreviousGameModeForPlayer(), - world.isDebug(), - world.isFlat(), - ClientboundRespawnPacket.KEEP_ALL_DATA, - entityPlayer.getLastDeathLocation() - ); - - sendPacket(entityPlayer, new ClientboundPlayerInfoRemovePacket(List.of(player.getUniqueId()))); - sendPacket(entityPlayer, ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityPlayer))); - - sendPacket(entityPlayer, respawn); - - entityPlayer.onUpdateAbilities(); - - entityPlayer.connection.teleport(player.getLocation()); - - entityPlayer.resetSentInfo(); - - PlayerList playerList = entityPlayer.server.getPlayerList(); - playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); - playerList.sendAllPlayerInfo(entityPlayer); - - for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { - ClientboundUpdateMobEffectPacket effect = new ClientboundUpdateMobEffectPacket(entityPlayer.getId(), mobEffect); - sendPacket(entityPlayer, effect); - } - return true; - } catch (Exception exception) { - throw new SkinException(exception); - } - }, runnable -> this.skinOverlay.getMinecraftScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); - } - - @Override - public void applySkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - this.skinOverlay.getMinecraftScheduler().getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { - Player player = playerObject.player(); - player.hidePlayer(skinOverlay.getSkinOverlay().plugin(), player); - player.showPlayer(skinOverlay.getSkinOverlay().plugin(), 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(skinOverlay.getPlugin(), () -> { - if (aBoolean) - skinOverlay.onlinePlayers().stream().filter(playerObjects -> playerObjects != playerObject).forEach(playerObjects -> { - Player p = playerObjects.player(); - p.hidePlayer(skinOverlay.getPlugin(), player); - p.showPlayer(skinOverlay.getPlugin(), player); - }); - })); - }, 20L); - } - - @Override - public GameProfile getInternalGameProfile(@NotNull PlayerObject playerObject) { - Player player = playerObject.player(); - final CraftPlayer craftPlayer = (CraftPlayer) player; - final ServerPlayer entityPlayer = craftPlayer.getHandle(); - return entityPlayer.getGameProfile(); - } - - @Override - public SGameProfile getGameProfile(@NotNull PlayerObject playerObject) { - if (sGameProfiles.containsKey(playerObject)) { - return sGameProfiles.get(playerObject); - } - return sGameProfiles.append(playerObject, wrapper(this.getInternalGameProfile(playerObject))).get(playerObject); - } - - private void sendPacket(@NotNull ServerPlayer player, Packet packet) { - player.connection.send(packet); - } -} - diff --git a/multiver/mc_1_20_R1/build.gradle b/multiver/mc_1_20_R1/build.gradle deleted file mode 100644 index 5178d570..00000000 --- a/multiver/mc_1_20_R1/build.gradle +++ /dev/null @@ -1,16 +0,0 @@ -plugins { - id "io.papermc.paperweight.userdev" version "1.5.5" -} - -repositories { - mavenLocal() - mavenCentral() -} - -dependencies { - paperweightDevelopmentBundle("io.papermc.paper:dev-bundle:1.20.1-R0.1-SNAPSHOT") - - implementation project(path: ':core') - - compileOnly "io.papermc:paperlib:1.0.8" -} diff --git a/multiver/mc_1_20_R2/build.gradle b/multiver/mc_1_20_R2/build.gradle deleted file mode 100644 index c68f5f8e..00000000 --- a/multiver/mc_1_20_R2/build.gradle +++ /dev/null @@ -1,16 +0,0 @@ -plugins { - id "io.papermc.paperweight.userdev" version "1.5.5" -} - -repositories { - mavenLocal() - mavenCentral() -} - -dependencies { - paperweightDevelopmentBundle("io.papermc.paper:dev-bundle:1.20.2-R0.1-SNAPSHOT") - - implementation project(path: ':core') - - compileOnly "io.papermc:paperlib:1.0.8" -} diff --git a/multiver/mc_1_20_R2/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_20_R2.java b/multiver/mc_1_20_R2/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_20_R2.java deleted file mode 100644 index 12df6826..00000000 --- a/multiver/mc_1_20_R2/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_20_R2.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.georgev22.skinoverlay.handler.handlers; - -import com.georgev22.library.maps.HashObjectMap; -import com.georgev22.library.maps.ObjectMap; -import com.georgev22.skinoverlay.exceptions.SkinException; -import com.georgev22.skinoverlay.handler.SGameProfile; -import com.georgev22.skinoverlay.handler.SProperty; -import com.georgev22.skinoverlay.handler.SkinHandler; -import com.georgev22.skinoverlay.handler.profile.SGameProfileBukkit; -import com.georgev22.skinoverlay.storage.data.Skin; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import com.mojang.authlib.GameProfile; -import net.minecraft.network.protocol.Packet; -import net.minecraft.network.protocol.game.*; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.server.players.PlayerList; -import net.minecraft.world.effect.MobEffectInstance; -import org.bukkit.craftbukkit.v1_20_R2.entity.CraftPlayer; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.logging.Level; - -public final class SkinHandler_1_20_R2 extends SkinHandler { - - @Override - public CompletableFuture updateSkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - return CompletableFuture.supplyAsync(() -> { - try { - Player player = playerObject.player(); - final CraftPlayer craftPlayer = (CraftPlayer) player; - final ServerPlayer entityPlayer = craftPlayer.getHandle(); - ServerLevel world = entityPlayer.serverLevel(); - - CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(world); - ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( - spawnInfo, - ClientboundRespawnPacket.KEEP_ALL_DATA - ); - - sendPacket(entityPlayer, new ClientboundPlayerInfoRemovePacket(List.of(player.getUniqueId()))); - sendPacket(entityPlayer, ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityPlayer))); - - sendPacket(entityPlayer, respawn); - - entityPlayer.onUpdateAbilities(); - - entityPlayer.connection.teleport(player.getLocation()); - - entityPlayer.resetSentInfo(); - - PlayerList playerList = entityPlayer.server.getPlayerList(); - playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); - playerList.sendAllPlayerInfo(entityPlayer); - - for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { - ClientboundUpdateMobEffectPacket effect = new ClientboundUpdateMobEffectPacket(entityPlayer.getId(), mobEffect); - sendPacket(entityPlayer, effect); - } - return true; - } catch (Exception exception) { - throw new SkinException(exception); - } - }, runnable -> this.skinOverlay.getMinecraftScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); - } - - @Override - public void applySkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - this.skinOverlay.getMinecraftScheduler().getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { - Player player = playerObject.player(); - player.hidePlayer(skinOverlay.getSkinOverlay().plugin(), player); - player.showPlayer(skinOverlay.getSkinOverlay().plugin(), 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(skinOverlay.getPlugin(), () -> { - if (aBoolean) - skinOverlay.onlinePlayers().stream().filter(playerObjects -> playerObjects != playerObject).forEach(playerObjects -> { - Player p = playerObjects.player(); - p.hidePlayer(skinOverlay.getPlugin(), player); - p.showPlayer(skinOverlay.getPlugin(), player); - }); - })); - }, 20L); - } - - @Override - public GameProfile getInternalGameProfile(@NotNull PlayerObject playerObject) { - Player player = playerObject.player(); - final CraftPlayer craftPlayer = (CraftPlayer) player; - final ServerPlayer entityPlayer = craftPlayer.getHandle(); - return entityPlayer.getGameProfile(); - } - - @Override - public SGameProfile getGameProfile(@NotNull PlayerObject playerObject) { - if (sGameProfiles.containsKey(playerObject)) { - return sGameProfiles.get(playerObject); - } - return sGameProfiles.append(playerObject, wrapper(this.getInternalGameProfile(playerObject))).get(playerObject); - } - - public static @NotNull SGameProfile wrapper(@NotNull GameProfile gameProfile) { - ObjectMap propertyObjectMap = new HashObjectMap<>(); - gameProfile.getProperties().forEach((s, property) -> propertyObjectMap.append(s, new SProperty(property.name(), property.value(), property.signature()))); - return new SGameProfileBukkit(gameProfile.getName(), gameProfile.getId(), propertyObjectMap); - } - - private void sendPacket(@NotNull ServerPlayer player, Packet packet) { - player.connection.send(packet); - } -} - diff --git a/multiver/mc_1_20_R3/build.gradle b/multiver/mc_1_20_R3/build.gradle deleted file mode 100644 index 9fe9f693..00000000 --- a/multiver/mc_1_20_R3/build.gradle +++ /dev/null @@ -1,16 +0,0 @@ -plugins { - id "io.papermc.paperweight.userdev" version "1.5.5" -} - -repositories { - mavenLocal() - mavenCentral() -} - -dependencies { - paperweightDevelopmentBundle("io.papermc.paper:dev-bundle:1.20.4-R0.1-SNAPSHOT") - - implementation project(path: ':core') - - compileOnly "io.papermc:paperlib:1.0.8" -} \ No newline at end of file diff --git a/multiver/mc_1_20_R3/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_20_R3.java b/multiver/mc_1_20_R3/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_20_R3.java deleted file mode 100644 index 698cf537..00000000 --- a/multiver/mc_1_20_R3/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_20_R3.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.georgev22.skinoverlay.handler.handlers; - -import com.georgev22.library.maps.HashObjectMap; -import com.georgev22.library.maps.ObjectMap; -import com.georgev22.skinoverlay.exceptions.SkinException; -import com.georgev22.skinoverlay.handler.SGameProfile; -import com.georgev22.skinoverlay.handler.SProperty; -import com.georgev22.skinoverlay.handler.SkinHandler; -import com.georgev22.skinoverlay.handler.profile.SGameProfileBukkit; -import com.georgev22.skinoverlay.storage.data.Skin; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import com.mojang.authlib.GameProfile; -import net.minecraft.network.protocol.Packet; -import net.minecraft.network.protocol.game.*; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.server.players.PlayerList; -import net.minecraft.world.effect.MobEffectInstance; -import org.bukkit.craftbukkit.v1_20_R3.entity.CraftPlayer; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.logging.Level; - -public final class SkinHandler_1_20_R3 extends SkinHandler { - - @Override - public CompletableFuture updateSkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - return CompletableFuture.supplyAsync(() -> { - try { - Player player = playerObject.player(); - final CraftPlayer craftPlayer = (CraftPlayer) player; - final ServerPlayer entityPlayer = craftPlayer.getHandle(); - - ServerLevel world = entityPlayer.serverLevel(); - - CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(world); - ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( - spawnInfo, - ClientboundRespawnPacket.KEEP_ALL_DATA - ); - - sendPacket(entityPlayer, new ClientboundPlayerInfoRemovePacket(List.of(player.getUniqueId()))); - sendPacket(entityPlayer, ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityPlayer))); - - sendPacket(entityPlayer, respawn); - - entityPlayer.onUpdateAbilities(); - - entityPlayer.connection.teleport(player.getLocation()); - - entityPlayer.resetSentInfo(); - - PlayerList playerList = entityPlayer.server.getPlayerList(); - playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); - playerList.sendAllPlayerInfo(entityPlayer); - - for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { - ClientboundUpdateMobEffectPacket effect = new ClientboundUpdateMobEffectPacket(entityPlayer.getId(), mobEffect); - sendPacket(entityPlayer, effect); - } - return true; - } catch (Exception exception) { - throw new SkinException(exception); - } - }, runnable -> this.skinOverlay.getMinecraftScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); - } - - @Override - public void applySkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - this.skinOverlay.getMinecraftScheduler().getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { - Player player = playerObject.player(); - player.hidePlayer(skinOverlay.getSkinOverlay().plugin(), player); - player.showPlayer(skinOverlay.getSkinOverlay().plugin(), 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(skinOverlay.getPlugin(), () -> { - if (aBoolean) - skinOverlay.onlinePlayers().stream().filter(playerObjects -> playerObjects != playerObject).forEach(playerObjects -> { - Player p = playerObjects.player(); - p.hidePlayer(skinOverlay.getPlugin(), player); - p.showPlayer(skinOverlay.getPlugin(), player); - }); - })); - }, 20L); - } - - @Override - public GameProfile getInternalGameProfile(@NotNull PlayerObject playerObject) { - Player player = playerObject.player(); - final CraftPlayer craftPlayer = (CraftPlayer) player; - final ServerPlayer entityPlayer = craftPlayer.getHandle(); - return entityPlayer.getGameProfile(); - } - - @Override - public SGameProfile getGameProfile(@NotNull PlayerObject playerObject) { - if (sGameProfiles.containsKey(playerObject)) { - return sGameProfiles.get(playerObject); - } - return sGameProfiles.append(playerObject, wrapper(this.getInternalGameProfile(playerObject))).get(playerObject); - } - - public static @NotNull SGameProfile wrapper(@NotNull GameProfile gameProfile) { - ObjectMap propertyObjectMap = new HashObjectMap<>(); - gameProfile.getProperties().forEach((s, property) -> propertyObjectMap.append(s, new SProperty(property.name(), property.value(), property.signature()))); - return new SGameProfileBukkit(gameProfile.getName(), gameProfile.getId(), propertyObjectMap); - } - - private void sendPacket(@NotNull ServerPlayer player, Packet packet) { - player.connection.send(packet); - } -} - diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 65b81585..00000000 --- a/settings.gradle +++ /dev/null @@ -1,35 +0,0 @@ -pluginManagement { - repositories { - maven { - name = "PaperMC" - url = "https://repo.papermc.io/repository/maven-public/" - } - gradlePluginPortal() - } -} - -rootProject.name = 'skinoverlay' -include 'core' -include 'bukkit' -include 'bungee' -include 'velocity' -include 'multiver' -include ':multiver:mc_1_17_R1' -include ':multiver:mc_1_18_R1' -include ':multiver:mc_1_18_R2' -include ':multiver:mc_1_19_R1' -include ':multiver:mc_1_19_R2' -include ':multiver:mc_1_19_R3' -include ':multiver:mc_1_20_R1' -include ':multiver:mc_1_20_R2' -include ':multiver:mc_1_20_R3' -findProject(':multiver:mc_1_17_R1')?.name = 'mc_1_17_R1' -findProject(':multiver:mc_1_18_R1')?.name = 'mc_1_18_R1' -findProject(':multiver:mc_1_18_R2')?.name = 'mc_1_18_R2' -findProject(':multiver:mc_1_19_R1')?.name = 'mc_1_19_R1' -findProject(':multiver:mc_1_19_R2')?.name = 'mc_1_19_R2' -findProject(':multiver:mc_1_19_R3')?.name = 'mc_1_19_R3' -findProject(':multiver:mc_1_20_R1')?.name = 'mc_1_20_R1' -findProject(':multiver:mc_1_20_R2')?.name = 'mc_1_20_R2' -findProject(':multiver:mc_1_20_R3')?.name = 'mc_1_20_R3' - diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..a8c93935 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,31 @@ +pluginManagement { + repositories { + maven("https://repo.papermc.io/repository/maven-public/") + maven("https://repo.maven.apache.org/maven2/") + maven("https://maven.fabricmc.net/") + gradlePluginPortal() + } +} + +rootProject.name = "skinoverlay" + +include("build-info") +include("common") +include("bukkit") +include("bukkit:versions:mc1_17_R1") +include("bukkit:versions:mc1_18_R1") +include("bukkit:versions:mc1_18_R2") +include("bukkit:versions:mc1_19_R1") +include("bukkit:versions:mc1_19_R2") +include("bukkit:versions:mc1_19_R3") +include("bukkit:versions:mc1_20_R1") +include("bukkit:versions:mc1_20_R2") +include("bukkit:versions:mc1_20_R3") +include("bukkit:versions:mc1_20_R4") +include("bukkit:versions:mc1_21_R1") +include("bukkit:versions:mc1_21_R2") +include("bukkit:versions:mc1_21_R3") +include("bukkit:versions:mc1_21_R4") +include("bukkit:versions:mc1_21_R5") +include("bukkit:versions:mc1_21_R6") +include("velocity") \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml deleted file mode 100644 index ce630b78..00000000 --- a/src/main/resources/config.yml +++ /dev/null @@ -1,134 +0,0 @@ -Options: - experimental features: false #Turn on experimental features - debug: false - proxy: false - secret: "SECRET HERE" - mineskin api key: "none" - # Supported locales: en, fr, de, it, es, pt, nl, no, sv, da, fi, pl, cs, sk, hu, ro, bg, el, ru, tr, zh, ja, ko, en_US, en_GR, es_ES, es_MX, pt_BR, fr_CA - # You must have a file with the locale you want for example messages_en_US.yml or messages_el.yml - locale: "en_US" - commands: #Disable or enable plugin commands - skinoverlay: true - updater: - enabled: true - download: false - restart: false - default skin uuid: "8667ba71-b85a-4004-af54-457a9734eed7" - # File, MySQL, PostgreSQL, SQLite, MongoDB - # Select in what type you want your data to be stored - # File is recommended - #SQL section is for MySQL(or MariaDB) and PostgreSQL - #Default PostgreSQL port: 5432 - #Default MySQL and MariaDB port: 3306 - database: - type: File - SQL: - host: localhost - port: 3306 - user: youruser - password: "yourpassword" - database: SkinOverlay - users table name: "skinoverlay_users" - skins table name: "skinoverlay_skins" - SQLite file name: skinoverlay - MongoDB: - host: "localhost" - port: 27017 - user: youruser - password: "yourpassword" - database: "skinoverlay" - collection: "skinoverlay_users" - #Skin hooks are only used to retrieve player skin properties for offline players or players who have changed their skin. - #Currently available Skin Hooks: None, SkinsRestorer and Custom. - #With the custom option, you need to have your own plugin that sets the SkinOverlay.getInstance().setSkinHook(SkinHook). - skin hook: None - parts: - overlay: - policeman: - hat: true - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - cape: true - mustache: - hat: true - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - cape: true - alley: - hat: false - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - cape: true - bubbo_transparent: - hat: false - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - cape: true - fire_demon: - hat: true - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - cape: true - flame: - hat: true - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - cape: true - glare: - hat: true - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - cape: true - hoodie: - hat: true - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - cape: true - migrator: - hat: true - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - cape: true - pirate: - hat: true - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - cape: true - smoking: - hat: true - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - cape: true \ No newline at end of file diff --git a/src/main/resources/messages_el.yml b/src/main/resources/messages_el.yml deleted file mode 100644 index 4f7d2cf7..00000000 --- a/src/main/resources/messages_el.yml +++ /dev/null @@ -1,16 +0,0 @@ -Messages: - No Permission: '&c&l(!)&c Δεν έχετε τις σωστές άδειες για να το κάνετε αυτό!' - Only Player Command: '&c&l(!)&c Μόνο οι παίκτες μπορούν να εκτελέσουν αυτήν την εντολή!' - Offline Player: '&c&l(!)&c Ο παίκτης %player% είναι εκτός σύνδεσης!' - Overlay Applied: '&a&l(!)&a Εφαρμόστηκε το επικάλυμμα %url% στον παίκτη!' - Overlay Reset: '&a&l(!)&a Επαναφορά προεπιλεγμένου επικαλύμματος(%player%)!!' - Overlay Not Found: '&c&l(!)&c Το επικάλυμμα %overlay% δεν βρέθηκε!' - Insufficient arguments: '&c&l(!)&c Μη επαρκείς παράμετροι (%command%)' -Commands: - Descriptions: - SkinOverlay: - help: 'Εμφανίζει τη σελίδα βοήθειας' - overlay: 'Εφαρμόζει μια συγκεκριμένη επικάλυψη από τα αρχεία του πρόσθετου' - clear: 'Αφαιρεί την επικάλυψη δέρματος' - url: 'Εφαρμόζει μια συγκεκριμένη επικάλυψη από ένα URL' - reload: 'Επαναφόρτωση των αρχείων ρύθμισης του πρόσθετου (ορισμένες ρυθμίσεις απαιτούν επανεκκίνηση του διακομιστή)' diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml deleted file mode 100644 index 21ce3723..00000000 --- a/src/main/resources/plugin.yml +++ /dev/null @@ -1,21 +0,0 @@ -main: ${bukkitMain} -name: ${pluginName} -version: ${version} -authors: - - ${author} -api-version: 1.13 -load: POSTWORLD -folia-supported: true -softdepend: - - PlaceholderAPI - - SkinsRestorer -libraries: - - org.jsoup:jsoup:1.15.3 - - org.mongodb:mongo-java-driver:3.12.13 - - mysql:mysql-connector-java:8.0.32 - - org.xerial:sqlite-jdbc:3.41.2.1 - - com.google.guava:guava:31.1-jre - - org.postgresql:postgresql:42.6.0 - - commons-io:commons-io:2.11.0 - - commons-lang:commons-lang:2.6 - - commons-codec:commons-codec:1.15 diff --git a/update-versions.sh b/update-versions.sh index dddf078f..d331e16f 100644 --- a/update-versions.sh +++ b/update-versions.sh @@ -1 +1,12 @@ -find ./README.md -type f -exec sed -E -i "s/(<)?(version|small|:)([:' >]+)?([0-9]\.[0-9]\.[0-9](-[a-z0-9]+\.?[0-9]*)?)(['<]?)/\1\2\3$1\6/g" {} \; +#!/bin/bash + +NEW_VERSION="$1" +if [ -z "$NEW_VERSION" ]; then + echo "Usage: $0 " + exit 1 +fi + +find ./README.md -type f -exec sed -E -i "s/(<)?(version|small|:)([:' >]+)?([0-9]\.[0-9]\.[0-9](-[a-z0-9]+\.?[0-9]*)?)(['<]?)/\1\2\3$NEW_VERSION\6/g" {} \; + + +echo "Version updated to $NEW_VERSION." diff --git a/velocity/build.gradle b/velocity/build.gradle deleted file mode 100644 index fd555f5b..00000000 --- a/velocity/build.gradle +++ /dev/null @@ -1,18 +0,0 @@ -repositories { - maven { - url "https://repo.georgev22.com/Other" - } -} - -dependencies { - compileOnly project(path: ':core') - compileOnly "com.velocitypowered:velocity:3.2.0" - annotationProcessor 'com.velocitypowered:velocity-api:3.1.1' -} - -blossom { - var constants = "src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java" - replaceToken('${version}', this.version, constants) - replaceToken('${pluginName}', this.pluginName, constants) - replaceToken('${author}', this.author, constants) -} \ No newline at end of file diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts new file mode 100644 index 00000000..a2fb89c4 --- /dev/null +++ b/velocity/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + id("buildlogic.java-conventions") + alias(libs.plugins.gradleup.shadow) +} + +apply(from = "$rootDir/gradle/publish.gradle") + +repositories { + mavenCentral() +} + +dependencies { + compileOnly("com.velocitypowered:velocity-api:3.4.0-SNAPSHOT") + annotationProcessor("com.velocitypowered:velocity-api:3.4.0-SNAPSHOT") + + implementation(project(":common")) { + exclude(group = "org.slf4j", module = "slf4j-api") + } + implementation(libs.bstats.velocity) + compileOnly(libs.adventure.platform.api) + compileOnly(libs.log4j.api) +} + +configurations.configureEach { + +} + +tasks.shadowJar { + archiveBaseName.set("skinoverlay") + archiveClassifier.set("velocity") + 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("redis.clients", "${project.property("packageName")}.lib.jedis") +} + +tasks.named("publish") { + dependsOn("shadowJar") +} \ No newline at end of file diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java b/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java index 47d949f9..b39d29b1 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java @@ -1,22 +1,21 @@ package com.georgev22.skinoverlay; -import co.aikar.commands.VelocityCommandManager; -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.VelocityMinecraftUtils; -import com.georgev22.library.minecraft.scheduler.VelocityMinecraftScheduler; -import com.georgev22.library.utilities.Utils; -import com.georgev22.skinoverlay.handler.handlers.SkinHandler_Velocity; -import com.georgev22.skinoverlay.listeners.velocity.DeveloperInformListener; +import com.georgev22.skinoverlay.appliers.VelocitySkinApplier; +import com.georgev22.skinoverlay.command.VelocityCommandManager; +import com.georgev22.skinoverlay.hooks.SkinHookNoop; +import com.georgev22.skinoverlay.hooks.SkinsRestorerHook; import com.georgev22.skinoverlay.listeners.velocity.PlayerListeners; -import com.georgev22.skinoverlay.utilities.VelocityPluginMessageUtils; +import com.georgev22.skinoverlay.messaging.MessageManagerNoop; +import com.georgev22.skinoverlay.messaging.RedisManager; +import com.georgev22.skinoverlay.messaging.VelocityPluginMessageManager; +import com.georgev22.skinoverlay.providers.VelocityGameProfileProvider; +import com.georgev22.skinoverlay.providers.VelocityPlayerProvider; +import com.georgev22.skinoverlay.registry.EntityManagerRegistry; +import com.georgev22.skinoverlay.scheduler.VelocityMinecraftScheduler; +import com.georgev22.skinoverlay.storage.data.PlayerData; +import com.georgev22.skinoverlay.storage.data.Skin; +import com.georgev22.skinoverlay.utilities.LoggerWrapper; 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.PlayerObjectVelocity; import com.google.inject.Inject; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; @@ -24,216 +23,103 @@ import com.velocitypowered.api.plugin.Dependency; import com.velocitypowered.api.plugin.Plugin; import com.velocitypowered.api.plugin.annotation.DataDirectory; -import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; -import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; -import com.velocitypowered.api.scheduler.ScheduledTask; -import net.kyori.adventure.platform.AudienceProvider; -import org.bstats.velocity.Metrics; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; import java.io.File; import java.nio.file.Path; -import java.time.Duration; -import java.util.Arrays; -import java.util.UUID; -import java.util.logging.Logger; - -@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("com.mojang:authlib:3.11.50:https://nexus.velocitypowered.com/repository/maven-public/") -@MavenLibrary("org.apache.commons:commons-lang3:3.12.0:https://repo1.maven.org/maven2/") -@Plugin(id = "skinoverlay", name = "${pluginName}", version = "${version}", description = "SkinOverlay", authors = {"${author}"}, dependencies = {@Dependency(id = "skinsrestorer", optional = true)}) -@ApiStatus.Internal -@ApiStatus.NonExtendable -public class SkinOverlayVelocity implements SkinOverlayImpl { +import static com.georgev22.skinoverlay.messaging.VelocityPluginMessageManager.inChannelIdentifier; + +@Plugin( + id = BuildParameters.PLUGIN_ID, + name = BuildParameters.PLUGIN_NAME, + version = BuildParameters.VERSION, + description = BuildParameters.DESCRIPTION, + authors = BuildParameters.AUTHOR, + url = BuildParameters.URL, + dependencies = { + @Dependency(id = "skinsrestorer", optional = true) + } +) +public class SkinOverlayVelocity { + private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); private final ProxyServer server; - private final Logger logger; - - private final Path dataDirectory; - - private final File dataFolder; - - private final Plugin pluginAnnotation; - - private final Metrics.Factory metricsFactory; - - private final SkinOverlay skinOverlay; - - private LibraryLoader libraryLoader; - private int tick = 0; - - private boolean enabled = false; - - private static SkinOverlayVelocity instance; - - @ApiStatus.Internal - @ApiStatus.NonExtendable - public static SkinOverlayVelocity getInstance() { - return instance; - } - - @Contract(pure = true) @Inject - public SkinOverlayVelocity(@NotNull ProxyServer server, @NotNull Logger logger, @DataDirectory @NotNull Path dataDirectory, Metrics.Factory metricsFactory) { - VelocityMinecraftUtils.setServer(server); - this.skinOverlay = new SkinOverlay(this); - instance = this; + public SkinOverlayVelocity(ProxyServer server, Logger logger, @DataDirectory @NotNull Path dataDirectory) { this.server = server; - this.logger = logger; - this.dataDirectory = dataDirectory; - this.dataFolder = dataDirectory.toFile(); - this.pluginAnnotation = this.getClass().getAnnotation(Plugin.class); - this.metricsFactory = metricsFactory; - this.server.getChannelRegistrar().register(MinecraftChannelIdentifier.create("skinoverlay", "messagechannel")); + this.skinOverlay.setLogger(new LoggerWrapper(logger)); + this.skinOverlay.setProxy(true); + File dataDirectoryFile = dataDirectory.toFile(); + if (!dataDirectoryFile.exists()) { + if (dataDirectoryFile.mkdirs()) { + this.skinOverlay.getLogger().info("Folder " + dataDirectory + " has been created!"); + } else { + this.skinOverlay.getLogger().warning("Failed to create folder " + dataDirectory); + } + } + this.skinOverlay.setDataFolder(dataDirectoryFile); + this.skinOverlay.setPlugin(this); + this.skinOverlay.setScheduler(new VelocityMinecraftScheduler<>(server)); + this.skinOverlay.setCommandManager(new VelocityCommandManager(server)); + this.skinOverlay.onLoad(); + if (OptionsUtil.CONNECTION_TYPE.getStringValue().equalsIgnoreCase("PluginMessage")) + this.server.getChannelRegistrar().register(inChannelIdentifier); } @Subscribe public void onProxyInitialization(ProxyInitializeEvent event) { - onLoad(); - } - - @Subscribe - public void onProxyShutdown(ProxyShutdownEvent event) { - onDisable(); - } - - public void onLoad() { - try { - this.libraryLoader = new LibraryLoader(this.getClass().getClassLoader(), this.dataFolder()); - this.libraryLoader.loadAll(this, true); - } catch (InvalidDependencyException | UnknownDependencyException e) { - throw new RuntimeException(e); - } - skinOverlay.onLoad(); - onEnable(); - } - - public void onEnable() { - this.server.getChannelRegistrar().register(MinecraftChannelIdentifier.create("skinoverlay", "test")); - this.skinOverlay.setMinecraftScheduler(new VelocityMinecraftScheduler<>()); - skinOverlay.setSkinHandler(new SkinHandler_Velocity()); - skinOverlay.setCommandManager(new VelocityCommandManager(getProxy(), this, dataFolder())); - skinOverlay.onEnable(); - skinOverlay.setPluginMessageUtils(new VelocityPluginMessageUtils()); - this.server.getChannelRegistrar().register(MinecraftChannelIdentifier.from("skinoverlay:message")); - VelocityMinecraftUtils.registerListeners(this, new DeveloperInformListener(), new PlayerListeners()); - if (OptionsUtil.METRICS.getBooleanValue()) - metricsFactory.make(this, 17476); - enabled = true; - } - - public void onDisable() { - skinOverlay.onDisable(); - enabled = false; - try { - this.libraryLoader.unloadAll(); - } catch (InvalidDependencyException e) { - throw new RuntimeException(e); - } - } - - @Override - public Type type() { - return Type.VELOCITY; - } - - public File dataFolder() { - return dataFolder; - } - - public Logger logger() { - return logger; - } - - @Override - public Description description() { - return new Description(pluginAnnotation.name(), pluginAnnotation.version(), this.getClass().getCanonicalName(), Arrays.stream(pluginAnnotation.authors()).toList()); - } - - @Override - public boolean enable(boolean enable) { - if (enable) { - onEnable(); + if (server.getPluginManager().getPlugin("skinsrestorer").isPresent()) { + this.skinOverlay.setSkinHook(new SkinsRestorerHook()); } else { - onDisable(); + this.skinOverlay.setSkinHook(new SkinHookNoop()); } - return enabled(); - } - - @Override - public boolean enabled() { - return enabled; - } - - @Override - public void saveResource(@NotNull String resource, boolean replace) { - try { - Utils.saveResource(resource, replace, dataFolder(), this.getClass()); - } catch (Exception e) { - throw new RuntimeException(e); + this.skinOverlay.setSkinApplier(new VelocitySkinApplier()); + this.skinOverlay.setGameProfileProvider(new VelocityGameProfileProvider()); + this.skinOverlay.setPlayerProvider(new VelocityPlayerProvider(server)); + this.skinOverlay.setAudienceProvider(new VelocityAudienceProvider(this, server)); + this.skinOverlay.setOnlineMode(server.getConfiguration().isOnlineMode()); + if (OptionsUtil.CONNECTION_TYPE.getStringValue().equalsIgnoreCase("PluginMessage")) { + VelocityPluginMessageManager velocityPluginMessageManager = new VelocityPluginMessageManager(server); + this.skinOverlay.setMessageManager(velocityPluginMessageManager); + this.server.getEventManager().register(this, velocityPluginMessageManager); + this.skinOverlay.getLogger().info("Plugin message connection type: " + OptionsUtil.CONNECTION_TYPE.getStringValue()); + } 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()); } - } - - @Override - public boolean onlineMode() { - return server.getConfiguration().isOnlineMode(); - } - - private final ObservableObjectMap players = new ObservableObjectMap<>(); - - @Override - public ObservableObjectMap onlinePlayers() { - for (Player player : server.getAllPlayers()) { - if (players.containsKey(player.getUniqueId())) { - continue; + this.skinOverlay.getMessageManager().subscribePlayerJoin(uuid -> { + if (OptionsUtil.DEBUG.getBooleanValue()) { + this.skinOverlay.getLogger().info("Player " + uuid + " has joined the server."); } - players.append(player.getUniqueId(), new PlayerObjectVelocity(player)); - } - return players; - } - - @Override - public boolean isPluginEnabled(String pluginName) { - return getProxy().getPluginManager().getPlugin(pluginName).isPresent(); - } - - public Path getDataDirectory() { - return dataDirectory; - } - - public ProxyServer getProxy() { - return server; + this.skinOverlay.getScheduler().runAsyncTask(this.skinOverlay.getPlugin(), () -> { + EntityManagerRegistry.getManager(PlayerData.class) + .flatMap(entityManager -> entityManager.findById(uuid)) + .ifPresent(playerData -> { + Skin skin = playerData.getCurrentSkin(); + if (skin != null) { + this.skinOverlay.getSkinApplier().setSkin( + this.skinOverlay.getPlayerProvider().getSPlayer(uuid), + skin + ); + } + }); + }); + }); + + this.server.getEventManager().register(this, new PlayerListeners()); + this.skinOverlay.onEnable(); } - @Override - public Object plugin() { - return this; - } - - @Override - public ProxyServer serverImpl() { - return server; - } - - @Override - public String serverVersion() { - return SkinOverlayVelocity.getInstance().getProxy().getVersion().getName() + "-" + SkinOverlayVelocity.getInstance().getProxy().getVersion().getVersion(); - } - - @Override - public AudienceProvider adventure() { - return null; + @Subscribe + public void onProxyShutdown(ProxyShutdownEvent event) { + this.skinOverlay.onDisable(); } -} \ No newline at end of file +} diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/VelocityAudienceProvider.java b/velocity/src/main/java/com/georgev22/skinoverlay/VelocityAudienceProvider.java new file mode 100644 index 00000000..12bbdaee --- /dev/null +++ b/velocity/src/main/java/com/georgev22/skinoverlay/VelocityAudienceProvider.java @@ -0,0 +1,99 @@ +package com.georgev22.skinoverlay; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.connection.LoginEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.platform.AudienceProvider; +import net.kyori.adventure.text.flattener.ComponentFlattener; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class VelocityAudienceProvider implements AudienceProvider { + + private final ProxyServer server; + private final Set players = ConcurrentHashMap.newKeySet(); + + public VelocityAudienceProvider(@NotNull Object plugin, @NotNull ProxyServer server) { + this.server = server; + server.getEventManager().register(plugin, this); + + players.addAll(server.getAllPlayers()); + } + + @Subscribe + public void onPlayerJoin(@NotNull LoginEvent event) { + players.add(event.getPlayer()); + } + + @Subscribe + public void onPlayerLeave(@NotNull DisconnectEvent event) { + players.remove(event.getPlayer()); + } + + @Override + public @NotNull Audience all() { + Collection all = new ArrayList<>(players); + all.add(server.getConsoleCommandSource()); + return Audience.audience(all); + } + + @Override + public @NotNull Audience console() { + return server.getConsoleCommandSource(); + } + + @Override + public @NotNull Audience players() { + return Audience.audience(players); + } + + @Override + public @NotNull Audience player(@NotNull UUID playerId) { + return server.getPlayer(playerId).map(p -> p).orElse(Audience.empty()); + } + + @Override + public @NotNull Audience permission(@NotNull String permission) { + return Audience.audience( + players.stream() + .filter(p -> p.hasPermission(permission)) + .toList() + ); + } + + @Override + public @NotNull Audience world(@NotNull Key world) { + // Velocity does not support worlds directly. Return empty. + return Audience.empty(); + } + + @Override + public @NotNull Audience server(@NotNull String serverName) { + return Audience.audience( + players.stream() + .filter(player -> player.getCurrentServer() + .map(serverConn -> serverConn.getServerInfo().getName().equals(serverName)) + .orElse(false)) + .toList() + ); + } + + @Override + public @NotNull ComponentFlattener flattener() { + return ComponentFlattener.basic(); + } + + @Override + public void close() { + players.clear(); + } +} diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/appliers/VelocitySkinApplier.java b/velocity/src/main/java/com/georgev22/skinoverlay/appliers/VelocitySkinApplier.java new file mode 100644 index 00000000..bf161726 --- /dev/null +++ b/velocity/src/main/java/com/georgev22/skinoverlay/appliers/VelocitySkinApplier.java @@ -0,0 +1,23 @@ +package com.georgev22.skinoverlay.appliers; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.storage.data.Skin; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + +public class VelocitySkinApplier extends SkinApplier { + + private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); + + @Override + protected void applySkin(@NotNull SPlayer player, @NotNull Skin skin) { + skinOverlay.getMessageManager().publishSkinProperty(player.getUniqueId(), skin); + } + + @Override + protected @NotNull CompletableFuture sendPackets(@NotNull SPlayer player) { + return CompletableFuture.completedFuture(true); + } +} diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityCommandIssuer.java b/velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityCommandIssuer.java new file mode 100644 index 00000000..cb3ca10c --- /dev/null +++ b/velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityCommandIssuer.java @@ -0,0 +1,65 @@ +package com.georgev22.skinoverlay.command; + +import com.georgev22.skinoverlay.utilities.Utils; +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.proxy.Player; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public class VelocityCommandIssuer implements CommandIssuer { + + private final CommandSource source; + + public VelocityCommandIssuer(CommandSource source) { + this.source = source; + } + + @Override + public boolean isPlayer() { + return this.source instanceof Player; + } + + @Override + public void sendMessage(@NotNull String message) { + this.source.sendMessage(Component.text(message)); + } + + @Override + public void sendMessage(@NotNull Component component) { + this.source.sendMessage(component); + } + + @Override + public @NotNull T getIssuer() { + return (T) this.source; + } + + @Override + public boolean hasPermission(String permission) { + return this.source.hasPermission(permission); + } + + @Override + public boolean isOp() { + return false; + } + + @Override + public UUID getUniqueId() { + if (isPlayer()) { + return ((Player) this.source).getUniqueId(); + } + return Utils.generateUUID("SkinOverlayConsole"); + } + + @Override + public String getName() { + if (isPlayer()) { + return ((Player) this.source).getUsername(); + } else { + return "Console"; + } + } +} diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityCommandManager.java b/velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityCommandManager.java new file mode 100644 index 00000000..32fbad5c --- /dev/null +++ b/velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityCommandManager.java @@ -0,0 +1,27 @@ +package com.georgev22.skinoverlay.command; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.velocitypowered.api.command.CommandMeta; +import com.velocitypowered.api.proxy.ProxyServer; +import org.jetbrains.annotations.NotNull; + +public class VelocityCommandManager extends CommandManager { + + private final ProxyServer proxyServer; + private final com.velocitypowered.api.command.CommandManager velocityServerCommandManager; + + public VelocityCommandManager(ProxyServer proxyServer) { + this.proxyServer = proxyServer; + this.velocityServerCommandManager = proxyServer.getCommandManager(); + } + + @Override + protected void registerCommand0(@NotNull BaseCommand command) { + for (String alias : command.getAliases(false)) { + CommandMeta commandMeta = velocityServerCommandManager.metaBuilder(alias) + .plugin(SkinOverlay.getInstance().getPlugin()) + .build(); + proxyServer.getCommandManager().register(commandMeta, new VelocityPluginCommandWrapper(command)); + } + } +} diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityPluginCommandWrapper.java b/velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityPluginCommandWrapper.java new file mode 100644 index 00000000..2e1f6367 --- /dev/null +++ b/velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityPluginCommandWrapper.java @@ -0,0 +1,45 @@ +package com.georgev22.skinoverlay.command; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.command.SimpleCommand; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.logging.Level; + +/** + * Wrapper class to adapt BaseCommand to Velocity SimpleCommand. + */ +public class VelocityPluginCommandWrapper implements SimpleCommand { + + private final BaseCommand baseCommand; + + public VelocityPluginCommandWrapper(BaseCommand baseCommand) { + this.baseCommand = baseCommand; + } + + @Override + public void execute(@NotNull Invocation invocation) { + CommandSource source = invocation.source(); + String[] args = invocation.arguments(); + CommandContext context = new CommandContext(); + try { + baseCommand.execute(new VelocityCommandIssuer(source), args, context); + } catch (Exception e) { + SkinOverlay.getInstance().getLogger().log(Level.SEVERE, "An error occurred while executing the command.", e); + } + } + + @Override + public boolean hasPermission(Invocation invocation) { + String permission = baseCommand.getPermission(); + return permission == null || permission.isEmpty() || invocation.source().hasPermission(permission); + } + + @Override + public List suggest(@NotNull Invocation invocation) { + CommandIssuer commandIssuer = new VelocityCommandIssuer(invocation.source()); + return baseCommand.tabComplete(commandIssuer, invocation.arguments()).stream().toList(); + } +} \ No newline at end of file diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_Velocity.java b/velocity/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_Velocity.java deleted file mode 100644 index 3b8dcf03..00000000 --- a/velocity/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_Velocity.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.georgev22.skinoverlay.handler.handlers; - -import com.georgev22.library.maps.HashObjectMap; -import com.georgev22.library.maps.ObjectMap; -import com.georgev22.library.utilities.Utils; -import com.georgev22.skinoverlay.handler.SGameProfile; -import com.georgev22.skinoverlay.handler.SProperty; -import com.georgev22.skinoverlay.handler.SkinHandler; -import com.georgev22.skinoverlay.handler.profile.SGameProfileVelocity; -import com.georgev22.skinoverlay.storage.data.Skin; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.util.GameProfile; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -import java.util.concurrent.CompletableFuture; -import java.util.logging.Level; - -@ApiStatus.Internal -@ApiStatus.NonExtendable -public class SkinHandler_Velocity extends SkinHandler { - - @Override - public CompletableFuture updateSkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - return CompletableFuture.supplyAsync(() -> { - try { - Player player = playerObject.player(); - skinOverlay.getPluginMessageUtils().setChannel("skinoverlay:bungee"); - skinOverlay.getPluginMessageUtils().setObject(player.getCurrentServer().orElseThrow().getServerInfo()); - if (skin.skinParts().getSkinName().equalsIgnoreCase("default")) { - skinOverlay.getPluginMessageUtils().sendDataToServer("reset", playerObject.playerUUID().toString(), Utils.serializeObjectToString(skin), "true"); - } else { - skinOverlay.getPluginMessageUtils().sendDataToServer("change", playerObject.playerUUID().toString(), Utils.serializeObjectToString(skin), "true"); - } - return true; - } catch (Exception exception) { - throw new RuntimeException(exception); - } - }); - } - - @Override - public void applySkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - skinOverlay.getSkinHandler().updateSkin(playerObject, skin).handleAsync((aBoolean, throwable) -> { - if (throwable != null) { - skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); - return false; - } - return aBoolean; - }); - } - - @Override - public GameProfile getInternalGameProfile(@NotNull PlayerObject playerObject) { - return ((Player) playerObject.player()).getGameProfile(); - } - - @Override - public SGameProfile getGameProfile(@NotNull PlayerObject playerObject) { - if (sGameProfiles.containsKey(playerObject)) { - return sGameProfiles.get(playerObject); - } - return sGameProfiles.append(playerObject, wrapper(this.getInternalGameProfile(playerObject))).get(playerObject); - } - - public static @NotNull SGameProfile wrapper(@NotNull GameProfile gameProfile) { - ObjectMap propertyObjectMap = new HashObjectMap<>(); - gameProfile.getProperties().forEach(property -> propertyObjectMap.append(property.getName(), new SProperty(property.getName(), property.getValue(), property.getSignature()))); - return new SGameProfileVelocity(gameProfile.getName(), gameProfile.getId(), propertyObjectMap); - } -} diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/handler/profile/SGameProfileVelocity.java b/velocity/src/main/java/com/georgev22/skinoverlay/handler/profile/SGameProfileVelocity.java deleted file mode 100644 index d0dc0d4c..00000000 --- a/velocity/src/main/java/com/georgev22/skinoverlay/handler/profile/SGameProfileVelocity.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.georgev22.skinoverlay.handler.profile; - -import com.georgev22.library.maps.ObjectMap; -import com.georgev22.skinoverlay.handler.SGameProfile; -import com.georgev22.skinoverlay.handler.SProperty; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import com.velocitypowered.api.util.GameProfile; -import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import org.jetbrains.annotations.ApiStatus; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -@ApiStatus.Internal -@ApiStatus.NonExtendable -public class SGameProfileVelocity extends SGameProfile { - - public SGameProfileVelocity(String name, UUID uuid) { - super(name, uuid); - } - - public SGameProfileVelocity(String name, UUID uuid, ObjectMap properties) { - super(name, uuid, properties); - } - - @Override - public void apply() { - PlayerObject playerObject = this.skinOverlay.getPlayer(getUUID()).orElseThrow(); - List propertyList = new ArrayList<>(); - this.getProperties().forEach((s, sProperty) -> propertyList.add(new GameProfile.Property(sProperty.name(), sProperty.value(), sProperty.signature()))); - ConnectedPlayer connectedPlayer = playerObject.player(); - - connectedPlayer.setGameProfileProperties(propertyList); - } -} diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/listeners/velocity/DeveloperInformListener.java b/velocity/src/main/java/com/georgev22/skinoverlay/listeners/velocity/DeveloperInformListener.java deleted file mode 100644 index 2d1dd746..00000000 --- a/velocity/src/main/java/com/georgev22/skinoverlay/listeners/velocity/DeveloperInformListener.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.georgev22.skinoverlay.listeners.velocity; - -import com.georgev22.skinoverlay.SkinOverlay; -import com.velocitypowered.api.event.Subscribe; -import com.velocitypowered.api.event.connection.PostLoginEvent; -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -@ApiStatus.NonExtendable -public class DeveloperInformListener { - - private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); - - @Subscribe - public void onJoin(PostLoginEvent e) { - skinOverlay.getPlayer(e.getPlayer().getUniqueId()).orElseThrow().developerInform(); - } -} diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/listeners/velocity/PlayerListeners.java b/velocity/src/main/java/com/georgev22/skinoverlay/listeners/velocity/PlayerListeners.java index 9b3eb467..b78a38c2 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/listeners/velocity/PlayerListeners.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/listeners/velocity/PlayerListeners.java @@ -1,65 +1,31 @@ package com.georgev22.skinoverlay.listeners.velocity; import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.event.events.player.PlayerObjectConnectionEvent; -import com.georgev22.skinoverlay.utilities.Utilities; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import com.google.common.io.ByteArrayDataInput; -import com.google.common.io.ByteStreams; -import com.velocitypowered.api.event.PostOrder; +import com.georgev22.skinoverlay.event.events.player.SPlayerJoinEvent; +import com.georgev22.skinoverlay.event.events.player.SPlayerLeaveEvent; +import com.georgev22.skinoverlay.player.SPlayer; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.connection.DisconnectEvent; -import com.velocitypowered.api.event.connection.PluginMessageEvent; -import com.velocitypowered.api.event.connection.PostLoginEvent; -import com.velocitypowered.api.proxy.ServerConnection; -import org.jetbrains.annotations.ApiStatus; +import com.velocitypowered.api.event.connection.LoginEvent; -import java.util.Objects; -import java.util.UUID; - -@ApiStatus.Internal -@ApiStatus.NonExtendable public class PlayerListeners { - private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); - - @Subscribe(order = PostOrder.FIRST) - public void onPostLogin(PostLoginEvent loginEvent) { - if (!loginEvent.getPlayer().isActive()) - return; - skinOverlay.getEventManager().callEvent( - new PlayerObjectConnectionEvent( - skinOverlay.getPlayer(loginEvent.getPlayer().getUniqueId()).orElseThrow(), - PlayerObjectConnectionEvent.ConnectionType.CONNECT, - true - ) - ); - } + private final SkinOverlay mainPlugin = SkinOverlay.getInstance(); - @Subscribe(order = PostOrder.FIRST) - public void onQuit(DisconnectEvent playerDisconnectEvent) { - skinOverlay.getEventManager().callEvent( - new PlayerObjectConnectionEvent( - skinOverlay.getPlayer(playerDisconnectEvent.getPlayer().getUniqueId()).orElseThrow(), - PlayerObjectConnectionEvent.ConnectionType.DISCONNECT, - true - ) - ); + @Subscribe + public void onPlayerJoin(LoginEvent event) { + SPlayer player = mainPlugin.getPlayerProvider().getSPlayer(event.getPlayer()); + if (player != null) { + mainPlugin.getEventBus().post(new SPlayerJoinEvent(player)); + } } @Subscribe - public void onPluginMessage(PluginMessageEvent pluginMessageEvent) { - if (!(pluginMessageEvent.getSource() instanceof ServerConnection)) { - return; - } - if (pluginMessageEvent.getIdentifier().getId().equalsIgnoreCase("skinoverlay:message")) { - ByteArrayDataInput in = ByteStreams.newDataInput(pluginMessageEvent.dataAsInputStream()); - String subChannel = in.readUTF(); - if (subChannel.equalsIgnoreCase("playerJoin")) { - UUID playerUUID = UUID.fromString(Objects.requireNonNull(Utilities.decrypt(in.readUTF()))); - this.skinOverlay.getMinecraftScheduler().runTask(skinOverlay.getPlugin(), () -> skinOverlay.getPlayer(playerUUID).ifPresent(PlayerObject::updateSkin)); - } + public void onPlayerQuit(DisconnectEvent event) { + SPlayer player = mainPlugin.getPlayerProvider().getSPlayer(event.getPlayer()); + if (player != null) { + mainPlugin.getEventBus().post(new SPlayerLeaveEvent(player)); } } -} +} diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/messaging/VelocityPluginMessageManager.java b/velocity/src/main/java/com/georgev22/skinoverlay/messaging/VelocityPluginMessageManager.java new file mode 100644 index 00000000..1b7cc208 --- /dev/null +++ b/velocity/src/main/java/com/georgev22/skinoverlay/messaging/VelocityPluginMessageManager.java @@ -0,0 +1,121 @@ +package com.georgev22.skinoverlay.messaging; + +import com.georgev22.skinoverlay.storage.data.Skin; +import com.georgev22.skinoverlay.utilities.config.OptionsUtil; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Optional; +import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.logging.Level; + +public class VelocityPluginMessageManager extends MessageManager { + @ApiStatus.Internal + public static final MinecraftChannelIdentifier outChannelIdentifier = MinecraftChannelIdentifier.from(CHANNEL_TO_BACKEND); + @ApiStatus.Internal + public static final MinecraftChannelIdentifier inChannelIdentifier = MinecraftChannelIdentifier.from(CHANNEL_FROM_BACKEND); + private final ProxyServer proxyServer; + private Consumer playerJoinHandler = uuid -> { + }; + + public VelocityPluginMessageManager(@NotNull ProxyServer proxyServer) { + this.proxyServer = proxyServer; + } + + @Override + public void publishSkinProperty(@NotNull UUID playerUUID, @NotNull Skin skin) { + byte[] data = toByteArray("skinupdate", playerUUID.toString(), skin.toBase64()); + + if (OptionsUtil.DEBUG.getBooleanValue()) { + this.mainPlugin.getLogger().info("Sending plugin message with size " + data.length + " bytes to player " + playerUUID); + } + + Optional playerOptional = proxyServer.getPlayer(playerUUID); + if (playerOptional.isPresent()) { + Player player = playerOptional.get(); + Optional currentServer = player.getCurrentServer(); + if (currentServer.isEmpty()) { + if (OptionsUtil.DEBUG.getBooleanValue()) { + this.mainPlugin.getLogger().info("Player " + player.getUsername() + " is not connected to a server."); + } + return; + } + currentServer.get().sendPluginMessage(outChannelIdentifier, data); + if (OptionsUtil.DEBUG.getBooleanValue()) { + this.mainPlugin.getLogger().info("Sent plugin message with size " + data.length + " bytes to server " + currentServer.get().getServerInfo().getName() + " from player " + player.getUsername()); + } + } else { + if (OptionsUtil.DEBUG.getBooleanValue()) { + this.mainPlugin.getLogger().info("Player " + playerUUID + " is not online."); + } + } + } + + @Override + public void subscribeSkinProperty(@NotNull BiConsumer handler) { + throw new UnsupportedOperationException("Velocity side does not need to subscribe to incoming skin changes."); + } + + @Override + public void publishPlayerJoin(@NotNull UUID playerUUID) { + throw new UnsupportedOperationException("Velocity side does not need to publish player joins."); + } + + @Override + public void subscribePlayerJoin(Consumer handler) { + this.playerJoinHandler = handler; + } + + @Override + public void close() { + proxyServer.getChannelRegistrar().unregister(inChannelIdentifier); + } + + + @Subscribe + public void onPluginMessageFromPlayer(PluginMessageEvent event) { + if (!inChannelIdentifier.equals(event.getIdentifier())) { + this.mainPlugin.getLogger().log(Level.SEVERE, "Error parsing channel from plugin message"); + return; + } + + if (!(event.getSource() instanceof ServerConnection serverConnection)) { + return; + } + + MessageData data; + try { + data = this.readByteArray(event.getData()); + } catch (IOException e) { + this.mainPlugin.getLogger().log(Level.SEVERE, "Error parsing plugin message", e); + return; + } + if (data.subChannel().isEmpty()) { + this.mainPlugin.getLogger().log(Level.SEVERE, "Error parsing channel from plugin message"); + return; + } + if (!data.subChannel().equalsIgnoreCase("playerjoin")) { + this.mainPlugin.getLogger().log(Level.SEVERE, "Error parsing channel from plugin message"); + return; + } + String stringUUID = data.dataEntries()[0]; + UUID uuid; + try { + uuid = UUID.fromString(stringUUID); + } catch (Exception e) { + this.mainPlugin.getLogger().log(Level.SEVERE, "Error parsing UUID from plugin message: " + Arrays.toString(data.dataEntries()), e); + return; + } + playerJoinHandler.accept(uuid); + } +} diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/player/VelocitySPlayer.java b/velocity/src/main/java/com/georgev22/skinoverlay/player/VelocitySPlayer.java new file mode 100644 index 00000000..55d9c905 --- /dev/null +++ b/velocity/src/main/java/com/georgev22/skinoverlay/player/VelocitySPlayer.java @@ -0,0 +1,24 @@ +package com.georgev22.skinoverlay.player; + +import com.georgev22.skinoverlay.command.VelocityCommandIssuer; +import com.velocitypowered.api.proxy.Player; + +public class VelocitySPlayer extends VelocityCommandIssuer implements SPlayer { + + private final Player player; + + public VelocitySPlayer(Player player) { + super(player); + this.player = player; + } + + @Override + public boolean isOnline() { + return this.player.isActive(); + } + + @Override + public T getPlayer() { + return (T) this.player; + } +} diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/providers/VelocityGameProfileProvider.java b/velocity/src/main/java/com/georgev22/skinoverlay/providers/VelocityGameProfileProvider.java new file mode 100644 index 00000000..9b0512ee --- /dev/null +++ b/velocity/src/main/java/com/georgev22/skinoverlay/providers/VelocityGameProfileProvider.java @@ -0,0 +1,43 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.skin.SGameProfile; +import com.georgev22.skinoverlay.skin.SProperty; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.util.GameProfile; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public class VelocityGameProfileProvider extends GameProfileProvider { + + @Override + public GameProfile getInternalGameProfile(@NotNull SPlayer player) { + return ((Player) player.getPlayer()).getGameProfile(); + } + + @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(property -> + propertyObjectMap.append(property.getName(), new SProperty(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) { + Player velocityPlayer = player.getPlayer(); + List propertyList = new ArrayList<>(); + getGameProfile(player).getProperties().forEach((s, sProperty) -> propertyList.add( + new GameProfile.Property(s, sProperty.value(), sProperty.signature()))); + velocityPlayer.setGameProfileProperties(propertyList); + } +} diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/providers/VelocityPlayerProvider.java b/velocity/src/main/java/com/georgev22/skinoverlay/providers/VelocityPlayerProvider.java new file mode 100644 index 00000000..fb362cb8 --- /dev/null +++ b/velocity/src/main/java/com/georgev22/skinoverlay/providers/VelocityPlayerProvider.java @@ -0,0 +1,84 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.command.CommandIssuer; +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.player.VelocitySPlayer; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public class VelocityPlayerProvider extends PlayerProvider { + + private final ProxyServer server; + + public VelocityPlayerProvider(ProxyServer server) { + this.server = server; + } + + @Override + public SPlayer getSPlayer(Object player) { + if (player instanceof Player velocityPlayer) { + return new VelocitySPlayer(velocityPlayer); + } + if (player instanceof String name) { + Optional onlinePlayer = this.server.getPlayer(name); + if (onlinePlayer.isPresent()) { + return new VelocitySPlayer(onlinePlayer.get()); + } + throw new IllegalArgumentException("Player " + name + " is not online"); + } + if (player instanceof UUID uuid) { + Optional onlinePlayer = this.server.getPlayer(uuid); + if (onlinePlayer.isPresent()) { + return new VelocitySPlayer(onlinePlayer.get()); + } + throw new IllegalArgumentException("Player " + uuid + " is not online"); + } + if (player instanceof CommandIssuer commandIssuer) { + if (!commandIssuer.isPlayer()) { + throw new IllegalArgumentException("CommandIssuer is not a player"); + } + return new VelocitySPlayer(commandIssuer.getIssuer()); + } + return null; + } + + @Override + public SPlayer getSPlayer(CommandIssuer commandIssuer) { + if (!commandIssuer.isPlayer()) { + throw new IllegalArgumentException("CommandIssuer is not a player"); + } + return new VelocitySPlayer(commandIssuer.getIssuer()); + } + + @Override + public SPlayer getSPlayer(String name) { + Optional onlinePlayer = server.getPlayer(name); + if (onlinePlayer.isPresent()) { + return new VelocitySPlayer(onlinePlayer.get()); + } + throw new IllegalArgumentException("Player " + name + " is not online"); + } + + @Override + public SPlayer getSPlayer(UUID uuid) { + Optional onlinePlayer = server.getPlayer(uuid); + if (onlinePlayer.isPresent()) { + return new VelocitySPlayer(onlinePlayer.get()); + } + throw new IllegalArgumentException("Player " + uuid + " is not online"); + } + + @Override + public List getOnlinePlayers() { + return new ArrayList<>( + server.getAllPlayers().stream() + .map(VelocitySPlayer::new) + .toList() + ); + } +} diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/scheduler/VelocityMinecraftScheduler.java b/velocity/src/main/java/com/georgev22/skinoverlay/scheduler/VelocityMinecraftScheduler.java new file mode 100644 index 00000000..c707add3 --- /dev/null +++ b/velocity/src/main/java/com/georgev22/skinoverlay/scheduler/VelocityMinecraftScheduler.java @@ -0,0 +1,364 @@ +package com.georgev22.skinoverlay.scheduler; + +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.scheduler.ScheduledTask; +import com.velocitypowered.api.scheduler.TaskStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public class VelocityMinecraftScheduler implements MinecraftScheduler { + + private final ProxyServer proxyServer; + + public VelocityMinecraftScheduler(ProxyServer proxyServer) { + this.proxyServer = proxyServer; + } + + private static final List tasks = new ArrayList<>(); + + /** + * {@inheritDoc} + */ + @Override + public SchedulerTask runTask(Plugin o, Runnable task) { + return new VelocitySchedulerTask(proxyServer.getScheduler().buildTask(o, task).schedule()); + } + + @Override + public CompletableFuture runTask(Plugin o, Supplier task) { + CompletableFuture future = new CompletableFuture<>(); + runTask(o, () -> { + try { + T result = task.get(); + future.complete(result); + } catch (Exception e) { + future.completeExceptionally(e); + } + }); + return future; + } + + /** + * {@inheritDoc} + */ + @Override + public SchedulerTask runAsyncTask(Plugin o, Runnable task) { + return new VelocitySchedulerTask(proxyServer.getScheduler().buildTask(o, task).schedule()); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture runAsyncTask(Plugin o, Supplier task) { + CompletableFuture future = new CompletableFuture<>(); + runAsyncTask(o, () -> { + try { + T result = task.get(); + future.complete(result); + } catch (Exception e) { + future.completeExceptionally(e); + } + }); + return future; + } + + /** + * {@inheritDoc} + */ + @Override + public SchedulerTask createDelayedTask(Plugin o, Runnable task, long delay) { + return new VelocitySchedulerTask(proxyServer.getScheduler().buildTask(o, task).delay((delay / 20), TimeUnit.SECONDS).schedule()); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture createDelayedTask(Plugin o, Supplier task, long delay) { + CompletableFuture future = new CompletableFuture<>(); + createDelayedTask(o, () -> { + try { + T result = task.get(); + future.complete(result); + } catch (Exception e) { + future.completeExceptionally(e); + } + }, delay); + return future; + } + + /** + * {@inheritDoc} + */ + @Override + public SchedulerTask createRepeatingTask(Plugin o, Runnable task, long delay, long period) { + return new VelocitySchedulerTask(proxyServer.getScheduler().buildTask(o, task).delay((delay / 20), TimeUnit.SECONDS).repeat((period / 20), TimeUnit.SECONDS).schedule()); + } + + /** + * {@inheritDoc} + */ + @Override + public SchedulerTask createAsyncDelayedTask(Plugin o, Runnable task, long delay) { + return new VelocitySchedulerTask(proxyServer.getScheduler().buildTask(o, task).delay((delay / 20), TimeUnit.SECONDS).schedule()); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture createAsyncDelayedTask(Plugin o, Supplier task, long delay) { + CompletableFuture future = new CompletableFuture<>(); + createAsyncDelayedTask(o, () -> { + try { + T result = task.get(); + future.complete(result); + } catch (Exception e) { + future.completeExceptionally(e); + } + }, delay); + return future; + } + + /** + * {@inheritDoc} + */ + @Override + public SchedulerTask createAsyncRepeatingTask(Plugin o, Runnable task, long delay, long period) { + return new VelocitySchedulerTask(proxyServer.getScheduler().buildTask(o, task).delay((delay / 20), TimeUnit.SECONDS).repeat((period / 20), TimeUnit.SECONDS).schedule()); + } + + /** + * {@inheritDoc} + */ + @Override + public SchedulerTask createDelayedTaskForWorld(Plugin o, Runnable task, World world, @NotNull Chunk chunk, long delay) { + return new VelocitySchedulerTask(proxyServer.getScheduler().buildTask(o, task).delay((delay / 20), TimeUnit.SECONDS).schedule()); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture createDelayedTaskForWorld(Plugin o, Supplier task, World world, @NotNull Chunk chunk, long delay) { + CompletableFuture future = new CompletableFuture<>(); + createDelayedTaskForWorld(o, () -> { + try { + T result = task.get(); + future.complete(result); + } catch (Exception e) { + future.completeExceptionally(e); + } + }, world, chunk, delay); + return future; + } + + /** + * {@inheritDoc} + */ + @Override + public SchedulerTask createDelayedForLocation(Plugin o, Runnable task, Location location, long delay) { + return new VelocitySchedulerTask(proxyServer.getScheduler().buildTask(o, task).delay((delay / 20), TimeUnit.SECONDS).schedule()); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture createDelayedForLocation(Plugin o, Supplier task, Location location, long delay) { + CompletableFuture future = new CompletableFuture<>(); + createDelayedForLocation(o, () -> { + try { + T result = task.get(); + future.complete(result); + } catch (Exception e) { + future.completeExceptionally(e); + } + }, location, delay); + return future; + } + + /** + * {@inheritDoc} + */ + @Override + public SchedulerTask createDelayedForEntity(Plugin o, Runnable task, Runnable retired, Entity entity, long delay) { + return new VelocitySchedulerTask(proxyServer.getScheduler().buildTask(o, task).delay((delay / 20), TimeUnit.SECONDS).schedule()); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture createDelayedForEntity(Plugin o, Supplier task, Runnable retired, Entity entity, long delay) { + CompletableFuture future = new CompletableFuture<>(); + createDelayedForEntity(o, () -> { + try { + T result = task.get(); + future.complete(result); + } catch (Exception e) { + future.completeExceptionally(e); + } + }, retired, entity, delay); + return future; + } + + /** + * {@inheritDoc} + */ + @Override + public SchedulerTask createTaskForWorld(Plugin o, Runnable task, World world, @NotNull Chunk chunk) { + return new VelocitySchedulerTask(proxyServer.getScheduler().buildTask(o, task).schedule()); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture createTaskForWorld(Plugin o, Supplier task, World world, @NotNull Chunk chunk) { + CompletableFuture future = new CompletableFuture<>(); + createTaskForWorld(o, () -> { + try { + T result = task.get(); + future.complete(result); + } catch (Exception e) { + future.completeExceptionally(e); + } + }, world, chunk); + return future; + } + + /** + * {@inheritDoc} + */ + @Override + public SchedulerTask createTaskForLocation(Plugin o, Runnable task, Location location) { + return new VelocitySchedulerTask(proxyServer.getScheduler().buildTask(o, task).schedule()); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture createTaskForLocation(Plugin o, Supplier task, Location location) { + CompletableFuture future = new CompletableFuture<>(); + createTaskForLocation(o, () -> { + try { + T result = task.get(); + future.complete(result); + } catch (Exception e) { + future.completeExceptionally(e); + } + }, location); + return future; + } + + /** + * {@inheritDoc} + */ + @Override + public SchedulerTask createTaskForEntity(Plugin o, Runnable task, Runnable retired, Entity entity) { + return new VelocitySchedulerTask(proxyServer.getScheduler().buildTask(o, task).schedule()); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture createTaskForEntity(Plugin o, Supplier task, Runnable retired, Entity entity) { + CompletableFuture future = new CompletableFuture<>(); + createTaskForEntity(o, () -> { + try { + T result = task.get(); + future.complete(result); + } catch (Exception e) { + future.completeExceptionally(e); + } + }, retired, entity); + return future; + } + + /** + * {@inheritDoc} + */ + @Override + public SchedulerTask createRepeatingTaskForWorld(Plugin o, Runnable task, World world, @NotNull Chunk chunk, long delay, long period) { + return new VelocitySchedulerTask(proxyServer.getScheduler().buildTask(o, task).delay((delay / 20), TimeUnit.SECONDS).repeat((period / 20), TimeUnit.SECONDS).schedule()); + } + + /** + * {@inheritDoc} + */ + @Override + public SchedulerTask createRepeatingTaskForLocation(Plugin o, Runnable task, Location location, long delay, long period) { + return new VelocitySchedulerTask(proxyServer.getScheduler().buildTask(o, task).delay((delay / 20), TimeUnit.SECONDS).repeat((period / 20), TimeUnit.SECONDS).schedule()); + } + + /** + * {@inheritDoc} + */ + @Override + public SchedulerTask createRepeatingTaskForEntity(Plugin o, Runnable task, Runnable retired, Entity entity, long delay, long period) { + return new VelocitySchedulerTask(proxyServer.getScheduler().buildTask(o, task).delay((delay / 20), TimeUnit.SECONDS).repeat((period / 20), TimeUnit.SECONDS).schedule()); + } + + /** + * {@inheritDoc} + */ + @Override + public void cancelTasks(Plugin o) { + new ArrayList<>(tasks).forEach(schedulerTask -> { + tasks.remove(schedulerTask); + if (schedulerTask.isCancelled()) { + return; + } + schedulerTask.cancel(); + }); + } + + /** + * {@inheritDoc} + */ + @Override + public MinecraftScheduler getScheduler() { + return this; + } + + @SuppressWarnings("ClassCanBeRecord") + private static class VelocitySchedulerTask implements SchedulerTask { + + private final ScheduledTask task; + + public VelocitySchedulerTask(ScheduledTask task) { + this.task = task; + tasks.add(this); + } + + + @Override + public void cancel() { + this.task.cancel(); + } + + @Override + public boolean isCancelled() { + return this.task.status().equals(TaskStatus.CANCELLED) || this.task.status().equals(TaskStatus.FINISHED); + } + + @Override + public int getTaskId() { + return 0; + } + + @Override + public boolean isRunning() { + return this.task.status().equals(TaskStatus.SCHEDULED); + } + } +} \ No newline at end of file diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/utilities/VelocityPluginMessageUtils.java b/velocity/src/main/java/com/georgev22/skinoverlay/utilities/VelocityPluginMessageUtils.java deleted file mode 100644 index 8daeb7dc..00000000 --- a/velocity/src/main/java/com/georgev22/skinoverlay/utilities/VelocityPluginMessageUtils.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.georgev22.skinoverlay.utilities; - -import com.georgev22.skinoverlay.SkinOverlayVelocity; -import com.georgev22.skinoverlay.utilities.player.PlayerObject; -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.proxy.server.ServerInfo; -import org.jetbrains.annotations.NotNull; - -public class VelocityPluginMessageUtils extends PluginMessageUtils { - - public void sendDataToServer(@NotNull String subChannel, String... dataArray) { - if (getObject() == null) { - skinOverlay.getLogger().severe("ServerInfo is null!!"); - return; - } - if (SkinOverlayVelocity.getInstance().getProxy().getServer(((ServerInfo) getObject()).getName()).isPresent()) { - SkinOverlayVelocity.getInstance().getProxy().getServer(((ServerInfo) getObject()).getName()).get().sendPluginMessage(this::getChannel, this.toByteArray(subChannel, dataArray)); - } - } - - public void sendDataToPlayer(@NotNull String subChannel, @NotNull PlayerObject player, String... dataArray) { - ((Player) player.player()).sendPluginMessage(this::getChannel, this.toByteArray(subChannel, dataArray)); - } -} diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/utilities/player/PlayerObjectVelocity.java b/velocity/src/main/java/com/georgev22/skinoverlay/utilities/player/PlayerObjectVelocity.java deleted file mode 100644 index 2c7e299b..00000000 --- a/velocity/src/main/java/com/georgev22/skinoverlay/utilities/player/PlayerObjectVelocity.java +++ /dev/null @@ -1,274 +0,0 @@ -package com.georgev22.skinoverlay.utilities.player; - -import com.georgev22.library.maps.ObjectMap; -import com.georgev22.library.minecraft.VelocityMinecraftUtils; -import com.georgev22.skinoverlay.SkinOverlay; -import com.google.common.collect.Lists; -import com.velocitypowered.api.network.ProtocolVersion; -import com.velocitypowered.api.permission.Tristate; -import com.velocitypowered.api.proxy.ConnectionRequestBuilder; -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.proxy.ProxyServer; -import com.velocitypowered.api.proxy.ServerConnection; -import com.velocitypowered.api.proxy.crypto.IdentifiedKey; -import com.velocitypowered.api.proxy.messages.ChannelIdentifier; -import com.velocitypowered.api.proxy.player.PlayerSettings; -import com.velocitypowered.api.proxy.player.ResourcePackInfo; -import com.velocitypowered.api.proxy.player.TabList; -import com.velocitypowered.api.proxy.server.RegisteredServer; -import com.velocitypowered.api.util.GameProfile; -import com.velocitypowered.api.util.ModInfo; -import net.kyori.adventure.audience.Audience; -import net.kyori.adventure.identity.Identity; -import net.kyori.adventure.text.Component; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.net.InetSocketAddress; -import java.util.List; -import java.util.Locale; -import java.util.Optional; -import java.util.UUID; - -public class PlayerObjectVelocity extends PlayerObject { - - private final Player player; - - public PlayerObjectVelocity(final UUID uuid, final String name) { - this.player = new Player() { - @Override - public @Nullable IdentifiedKey getIdentifiedKey() { - return null; - } - - @Override - public String getUsername() { - return name; - } - - @Override - public @Nullable Locale getEffectiveLocale() { - return Locale.US; - } - - @Override - public void setEffectiveLocale(Locale locale) { - throw new UnsupportedOperationException(); - } - - @Override - public UUID getUniqueId() { - return uuid; - } - - @Override - public Optional getCurrentServer() { - return Optional.empty(); - } - - @Override - public PlayerSettings getPlayerSettings() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean hasSentPlayerSettings() { - return false; - } - - @Override - public Optional getModInfo() { - return Optional.empty(); - } - - @Override - public long getPing() { - return 0; - } - - @Override - public boolean isOnlineMode() { - return SkinOverlay.getInstance().isOnlineMode(); - } - - @Override - public ConnectionRequestBuilder createConnectionRequest(RegisteredServer server) { - throw new UnsupportedOperationException(); - } - - @Override - public List getGameProfileProperties() { - return getGameProfile().getProperties(); - } - - @Override - public void setGameProfileProperties(List properties) { - throw new UnsupportedOperationException(); - } - - @Override - public GameProfile getGameProfile() { - return new GameProfile(uuid, name, Lists.newArrayList()); - } - - @Override - public void clearHeaderAndFooter() { - throw new UnsupportedOperationException(); - } - - @Override - public Component getPlayerListHeader() { - throw new UnsupportedOperationException(); - } - - @Override - public Component getPlayerListFooter() { - throw new UnsupportedOperationException(); - } - - @Override - public TabList getTabList() { - throw new UnsupportedOperationException(); - } - - @Override - public void disconnect(Component reason) { - throw new UnsupportedOperationException(); - } - - @Override - public void spoofChatInput(String input) { - throw new UnsupportedOperationException(); - } - - @Override - public void sendResourcePack(String url) { - throw new UnsupportedOperationException(); - } - - @Override - public void sendResourcePack(String url, byte[] hash) { - throw new UnsupportedOperationException(); - } - - @Override - public void sendResourcePackOffer(ResourcePackInfo packInfo) { - throw new UnsupportedOperationException(); - } - - @Override - public @Nullable ResourcePackInfo getAppliedResourcePack() { - return null; - } - - @Override - public @Nullable ResourcePackInfo getPendingResourcePack() { - return null; - } - - @Override - public boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data) { - return false; - } - - @Override - public @Nullable String getClientBrand() { - return null; - } - - @Override - public Tristate getPermissionValue(String permission) { - throw new UnsupportedOperationException(); - } - - @Override - public InetSocketAddress getRemoteAddress() { - return ((ProxyServer) SkinOverlay.getInstance().getSkinOverlay().serverImpl()).getBoundAddress(); - } - - @Override - public Optional getVirtualHost() { - return Optional.empty(); - } - - @Override - public boolean isActive() { - return false; - } - - @Override - public ProtocolVersion getProtocolVersion() { - return ProtocolVersion.MINECRAFT_1_12_2; - } - - @Override - public @NotNull Identity identity() { - throw new UnsupportedOperationException(); - } - }; - } - - public PlayerObjectVelocity(final Player player) { - this.player = player; - } - - @Override - public Player player() { - return player; - } - - @Override - public Audience audience() { - return player; - } - - @Override - public UUID playerUUID() { - return player.getUniqueId(); - } - - @Override - public String playerName() { - return player.getUsername(); - } - - @Override - public void sendMessage(String input) { - VelocityMinecraftUtils.msg(player, input); - } - - @Override - public void sendMessage(@NotNull List input) { - VelocityMinecraftUtils.msg(player, input); - } - - @Override - public void sendMessage(String @NotNull ... input) { - VelocityMinecraftUtils.msg(player, input); - } - - @Override - public void sendMessage(String input, ObjectMap placeholders, boolean ignoreCase) { - VelocityMinecraftUtils.msg(player, input, placeholders, ignoreCase); - } - - @Override - public void sendMessage(List input, ObjectMap placeholders, boolean ignoreCase) { - VelocityMinecraftUtils.msg(player, input, placeholders, ignoreCase); - } - - @Override - public void sendMessage(String[] input, ObjectMap placeholders, boolean ignoreCase) { - VelocityMinecraftUtils.msg(player, input, placeholders, ignoreCase); - } - - @Override - public boolean isOnline() { - return player.isActive(); - } - - @Override - public boolean permission(String permission) { - return isOnline() && player.hasPermission(permission); - } -}